Skip to content

Commit b160b03

Browse files
CopilotTechQuery
andauthored
[refactor] extract independent Countdown class component from Hackathon page with MobX (#69)
Co-authored-by: TechQuery <shiy2008@gmail.com>
1 parent 760d78e commit b160b03

8 files changed

Lines changed: 275 additions & 207 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@import './theme.less';
2+
3+
.wrap {
4+
display: grid;
5+
gap: 0.75rem;
6+
max-width: 520px;
7+
}
8+
9+
.label {
10+
margin: 0;
11+
color: @muted;
12+
font-size: 0.72rem;
13+
font-family: @heading;
14+
letter-spacing: 0.1em;
15+
text-transform: uppercase;
16+
}
17+
18+
.grid {
19+
display: grid;
20+
grid-template-columns: repeat(4, minmax(0, 1fr));
21+
gap: 0.8rem;
22+
23+
li {
24+
gap: 0.7rem;
25+
box-shadow:
26+
inset 0 0 0 1px rgba(255, 255, 255, 0.03),
27+
0 0 26px rgba(44, 232, 255, 0.08);
28+
border: 1px solid rgba(44, 232, 255, 0.26);
29+
border-radius: 18px;
30+
background: linear-gradient(180deg, rgba(44, 232, 255, 0.08), rgba(44, 232, 255, 0.03));
31+
min-height: 120px;
32+
33+
strong {
34+
color: #fff;
35+
font-size: clamp(2.3rem, 4vw, 3.8rem);
36+
line-height: 1;
37+
font-family: @heading;
38+
letter-spacing: 0.08em;
39+
}
40+
41+
span {
42+
color: rgba(255, 255, 255, 0.72);
43+
font-size: 0.82rem;
44+
font-family: @heading;
45+
letter-spacing: 0.2em;
46+
text-transform: uppercase;
47+
}
48+
}
49+
}
50+
51+
@media (max-width: 767px) {
52+
.grid {
53+
grid-template-columns: repeat(2, minmax(0, 1fr));
54+
55+
li {
56+
min-height: 96px;
57+
58+
strong {
59+
font-size: 2rem;
60+
}
61+
}
62+
}
63+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { TableCellValue } from 'mobx-lark';
2+
import { observer } from 'mobx-react';
3+
import { FC, useContext, useState } from 'react';
4+
5+
import { Agenda } from '../../../models/Hackathon';
6+
import { I18nContext } from '../../../models/Translation';
7+
import { Countdown, TimeUnit } from '../../Base/Countdown';
8+
import styles from './AgendaCountdown.module.less';
9+
import { agendaTypeLabelOf, resolveCountdownState } from './utility';
10+
11+
export interface AgendaCountdownProps {
12+
agendaItems: Agenda[];
13+
endTime?: TableCellValue;
14+
startTime?: TableCellValue;
15+
units: TimeUnit[];
16+
}
17+
18+
export const AgendaCountdown: FC<AgendaCountdownProps> = observer(
19+
({ agendaItems, endTime, startTime, units }) => {
20+
const { t } = useContext(I18nContext);
21+
const [referenceTime, setReferenceTime] = useState(Date.now());
22+
const { nextItem: nextAgendaItem, countdownTo } = resolveCountdownState(
23+
agendaItems,
24+
referenceTime,
25+
startTime,
26+
endTime,
27+
);
28+
29+
if (!countdownTo) return null;
30+
31+
const countdownLabel = nextAgendaItem
32+
? agendaTypeLabelOf(nextAgendaItem.type, t, t('agenda'))
33+
: t('event_duration');
34+
35+
return (
36+
<div className={styles.wrap}>
37+
{countdownLabel && <p className={styles.label}>{countdownLabel}</p>}
38+
39+
<Countdown
40+
className={styles.grid}
41+
endTime={countdownTo}
42+
onEnd={() => setReferenceTime(Date.now())}
43+
units={units}
44+
/>
45+
</div>
46+
);
47+
},
48+
);

components/Activity/Hackathon/Hero.module.less

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -179,54 +179,10 @@
179179
}
180180
}
181181

182-
.countdownWrap {
183-
display: grid;
184-
gap: 0.75rem;
185-
max-width: 520px;
186-
}
187-
188-
.countdownLabel {
189-
color: @muted;
190-
font-size: 0.72rem;
191-
font-family: @heading;
192-
letter-spacing: 0.1em;
193-
text-transform: uppercase;
194-
}
195-
196-
.countdownGrid {
197-
display: grid;
198-
grid-template-columns: repeat(4, minmax(0, 1fr));
199-
gap: 0.8rem;
200-
}
201-
202-
.countdownCell {
203-
gap: 0.7rem;
204-
box-shadow:
205-
inset 0 0 0 1px rgba(255, 255, 255, 0.03),
206-
0 0 26px rgba(44, 232, 255, 0.08);
207-
border: 1px solid rgba(44, 232, 255, 0.26);
208-
border-radius: 18px;
209-
background: linear-gradient(180deg, rgba(44, 232, 255, 0.08), rgba(44, 232, 255, 0.03));
210-
min-height: 120px;
211-
212-
strong {
213-
color: #fff;
214-
font-size: clamp(2.3rem, 4vw, 3.8rem);
215-
line-height: 1;
216-
font-family: @heading;
217-
letter-spacing: 0.08em;
218-
}
219-
220-
span {
221-
color: rgba(255, 255, 255, 0.72);
222-
font-size: 0.82rem;
223-
font-family: @heading;
224-
letter-spacing: 0.2em;
225-
text-transform: uppercase;
226-
}
227-
}
228-
229182
.actionButton {
183+
// prettier-ignore
184+
.button-primary();
185+
230186
box-shadow: 0 0 28px rgba(44, 232, 255, 0.14);
231187
border-color: rgba(44, 232, 255, 0.48);
232188
background: rgba(44, 232, 255, 0.08);
@@ -240,6 +196,9 @@
240196
}
241197

242198
.actionButtonGhost {
199+
// prettier-ignore
200+
.button-ghost();
201+
243202
border-color: rgba(255, 255, 255, 0.16);
244203
background: rgba(255, 255, 255, 0.03);
245204
color: rgba(255, 255, 255, 0.82);
@@ -442,16 +401,4 @@
442401
.heroBadge {
443402
font-size: 0.72rem;
444403
}
445-
446-
.countdownGrid {
447-
grid-template-columns: repeat(2, minmax(0, 1fr));
448-
}
449-
450-
.countdownCell {
451-
min-height: 96px;
452-
453-
strong {
454-
font-size: 2rem;
455-
}
456-
}
457404
}

components/Activity/Hackathon/Hero.tsx

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { TableCellValue } from 'mobx-lark';
2-
import { FC, useEffect, useMemo, useState } from 'react';
2+
import { FC } from 'react';
33
import { Container } from 'react-bootstrap';
44

5+
import { Agenda } from '../../../models/Hackathon';
56
import { LarkImage } from '../../LarkImage';
7+
import { AgendaCountdown } from './AgendaCountdown';
8+
import { TimeUnit } from '../../Base/Countdown';
69
import styles from './Hero.module.less';
710

811
export type HackathonHeroNavItem = Record<'label' | 'href', string>;
@@ -22,13 +25,14 @@ export interface HackathonHeroProps extends Record<
2225
| 'imageFallback',
2326
string
2427
> {
28+
agendaItems: Agenda[];
2529
badges: string[];
2630
bottomCard?: HackathonHeroCard;
2731
chips?: string[];
28-
countdownLabel?: string;
29-
countdownUnitLabels: string[];
30-
countdownTo?: string;
32+
countdownUnits: TimeUnit[];
33+
endTime?: TableCellValue;
3134
image?: TableCellValue;
35+
startTime?: TableCellValue;
3236
navigation: HackathonHeroNavItem[];
3337
primaryAction: HackathonHeroAction;
3438
secondaryAction: HackathonHeroAction;
@@ -74,38 +78,6 @@ const FloatingCard: FC<{
7478
</div>
7579
);
7680

77-
const useCountdown = (countdownTo?: string) => {
78-
const target = useMemo(() => {
79-
const value = countdownTo ? new Date(countdownTo).getTime() : NaN;
80-
81-
return Number.isFinite(value) ? value : NaN;
82-
}, [countdownTo]);
83-
const [now, setNow] = useState<number | null>(null);
84-
85-
useEffect(() => {
86-
if (!Number.isFinite(target)) return;
87-
88-
setNow(Date.now());
89-
90-
const timer = window.setInterval(() => setNow(Date.now()), 1000);
91-
92-
return () => window.clearInterval(timer);
93-
}, [target]);
94-
95-
return useMemo(() => {
96-
if (!Number.isFinite(target) || now === null) return ['--', '--', '--', '--'];
97-
98-
const rest = Math.max(0, target - now);
99-
const totalSeconds = Math.floor(rest / 1000);
100-
const days = Math.floor(totalSeconds / 86400);
101-
const hours = Math.floor((totalSeconds % 86400) / 3600);
102-
const minutes = Math.floor((totalSeconds % 3600) / 60);
103-
const seconds = totalSeconds % 60;
104-
105-
return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0'));
106-
}, [now, target]);
107-
};
108-
10981
const splitHeroTitle = (name: string, subtitle: string) => {
11082
const segments = name.split(/\s+/).filter(Boolean);
11183

@@ -122,28 +94,28 @@ const splitHeroTitle = (name: string, subtitle: string) => {
12294
};
12395

12496
export const HackathonHero: FC<HackathonHeroProps> = ({
97+
agendaItems,
12598
badges,
12699
bottomCard,
127100
chips,
128-
countdownLabel,
129-
countdownUnitLabels,
130-
countdownTo,
101+
countdownUnits,
131102
description,
103+
endTime,
132104
image,
133105
imageFallback,
134106
locationText,
135107
name,
136108
navigation,
137109
primaryAction,
138110
secondaryAction,
111+
startTime,
139112
subtitle,
140113
topCard,
141114
visualChip,
142115
visualCopy,
143116
visualKicker,
144117
visualTitle,
145118
}) => {
146-
const countdown = useCountdown(countdownTo);
147119
const title = splitHeroTitle(name, subtitle);
148120

149121
return (
@@ -192,25 +164,12 @@ export const HackathonHero: FC<HackathonHeroProps> = ({
192164

193165
<p className={styles.description}>{description}</p>
194166

195-
{countdownTo && (
196-
<div className={styles.countdownWrap}>
197-
{countdownLabel && (
198-
<p className={`${styles.countdownLabel} m-0`}>{countdownLabel}</p>
199-
)}
200-
201-
<ol className={`list-unstyled ${styles.countdownGrid} m-0`}>
202-
{countdown.map((value, index) => (
203-
<li
204-
key={`${index}-${countdownUnitLabels[index]}`}
205-
className={`${styles.countdownCell} d-flex flex-column justify-content-center align-items-center`}
206-
>
207-
<strong>{value}</strong>
208-
<span>{countdownUnitLabels[index]}</span>
209-
</li>
210-
))}
211-
</ol>
212-
</div>
213-
)}
167+
<AgendaCountdown
168+
agendaItems={agendaItems}
169+
endTime={endTime}
170+
startTime={startTime}
171+
units={countdownUnits}
172+
/>
214173

215174
<nav className="d-flex flex-wrap gap-2 gap-md-3" aria-label={subtitle}>
216175
<HeroLink action={primaryAction} variant="primary" />

components/Activity/Hackathon/constant.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,6 @@ export const heroNavigation = ({ t }: typeof i18n) => [
9191
{ href: '#faq', label: t('common_questions') },
9292
];
9393

94-
export const buildCountdownUnitLabels = ({ t }: typeof i18n) => [
95-
t('countdown_days'),
96-
t('countdown_hours'),
97-
t('countdown_minutes'),
98-
t('countdown_seconds'),
99-
];
100-
10194
export const buildHighlightCards = (
10295
{ t }: typeof i18n,
10396
{

components/Activity/Hackathon/useLiveCountdownState.ts

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

0 commit comments

Comments
 (0)