diff --git a/components/Activity/Hackathon/AgendaCountdown.module.less b/components/Activity/Hackathon/AgendaCountdown.module.less new file mode 100644 index 0000000..8d12cf9 --- /dev/null +++ b/components/Activity/Hackathon/AgendaCountdown.module.less @@ -0,0 +1,63 @@ +@import './theme.less'; + +.wrap { + display: grid; + gap: 0.75rem; + max-width: 520px; +} + +.label { + margin: 0; + color: @muted; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.8rem; + + li { + gap: 0.7rem; + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.03), + 0 0 26px rgba(44, 232, 255, 0.08); + border: 1px solid rgba(44, 232, 255, 0.26); + border-radius: 18px; + background: linear-gradient(180deg, rgba(44, 232, 255, 0.08), rgba(44, 232, 255, 0.03)); + min-height: 120px; + + strong { + color: #fff; + font-size: clamp(2.3rem, 4vw, 3.8rem); + line-height: 1; + font-family: @heading; + letter-spacing: 0.08em; + } + + span { + color: rgba(255, 255, 255, 0.72); + font-size: 0.82rem; + font-family: @heading; + letter-spacing: 0.2em; + text-transform: uppercase; + } + } +} + +@media (max-width: 767px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + li { + min-height: 96px; + + strong { + font-size: 2rem; + } + } + } +} diff --git a/components/Activity/Hackathon/AgendaCountdown.tsx b/components/Activity/Hackathon/AgendaCountdown.tsx new file mode 100644 index 0000000..7b69487 --- /dev/null +++ b/components/Activity/Hackathon/AgendaCountdown.tsx @@ -0,0 +1,48 @@ +import { TableCellValue } from 'mobx-lark'; +import { observer } from 'mobx-react'; +import { FC, useContext, useState } from 'react'; + +import { Agenda } from '../../../models/Hackathon'; +import { I18nContext } from '../../../models/Translation'; +import { Countdown, TimeUnit } from '../../Base/Countdown'; +import styles from './AgendaCountdown.module.less'; +import { agendaTypeLabelOf, resolveCountdownState } from './utility'; + +export interface AgendaCountdownProps { + agendaItems: Agenda[]; + endTime?: TableCellValue; + startTime?: TableCellValue; + units: TimeUnit[]; +} + +export const AgendaCountdown: FC = observer( + ({ agendaItems, endTime, startTime, units }) => { + const { t } = useContext(I18nContext); + const [referenceTime, setReferenceTime] = useState(Date.now()); + const { nextItem: nextAgendaItem, countdownTo } = resolveCountdownState( + agendaItems, + referenceTime, + startTime, + endTime, + ); + + if (!countdownTo) return null; + + const countdownLabel = nextAgendaItem + ? agendaTypeLabelOf(nextAgendaItem.type, t, t('agenda')) + : t('event_duration'); + + return ( +
+ {countdownLabel &&

{countdownLabel}

} + + setReferenceTime(Date.now())} + units={units} + /> +
+ ); + }, +); diff --git a/components/Activity/Hackathon/Hero.module.less b/components/Activity/Hackathon/Hero.module.less index c04735b..e32e4e4 100644 --- a/components/Activity/Hackathon/Hero.module.less +++ b/components/Activity/Hackathon/Hero.module.less @@ -179,54 +179,10 @@ } } -.countdownWrap { - display: grid; - gap: 0.75rem; - max-width: 520px; -} - -.countdownLabel { - color: @muted; - font-size: 0.72rem; - font-family: @heading; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.countdownGrid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 0.8rem; -} - -.countdownCell { - gap: 0.7rem; - box-shadow: - inset 0 0 0 1px rgba(255, 255, 255, 0.03), - 0 0 26px rgba(44, 232, 255, 0.08); - border: 1px solid rgba(44, 232, 255, 0.26); - border-radius: 18px; - background: linear-gradient(180deg, rgba(44, 232, 255, 0.08), rgba(44, 232, 255, 0.03)); - min-height: 120px; - - strong { - color: #fff; - font-size: clamp(2.3rem, 4vw, 3.8rem); - line-height: 1; - font-family: @heading; - letter-spacing: 0.08em; - } - - span { - color: rgba(255, 255, 255, 0.72); - font-size: 0.82rem; - font-family: @heading; - letter-spacing: 0.2em; - text-transform: uppercase; - } -} - .actionButton { + // prettier-ignore + .button-primary(); + box-shadow: 0 0 28px rgba(44, 232, 255, 0.14); border-color: rgba(44, 232, 255, 0.48); background: rgba(44, 232, 255, 0.08); @@ -240,6 +196,9 @@ } .actionButtonGhost { + // prettier-ignore + .button-ghost(); + border-color: rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.03); color: rgba(255, 255, 255, 0.82); @@ -442,16 +401,4 @@ .heroBadge { font-size: 0.72rem; } - - .countdownGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .countdownCell { - min-height: 96px; - - strong { - font-size: 2rem; - } - } } diff --git a/components/Activity/Hackathon/Hero.tsx b/components/Activity/Hackathon/Hero.tsx index 855aee7..60495fb 100644 --- a/components/Activity/Hackathon/Hero.tsx +++ b/components/Activity/Hackathon/Hero.tsx @@ -1,8 +1,11 @@ import { TableCellValue } from 'mobx-lark'; -import { FC, useEffect, useMemo, useState } from 'react'; +import { FC } from 'react'; import { Container } from 'react-bootstrap'; +import { Agenda } from '../../../models/Hackathon'; import { LarkImage } from '../../LarkImage'; +import { AgendaCountdown } from './AgendaCountdown'; +import { TimeUnit } from '../../Base/Countdown'; import styles from './Hero.module.less'; export type HackathonHeroNavItem = Record<'label' | 'href', string>; @@ -22,13 +25,14 @@ export interface HackathonHeroProps extends Record< | 'imageFallback', string > { + agendaItems: Agenda[]; badges: string[]; bottomCard?: HackathonHeroCard; chips?: string[]; - countdownLabel?: string; - countdownUnitLabels: string[]; - countdownTo?: string; + countdownUnits: TimeUnit[]; + endTime?: TableCellValue; image?: TableCellValue; + startTime?: TableCellValue; navigation: HackathonHeroNavItem[]; primaryAction: HackathonHeroAction; secondaryAction: HackathonHeroAction; @@ -74,38 +78,6 @@ const FloatingCard: FC<{ ); -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(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); @@ -122,13 +94,13 @@ const splitHeroTitle = (name: string, subtitle: string) => { }; export const HackathonHero: FC = ({ + agendaItems, badges, bottomCard, chips, - countdownLabel, - countdownUnitLabels, - countdownTo, + countdownUnits, description, + endTime, image, imageFallback, locationText, @@ -136,6 +108,7 @@ export const HackathonHero: FC = ({ navigation, primaryAction, secondaryAction, + startTime, subtitle, topCard, visualChip, @@ -143,7 +116,6 @@ export const HackathonHero: FC = ({ visualKicker, visualTitle, }) => { - const countdown = useCountdown(countdownTo); const title = splitHeroTitle(name, subtitle); return ( @@ -192,25 +164,12 @@ export const HackathonHero: FC = ({

{description}

- {countdownTo && ( -
- {countdownLabel && ( -

{countdownLabel}

- )} - -
    - {countdown.map((value, index) => ( -
  1. - {value} - {countdownUnitLabels[index]} -
  2. - ))} -
-
- )} +