diff --git a/src/common/ui/layout/svg/Trophy.tsx b/src/common/ui/layout/svg/Trophy.tsx new file mode 100644 index 00000000..480a537a --- /dev/null +++ b/src/common/ui/layout/svg/Trophy.tsx @@ -0,0 +1,14 @@ +// TODO!: Delete this file and use an icon library package component instead! + +const Trophy = (props: any) => { + return ( + + + + ); +}; + +export default Trophy; diff --git a/src/features/donation/components/DonationLeaderboardEntry.tsx b/src/features/donation/components/DonationLeaderboardEntry.tsx new file mode 100644 index 00000000..7752b291 --- /dev/null +++ b/src/features/donation/components/DonationLeaderboardEntry.tsx @@ -0,0 +1,57 @@ +import Image from "next/image"; + +import Trophy from "@/common/ui/layout/svg/Trophy"; + +export type DonationLeaderboardEntryProps = { + rank: number; + image: string; + name: string; + amount: number; + type: "donor" | "sponsor"; +}; + +export const DonationLeaderboardEntry: React.FC = ({ + rank, + image, + name, + amount, + type, +}) => { + const bgClass = + rank === 1 + ? "bg-gradient-to-r from-orange-400 to-red-500 text-white" + : rank === 2 + ? "bg-gradient-to-r from-#F7F7F7 to-#DBDBDB" + : "bg-gradient-to-r from-#FCE9D5 to-#F8D3B0"; + + const rankText = ["1st", "2nd", "3rd"][rank - 1]; + + const iconClass = rank === 1 ? "fill-yellow-400" : rank === 2 ? "fill-gray-400" : "fill-red-800"; + + return ( +
+
+
+ +
+ {rankText} +
+
+ {name} +

{name}

+

+ {amount}{" "} + + NEAR {type === "donor" ? "Donated" : "Sponsored"} + +

+
+
+ ); +}; diff --git a/src/features/donation/index.ts b/src/features/donation/index.ts index 6e533be2..607bc1e3 100644 --- a/src/features/donation/index.ts +++ b/src/features/donation/index.ts @@ -1,4 +1,5 @@ export * from "./constants"; +export * from "./components/DonationLeaderboardEntry"; export * from "./components/summary"; export * from "./components/human-verification-alert"; export * from "./components/user-entrypoints"; diff --git a/src/layout/components/app-bar.tsx b/src/layout/components/app-bar.tsx index 9ddb190a..02cf390c 100644 --- a/src/layout/components/app-bar.tsx +++ b/src/layout/components/app-bar.tsx @@ -24,7 +24,7 @@ const links = [ }, { label: "Feed", url: rootPathnames.FEED, disabled: false }, - // { label: "Donors", url: rootPathnames.DONORS, disabled: false }, + { label: "Donors", url: rootPathnames.DONORS, disabled: false }, { label: "Lists", diff --git a/src/pages/donors/index.tsx b/src/pages/donors/index.tsx new file mode 100644 index 00000000..e9b991a8 --- /dev/null +++ b/src/pages/donors/index.tsx @@ -0,0 +1,622 @@ +import { useState } from "react"; + +import { Lora } from "next/font/google"; +import Image from "next/image"; + +import { coingeckoHooks } from "@/common/api/coingecko"; +import { Account, indexer } from "@/common/api/indexer"; +import { daysAgo, parseNumber } from "@/common/lib"; +import { FilterChip, SearchBar, ToggleGroup } from "@/common/ui/layout/components"; +import { NearIcon } from "@/common/ui/layout/svg"; +import { AccountListItem } from "@/entities/_shared/account"; +import { DonationLeaderboardEntry } from "@/features/donation"; + +const lora = Lora({ + subsets: ["latin"], + variable: "--font-lora", + weight: ["400", "500", "600", "700"], +}); + +interface Participant { + rank: number; + image: string; + name: string; + amount: number; + amountUsd: number; +} + +interface Activity { + sender: string; + senderImage: string; + amount: number; + amountUsd: number; + currency: string; + receiver: string; + receiverImage: string; + timestamp: number; +} + +const topDonors: Participant[] = [ + { + rank: 1, + image: "https://picsum.photos/200/200/?blur", + name: "nearcollective.near", + amount: 731.25, + amountUsd: 0, + }, + { + rank: 2, + image: "https://picsum.photos/200/200/?blur", + name: "nf-payments.near", + amount: 731.25, + amountUsd: 0, + }, + { + rank: 3, + image: "https://picsum.photos/200/200/?blur", + name: "creatives.potlock.near", + amount: 731.25, + amountUsd: 0, + }, +]; + +const otherDonors: Participant[] = [ + { + rank: 4, + image: "https://picsum.photos/200/200/?blur", + name: "creativesportfolio.near", + amount: 2000, + amountUsd: 2000, + }, + { + rank: 5, + image: "https://picsum.photos/200/200/?blur", + name: "mike.near", + amount: 2000, + amountUsd: 2000, + }, + { + rank: 6, + image: "https://picsum.photos/200/200/?blur", + name: "mike.near", + amount: 2000, + amountUsd: 2000, + }, +]; + +const topSponsors: Participant[] = [ + { + rank: 1, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor1.near", + amount: 1000, + amountUsd: 1000, + }, + { + rank: 2, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor2.near", + amount: 900, + amountUsd: 900, + }, + { + rank: 3, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor3.near", + amount: 800, + amountUsd: 800, + }, + { + rank: 4, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor4.near", + amount: 800, + amountUsd: 800, + }, + { + rank: 5, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor5.near", + amount: 100, + amountUsd: 200, + }, + { + rank: 6, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor6.near", + amount: 10, + amountUsd: 10, + }, + { + rank: 7, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor7.near", + amount: 30, + amountUsd: 800, + }, + { + rank: 8, + image: "https://picsum.photos/200/200/?blur", + name: "sponsor8.near", + amount: 800, + amountUsd: 800, + }, +]; + +const ACTIVITY: Activity[] = [ + { + sender: "nearcollective.near", + senderImage: "https://picsum.photos/200/200/?blur", + amount: 1000, + amountUsd: 1000, + currency: "NEAR", + receiver: "creativesportfolio.near", + receiverImage: "https://picsum.photos/200/200/?blur", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, + { + sender: "nf-payments.near", + senderImage: "https://picsum.photos/200/200/?blur", + amount: 1000, + amountUsd: 1000, + currency: "NEAR", + receiver: "mike.near", + receiverImage: "https://picsum.photos/200/200/?blur", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, + { + sender: "creatives.potlock.near", + senderImage: "https://picsum.photos/200/200/?blur", + amount: 1000, + amountUsd: 1000, + currency: "NEAR", + receiver: "mike.near", + receiverImage: "https://picsum.photos/200/200/?blur", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, + { + sender: "nearcollective.near", + senderImage: "https://picsum.photos/200/200/?blur", + amount: 1000, + amountUsd: 1000, + currency: "NEAR", + receiver: "creativesportfolio.near", + receiverImage: "https://picsum.photos/200/200/?blur", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, + { + sender: "nf-payments.near", + senderImage: "https://picsum.photos/200/200/?blur", + amount: 1000, + amountUsd: 1000, + currency: "NEAR", + receiver: "mike.near", + receiverImage: "https://picsum.photos/200/200/?blur", + timestamp: Date.now() - 1000 * 60 * 60 * 24 * 2, + }, +]; + +// Define a type guard to check if an object is of type Account +function isAccount(obj: any): obj is Account { + return "total_donations_out_usd" in obj; +} + +export default function LeaderboardPage() { + const [searchTerm, setSearchTerm] = useState(""); + const [searchActivity, setSearchActivity] = useState(""); + const [timeFilter, setTimeFilter] = useState("All time"); + + const [selectedTab, setSelectedTab] = useState<"donors" | "sponsors" | "activities">( + "activities", + ); + + const toggleTab = (tab: "donors" | "sponsors" | "activities") => { + setSelectedTab(tab); + }; + + const { data: donors } = { data: [] }; // indexer.useDonors({}); + + const sponsors: Participant[] = []; + const { data: priceOfOneNear } = coingeckoHooks.useNativeTokenUsdPrice(); + const price = priceOfOneNear ? parseNumber(priceOfOneNear) : 0; + + console.log({ donors, priceOfOneNear }); + + const handleSearch = (participant: any) => { + if (!participant) return false; + + const searchLower = searchTerm.toLowerCase(); + + const idMatches = isAccount(participant) + ? participant.id?.toLowerCase().includes(searchLower) + : participant.name.includes(searchLower); + + const nameMatches = participant.near_social_profile_data?.name + ?.toLowerCase() + .includes(searchLower); + + return idMatches || nameMatches; + }; + + const renderLeaderboard = (participants: Participant[], type: "donor" | "sponsor") => { + const data = type === "donor" ? [...(donors || [])] : [...participants]; + + console.log("data now", data); + + return ( + <> +
+ setSearchTerm(e.target.value)} + /> +
+ {["All time", "1Y", "30D", "1W", "1D"].map((filter) => ( + setTimeFilter(filter)} + className="text-sm" + > + {filter} + + ))} +
+
+
+
+ {data + ?.sort((a, b) => { + const aAmount = isAccount(a) ? a.total_donations_out_usd : a.amountUsd; + + const bAmount = isAccount(b) ? b.total_donations_out_usd : b.amountUsd; + + return bAmount - aAmount; + }) + .slice(0, 3) + .map((participant, index) => { + const name = isAccount(participant) + ? (participant.near_social_profile_data?.name ?? + (participant.id?.length <= 20 + ? participant.id + : `${participant.id.substring(0, 16)}...${participant.id.substring(participant.id.length - 4)}`)) + : participant.name; + + return ( + + ); + })} +
+
+ +
+ + + + + + + + + + + {data + ?.sort((a, b) => { + const aAmount = isAccount(a) ? a.total_donations_out_usd : 0; + const bAmount = isAccount(b) ? b.total_donations_out_usd : 0; + return bAmount - aAmount; + }) + .slice(3) + .filter(handleSearch) + .map((donor, index) => ( + + + + + + + ))} + +
+ Rank + + Projects + + Amount + + AMT (USD) +
+
+ + #{isAccount(donor) ? index + 1 : donor.rank} + + {(isAccount(donor) ? index + 1 : donor.rank) === 4 ? ( +
+ ) : ( +
+ )} +
+
+ + {/*
+
+ {participant.near_social_profile_data?.name ?? + participant.id} +
+
*/} +
+
+ + + {isAccount(donor) + ? (donor.total_donations_out_usd / price).toFixed(2) + : donor.amount} + +
+
+ $ {isAccount(donor) ? donor.total_donations_out_usd : donor.amountUsd} +
+
+
+ {data?.map((participant, index) => ( +
+
+
+ profile picture +
+
+
+
+
+
+ {isAccount(participant) + ? (participant?.near_social_profile_data?.name ?? participant.id) + : participant.name} +
+
+
+
+ #{index + 1} + {index === 4 ? ( +
+ ) : ( +
+ )} +
+
+
+
+
+ + + {isAccount(participant) + ? participant.total_donations_out_usd / price + : participant.amount} + +
+
+ ~${" "} + {isAccount(participant) + ? participant.total_donations_out_usd + : participant.amountUsd} +
+
+
+
+ ))} +
+ + ); + }; + + const TABs = [ + { + name: "activities", + label: "All Activities", + count: 20000, + }, + { + name: "donors", + label: "Donor Leaderboard", + count: donors?.length || 0, + }, + { + name: "sponsors", + label: "Sponsor Leaderboard", + count: 68, + }, + ]; + + return ( +
+ +
+
+ {TABs.map((tab) => ( +
toggleTab(tab.name as "donors" | "sponsors" | "activities")} + > + + {tab.label} + + + {tab.count} + +
+ ))} +
+
+
+
+
+
+ {selectedTab === "activities" ? ( +
+

+ All Activities +

+ <> +
+ setSearchTerm(e.target.value)} + /> +
+ {["All time", "1Y", "30D", "1W", "1D"].map((filter) => ( + setTimeFilter(filter)} + className="text-sm" + > + {filter} + + ))} +
+
+
+ {ACTIVITY.map((activity, index) => ( +
+
+
+ sender image +

+ {activity.sender} +

+
+
+
+ Donated +
+
+
+ {" "} + {activity.amount} +
+
+
to
+
+
+
+
+ receiver image +

+ {activity.receiver} +

+
+
+ {" "} + {daysAgo(activity.timestamp)} +
+
+
+ ))} +
+ +
+ ) : null} + {selectedTab === "donors" ? ( +
+

+ Donor Leaderboard +

+ {renderLeaderboard([...topDonors, ...otherDonors], "donor")} +
+ ) : null} + {selectedTab === "sponsors" ? ( +
+

+ Sponsor Leaderboard +

+ {renderLeaderboard(topSponsors, "sponsor")} +
+ ) : null} +
+
+
+
+ ); +}