Skip to content

Commit

Permalink
feat(explorer): show project list in preround page (#3507)
Browse files Browse the repository at this point in the history
* feat: implemented getTimeLeftutils and parseTimeLeft reusing the logic of getDaysLeft

* feat: implemented RoundStartCountdownBadge component

* feat: implemented ApplicationsCountdownBanner component

* feat: extended TW theme with rainbow gradiend background

* feat: hide AddToCart button in Project Details page before round start

* feat: implemented RoundPage for pre/during/post round, removed unused components

* test: updated tests with the changes made
  • Loading branch information
hussedev authored Jun 11, 2024
1 parent ed764a8 commit 8da3e2e
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 322 deletions.
36 changes: 32 additions & 4 deletions packages/grant-explorer/src/features/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
1 change: 1 addition & 0 deletions packages/grant-explorer/src/features/common/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center bg-grey-50 w-fit py-6 px-36 rounded-2xl">
<p>{bannerString}</p>
<ApplyButton status={status} applicationURL={applicationURL} />
</div>
);
}

const ApplyButton = (props: {
status: ApplicationPeriodStatus;
applicationURL: string;
testid?: string;
}) => {
const { status, applicationURL } = props;

return (
<Button
type="button"
onClick={() => window.open(applicationURL, "_blank")}
className="bg-orange-100 text-grey-500 mt-2 basis-full items-center justify-center shadow-sm text-sm rounded md:h-12"
data-testid={
status === "during-application"
? "apply-button"
: "view-requirements-button"
}
>
{status === "during-application" ? "Apply now!" : "Check requirements"}
</Button>
);
};

export default ApplicationsCountdownBanner;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { TimeLeft, getTimeLeft, parseTimeLeftString } from "../api/utils";
import { Badge } from "../common/styles";

function RoundStartCountdownBadge(props: { targetDate: Date }) {
const { targetDate } = props;

const targetDateString = Math.round(targetDate.getTime() / 1000).toString();

const timeLeft: TimeLeft = getTimeLeft(targetDateString);
const timeLeftString: string = parseTimeLeftString(timeLeft);

const badgeString = `Donations start in ${timeLeftString}`;

return (
<Badge
color="rainbow"
rounded="full"
className="flex-shrink-0 px-2.5 font-modern-era-bold"
data-testid="donations-countdown-badge"
>
{badgeString}
</Badge>
);
}

export default RoundStartCountdownBadge;
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ export default function ViewProjectDetails() {
? false
: round && round.roundEndTime <= currentTime);

const isBeforeRoundStartDate =
round &&
(isInfiniteDate(round.roundStartTime)
? false
: round && currentTime < round.roundStartTime);

const alloVersion = getAlloVersion();

useEffect(() => {
Expand All @@ -154,7 +160,8 @@ export default function ViewProjectDetails() {

const disableAddToCartButton =
(alloVersion === "allo-v2" && roundId.startsWith("0x")) ||
isAfterRoundEndDate;
isAfterRoundEndDate ||
isBeforeRoundStartDate;
const { projects, add, remove } = useCartStorage();

const isAlreadyInCart = projects.some(
Expand Down
Loading

0 comments on commit 8da3e2e

Please sign in to comment.