diff --git a/packages/grant-explorer/src/features/api/utils.ts b/packages/grant-explorer/src/features/api/utils.ts index 3869ada4a..59f2bd853 100644 --- a/packages/grant-explorer/src/features/api/utils.ts +++ b/packages/grant-explorer/src/features/api/utils.ts @@ -77,25 +77,53 @@ export const pinToIPFS = (obj: IPFSObject) => { } }; -export const getDaysLeft = (fromNowToTimestampStr: string) => { +export interface TimeLeft { + days?: number; + hours?: number; + minutes?: number; + seconds?: number; +} + +export const getTimeLeft = (fromNowToTimestampStr: string): TimeLeft => { const targetTimestamp = Number(fromNowToTimestampStr); // Some timestamps are returned as overflowed (1.15e+77) // We parse these into undefined to show as "No end date" rather than make the date diff calculation if (targetTimestamp > Number.MAX_SAFE_INTEGER) { - return undefined; + return {}; } // TODO replace with differenceInCalendarDays from 'date-fns' const currentTimestampInSeconds = Math.floor(Date.now() / 1000); // current timestamp in seconds const secondsPerDay = 60 * 60 * 24; // number of seconds per day + const secondsPerHour = 60 * 60; // number of seconds per day + const secondsPerMinute = 60; const differenceInSeconds = targetTimestamp - currentTimestampInSeconds; - const differenceInDays = Math.floor(differenceInSeconds / secondsPerDay); - return differenceInDays; + const days = Math.floor(differenceInSeconds / secondsPerDay); + const hours = Math.floor(differenceInSeconds / secondsPerHour) % 24; // % 24 to substract total days + const minutes = Math.floor(differenceInSeconds / secondsPerMinute) % 60; // % 60 to substract total hours + const seconds = Math.floor(differenceInSeconds) % 60; // % 60 to substract total minutes + + return { days, hours, minutes, seconds }; +}; + +export const parseTimeLeftString = (timeLeft: TimeLeft): string => { + const { days = 0, hours = 0, minutes = 0 } = timeLeft; + + const daysString = days > 0 ? `${days} ${days === 1 ? "day" : "days"}, ` : ""; + const hoursString = + hours > 0 ? `${hours} ${hours === 1 ? "hour" : "hours"}, ` : ""; + const minutesString = + minutes > 0 ? `${minutes} ${minutes === 1 ? "minute" : "minutes"}` : ""; + + return `${daysString}${hoursString}${minutesString}`; }; +export const getDaysLeft = (fromNowToTimestampStr: string) => + getTimeLeft(fromNowToTimestampStr).days; + export const isDirectRound = (round: Round) => // @ts-expect-error support old rounds round.payoutStrategy.strategyName === ROUND_PAYOUT_DIRECT_OLD || diff --git a/packages/grant-explorer/src/features/common/styles.tsx b/packages/grant-explorer/src/features/common/styles.tsx index 192a98deb..e6d427ce8 100644 --- a/packages/grant-explorer/src/features/common/styles.tsx +++ b/packages/grant-explorer/src/features/common/styles.tsx @@ -71,6 +71,7 @@ const colorMap = { grey: "bg-grey-100", yellow: "bg-yellow-100", orange: "bg-orange-100", + rainbow: "bg-rainbow-gradient", } as const; const roundedMap = { diff --git a/packages/grant-explorer/src/features/round/ApplicationsCountdownBanner.tsx b/packages/grant-explorer/src/features/round/ApplicationsCountdownBanner.tsx new file mode 100644 index 000000000..49f7d249d --- /dev/null +++ b/packages/grant-explorer/src/features/round/ApplicationsCountdownBanner.tsx @@ -0,0 +1,95 @@ +import { Button } from "common/src/styles"; +import { TimeLeft, getTimeLeft, parseTimeLeftString } from "../api/utils"; + +type ApplicationPeriodStatus = + | "pre-application" + | "post-application" + | "during-application"; + +const generateCountdownString = ( + targetDate: Date | undefined +): string | undefined => { + if (!targetDate) return undefined; + + const targetDateString = Math.round(targetDate.getTime() / 1000).toString(); + const timeLeft: TimeLeft = getTimeLeft(targetDateString); + + const timeLeftString: string = parseTimeLeftString(timeLeft); + + return timeLeftString; +}; + +const generateBannerString = ( + status: ApplicationPeriodStatus, + targetDate: Date | undefined +): string => { + switch (status) { + case "pre-application": + return `Applications open in ${generateCountdownString(targetDate)}!`; + case "during-application": + return `Applications close in ${generateCountdownString(targetDate)}!`; + case "post-application": + return `Applications are closed`; + default: + throw new Error("Unknown ApplicationPeriodStatus"); + } +}; + +function ApplicationsCountdownBanner(props: { + startDate: Date; + endDate: Date; + applicationURL: string; +}) { + const { startDate, endDate, applicationURL } = props; + + let targetDate: Date | undefined = undefined; + let status: ApplicationPeriodStatus = "post-application"; + + const currentTime = new Date(); + + const isBeforeApplicationPeriod = currentTime < startDate; + const isDuringApplicationPeriod = + currentTime >= startDate && currentTime < endDate; + + if (isDuringApplicationPeriod) { + targetDate = endDate; + status = "during-application"; + } else if (isBeforeApplicationPeriod) { + targetDate = startDate; + status = "pre-application"; + } + + const bannerString = generateBannerString(status, targetDate); + + return ( +
{bannerString}
++
+ Apply
+
Donate
{round.roundMetadata?.quadraticFundingConfig?.matchingFundsAvailable.toLocaleString()} @@ -541,36 +553,42 @@ function AfterRoundStart(props: {
{round.roundMetadata?.eligibility?.description}
- - {isDirectRound(round) && isBeforeApplicationEndDate && ( -- {round.roundMetadata?.name} -
-- Application Period: - - - {formatLocalDateAsISOString(round.applicationsStartTime)} - - - ( {getLocalTime(round.applicationsStartTime)} ) - - - - - {!isInfiniteDate(round.applicationsEndTime) && ( - <> - - {formatLocalDateAsISOString(round.applicationsEndTime)} - - - {getLocalTime(round.applicationsEndTime)} - > - )} - {isInfiniteDate(round.applicationsEndTime) && ( - <> - No End Date - > - )} - -
-- Round Period: - - - {formatLocalDateAsISOString(round.roundStartTime)} - - - ( {getLocalTime(round.roundStartTime)} ) - - - - - {!isInfiniteDate(round.roundEndTime) && ( - <> - - {formatLocalDateAsISOString(round.roundEndTime)} - - - {getLocalTime(round.roundEndTime)} - > - )} - {isInfiniteDate(round.roundEndTime) && ( - <> - No End Date - > - )} - -
- {!isDirectRound(round) && ( -- Matching Funds Available: - - {" "} - - {round.roundMetadata?.quadraticFundingConfig?.matchingFundsAvailable.toLocaleString()} - - {tokenData?.code ?? "..."} - -
-- Matching Cap: - {round.roundMetadata?.quadraticFundingConfig - ?.matchingCapAmount ? ( - - {" "} - - { - round.roundMetadata?.quadraticFundingConfig - ?.matchingCapAmount - } - - {"%"} - - ) : ( - None - )} -
-- {round.roundMetadata?.eligibility.description} -
-- Round Eligibility -
-