Skip to content

Commit ac10cfb

Browse files
CopilotTechQuery
andauthored
refactor: extract Countdown as independent class component, simplify Hero and [id].tsx
Agent-Logs-Url: https://github.com/Open-Source-Bazaar/Open-Source-Bazaar.github.io/sessions/7cd3ee7e-073a-494f-b0a1-a5eca022d3e2 Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
1 parent a76172e commit ac10cfb

4 files changed

Lines changed: 135 additions & 167 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { computed, observable } from 'mobx';
2+
import { observer } from 'mobx-react';
3+
import { Component } from 'react';
4+
5+
import styles from './Hero.module.less';
6+
7+
export interface CountdownProps {
8+
countdownTo?: string;
9+
label?: string;
10+
unitLabels: string[];
11+
}
12+
13+
@observer
14+
export class Countdown extends Component<CountdownProps> {
15+
@observable
16+
accessor rest: number | null = null;
17+
18+
private timer?: number;
19+
20+
private get target() {
21+
const { countdownTo } = this.props;
22+
const value = countdownTo ? new Date(countdownTo).getTime() : NaN;
23+
24+
return Number.isFinite(value) ? value : NaN;
25+
}
26+
27+
@computed
28+
get sections() {
29+
const { rest } = this;
30+
31+
if (rest === null) return ['--', '--', '--', '--'];
32+
33+
const totalSeconds = Math.floor(Math.max(0, rest) / 1000);
34+
const days = Math.floor(totalSeconds / 86400);
35+
const hours = Math.floor((totalSeconds % 86400) / 3600);
36+
const minutes = Math.floor((totalSeconds % 3600) / 60);
37+
const seconds = totalSeconds % 60;
38+
39+
return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0'));
40+
}
41+
42+
tick = () => {
43+
this.rest = Math.max(0, this.target - Date.now());
44+
};
45+
46+
componentDidMount() {
47+
if (Number.isFinite(this.target)) {
48+
this.tick();
49+
this.timer = window.setInterval(this.tick, 1000);
50+
}
51+
}
52+
53+
componentDidUpdate(prevProps: CountdownProps) {
54+
if (prevProps.countdownTo !== this.props.countdownTo) {
55+
if (this.timer) {
56+
window.clearInterval(this.timer);
57+
this.timer = undefined;
58+
}
59+
60+
this.rest = null;
61+
62+
if (Number.isFinite(this.target)) {
63+
this.tick();
64+
this.timer = window.setInterval(this.tick, 1000);
65+
}
66+
}
67+
}
68+
69+
componentWillUnmount() {
70+
if (this.timer) window.clearInterval(this.timer);
71+
}
72+
73+
render() {
74+
const { label, unitLabels } = this.props;
75+
const { sections } = this;
76+
77+
return (
78+
<div className={styles.countdownWrap}>
79+
{label && <p className={`${styles.countdownLabel} m-0`}>{label}</p>}
80+
81+
<ol className={`list-unstyled ${styles.countdownGrid} m-0`}>
82+
{sections.map((value, index) => (
83+
<li
84+
key={`${index}-${unitLabels[index]}`}
85+
className={`${styles.countdownCell} d-flex flex-column justify-content-center align-items-center`}
86+
>
87+
<strong>{value}</strong>
88+
<span>{unitLabels[index]}</span>
89+
</li>
90+
))}
91+
</ol>
92+
</div>
93+
);
94+
}
95+
}

components/Activity/Hackathon/Hero.tsx

Lines changed: 32 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { computed, observable } from 'mobx';
21
import { TableCellValue } from 'mobx-lark';
3-
import { observer } from 'mobx-react';
4-
import { Component, FC } from 'react';
2+
import { FC } from 'react';
53
import { Container } from 'react-bootstrap';
64

75
import { LarkImage } from '../../LarkImage';
6+
import { Countdown } from './Countdown';
87
import styles from './Hero.module.less';
98

109
export type HackathonHeroNavItem = Record<'label' | 'href', string>;
@@ -91,91 +90,29 @@ const splitHeroTitle = (name: string, subtitle: string) => {
9190
};
9291
};
9392

94-
@observer
95-
export class HackathonHero extends Component<HackathonHeroProps> {
96-
@observable
97-
accessor rest: number | null = null;
98-
99-
private timer?: number;
100-
101-
private get target() {
102-
const { countdownTo } = this.props;
103-
const value = countdownTo ? new Date(countdownTo).getTime() : NaN;
104-
105-
return Number.isFinite(value) ? value : NaN;
106-
}
107-
108-
@computed
109-
get countdown(): string[] {
110-
const { rest } = this;
111-
112-
if (rest === null) return ['--', '--', '--', '--'];
113-
114-
const totalSeconds = Math.floor(Math.max(0, rest) / 1000);
115-
const days = Math.floor(totalSeconds / 86400);
116-
const hours = Math.floor((totalSeconds % 86400) / 3600);
117-
const minutes = Math.floor((totalSeconds % 3600) / 60);
118-
const seconds = totalSeconds % 60;
119-
120-
return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0'));
121-
}
122-
123-
tick = () => {
124-
this.rest = Math.max(0, this.target - Date.now());
125-
};
126-
127-
componentDidMount() {
128-
if (Number.isFinite(this.target)) {
129-
this.tick();
130-
this.timer = window.setInterval(this.tick, 1000);
131-
}
132-
}
133-
134-
componentDidUpdate(prevProps: HackathonHeroProps) {
135-
if (prevProps.countdownTo !== this.props.countdownTo) {
136-
if (this.timer) {
137-
window.clearInterval(this.timer);
138-
this.timer = undefined;
139-
}
140-
141-
this.rest = null;
142-
143-
if (Number.isFinite(this.target)) {
144-
this.tick();
145-
this.timer = window.setInterval(this.tick, 1000);
146-
}
147-
}
148-
}
149-
150-
componentWillUnmount() {
151-
if (this.timer) window.clearInterval(this.timer);
152-
}
153-
154-
render() {
155-
const {
156-
badges,
157-
bottomCard,
158-
chips,
159-
countdownLabel,
160-
countdownUnitLabels,
161-
countdownTo,
162-
description,
163-
image,
164-
imageFallback,
165-
locationText,
166-
name,
167-
navigation,
168-
primaryAction,
169-
secondaryAction,
170-
subtitle,
171-
topCard,
172-
visualChip,
173-
visualCopy,
174-
visualKicker,
175-
visualTitle,
176-
} = this.props;
177-
const { countdown } = this;
178-
const title = splitHeroTitle(name, subtitle);
93+
export const HackathonHero: FC<HackathonHeroProps> = ({
94+
badges,
95+
bottomCard,
96+
chips,
97+
countdownLabel,
98+
countdownUnitLabels,
99+
countdownTo,
100+
description,
101+
image,
102+
imageFallback,
103+
locationText,
104+
name,
105+
navigation,
106+
primaryAction,
107+
secondaryAction,
108+
subtitle,
109+
topCard,
110+
visualChip,
111+
visualCopy,
112+
visualKicker,
113+
visualTitle,
114+
}) => {
115+
const title = splitHeroTitle(name, subtitle);
179116

180117
return (
181118
<section id="top" className={styles.hero}>
@@ -224,23 +161,11 @@ export class HackathonHero extends Component<HackathonHeroProps> {
224161
<p className={styles.description}>{description}</p>
225162

226163
{countdownTo && (
227-
<div className={styles.countdownWrap}>
228-
{countdownLabel && (
229-
<p className={`${styles.countdownLabel} m-0`}>{countdownLabel}</p>
230-
)}
231-
232-
<ol className={`list-unstyled ${styles.countdownGrid} m-0`}>
233-
{countdown.map((value, index) => (
234-
<li
235-
key={`${index}-${countdownUnitLabels[index]}`}
236-
className={`${styles.countdownCell} d-flex flex-column justify-content-center align-items-center`}
237-
>
238-
<strong>{value}</strong>
239-
<span>{countdownUnitLabels[index]}</span>
240-
</li>
241-
))}
242-
</ol>
243-
</div>
164+
<Countdown
165+
countdownTo={countdownTo}
166+
label={countdownLabel}
167+
unitLabels={countdownUnitLabels}
168+
/>
244169
)}
245170

246171
<nav className="d-flex flex-wrap gap-2 gap-md-3" aria-label={subtitle}>
@@ -295,6 +220,5 @@ export class HackathonHero extends Component<HackathonHeroProps> {
295220
</div>
296221
</Container>
297222
</section>
298-
);
299-
}
300-
}
223+
);
224+
};

components/Activity/Hackathon/LiveCountdownStore.ts

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

pages/hackathon/[id].tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TableCellLocation, TableFormView } from 'mobx-lark';
22
import { observer } from 'mobx-react';
33
import { cache, compose, errorLogger } from 'next-ssr-middleware';
4-
import { FC, useContext, useEffect, useState } from 'react';
4+
import { FC, useContext } from 'react';
55

66
import {
77
HackathonActionHub,
@@ -28,7 +28,6 @@ import {
2828
} from '../../components/Activity/Hackathon/constant';
2929
import { HackathonFAQ } from '../../components/Activity/Hackathon/FAQ';
3030
import { HackathonHero } from '../../components/Activity/Hackathon/Hero';
31-
import { LiveCountdownStore } from '../../components/Activity/Hackathon/LiveCountdownStore';
3231
import { HackathonOverview } from '../../components/Activity/Hackathon/Overview';
3332
import { HackathonParticipants } from '../../components/Activity/Hackathon/Participants';
3433
import { HackathonResources } from '../../components/Activity/Hackathon/Resources';
@@ -44,6 +43,7 @@ import {
4443
isPublicForm,
4544
normalizeAgendaType,
4645
previewText,
46+
resolveCountdownState,
4747
timeOf,
4848
} from '../../components/Activity/Hackathon/utility';
4949
import { PageHead } from '../../components/Layout/PageHead';
@@ -189,15 +189,12 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
189189
};
190190
})
191191
.filter(({ date, label }) => Boolean(date && label));
192-
const [countdownStore] = useState(() => new LiveCountdownStore(agendaItems, startTime, endTime));
193-
194-
useEffect(() => {
195-
countdownStore.tick();
196-
197-
return () => countdownStore.dispose();
198-
}, [countdownStore]);
199-
200-
const { nextItem: nextAgendaItem, countdownTo } = countdownStore.countdownState;
192+
const { nextItem: nextAgendaItem, countdownTo } = resolveCountdownState(
193+
agendaItems,
194+
Date.now(),
195+
startTime,
196+
endTime,
197+
);
201198
const countdownLabel = nextAgendaItem
202199
? agendaTypeLabelOf(nextAgendaItem.type, t, t('agenda'))
203200
: t('event_duration');

0 commit comments

Comments
 (0)