Skip to content

Commit

Permalink
Merge pull request #105 from Lilypad-Tech/rewards-dashboard
Browse files Browse the repository at this point in the history
Feat: Rewards dashboard
  • Loading branch information
PBillingsby authored Oct 11, 2024
2 parents ac2376c + 8dda64e commit 0ccf8e6
Show file tree
Hide file tree
Showing 40 changed files with 1,745 additions and 21 deletions.
62 changes: 62 additions & 0 deletions .github/workflows/rewards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Build and Deploy Next.js to GCP

on:
push:
branches:
- rewards-dashboard
- main

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Authenticate with Google Cloud
uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v1
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}

- name: Install bun
uses: oven-sh/setup-bun@v1

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "22"

- name: Install pnpm
run: npm install -g pnpm

- name: Install Doppler CLI
run: |
(curl -Ls --tlsv1.2 --proto "=https" --retry 3 https://cli.doppler.com/install.sh || wget -t 3 -qO- https://cli.doppler.com/install.sh) | sudo sh
- name: Set up Doppler
run: doppler configure set token ${{ secrets.DEVDEPLOY_DOPPLER_TOKEN }}

- name: Fetch secrets from Doppler and boot
run: doppler run -- npm run boot
env:
DOPPLER_TOKEN: ${{ secrets.DEVDEPLOY_DOPPLER_TOKEN }}

- name: Build Next.js app
working-directory: ./apps/rewards-dashboard
run: pnpm run build

# Step 11: Upload build output to GCS
- name: Upload build output to GCS
run: |
gsutil -m rsync -r -x "^\..*|README.md|gha-creds-.*\.json" ./apps/rewards-dashboard/out gs://${{ secrets.GCS_BUCKET_NAME }}
# Step 12: Set public read access to files
- name: Set public read access to files
run: |
gsutil iam ch allUsers:objectViewer gs://${{ secrets.GCS_BUCKET_NAME }}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ Each UI is seperated into it's own pnpm workspace, see details in `## Project st
**Apps**

- `/apps/info-dashboard`: The Lilypad network metrics UI hosted at https://info.lilypad.tech
- `/apps/website`: The Lilypad marketing site
- `/apps/website`: The Lilypad main site
- `/apps/website-cms`: (WIP!) Payload CMS that will potentially power the info-dashboard and website
- `/apps/rewards-dashboard`: Community Rewards and Recognition site hosted at https://oss.lilypad.tech

**Packages**

Expand Down
1 change: 1 addition & 0 deletions apps/rewards-dashboard/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DISCORD_TOKEN=
3 changes: 3 additions & 0 deletions apps/rewards-dashboard/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions apps/rewards-dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
40 changes: 40 additions & 0 deletions apps/rewards-dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Deployment

The site is set up to automatically deploy to a bucket in GCP via a Github action found within this repository, to the rewards site project. Within the configuration is a cloud storage bucket called oss-contrib which is referenced by a load balancer named oss-contributions which has a GCP issued SSL cert.

This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
91 changes: 91 additions & 0 deletions apps/rewards-dashboard/app/api/ambassador/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { promises as fs } from 'fs';
import path from 'path';

const DISCORD_TOKEN = process.env.DISCORD_TOKEN;

async function fetchDiscordUser(discordUserId) {
const discordAPIBase = 'https://discord.com/api/v10';

try {
const response = await fetch(`${discordAPIBase}/users/${discordUserId}`, {
headers: {
Authorization: `Bot ${DISCORD_TOKEN}`,
},
});

if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to fetch Discord user with ID: ${discordUserId}. Status: ${response.status}. Body: ${errorBody}`);
}

const data = await response.json();
return {
username: data?.global_name ?? data?.username,
avatar: data.avatar ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png` : null,
};
} catch (error) {
console.error(`Error fetching Discord user with ID ${discordUserId}:`, error);
return null;
}
}

async function parseCSVFile(filePath) {
try {
const data = await fs.readFile(filePath, 'utf8');
const lines = data.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, index) => {
obj[header.trim()] = values[index] ? values[index].trim() : null;
return obj;
}, {});
});
} catch (error) {
console.error('Error reading or parsing CSV file:', error);
throw error;
}
}

export async function GET(req) {
const csvFilePath = path.join(process.cwd(), 'public', 'ambassador_rewards.csv');

if (!DISCORD_TOKEN) {
console.error('Bot token not set in environment variables');
return new Response('Server configuration error: Bot token not set', { status: 500 });
}

try {
const ambassadors = await parseCSVFile(csvFilePath);

const enrichedAmbassadors = await Promise.all(
ambassadors.map(async (ambassador) => {
if (ambassador.id) {
const discordUser = await fetchDiscordUser(ambassador.id);
return {
id: ambassador.id,
username: discordUser?.username ?? 'Unknown',
avatar: discordUser?.avatar ?? null,
wallet_address: ambassador.wallet_address,
rewards: null,
contributions: null
};
} else {
console.warn(`Ambassador ${ambassador.username} has no ID`);
return null;
}
})
);

return new Response(JSON.stringify(enrichedAmbassadors), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Error processing ambassadors:', error);
return new Response(JSON.stringify({ error: 'Error processing ambassadors', details: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
35 changes: 35 additions & 0 deletions apps/rewards-dashboard/app/api/openSource/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { promises as fs } from 'fs';
import path from 'path';

export async function GET() {
try {
const csvFilePath = path.join(process.cwd(), 'public', 'community_rewards.csv');
const data = await fs.readFile(csvFilePath, 'utf8');
const lines = data.trim().split('\n');
const headers = lines[0].split(',');
const contributors = lines.slice(1).map((line) => {
const values = line.split(',');
const contributor = {};
headers.forEach((header, index) => {
contributor[header.trim()] = values[index] ? values[index].trim() : null;
});

// Map to unified structure
return {
username: contributor.username,
avatar: null, // Contributors don't have avatars, use a default if needed
wallet_address: contributor.wallet_address,
rewards: contributor.rewards,
contributions: contributor.contributions,
};
});

return new Response(JSON.stringify(contributors), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Error reading CSV file:', error);
return new Response('Error reading CSV file', { status: 500 });
}
}
17 changes: 17 additions & 0 deletions apps/rewards-dashboard/app/components/Navbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState } from 'react';

export default function Navbar({ setView }) {
return (
<div className="navbar">
<img src="/lilypad-logo.svg" alt="Lilypad Logo" />
<span className="navbar-links">
<a href="#" className="navbar-link" onClick={() => setView('openSource')}>
Open Source Rewards
</a>
<a href="#" className="navbar-link" onClick={() => setView('ambassador')}>
Ambassador Rewards
</a>
</span>
</div>
);
}
45 changes: 45 additions & 0 deletions apps/rewards-dashboard/app/components/SocialLinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const socialLinks = [
{ href: "https://twitter.com/lilypad_tech", iconUrl: "/x.svg" },
{ href: "https://discord.gg/lilypad-network", iconUrl: "/discord.svg" },
{ href: "https://t.me/lilypadnetwork", iconUrl: "/telegram.svg" },
{ href: "https://github.com/Lilypad-Tech", iconUrl: "/github.svg" },
{ href: "https://www.linkedin.com/company/lilypad-network/", iconUrl: "/linkedin.svg" },
{ href: "https://www.youtube.com/@LilypadNetwork/featured", iconUrl: "/youtube.svg" }
];

export default function SocialLinks({currentView}) {
return (
<div className="md:flex md:justify-between container w-full py-12 md:py-4">
<div className="cta md:order-2 text-center md:text-left mb-8 md:mb-0">
<div className="cta-text mb-4">
<h3 className="cta-title text-xl font-bold">Get involved!</h3>
{currentView === "openSource" ?
<p className="cta-subtitle text-md">Join the Lilypad open source initiative now.</p>
:
<p className="cta-subtitle text-md">Explore the Lilypad ambassador program now.</p>
}
</div>
<a
href={currentView === "openSource" ?
"https://blog.lilypadnetwork.org/lilypad-launches-open-source-initiative-as-part-of-incentivenet"
:
"https://lilypadnetwork.notion.site/Lilypad-Ambassadors-f11f73e91f684fa192fc2fab7985fe0d"
}
target="_blank"
rel="noopener noreferrer"
className="cta-button bg-[#095856] text-[#E0FFF9] px-4 py-2 rounded-md hover:bg-[#0c7472]"
>
Find out more!
</a>
</div>

<div className="flex order-1 md:order-1 gap-4 justify-center md:justify-start items-center">
{socialLinks.map((link, index) => (
<a key={index} href={link.href} target="_blank" rel="noopener noreferrer">
<img src={link.iconUrl} alt="social icon" />
</a>
))}
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions apps/rewards-dashboard/app/components/loadingIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

export const LoadingIcon = () => {
return (
<div className="mx-auto border border-gray-600 rounded-lg shadow-md ring-0 ring-offset-0 bg-[#181c21] w-12 h-12 flex items-center justify-center">
<img
src="/loading-icon.svg"
className="animate-spin w-6 h-6 filter invert"
alt="Loading Icon"
/>
</div>
);
};
Binary file added apps/rewards-dashboard/app/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file added apps/rewards-dashboard/app/fonts/GeistVF.woff
Binary file not shown.
Loading

0 comments on commit 0ccf8e6

Please sign in to comment.