Skip to content

Commit 2be0198

Browse files
committed
contest-tile: add support for rolling triage
1 parent 9f1fe7c commit 2be0198

File tree

8 files changed

+609
-42
lines changed

8 files changed

+609
-42
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/ContestTile/CompactTemplate.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import clsx from "clsx";
33
import { BountyTileData, ContestSchedule, ContestTileData, ContestTileProps, ContestTileVariant } from "./ContestTile.types";
44
import { Status, TagSize, TagVariant } from '../types';
55
import { ContestStatus } from '../ContestStatus';
6-
import { Countdown } from './ContestTile';
6+
import { ContestCountdown } from './ContestTile';
77
import { getDates } from '../../utils/time';
88
import { Tag } from '../Tag';
99
import { Icon } from '../Icon';
@@ -28,7 +28,7 @@ export default function CompactTemplate({
2828
c4contesttile: true,
2929
compact: true
3030
});
31-
31+
3232
return <div className={clsx("c4tilewrapper", variantClasses)}>
3333
<div id={htmlId ?? undefined} className={clsx(variantClasses, tileClasses)}>
3434
<div className="container--inner compact-content">
@@ -82,15 +82,15 @@ const IsContest = ({title, isDarkTile = true, contestData, sponsorUrl, sponsorIm
8282
}
8383

8484
if (startDate && endDate) {
85-
const newTimelineObject = getDates(contestData.startDate, contestData.endDate);
85+
const newTimelineObject = getDates(contestData.startDate, contestData.endDate, contestData.cohorts);
8686
setContestTimelineObject(newTimelineObject);
8787
}
8888
}
8989
}, [contestData]);
9090

9191
useEffect(() => {
9292
if (contestData && startDate && endDate) {
93-
const newTimelineObject = getDates(startDate, endDate);
93+
const newTimelineObject = getDates(startDate, endDate, contestData.cohorts);
9494
setContestTimelineObject(newTimelineObject);
9595
}
9696
}, [contestData])
@@ -104,14 +104,12 @@ const IsContest = ({title, isDarkTile = true, contestData, sponsorUrl, sponsorIm
104104
status={contestTimelineObject.contestStatus} />
105105
{contestTimelineObject.contestStatus !== Status.ENDED && (
106106
<div className="timer">
107-
<Countdown
108-
start={startDate}
109-
end={endDate}
107+
<ContestCountdown
108+
schedule={contestTimelineObject}
110109
updateContestStatus={updateContestTileStatus}
111-
text={contestTimelineObject.contestStatus === Status.UPCOMING ? 'Starts in ' : 'Ends in '}
112110
/>
113111
</div>
114-
)}
112+
)}
115113
</span>}
116114
<p className="type">
117115
{contestType === "Audit + mitigation review"
@@ -208,7 +206,7 @@ const IsBounty = ({title, isDarkTile = true, bountyData, sponsorUrl, sponsorImag
208206
break;
209207
}
210208
}
211-
209+
212210
return (
213211
<div className="body--bounty">
214212
<header>

src/lib/ContestTile/ContestTile.stories.tsx

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { Fragment } from "react";
2+
import { addDays, subDays } from "date-fns";
23
import { ContestTile } from "./ContestTile";
34
import { Meta, StoryObj } from "@storybook/react";
45
import { CodingLanguage, ContestEcosystem, ContestTileVariant } from "./ContestTile.types";
@@ -36,6 +37,7 @@ const defaultArgs = {
3637
htmlId: "",
3738
contestData: {
3839
codeAccess: "public",
40+
cohorts: [],
3941
contestType: "Open Audit",
4042
isUserCertified: false,
4143
contestId: 321,
@@ -75,6 +77,25 @@ export const ContestTileUpcoming: Story = (args) => {
7577
</Fragment>
7678
};
7779

80+
export const ContestTileUpcomingRollingTriage: Story = (args) => {
81+
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
82+
83+
return <Fragment>
84+
<ContestTile
85+
{...args}
86+
variant={isDark ? ContestTileVariant.DARK : ContestTileVariant.LIGHT}
87+
startDate={new Date(args.contestData.startDate).toISOString()}
88+
endDate={new Date(args.contestData.endDate).toISOString()}
89+
/>
90+
<ContestTile
91+
{...args}
92+
variant={isDark ? ContestTileVariant.COMPACT_DARK : ContestTileVariant.COMPACT_LIGHT }
93+
startDate={new Date(args.contestData.startDate).toISOString()}
94+
endDate={new Date(args.contestData.endDate).toISOString()}
95+
/>
96+
</Fragment>
97+
};
98+
7899
export const ContestTileLive: Story = (args) => {
79100
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
80101

@@ -94,6 +115,61 @@ export const ContestTileLive: Story = (args) => {
94115
</Fragment>
95116
};
96117

118+
export const ContestTileLiveCohort1: Story = (args) => {
119+
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
120+
121+
return <Fragment>
122+
<ContestTile
123+
{...args}
124+
variant={isDark ? ContestTileVariant.DARK : ContestTileVariant.LIGHT}
125+
startDate={new Date(args.contestData.startDate).toISOString()}
126+
endDate={new Date(args.contestData.endDate).toISOString()}
127+
/>
128+
<ContestTile
129+
{...args}
130+
variant={isDark ? ContestTileVariant.COMPACT_DARK : ContestTileVariant.COMPACT_LIGHT }
131+
startDate={new Date(args.contestData.startDate).toISOString()}
132+
endDate={new Date(args.contestData.endDate).toISOString()}
133+
/>
134+
</Fragment>
135+
};
136+
export const ContestTileLivePreCohort2: Story = (args) => {
137+
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
138+
139+
return <Fragment>
140+
<ContestTile
141+
{...args}
142+
variant={isDark ? ContestTileVariant.DARK : ContestTileVariant.LIGHT}
143+
startDate={new Date(args.contestData.startDate).toISOString()}
144+
endDate={new Date(args.contestData.endDate).toISOString()}
145+
/>
146+
<ContestTile
147+
{...args}
148+
variant={isDark ? ContestTileVariant.COMPACT_DARK : ContestTileVariant.COMPACT_LIGHT }
149+
startDate={new Date(args.contestData.startDate).toISOString()}
150+
endDate={new Date(args.contestData.endDate).toISOString()}
151+
/>
152+
</Fragment>
153+
};
154+
export const ContestTileLiveCohort3: Story = (args) => {
155+
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
156+
157+
return <Fragment>
158+
<ContestTile
159+
{...args}
160+
variant={isDark ? ContestTileVariant.DARK : ContestTileVariant.LIGHT}
161+
startDate={new Date(args.contestData.startDate).toISOString()}
162+
endDate={new Date(args.contestData.endDate).toISOString()}
163+
/>
164+
<ContestTile
165+
{...args}
166+
variant={isDark ? ContestTileVariant.COMPACT_DARK : ContestTileVariant.COMPACT_LIGHT }
167+
startDate={new Date(args.contestData.startDate).toISOString()}
168+
endDate={new Date(args.contestData.endDate).toISOString()}
169+
/>
170+
</Fragment>
171+
};
172+
97173
export const ContestTileEnded: Story = (args) => {
98174
const isDark = args.variant === ContestTileVariant.DARK || args.variant === ContestTileVariant.COMPACT_DARK;
99175

@@ -127,7 +203,11 @@ export const BountyTile: Story = (args) => {
127203
}
128204

129205
ContestTileUpcoming.parameters = parameters;
206+
ContestTileUpcomingRollingTriage.parameters = parameters;
130207
ContestTileLive.parameters = parameters;
208+
ContestTileLiveCohort1.parameters = parameters;
209+
ContestTileLivePreCohort2.parameters = parameters;
210+
ContestTileLiveCohort3.parameters = parameters;
131211
ContestTileEnded.parameters = parameters;
132212
BountyTile.parameters = parameters;
133213

@@ -139,6 +219,28 @@ ContestTileUpcoming.args = {
139219
endDate: "2030-07-21T18:00:00.000Z"
140220
}
141221
};
222+
ContestTileUpcomingRollingTriage.args = {
223+
...defaultArgs,
224+
contestData: {
225+
...defaultArgs.contestData,
226+
cohorts: [{
227+
name: "cohort-1",
228+
pauseTime: addDays(Date.now(), 6).toISOString(),
229+
resumeTime: null
230+
}, {
231+
name: "cohort-2",
232+
pauseTime: addDays(Date.now(), 13).toISOString(),
233+
resumeTime: addDays(Date.now(), 9).toISOString(),
234+
}, {
235+
name: "cohort-3",
236+
pauseTime: null,
237+
resumeTime: addDays(Date.now(), 16).toISOString(),
238+
}],
239+
startDate: addDays(Date.now(), 3).toISOString(),
240+
endDate: addDays(Date.now(), 20).toISOString(),
241+
}
242+
};
243+
142244

143245
ContestTileLive.args = {
144246
...defaultArgs,
@@ -148,6 +250,69 @@ ContestTileLive.args = {
148250
endDate: "2030-07-21T18:00:00.000Z"
149251
}
150252
};
253+
ContestTileLiveCohort1.args = {
254+
...defaultArgs,
255+
contestData: {
256+
...defaultArgs.contestData,
257+
cohorts: [{
258+
name: "cohort-1",
259+
pauseTime: addDays(Date.now(), 4).toISOString(),
260+
resumeTime: null
261+
}, {
262+
name: "cohort-2",
263+
pauseTime: addDays(Date.now(), 11).toISOString(),
264+
resumeTime: addDays(Date.now(), 7).toISOString(),
265+
}, {
266+
name: "cohort-3",
267+
pauseTime: null,
268+
resumeTime: addDays(Date.now(), 14).toISOString(),
269+
}],
270+
startDate: subDays(Date.now(), 1).toISOString(),
271+
endDate: addDays(Date.now(), 18).toISOString(),
272+
}
273+
};
274+
ContestTileLivePreCohort2.args = {
275+
...defaultArgs,
276+
contestData: {
277+
...defaultArgs.contestData,
278+
cohorts: [{
279+
name: "cohort-1",
280+
pauseTime: subDays(Date.now(), 1).toISOString(),
281+
resumeTime: null
282+
}, {
283+
name: "cohort-2",
284+
pauseTime: addDays(Date.now(), 6).toISOString(),
285+
resumeTime: addDays(Date.now(), 2).toISOString(),
286+
}, {
287+
name: "cohort-3",
288+
pauseTime: null,
289+
resumeTime: addDays(Date.now(), 9).toISOString(),
290+
}],
291+
startDate: subDays(Date.now(), 6).toISOString(),
292+
endDate: addDays(Date.now(), 16).toISOString(),
293+
}
294+
};
295+
ContestTileLiveCohort3.args = {
296+
...defaultArgs,
297+
contestData: {
298+
...defaultArgs.contestData,
299+
cohorts: [{
300+
name: "cohort-1",
301+
pauseTime: subDays(Date.now(), 11).toISOString(),
302+
resumeTime: null
303+
}, {
304+
name: "cohort-2",
305+
pauseTime: subDays(Date.now(), 4).toISOString(),
306+
resumeTime: subDays(Date.now(), 8).toISOString(),
307+
}, {
308+
name: "cohort-3",
309+
pauseTime: null,
310+
resumeTime: subDays(Date.now(), 1).toISOString(),
311+
}],
312+
startDate: subDays(Date.now(), 16).toISOString(),
313+
endDate: addDays(Date.now(), 6).toISOString(),
314+
}
315+
};
151316

152317
ContestTileEnded.args = {
153318
...defaultArgs,

src/lib/ContestTile/ContestTile.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const Countdown = ({
5757
}
5858

5959
const countDown = useCallback(() => {
60-
const newTimer = getDates(start, end);
60+
const newTimer = getDates(start, end, []);
6161
const target = getCountdownTarget(newTimer);
6262
// Get total number of seconds remaining
6363
const totalSeconds = formatDistanceToNowStrict(target, {
@@ -78,7 +78,7 @@ export const Countdown = ({
7878
useEffect(() => {
7979
const timer = setInterval(
8080
() => {
81-
const newTimer = getDates(start, end);
81+
const newTimer = getDates(start, end, []);
8282
if (
8383
contestTimer &&
8484
(contestTimer.contestStatus !== newTimer.contestStatus ||
@@ -124,6 +124,35 @@ export const Countdown = ({
124124
);
125125
};
126126

127+
export const ContestCountdown = ({
128+
schedule,
129+
updateContestStatus
130+
}: {
131+
schedule: ContestSchedule,
132+
updateContestStatus: CountdownProps["updateContestStatus"]
133+
}) => {
134+
let text = "Ends in ";
135+
let start = schedule.start.toISOString();
136+
let end = schedule.end.toISOString();
137+
if (schedule.contestStatus === Status.UPCOMING) {
138+
text = "Starts in ";
139+
} else if (schedule.contestStatus === Status.LIVE) {
140+
if (schedule.resume && +schedule.resume >= Date.now()) {
141+
text = "Cohort resumes in ";
142+
start = schedule.resume.toISOString();
143+
} else if (schedule.pause && +schedule.pause >= Date.now()) {
144+
text = "Cohort pauses in ";
145+
end = schedule.pause.toISOString();
146+
}
147+
}
148+
return Countdown({
149+
start,
150+
end,
151+
text,
152+
updateContestStatus,
153+
});
154+
};
155+
127156
/**
128157
* A stylized Code4rena contest tile for displaying information pertaining to upcoming, live, and finalized contests.
129158
* This component has 4 available variants.

src/lib/ContestTile/ContestTile.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export type ContestEcosystem = "Algorand" | "Aptos" | "Blast" | "Cosmos" | "EVM"
1212

1313
export type CodingLanguage = "Cairo" | "GO" | "HUFF" | "Ink" | "Move" | "Noir" | "Other" | "Rain" | "Rust" | "Rust evm" | "Solidity" | "Vyper" | "Yul";
1414

15+
export interface ContestCohort {
16+
resumeTime: string | null;
17+
pauseTime: string | null;
18+
name: string;
19+
}
20+
1521
export interface ContestTileProps {
1622
/** An html `id` for the contest tile's wrapping div. */
1723
htmlId?: string;
@@ -53,6 +59,8 @@ export interface BountyTileData {
5359
export interface ContestTileData {
5460
/** String indicating required access for viewing contest. */
5561
codeAccess: string;
62+
/** Array of cohorts for Rolling Triage audits. Empty for normal audits */
63+
cohorts: ContestCohort[];
5664
/** String indicating a specific categorization for the current contest. */
5765
contestType?: string;
5866
/** Unique numerical identifier for the current contest. */
@@ -86,6 +94,10 @@ export interface ContestSchedule {
8694
botRaceStatus?: Status;
8795
start: Date;
8896
end: Date;
97+
/** The time the current cohort will pause. */
98+
pause: Date | null;
99+
/** The time the current cohort will resume. */
100+
resume: Date | null;
89101
botRaceEnd: Date;
90102
formattedStart: string;
91103
formattedEnd: string;

0 commit comments

Comments
 (0)