Skip to content

Commit

Permalink
add module creator tab and csv
Browse files Browse the repository at this point in the history
  • Loading branch information
PBillingsby committed Jan 3, 2025
1 parent f6049fb commit 100dce5
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 55 deletions.
1 change: 1 addition & 0 deletions apps/rewards-dashboard/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
35 changes: 35 additions & 0 deletions apps/rewards-dashboard/app/api/modules/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', 'module_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,
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 });
}
}
50 changes: 50 additions & 0 deletions apps/rewards-dashboard/app/components/ContributorList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState, useEffect } from "react";

const ContributionList = ({ contributions }) => {
const [titles, setTitles] = useState([]);

useEffect(() => {
const fetchTitles = async () => {
const fetchedTitles = await Promise.all(
contributions.split(";").map(async (contribution) => {
try {
const apiUrl = contribution
.replace('https://github.com/', 'https://api.github.com/repos/')
.replace('/pull/', '/pulls/');


const res = await fetch(apiUrl);
if (!res.ok) {
throw new Error(`Error ${res.status}: ${res.statusText}`);
}
const data = await res.json();
return data.title || "Unknown Title";
} catch (error) {
console.error(`Error fetching title for ${contribution}:`, error);
return "Error Loading Title";
}
})
);
setTitles(fetchedTitles);
};


fetchTitles();
}, [contributions]);

return (
<ul>
{contributions && contributions.split(";").map((contribution, index) => (
<li key={index}>
<a href={contribution} target="_blank" rel="noopener noreferrer">
{titles[index] !== undefined
? `${index + 1}. ${titles[index]}`
: "Loading..."}
</a>
</li>
))}
</ul>
);
};

export default ContributionList;
155 changes: 100 additions & 55 deletions apps/rewards-dashboard/app/page.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, Fragment } from "react";
import SocialLinks from "./components/SocialLinks";
import { LoadingIcon } from "./components/loadingIcon";
import ContributionList from "./components/ContributorList"
import { sortByRewards, sortByContributions } from "./utils/sort";

export default function Home() {
const [contributors, setContributors] = useState([]);
const [currentView, setCurrentView] = useState("openSource");
const [sortOption, setSortOption] = useState("");
const [loading, setLoading] = useState(true);
const [expandedRow, setExpandedRow] = useState(null);

const fetchContributors = async (view) => {
setContributors([]);
Expand Down Expand Up @@ -39,56 +41,89 @@ export default function Home() {
sortByRewards(contributors, setContributors);
} else if (selectedOption === "contributions") {
sortByContributions(contributors, setContributors);
}
} else if (selectedOption === "modules") {
sortByContributions(contributors, setContributors);
}
};

const renderContributors = () => {
return contributors.map((contributor) => {
const isAmbassador = !contributor?.contributions;
return (
<tr key={contributor?.id || contributor?.username} className="border-b border-gray-700">
<td className="px-4 py-2" title={contributor.id && `User ID: #${contributor.id}`}>
<div className="flex items-center gap-2">
<img
className="contributor-avatar w-8 h-8 rounded-full"
src={
isAmbassador
? contributor.avatar || "/default-avatar.png"
: `https://github.com/${contributor.username}.png`
}
alt={`Avatar of ${contributor.username}`}
loading="lazy"
/>
<div>
<div className="contributor-name text-sm font-semibold">
{contributor.username}
</div>
{currentView === "openSource" && (
<a
href={`https://github.com/${contributor?.username}`}
target="_blank"
rel="noreferrer"
className="text-xs text-blue-500 underline"
aria-label={`Visit ${contributor?.username}'s GitHub profile`}
>
GitHub Profile
</a>
)}
</div>
</div>
</td>
<td className="text-center px-4 py-2">{contributor.rewards || "0"}</td>
{currentView === "openSource" && (
<td className="text-center px-4 py-2">
{contributor.contributions
?.split(";")
.filter((item) => item).length || "0"}
</td>
)}
<td className="text-center px-4 py-2">{contributor.wallet_address || "N/A"}</td>
</tr>
);
});
const renderContributors = () => {
const toggleRow = (rowId) => {
setExpandedRow((prev) => (prev === rowId ? null : rowId));
};

return contributors.map((contributor) => {
const isAmbassador = currentView === "ambassador";
const isExpanded = expandedRow === contributor?.id || expandedRow === contributor?.username;

return (
<Fragment key={contributor?.id || contributor?.username}>
<tr
className={`border-b border-gray-700 hover:bg-gray-800 cursor-pointer ${
isExpanded ? "bg-gray-900" : ""
}`}
onClick={() => !isAmbassador && toggleRow(contributor?.id || contributor?.username)}
>
<td className="px-4 py-2">
<div className="flex items-center gap-2">
<img
className="contributor-avatar w-8 h-8 rounded-full"
src={
contributor.avatar || `https://github.com/${contributor.username}.png`
}
alt={`Avatar of ${contributor.username}`}
loading="lazy"
/>
<div>
<div className="contributor-name text-sm font-semibold">
{contributor.username}
</div>
{!isAmbassador && (
<a
href={`https://github.com/${contributor?.username}`}
target="_blank"
rel="noreferrer"
className="text-xs text-blue-500 underline"
aria-label={`Visit ${contributor?.username}'s GitHub profile`}
>
GitHub Profile
</a>
)}
</div>
</div>
</td>
<td className="text-center px-4 py-2">{contributor.rewards || "0"}</td>
{!isAmbassador && (
<td className="text-center px-4 py-2">
{contributor.contributions
?.split(";")
.filter((item) => item).length || "0"}
</td>
)}
<td className="text-center px-4 py-2">{contributor.wallet_address || "N/A"}</td>
</tr>

{!isAmbassador && isExpanded && (
<tr>
<td colSpan="4" className="px-4 py-4">
<div className="p-4 rounded-lg">
<ol className="menu bg-base-200 rounded-box">
{contributor?.contributions && (
<ContributionList contributions={contributor.contributions} />
)}
</ol>
</div>
</td>
</tr>
)}
</Fragment>
);
});
};

const viewTitles = {
openSource: "Open Sourcerors",
ambassador: "Ambassadors",
modules: "Module Creators",
};

return (
Expand All @@ -98,7 +133,7 @@ export default function Home() {
<div className="flex flex-col gap-2 text-center md:text-left">
<img src="/lilypad-logo.svg" alt="Lilypad Logo" className="mx-auto md:mx-0" />
<h1 className="text-xl font-bold text-[#b8f4f3]">
{currentView === "openSource" ? "Open Sourcerors" : "Ambassadors"}
{viewTitles[currentView] || "Default Title"}
</h1>
</div>
<div className="flex items-center gap-2 md:gap-4">
Expand All @@ -110,7 +145,7 @@ export default function Home() {
}`}
onClick={() => setCurrentView("openSource")}
>
Open Source Rewards
Open Source
</button>
<button
className={`rounded p-1 md:px-4 md:py-2 text-sm md:text-lg text-center cursor-pointer hover:bg-[#272d35] ${
Expand All @@ -120,14 +155,24 @@ export default function Home() {
}`}
onClick={() => setCurrentView("ambassador")}
>
Ambassador Rewards
Ambassadors
</button>
<button
className={`rounded p-1 md:px-4 md:py-2 text-sm md:text-lg text-center cursor-pointer hover:bg-[#272d35] ${
currentView === "modules"
? "bg-[#272D35] text-[#e0fff9] border"
: "bg-[#181c21] text-text-color"
}`}
onClick={() => setCurrentView("modules")}
>
Module Creators
</button>
</div>
<div className="flex flex-col items-center gap-2">
<div className="text-center text-sm md:text-md leading-7 text-[#E0FFF9] font-semibold antialiased" style={{ minWidth: '200px' }}>
{loading ? (
<span aria-live="polite">Loading...</span>
) : currentView === "openSource" ? (
) : currentView !== "ambassador" ? (
<span>
{`Total Contributions: ${contributors.reduce((total, contributor) => {
const count = contributor?.contributions?.split(";").filter(Boolean).length || 0;
Expand All @@ -138,7 +183,7 @@ export default function Home() {
<span>{`Total Contributors: ${contributors?.length || 0}`}</span>
)}
</div>
{currentView === "openSource" && (
{currentView !== "ambassador" && (
<label className="block">
<span className="sr-only">Sort contributions</span>
<select
Expand Down Expand Up @@ -169,7 +214,7 @@ export default function Home() {
<tr className="border-b border-gray-600">
<th className="text-left px-4 py-2">Contributor</th>
<th className="text-center px-4 py-2">Lilybit Rewards</th>
{currentView === "openSource" && (
{currentView !== "ambassador" && (
<th className="text-center px-4 py-2">Contributions</th>
)}
<th className="text-center px-4 py-2">Wallet ID</th>
Expand Down
1 change: 1 addition & 0 deletions apps/rewards-dashboard/public/module_rewards.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
username,github,wallet_address,rewards,contributions

0 comments on commit 100dce5

Please sign in to comment.