/**
* Generates the TqCountdown wrapper component source.
*
* It uses react-countdown (the SAME library the editor canvas drives via
* `calcTimeDelta`/`zeroPad`) so the published countdown shows identical digits.
* The markup + class names (.tq-countdown / .tq-countdown-unit /
* .tq-countdown-value / .tq-countdown-label) match the renderer one-for-one.
*
* SSR safety: the live value depends on the visitor's clock, which differs from
* the server render and would throw a hydration mismatch. The component renders
* a deterministic placeholder (zeros, same structure) until mounted, then swaps
* to the live countdown — so hydration matches and ticking starts client-side.
*/
export const generateCountdownComponentCode = (): string => {
return `import React, { useState, useEffect, useMemo } from 'react'
import Countdown, { zeroPad } from 'react-countdown'
const ALL_UNITS = ['days', 'hours', 'minutes', 'seconds']
const DEFAULT_LABELS = { days: 'Days', hours: 'Hours', minutes: 'Minutes', seconds: 'Seconds' }
const parseCsv = (value) =>
typeof value === 'string' ? value.split(',').map((s) => s.trim()).filter(Boolean) : []
const resolveUnits = (units) => {
const parsed = parseCsv(units).filter((u) => ALL_UNITS.indexOf(u) !== -1)
return parsed.length > 0 ? parsed : ALL_UNITS
}
const resolveLabels = (units, labels) => {
const raw = parseCsv(labels)
return units.map((u, i) => raw[i] || DEFAULT_LABELS[u] || u)
}
const wrapperStyle = (format) => ({
display: 'flex',
alignItems: 'center',
gap: format === 'inline' ? 0 : 16,
fontVariantNumeric: 'tabular-nums',
})
const renderSegments = (segments, format) => {
if (format === 'inline') {
return (
{segments.map((s) => s.value).join(':')}
)
}
return (
{segments.map((s, i) => (
{s.value}
{s.label ? (
{s.label}
) : null}
))}
)
}
const dateSegments = (timeDelta, units, labels) => {
const showDays = units.indexOf('days') !== -1
const hours = showDays ? timeDelta.hours : timeDelta.days * 24 + timeDelta.hours
const byUnit = {
days: timeDelta.days,
hours: hours,
minutes: timeDelta.minutes,
seconds: timeDelta.seconds,
}
return units.map((unit, i) => ({ value: zeroPad(byUnit[unit] != null ? byUnit[unit] : 0), label: labels[i] || '' }))
}
const TqCountdown = ({
mode = 'date',
target,
from = '100',
to = '0',
format = 'blocks',
units,
labels,
onComplete = 'keep-zero',
completeMessage = '',
...rest
}) => {
const [mounted, setMounted] = useState(false)
const [numericStart] = useState(() => Date.now())
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
setMounted(true)
}, [])
// Numeric mode ticks via its own interval.
useEffect(() => {
if (mode !== 'numeric') {
return undefined
}
const id = setInterval(() => setNow(Date.now()), 1000)
return () => clearInterval(id)
}, [mode])
const resolvedUnits = useMemo(() => resolveUnits(units), [units])
const resolvedLabels = useMemo(() => resolveLabels(resolvedUnits, labels), [resolvedUnits, labels])
// Every hook must run on EVERY render: the mounted-guard and the numeric-mode
// branch below both return early, so a hook placed after them would only run
// on some renders ("Rendered more hooks than during the previous render").
const targetDate = useMemo(() => {
const d = new Date(target)
return isNaN(d.getTime()) ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : d
}, [target])
// Deterministic placeholder (zeros) for SSR + first paint, so hydration matches.
if (!mounted) {
const zeros =
mode === 'numeric'
? [{ value: String(from), label: resolvedLabels[0] || '' }]
: resolvedUnits.map((u, i) => ({ value: '00', label: resolvedLabels[i] || '' }))
return {renderSegments(zeros, format)}
}
if (mode === 'numeric') {
const fromN = parseFloat(from) || 0
const toN = parseFloat(to) || 0
const elapsed = Math.max(0, Math.floor((now - numericStart) / 1000))
const current = fromN > toN ? Math.max(toN, fromN - elapsed) : Math.min(toN, fromN + elapsed)
const completed = current === toN
if (completed && onComplete === 'hide') {
return
}
if (completed && onComplete === 'show-message') {
return (
{completeMessage}
)
}
return {renderSegments([{ value: String(current), label: resolvedLabels[0] || '' }], format)}
}
const renderer = ({ days, hours, minutes, seconds, completed }) => {
if (completed && onComplete === 'hide') {
return
}
if (completed && onComplete === 'show-message') {
return {completeMessage}
}
const segments = dateSegments({ days, hours, minutes, seconds }, resolvedUnits, resolvedLabels)
return renderSegments(segments, format)
}
return (
)
}
export default TqCountdown
`
}