Skip to content

[refactor] extract independent Countdown class component from Hackathon page with MobX#69

Merged
TechQuery merged 8 commits intomainfrom
copilot/refactor-countdown-component-code
Apr 28, 2026
Merged

[refactor] extract independent Countdown class component from Hackathon page with MobX#69
TechQuery merged 8 commits intomainfrom
copilot/refactor-countdown-component-code

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 28, 2026

PR-69 PR-69 PR-69 Powered by Pull Request Badge

useLiveCountdownState and useCountdown (in Hero.tsx) used chains of useState/useEffect/useMemo/useCallback for timer management. These are replaced following the pattern from BootCell's CountDown component: an independent MobX decorator class component for the visual countdown, and a plain function call for agenda-state resolution.

New: Countdown.tsx — independent MobX class component

The countdown logic is extracted into a self-contained @observer class component extending ObservedComponent from mobx-react-helper, decoupled from Hero. A dedicated initTimer() method carries the @reaction decorator watching observedProps.endTime, and componentDidMount starts it explicitly:

@observer
export class Countdown extends ObservedComponent<CountdownProps> {
  @observable accessor rest = 0;

  // BootCell-style TimeUnit[] algorithm — supports arbitrary number of units
  @computed get timeSections(): TimeSection[] { /* reduce over units with scale+label */ }

  tick = () => {
    const rest = this.target - Date.now();
    if (rest > 0) { this.rest = rest; }
    else { this.rest = 0; if (this.timer) { clearInterval(this.timer); this.props.onEnd?.(); } }
  };

  componentDidMount() { super.componentDidMount(); this.initTimer(); }

  @reaction((_this: Countdown) => _this.observedProps.endTime)
  initTimer() {
    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); }
}

Props:

  • endTime?: string | Date | number — the target timestamp (replaces countdownTo)
  • units: TimeUnit[] — arbitrary time units each with { scale: number, label: string }, porting BootCell's reduction algorithm
  • className?: string — passed to the <ol> so callers (e.g. Hero) control grid layout via their own CSS module
  • onEnd?: () => void — fired when the countdown reaches zero, allowing callers to decide whether to start a new phase

The component renders only a pure <ol> — no label or surrounding layout — keeping it fully customisable by callers. render() uses this.props directly; no internal CSS module classes are applied.

Hero.tsxHackathonHero remains a simple functional component

The useCountdown hook is removed. The Hero owns the countdown wrapper, optional label, and grid styling, passing them to <Countdown>:

{countdownTo && (
  <div className={styles.countdownWrap}>
    {countdownLabel && <p className={`${styles.countdownLabel} m-0`}>{countdownLabel}</p>}
    <Countdown
      className={styles.countdownGrid}
      endTime={countdownTo}
      units={countdownUnitLabels.map((label, i) => ({ scale: DEFAULT_UNIT_SCALES[i] ?? 1, label }))}
    />
  </div>
)}

Hero.module.less — cell styles moved to .countdownGrid li

The separate .countdownCell class is removed. Its styles (border, background, min-height, typography) are now nested under .countdownGrid li, keeping all countdown-related styling in one place and removing the dependency on an internal class name from Countdown.

useLiveCountdownState.ts → removed

Replaced by a direct call to the already-existing resolveCountdownState() pure function in pages/hackathon/[id].tsx — no reactive state or lifecycle re-renders needed:

const { nextItem: nextAgendaItem, countdownTo } = resolveCountdownState(
  agendaItems,
  Date.now(),
  startTime,
  endTime,
);

Copilot AI changed the title [WIP] Refactor countdown component code to pure JavaScript Refactor Hackathon countdown: Replace React hooks with MobX class approach Apr 28, 2026
Copilot AI requested a review from TechQuery April 28, 2026 20:11
Comment thread components/Activity/Hackathon/Hero.tsx
Comment thread components/Activity/Hackathon/LiveCountdownStore.ts Outdated
Copilot AI changed the title Refactor Hackathon countdown: Replace React hooks with MobX class approach Refactor Hackathon countdown: Extract independent Countdown class component, replace hooks with MobX Apr 28, 2026
Copilot AI requested a review from TechQuery April 28, 2026 20:29
Comment thread components/Activity/Hackathon/Countdown.tsx Outdated
Copilot AI requested a review from TechQuery April 28, 2026 20:39
Comment thread components/Base/Countdown.tsx
Copilot AI requested a review from TechQuery April 28, 2026 20:54
Comment thread components/Activity/Hackathon/Countdown.tsx Outdated
Comment thread components/Base/Countdown.tsx
Comment thread components/Activity/Hackathon/Countdown.tsx Outdated
Comment thread components/Base/Countdown.tsx
Copilot AI requested a review from TechQuery April 28, 2026 21:25
TechQuery and others added 2 commits April 29, 2026 06:44
@TechQuery TechQuery marked this pull request as ready for review April 28, 2026 23:01
Copilot AI review requested due to automatic review settings April 28, 2026 23:01
@TechQuery TechQuery changed the title Refactor Hackathon countdown: Extract independent Countdown class component, replace hooks with MobX [refactor] extract independent Countdown class component from Hackathon page with MobX Apr 28, 2026
@TechQuery TechQuery added the feature New feature or request label Apr 28, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the Hackathon countdown by extracting a reusable MobX-based Countdown component and moving agenda-driven countdown state resolution into a dedicated AgendaCountdown wrapper.

Changes:

  • Added components/Base/Countdown.tsx MobX class component that renders an <ol> countdown for arbitrary time units.
  • Replaced Hero’s hook-based countdown rendering with AgendaCountdown, and removed useLiveCountdownState.
  • Moved countdown-specific styling out of Hero.module.less into AgendaCountdown.module.less.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pages/hackathon/[id].tsx Updates Hackathon page to pass agenda + time window + TimeUnit[] into the hero countdown flow.
components/Base/Countdown.tsx Introduces the extracted MobX countdown component and time-unit section calculation.
components/Activity/Hackathon/useLiveCountdownState.ts Removes the now-replaced countdown hook implementation.
components/Activity/Hackathon/constant.ts Removes buildCountdownUnitLabels helper no longer used by the hero countdown.
components/Activity/Hackathon/Hero.tsx Switches hero countdown rendering from local hooks to AgendaCountdown.
components/Activity/Hackathon/Hero.module.less Removes countdown styles from hero and keeps only button styling updates.
components/Activity/Hackathon/AgendaCountdown.tsx Adds agenda-aware countdown wrapper calling resolveCountdownState and re-arming on countdown end.
components/Activity/Hackathon/AgendaCountdown.module.less Adds relocated countdown layout + cell styling and responsive behavior.

Comment on lines +90 to +96
render() {
const { className = '', ...props } = this.props;
const { timeSections } = this;

return (
<ol className={`list-unstyled m-0 ${className}`} {...props}>
{timeSections.map(({ value, label }) => (
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render() spreads ...props from this.props onto the <ol>, but props still contains non-DOM fields like units, endTime, and onEnd. React will forward these as invalid attributes (and units will stringify as "[object Object]"). Destructure and omit the component-only props before spreading onto the DOM element.

Copilot uses AI. Check for mistakes.
private timer = 0;

get endTimestamp() {
return +new Date(this.props.endTime || Date.now());
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endTimestamp uses this.props.endTime || Date.now(). If endTime is a valid numeric timestamp of 0 (or another falsy-but-valid value), this will incorrectly fall back to Date.now(). Prefer nullish coalescing (??) or avoid the fallback since endTime is required by the props type.

Suggested change
return +new Date(this.props.endTime || Date.now());
return +new Date(this.props.endTime);

Copilot uses AI. Check for mistakes.
.map(({ scale }) => scale)
.reduce((sum, scale) => sum * scale, 1);

const value = ~~(rest / scale);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~~(rest / scale) relies on 32-bit bitwise conversion and can overflow/wrap for large values. Use Math.floor(rest / scale) (or an equivalent safe integer conversion) to avoid incorrect countdown sections for longer durations or custom unit configurations.

Suggested change
const value = ~~(rest / scale);
const value = Math.floor(rest / scale);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
{timeSections.map(({ value, label }) => (
<li key={label} className="d-flex flex-column justify-content-center align-items-center">
<strong>{(value + '').padStart(2, '0')}</strong>
<span>{label}</span>
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeSections.map uses key={label}. label is user-provided (and may not be unique across units or locales), which can cause duplicate keys and unstable rendering. Use a guaranteed-unique key (e.g., include the index or use a stable id field in TimeUnit).

Copilot uses AI. Check for mistakes.
@TechQuery TechQuery merged commit b160b03 into main Apr 28, 2026
7 of 8 checks passed
@TechQuery TechQuery deleted the copilot/refactor-countdown-component-code branch April 28, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

精简飞书黑客松主页“倒计时”组件代码

3 participants