import React, { useCallback, useEffect, useMemo, useState } from 'react' import DateTimePicker from '../ui/DateTimePicker' /** * 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 [localDateTime, setLocalDateTime] = useState(initial.date && initial.time ? `${initial.date}T${initial.time}` : '') const [error, setError] = useState('') const minScheduleLocalDateTime = (() => { const next = toLocalDateTimeString(new Date(Date.now() + MIN_FUTURE_MS).toISOString(), timezone) return next.date && next.time ? `${next.date}T${next.time}` : '' })() const validate = useCallback( (value) => { const [datePart = '', timePart = ''] = String(value || '').split('T') if (!datePart || !timePart) return 'Date and time are required.' const iso = localToUtcIso(datePart, timePart.slice(0, 5), 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(() => { const next = toLocalDateTimeString(scheduledAt, timezone) setLocalDateTime(next.date && next.time ? `${next.date}T${next.time}` : '') }, [scheduledAt, timezone]) useEffect(() => { if (mode !== 'schedule') { setError('') return } if (!localDateTime) { setError('') onScheduleAt?.(null) return } const err = validate(localDateTime) setError(err) if (!err) { const [datePart = '', timePart = ''] = localDateTime.split('T') onScheduleAt?.(localToUtcIso(datePart, timePart.slice(0, 5), timezone)) } else { onScheduleAt?.(null) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [localDateTime, mode, timezone]) const previewLabel = useMemo(() => { if (mode !== 'schedule' || error) return null const [datePart = '', timePart = ''] = localDateTime.split('T') const iso = localToUtcIso(datePart, timePart.slice(0, 5), timezone) return formatPreviewLabel(iso, timezone) }, [mode, error, localDateTime, timezone]) return (
Will publish on: {previewLabel}
)}