Skip to content
82 changes: 82 additions & 0 deletions components/Activity/Hackathon/Countdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { ObservedComponent, reaction } from 'mobx-react-helper';

import styles from './Hero.module.less';

export interface CountdownProps {
countdownTo?: string;
unitLabels: string[];
}

@observer
export class Countdown extends ObservedComponent<CountdownProps> {
@observable
accessor rest: number | null = null;

private timer?: number;

private get target() {
const { countdownTo } = this.observedProps;
const value = countdownTo ? new Date(countdownTo).getTime() : NaN;

return Number.isFinite(value) ? value : NaN;
}

@computed
get sections() {
const { rest } = this;

if (rest === null) return ['--', '--', '--', '--'];

const totalSeconds = Math.floor(Math.max(0, rest) / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;

return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0'));
}
Comment thread
TechQuery marked this conversation as resolved.
Outdated

tick = () => {
this.rest = Math.max(0, this.target - Date.now());
};

@reaction((_this: Countdown) => _this.observedProps.countdownTo)
componentDidMount() {
super.componentDidMount();

if (this.timer) {
window.clearInterval(this.timer);
this.timer = undefined;
}

this.tick();
this.timer = window.setInterval(this.tick, 1000);
}

componentWillUnmount() {
super.componentWillUnmount();

if (this.timer) window.clearInterval(this.timer);
}

render() {
const { unitLabels } = this.observedProps;
const { sections } = this;

return (
<ol className={`list-unstyled ${styles.countdownGrid} m-0`}>
{sections.map((value, index) => (
<li
key={`${index}-${unitLabels[index]}`}
className={`${styles.countdownCell} d-flex flex-column justify-content-center align-items-center`}
Comment thread
TechQuery marked this conversation as resolved.
Outdated
>
<strong>{value}</strong>
<span>{unitLabels[index]}</span>
</li>
))}
</ol>
);
}
}
49 changes: 3 additions & 46 deletions components/Activity/Hackathon/Hero.tsx
Comment thread
TechQuery marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TableCellValue } from 'mobx-lark';
import { FC, useEffect, useMemo, useState } from 'react';
import { FC } from 'react';
import { Container } from 'react-bootstrap';

import { LarkImage } from '../../LarkImage';
import { Countdown } from './Countdown';
import styles from './Hero.module.less';

export type HackathonHeroNavItem = Record<'label' | 'href', string>;
Expand Down Expand Up @@ -74,38 +75,6 @@ const FloatingCard: FC<{
</div>
);

const useCountdown = (countdownTo?: string) => {
const target = useMemo(() => {
const value = countdownTo ? new Date(countdownTo).getTime() : NaN;

return Number.isFinite(value) ? value : NaN;
}, [countdownTo]);
const [now, setNow] = useState<number | null>(null);

useEffect(() => {
if (!Number.isFinite(target)) return;

setNow(Date.now());

const timer = window.setInterval(() => setNow(Date.now()), 1000);

return () => window.clearInterval(timer);
}, [target]);

return useMemo(() => {
if (!Number.isFinite(target) || now === null) return ['--', '--', '--', '--'];

const rest = Math.max(0, target - now);
const totalSeconds = Math.floor(rest / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;

return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0'));
}, [now, target]);
};

const splitHeroTitle = (name: string, subtitle: string) => {
const segments = name.split(/\s+/).filter(Boolean);

Expand Down Expand Up @@ -143,7 +112,6 @@ export const HackathonHero: FC<HackathonHeroProps> = ({
visualKicker,
visualTitle,
}) => {
const countdown = useCountdown(countdownTo);
const title = splitHeroTitle(name, subtitle);

return (
Expand Down Expand Up @@ -197,18 +165,7 @@ export const HackathonHero: FC<HackathonHeroProps> = ({
{countdownLabel && (
<p className={`${styles.countdownLabel} m-0`}>{countdownLabel}</p>
)}

<ol className={`list-unstyled ${styles.countdownGrid} m-0`}>
{countdown.map((value, index) => (
<li
key={`${index}-${countdownUnitLabels[index]}`}
className={`${styles.countdownCell} d-flex flex-column justify-content-center align-items-center`}
>
<strong>{value}</strong>
<span>{countdownUnitLabels[index]}</span>
</li>
))}
</ol>
<Countdown countdownTo={countdownTo} unitLabels={countdownUnitLabels} />
</div>
)}

Expand Down
44 changes: 0 additions & 44 deletions components/Activity/Hackathon/useLiveCountdownState.ts

This file was deleted.

53 changes: 27 additions & 26 deletions pages/hackathon/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,10 @@ import {
HackathonActionHubLink,
} from '../../components/Activity/Hackathon/ActionHub';
import { HackathonAwards } from '../../components/Activity/Hackathon/Awards';
import { HackathonFAQ } from '../../components/Activity/Hackathon/FAQ';
import { HackathonHero } from '../../components/Activity/Hackathon/Hero';
import { HackathonOverview } from '../../components/Activity/Hackathon/Overview';
import { HackathonParticipants } from '../../components/Activity/Hackathon/Participants';
import { HackathonResources } from '../../components/Activity/Hackathon/Resources';
import { HackathonSchedule } from '../../components/Activity/Hackathon/Schedule';
import { useLiveCountdownState } from '../../components/Activity/Hackathon/useLiveCountdownState';
import { PageHead } from '../../components/Layout/PageHead';
import { Activity, ActivityModel } from '../../models/Activity';
import {
Agenda,
AgendaModel,
Organization,
OrganizationModel,
Person,
PersonModel,
Prize,
PrizeModel,
Project,
ProjectModel,
Template,
TemplateModel,
} from '../../models/Hackathon';
import { I18nContext } from '../../models/Translation';
import {
buildCountdownUnitLabels,
buildFAQItems,
buildFormSectionMeta,
buildHighlightCards,
buildJudgingCriteria,
buildOrganizationItems,
Expand All @@ -46,10 +23,15 @@ import {
FormButtonBar,
FormGroupKey,
FormGroupView,
buildFormSectionMeta,
heroNavigation,
RequiredTableKeys,
} from '../../components/Activity/Hackathon/constant';
import { HackathonFAQ } from '../../components/Activity/Hackathon/FAQ';
import { HackathonHero } from '../../components/Activity/Hackathon/Hero';
import { HackathonOverview } from '../../components/Activity/Hackathon/Overview';
import { HackathonParticipants } from '../../components/Activity/Hackathon/Participants';
import { HackathonResources } from '../../components/Activity/Hackathon/Resources';
import { HackathonSchedule } from '../../components/Activity/Hackathon/Schedule';
import {
agendaTypeLabelOf,
compactDateKeyOf,
Expand All @@ -61,8 +43,26 @@ import {
isPublicForm,
normalizeAgendaType,
previewText,
resolveCountdownState,
timeOf,
} from '../../components/Activity/Hackathon/utility';
import { PageHead } from '../../components/Layout/PageHead';
import { Activity, ActivityModel } from '../../models/Activity';
import {
Agenda,
AgendaModel,
Organization,
OrganizationModel,
Person,
PersonModel,
Prize,
PrizeModel,
Project,
ProjectModel,
Template,
TemplateModel,
} from '../../models/Hackathon';
import { I18nContext } from '../../models/Translation';

interface HackathonDetailProps {
activity: Activity;
Expand Down Expand Up @@ -189,8 +189,9 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
};
})
.filter(({ date, label }) => Boolean(date && label));
const { nextItem: nextAgendaItem, countdownTo } = useLiveCountdownState(
const { nextItem: nextAgendaItem, countdownTo } = resolveCountdownState(
agendaItems,
Date.now(),
startTime,
endTime,
);
Expand Down
Loading