Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/components/hero/countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useCountdown } from "@/hooks/use-countdown";
import { cn } from "@/lib/utils";

const PLACEHOLDER = { days: "0", hours: "00", minutes: "00", seconds: "00" };

type CountdownProps = {
className?: string;
/**
Expand All @@ -18,8 +20,11 @@ type CountdownProps = {
* the whole block by setting a font-size (and colour) on `className`.
*/
export const Countdown = ({ className, compact = false }: CountdownProps) => {
const { days, hours, minutes, seconds } = useCountdown();
const hasDays = Number(days) > 0;
const countdown = useCountdown();
// Before mount there's no "now" yet; reserve the usual days-remaining
// layout with hidden zeros so nothing shifts when the real values land.
const { days, hours, minutes, seconds } = countdown ?? PLACEHOLDER;
const hasDays = countdown === null || Number(days) > 0;

const units: [label: string, value: string][] = [
...(hasDays ? [["Days", days] as [string, string]] : []),
Expand All @@ -33,7 +38,9 @@ export const Countdown = ({ className, compact = false }: CountdownProps) => {
className={cn("flex flex-col gap-[0.08em] w-max items-center", className)}
>
<div className="font-title text-[0.37em]">Applications close in</div>
<div className="flex gap-[0.37em]">
<div
className={cn("flex gap-[0.37em]", countdown === null && "invisible")}
>
{units.map(([label, value]) => (
<div key={label} className="flex flex-col items-center">
<div className="leading-none font-countdown">
Expand All @@ -43,11 +50,7 @@ export const Countdown = ({ className, compact = false }: CountdownProps) => {
widest one and the block stays still as the numbers tick.
*/}
{[...value].map((digit, i) => (
<span
key={i}
className="inline-block w-[0.62em] text-center"
suppressHydrationWarning
>
<span key={i} className="inline-block w-[0.62em] text-center">
{digit}
</span>
))}
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/use-countdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ const getReturnValues = (countdown: number) => {
* Starts from {@link APPLICATION_DEADLINE} so the hero renders immediately,
* then live-follows `InternalWebsites/Portal.applicationDeadline.hackcamp` in
* Firestore (the CMS source of truth) once it loads — no redeploy needed.
*
* Returns `null` until mounted: the site is a static export, so anything
* computed from `Date.now()` during render would be frozen at build time in
* the HTML and never corrected on hydration.
*/
export const useCountdown = () => {
const [deadlineMs, setDeadlineMs] = useState(FALLBACK_DEADLINE_MS);
const [now, setNow] = useState(() => Date.now());
const [now, setNow] = useState<number | null>(null);

useEffect(() => {
setNow(Date.now());
const interval = setInterval(() => {
setNow(Date.now());
}, 1000);
Expand Down Expand Up @@ -68,5 +73,5 @@ export const useCountdown = () => {
};
}, []);

return getReturnValues(deadlineMs - now);
return now === null ? null : getReturnValues(deadlineMs - now);
};
Loading