Skip to content

Commit be2299a

Browse files
hussmartineznijoe1
andauthored
chore: added staking countdown functionality (#3800)
* chore: added staking countdown functionality * feat: staking explorer integration (#3802) * chore: update env.example * feat: new RoundViewLayout * chore: small fix on dropdown component * feat: updated stakingBannerAndModal to support all the different variations * feat: added staking hooks * fix: project card to support a staking banner * feat: staking sort dropdown * feat: integrates staking round view --------- Co-authored-by: Nick Lionis <nikolaos@gitcoin.co>
1 parent ec27cc5 commit be2299a

File tree

19 files changed

+846
-146
lines changed

19 files changed

+846
-146
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ REACT_APP_GRANT_EXPLORER=https://explorer.gitcoin.co
7878
REACT_APP_COINGECKO_API_KEY=
7979
# ---------------------------
8080

81-
REACT_APP_STAKING_APP=https://staking-hub-mu.vercel.app
81+
REACT_APP_STAKING_APP=https://boost.explorer.gitcoin.co/

packages/grant-explorer/src/features/common/DefaultLayout.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,30 @@ export function DefaultLayout({
2424
);
2525
}
2626

27+
export function RoundViewLayout({
28+
showWalletInteraction = true,
29+
children,
30+
infoCard,
31+
}: LayoutProps & { infoCard?: React.ReactNode }) {
32+
return (
33+
<main className={"font-sans min-h-screen text-grey-500"}>
34+
<Navbar showWalletInteraction={showWalletInteraction} />
35+
{infoCard && (
36+
<div className="relative pt-16 w-full items-center">{infoCard}</div>
37+
)}
38+
<div
39+
className={classNames(
40+
"container pt-16 relative z-10 mx-auto px-4 sm:px-6 lg:px-20 max-w-screen-2xl",
41+
infoCard ? "pt-0" : "pt-16"
42+
)}
43+
>
44+
{children}
45+
</div>
46+
<Footer />
47+
</main>
48+
);
49+
}
50+
2751
export function GradientLayout({
2852
showWalletInteraction = true,
2953
showAlloVersionBanner = false,

packages/grant-explorer/src/features/common/Dropdown.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type DropdownProps<T> = PropsWithChildren<{
99
keepOpen?: boolean;
1010
renderItem: (p: { active: boolean; close: () => void } & T) => ReactElement;
1111
headerElement?: (close: () => void) => ReactElement;
12+
labelClassName?: string;
1213
}>;
1314

1415
export function Dropdown<T>({
@@ -17,14 +18,19 @@ export function Dropdown<T>({
1718
keepOpen,
1819
renderItem,
1920
headerElement,
21+
labelClassName,
2022
}: DropdownProps<T>) {
2123
return (
2224
<Menu as="div" className="md:relative inline-block text-left z-20">
2325
{({ close }) => (
2426
<>
2527
<div>
2628
<Menu.Button className="inline-flex gap-2 items-center">
27-
<span className="text-white py-2">{label}</span>
29+
<span
30+
className={`${labelClassName ? labelClassName : "text-white py-2 "}`}
31+
>
32+
{label}
33+
</span>
2834
<ChevronDownIcon
2935
className="h-5 w-5 text-black"
3036
aria-hidden="true"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { RoundWithApplications } from "data-layer";
3+
4+
export const useRoundStakingSummary = (
5+
alloPoolId: string,
6+
chainId: string,
7+
isStakableRound: boolean
8+
) => {
9+
const query = useQuery({
10+
enabled: isStakableRound,
11+
queryKey: ["poolSummary", alloPoolId, chainId],
12+
queryFn: () => getPoolSummary(alloPoolId, Number(chainId)),
13+
});
14+
15+
return {
16+
data: query.data,
17+
isLoading: query.isLoading,
18+
isError: query.isError,
19+
error: query.error,
20+
refetch: query.refetch,
21+
};
22+
};
23+
24+
export interface RoundWithStakes extends RoundWithApplications {
25+
stakes: Stake[];
26+
totalStakesByAnchorAddress: Record<string, string>;
27+
}
28+
29+
export interface Stake {
30+
chainId: number;
31+
amount: string;
32+
poolId: string;
33+
recipient: string;
34+
sender: string;
35+
blockTimestamp: string;
36+
}
37+
38+
const GET = async (url: string) => {
39+
const response = await fetch(url, {
40+
method: "GET",
41+
headers: {
42+
"Content-Type": "application/json",
43+
},
44+
});
45+
46+
if (!response.ok) {
47+
const errorData = await response.json();
48+
throw new Error(
49+
`Error: ${response.status} - ${errorData.message || "Unknown error"}`
50+
);
51+
}
52+
53+
return response.json();
54+
};
55+
56+
export async function getPoolSummary(
57+
alloPoolId: string,
58+
chainId: number
59+
): Promise<RoundWithStakes> {
60+
try {
61+
const url = `${process.env.REACT_APP_STAKING_HUB_ENDPOINT}/api/pools/${chainId}/${alloPoolId}/summary`;
62+
const response: RoundWithStakes = await GET(url);
63+
return response;
64+
} catch (error) {
65+
console.error("Error fetching pool info and stakes:", error);
66+
throw error;
67+
}
68+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useMemo } from "react";
2+
import { getAddress } from "viem";
3+
import { RoundWithStakes } from "./useRoundStakingSummary";
4+
import { Application } from "data-layer";
5+
6+
export type ApplicationData = Application & {
7+
totalStaked: number;
8+
numberOfContributors: number;
9+
totalDonations: number;
10+
};
11+
12+
export type SortOption =
13+
| "totalStakedDesc"
14+
| "totalDonationsDesc"
15+
| "totalContributorsDesc"
16+
| "totalStakedAsc"
17+
| "totalDonationsAsc"
18+
| "totalContributorsAsc";
19+
20+
export const useSortApplications = (
21+
poolSummary: RoundWithStakes | undefined,
22+
chainId: string | undefined,
23+
roundId: string | undefined,
24+
sortOption?: SortOption
25+
) => {
26+
return useMemo(() => {
27+
if (!poolSummary || !chainId || !roundId) return [];
28+
const applications =
29+
poolSummary.applications.map((application) => {
30+
application.project.metadata = application.metadata.application.project;
31+
return application;
32+
}) ?? [];
33+
34+
const mappedProjects = applications.map((app) => {
35+
return {
36+
...app,
37+
totalStaked:
38+
Number(
39+
poolSummary.totalStakesByAnchorAddress[
40+
getAddress(app.anchorAddress ?? "")
41+
] ?? 0
42+
) / 1e18,
43+
uniqueDonorsCount: Number(app.uniqueDonorsCount),
44+
numberOfContributors: Number(app.totalDonationsCount),
45+
totalDonations: app.totalAmountDonatedInUsd,
46+
};
47+
});
48+
49+
// Sort based on selected option and update ranks
50+
return sortProjects(mappedProjects, sortOption ?? "totalStakedDesc");
51+
}, [poolSummary, chainId, roundId, sortOption]);
52+
};
53+
54+
export const sortProjects = (
55+
projects: ApplicationData[],
56+
sortOption: SortOption
57+
): ApplicationData[] => {
58+
// First sort the projects
59+
const sortedProjects = [...projects].sort((a, b) => {
60+
switch (sortOption) {
61+
case "totalStakedDesc":
62+
// If one has stakes and the other doesn't, the one with stakes ranks higher
63+
if (a.totalStaked > 0 && b.totalStaked === 0) return -1;
64+
if (b.totalStaked > 0 && a.totalStaked === 0) return 1;
65+
// If both have stakes, compare by stake amount
66+
if (a.totalStaked !== b.totalStaked) {
67+
return b.totalStaked - a.totalStaked;
68+
}
69+
// If stakes are equal, sort by contributor count
70+
return b.uniqueDonorsCount - a.uniqueDonorsCount;
71+
72+
case "totalDonationsDesc":
73+
return b.totalDonations - a.totalDonations;
74+
75+
case "totalContributorsDesc":
76+
return b.uniqueDonorsCount - a.uniqueDonorsCount;
77+
78+
case "totalStakedAsc":
79+
return a.totalStaked - b.totalStaked;
80+
81+
case "totalDonationsAsc":
82+
return a.totalDonations - b.totalDonations;
83+
84+
case "totalContributorsAsc":
85+
return a.uniqueDonorsCount - b.uniqueDonorsCount;
86+
87+
default:
88+
return 0;
89+
}
90+
});
91+
92+
// Then update the ranks based on the new order
93+
return sortedProjects.map((project, index) => ({
94+
...project,
95+
rank: index + 1,
96+
}));
97+
};
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,57 @@
1-
import { Button } from "common/src/styles";
1+
import { PropsWithChildren } from "react";
22

33
const STAKING_BANNER_TITLE =
44
"🔥 Boost grants, earn rewards, and shape the round!";
5+
6+
const STAKING_BANNER_TITLE_ROUND_VIEW =
7+
"🔥 Boost grants, earn rewards, and shape the round—staking is available only during GG23!";
8+
9+
const STAKING_BANNER_TITLE_ROUND_VIEW_CLAIM_PERIOD =
10+
"🎉 Staking’s a wrap! If you staked GTC, it’s time to claim your rewards!";
11+
12+
const STAKING_BANNER_TITLE_CLAIM_PERIOD =
13+
"Staking’s done—time to claim your rewards! 🎉";
14+
515
const STAKING_BANNER_TEXT =
616
"Stake GTC during GG23 to upvote your favorite grants and increase their visibility in the round. The more you stake, the higher they rank—and the more rewards you can claim from the 3% rewards pool!";
7-
const STAKING_BUTTON_TEXT = "Stake on this project";
8-
const STAKING_BUTTON_TEXT_MOBILE = "Stake";
917

10-
export const StakingBanner = ({ onClick }: { onClick?: () => void }) => {
18+
const STAKING_BANNER_TEXT_CLAIM_PERIOD =
19+
"Staking is closed! If you staked GTC during GG23, it’s time to claim your rewards from the 3% rewards pool. Thanks for boosting your favorite grants!";
20+
21+
export const StakingBanner = ({
22+
children,
23+
isRoundView,
24+
isClaimPeriod,
25+
}: PropsWithChildren<{ isRoundView?: boolean; isClaimPeriod?: boolean }>) => {
1126
return (
12-
<div className="bg-[#F2FBF8] rounded-3xl p-6 flex items-center justify-between w-full gap-4">
13-
<div className="flex flex-col gap-4 font-sans text-black max-w-[609px]">
14-
<h3 className="text-2xl font-medium">{STAKING_BANNER_TITLE}</h3>
15-
<p className="text-base/[1.75rem] font-normal">{STAKING_BANNER_TEXT}</p>
16-
</div>
17-
<Button
18-
className="text-white bg-[#22635A] max-h-[40px] font-mono"
19-
onClick={onClick}
27+
<div
28+
className={`p-6 flex flex-col xl:flex-row items-center w-full gap-6 ${
29+
isRoundView ? "justify-center" : "rounded-3xl justify-between"
30+
} ${isClaimPeriod ? "bg-[#F5F4FE]" : "bg-[#F2FBF8]"}`}
31+
>
32+
<div
33+
className={`flex flex-col gap-4 font-sans text-black ${
34+
isRoundView ? "items-center" : "max-w-[609px]"
35+
}`}
2036
>
21-
<span className="block lg:hidden whitespace-nowrap">
22-
{STAKING_BUTTON_TEXT_MOBILE}
23-
</span>
24-
<span className="hidden lg:block whitespace-nowrap">
25-
{STAKING_BUTTON_TEXT}
26-
</span>
27-
</Button>
37+
<h3 className={`font-medium ${isRoundView ? "text-xl" : "text-2xl"}`}>
38+
{isRoundView
39+
? isClaimPeriod
40+
? STAKING_BANNER_TITLE_ROUND_VIEW_CLAIM_PERIOD
41+
: STAKING_BANNER_TITLE_ROUND_VIEW
42+
: isClaimPeriod
43+
? STAKING_BANNER_TITLE_CLAIM_PERIOD
44+
: STAKING_BANNER_TITLE}
45+
</h3>
46+
{!isRoundView && (
47+
<p className="text-base/[1.75rem] font-normal">
48+
{isClaimPeriod
49+
? STAKING_BANNER_TEXT_CLAIM_PERIOD
50+
: STAKING_BANNER_TEXT}
51+
</p>
52+
)}
53+
</div>
54+
{children}
2855
</div>
2956
);
3057
};

0 commit comments

Comments
 (0)