import React, { useCallback, useEffect, useMemo, useState } from 'react' /** * SchedulePublishPicker * * Toggle between "Publish now" and "Schedule publish". * When scheduled, shows a date + time input with validation * (must be >= now + 5 minutes). * * Props: * mode 'now' | 'schedule' * scheduledAt ISO string | null – current scheduled datetime (UTC) * timezone string – IANA tz (e.g. 'Europe/Ljubljana') * onModeChange (mode) => void * onScheduleAt (iso | null) => void * disabled bool */ function toLocalDateTimeString(isoString, tz) { if (!isoString) return { date: '', time: '' } try { const d = new Date(isoString) const opts = { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' } const dateStr = new Intl.DateTimeFormat('en-CA', opts).format(d) // en-CA gives YYYY-MM-DD const timeStr = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false, }).format(d) return { date: dateStr, time: timeStr } } catch { return { date: '', time: '' } } } function formatPreviewLabel(isoString, tz) { if (!isoString) return null try { return new Intl.DateTimeFormat('en-GB', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, timeZoneName: 'short', }).format(new Date(isoString)) } catch { return isoString } } function localToUtcIso(dateStr, timeStr, tz) { if (!dateStr || !timeStr) return null try { const dtStr = `${dateStr}T${timeStr}:00` const local = new Date( new Date(dtStr).toLocaleString('en-US', { timeZone: tz }) ) const utcOffset = new Date(dtStr) - local const utcDate = new Date(new Date(dtStr).getTime() + utcOffset) return utcDate.toISOString() } catch { return null } } const MIN_FUTURE_MS = 5 * 60 * 1000 // 5 minutes export default function SchedulePublishPicker({ mode = 'now', scheduledAt = null, timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, onModeChange, onScheduleAt, disabled = false, }) { const initial = useMemo( () => toLocalDateTimeString(scheduledAt, timezone), // eslint-disable-next-line react-hooks/exhaustive-deps [] ) const [dateStr, setDateStr] = useState(initial.date || '') const [timeStr, setTimeStr] = useState(initial.time || '') const [error, setError] = useState('') const validate = useCallback( (d, t) => { if (!d || !t) return 'Date and time are required.' const iso = localToUtcIso(d, t, timezone) if (!iso) return 'Invalid date or time.' const target = new Date(iso) if (Number.isNaN(target.getTime())) return 'Invalid date or time.' if (target.getTime() - Date.now() < MIN_FUTURE_MS) { return 'Scheduled time must be at least 5 minutes in the future.' } return '' }, [timezone] ) useEffect(() => { if (mode !== 'schedule') { setError('') return } if (!dateStr && !timeStr) { setError('') onScheduleAt?.(null) return } const err = validate(dateStr, timeStr) setError(err) if (!err) { onScheduleAt?.(localToUtcIso(dateStr, timeStr, timezone)) } else { onScheduleAt?.(null) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dateStr, timeStr, mode]) const previewLabel = useMemo(() => { if (mode !== 'schedule' || error) return null const iso = localToUtcIso(dateStr, timeStr, timezone) return formatPreviewLabel(iso, timezone) }, [mode, error, dateStr, timeStr, timezone]) return (
Timezone: {timezone}
{error && ({error}
)} {previewLabel && (Will publish on: {previewLabel}
)}