Skip to content

Commit f199cca

Browse files
committed
chore: added staking countdown functionality
1 parent 5b48fab commit f199cca

File tree

9 files changed

+260
-86
lines changed

9 files changed

+260
-86
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
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!";
55
const STAKING_BANNER_TEXT =
66
"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";
97

10-
export const StakingBanner = ({ onClick }: { onClick?: () => void }) => {
8+
export const StakingBanner = ({ children }: PropsWithChildren) => {
119
return (
12-
<div className="bg-[#F2FBF8] rounded-3xl p-6 flex items-center justify-between w-full gap-4">
10+
<div className="bg-[#F2FBF8] rounded-3xl p-6 flex flex-col xl:flex-row items-center justify-between w-full gap-4">
1311
<div className="flex flex-col gap-4 font-sans text-black max-w-[609px]">
1412
<h3 className="text-2xl font-medium">{STAKING_BANNER_TITLE}</h3>
1513
<p className="text-base/[1.75rem] font-normal">{STAKING_BANNER_TEXT}</p>
1614
</div>
17-
<Button
18-
className="text-white bg-[#22635A] max-h-[40px] font-mono"
19-
onClick={onClick}
20-
>
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>
15+
{children}
2816
</div>
2917
);
3018
};

packages/grant-explorer/src/features/round/ViewProjectDetails/components/StakingBannerAndModal/StakingBannerAndModal.tsx

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { useCallback, useState } from "react";
33
import { StakingBanner } from "./StakingBanner";
44
import { useProjectDetailsParams } from "../../hooks/useProjectDetailsParams";
55
import { useIsStakable } from "./hooks/useIsStakable";
6+
import { useDonationPeriod } from "./hooks/useDonationPeriod";
7+
import { StakingButton } from "./StakingButton";
8+
import { StakingCountDownLabel } from "./StakingCountDownLabel";
9+
10+
const COUNTDOWN_DAYS = 3;
11+
const DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
12+
const COUNTDOWN_STARTS_IN_MILLISECONDS = COUNTDOWN_DAYS * DAY_IN_MILLISECONDS;
613

714
export const StakingBannerAndModal = () => {
815
const [isOpen, setIsOpen] = useState(false);
@@ -38,21 +45,43 @@ export const StakingBannerAndModal = () => {
3845
const isStakable = useIsStakable({
3946
chainId: chainIdNumber,
4047
roundId,
48+
});
49+
50+
const { isDonationPeriod, timeToDonationStart } = useDonationPeriod({
51+
chainId: chainIdNumber,
52+
roundId,
4153
applicationId,
54+
refreshInterval: 60 * 1000, // 1 minute
4255
});
4356

44-
if (!isStakable) {
45-
return null;
57+
const isCountDownToStartPeriod =
58+
timeToDonationStart &&
59+
timeToDonationStart.totalMilliseconds > 0 &&
60+
timeToDonationStart.totalMilliseconds < COUNTDOWN_STARTS_IN_MILLISECONDS;
61+
62+
if (isStakable && isCountDownToStartPeriod) {
63+
return (
64+
<div className="mt-2 mb-6">
65+
<StakingBanner>
66+
<StakingCountDownLabel timeLeft={timeToDonationStart} />
67+
</StakingBanner>
68+
</div>
69+
);
4670
}
4771

48-
return (
49-
<div className="mt-2 mb-6">
50-
<StakingBanner onClick={handleOpenModal} />
51-
<StakingModal
52-
isOpen={isOpen}
53-
onClose={handleCloseModal}
54-
onStake={handleStake}
55-
/>
56-
</div>
57-
);
72+
if (isStakable && isDonationPeriod) {
73+
return (
74+
<div className="mt-2 mb-6">
75+
<StakingBanner>
76+
<StakingButton onClick={handleOpenModal} />
77+
</StakingBanner>
78+
<StakingModal
79+
isOpen={isOpen}
80+
onClose={handleCloseModal}
81+
onStake={handleStake}
82+
/>
83+
</div>
84+
);
85+
}
86+
return null;
5887
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Button } from "common/src/styles";
2+
3+
const STAKING_BUTTON_TEXT = "Stake on this project";
4+
5+
export const StakingButton = ({ onClick }: { onClick?: () => void }) => {
6+
return (
7+
<Button
8+
className="text-white bg-[#22635A] max-h-[40px] font-mono whitespace-nowrap"
9+
onClick={onClick}
10+
>
11+
{STAKING_BUTTON_TEXT}
12+
</Button>
13+
);
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const generateCountDownLabel = (
2+
days: number,
3+
hours: number,
4+
minutes: number
5+
) => {
6+
const limitMinutes = 1;
7+
if (days > 0) {
8+
const dayText = days === 1 ? "day" : "days";
9+
if (hours === 0) {
10+
return `${days} ${dayText}`;
11+
}
12+
const hourText = hours === 1 ? "hour" : "hours";
13+
return `${days} ${dayText}, ${hours} ${hourText}`;
14+
} else if (hours > 0) {
15+
const hourText = hours === 1 ? "hour" : "hours";
16+
if (minutes === 0) {
17+
return `${hours} ${hourText}`;
18+
}
19+
const minuteText = minutes === 1 ? "minute" : "minutes";
20+
return `${hours} ${hourText}, ${minutes} ${minuteText}`;
21+
} else if (minutes >= limitMinutes) {
22+
const minuteText = minutes === 1 ? "minute" : "minutes";
23+
return `${minutes} ${minuteText}`;
24+
} else {
25+
return "in a few minutes";
26+
}
27+
};
28+
29+
export const StakingCountDownLabel = ({
30+
label = "Staking begins in",
31+
timeLeft,
32+
}: {
33+
label?: string;
34+
timeLeft: {
35+
days: number;
36+
hours: number;
37+
minutes: number;
38+
};
39+
}) => {
40+
const { days, hours, minutes } = timeLeft;
41+
return (
42+
<div className="py-4 px-8 rounded-2xl text-white bg-[#22635A] font-sans text-lg font-medium flex flex-row xl:flex-col gap-1">
43+
<div className="whitespace-nowrap">{label}</div>
44+
<div className="whitespace-nowrap">
45+
{generateCountDownLabel(days, hours, minutes)}
46+
</div>
47+
</div>
48+
);
49+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useApplication } from "../../../../../projects/hooks/useApplication";
2+
import { useDataLayer } from "data-layer";
3+
import { useEffect, useMemo, useState } from "react";
4+
import { DonationPeriodResult, TimeRemaining } from "../types";
5+
import { calculateDonationPeriod, isValidStringDate } from "../utils";
6+
7+
interface Params {
8+
chainId: number;
9+
roundId: string;
10+
applicationId: string;
11+
refreshInterval?: number;
12+
}
13+
14+
interface Result {
15+
isDonationPeriod?: boolean;
16+
timeToDonationStart?: TimeRemaining;
17+
timeToDonationEnd?: TimeRemaining;
18+
}
19+
20+
// Returns undefined if the application is not stakable or if the donation period is not valid
21+
export const useDonationPeriod = ({
22+
chainId,
23+
roundId,
24+
applicationId,
25+
refreshInterval = 5 * 60 * 1000,
26+
}: Params): Result => {
27+
const dataLayer = useDataLayer();
28+
const { data: application } = useApplication(
29+
{
30+
chainId,
31+
roundId,
32+
applicationId,
33+
},
34+
dataLayer
35+
);
36+
37+
const hasValidDonationDates = useMemo(() => {
38+
if (!application) return false;
39+
const { donationsStartTime, donationsEndTime } = application?.round ?? {};
40+
return (
41+
isValidStringDate(donationsStartTime) &&
42+
isValidStringDate(donationsEndTime)
43+
);
44+
}, [application]);
45+
46+
const [currentTime, setCurrentTime] = useState(new Date());
47+
48+
useEffect(() => {
49+
const interval = setInterval(() => {
50+
if (hasValidDonationDates) setCurrentTime(new Date());
51+
}, refreshInterval);
52+
53+
return () => clearInterval(interval);
54+
}, [hasValidDonationDates, refreshInterval]);
55+
56+
const { isDonationPeriod, timeToDonationStart, timeToDonationEnd } =
57+
useMemo<DonationPeriodResult>(
58+
() =>
59+
calculateDonationPeriod({
60+
application,
61+
currentTime,
62+
hasValidDonationDates,
63+
}),
64+
[application, currentTime, hasValidDonationDates]
65+
);
66+
return { isDonationPeriod, timeToDonationStart, timeToDonationEnd };
67+
};

packages/grant-explorer/src/features/round/ViewProjectDetails/components/StakingBannerAndModal/hooks/useIsDonationPeriod.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

packages/grant-explorer/src/features/round/ViewProjectDetails/components/StakingBannerAndModal/hooks/useIsStakable.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useMemo } from "react";
2-
import { useIsDonationPeriod } from "./useIsDonationPeriod";
32

43
// TODO: either from metadata or from env value
54
// ONLY GITCOIN ROUNDS OF GG23
@@ -21,24 +20,15 @@ const STAKABLE_ROUNDS: Array<{ chainId: number; roundId: string }> = [
2120
export const useIsStakable = ({
2221
chainId,
2322
roundId,
24-
applicationId,
2523
}: {
2624
chainId: number;
2725
roundId: string;
28-
applicationId: string;
2926
}) => {
30-
const isDonationPeriod = useIsDonationPeriod({
31-
chainId,
32-
roundId,
33-
applicationId,
34-
});
35-
3627
const isStakable = useMemo(() => {
37-
if (!isDonationPeriod) return false;
3828
return STAKABLE_ROUNDS.some(
3929
(round) => round.chainId === chainId && round.roundId === roundId
4030
);
41-
}, [isDonationPeriod, chainId, roundId]);
31+
}, [chainId, roundId]);
4232

4333
return isStakable;
4434
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface TimeRemaining {
2+
days: number;
3+
hours: number;
4+
minutes: number;
5+
seconds: number;
6+
totalMilliseconds: number;
7+
}
8+
9+
export type DonationPeriodResult = {
10+
isDonationPeriod?: boolean;
11+
timeToDonationStart?: TimeRemaining;
12+
timeToDonationEnd?: TimeRemaining;
13+
};

0 commit comments

Comments
 (0)