From 339cbc898ca3fdd984ee4b01009a6966415d63b6 Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Sun, 11 Oct 2026 14:20:23 +0000 Subject: [PATCH] Deploy --- src/components/BusinessHours.tsx | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/components/BusinessHours.tsx diff --git a/src/components/BusinessHours.tsx b/src/components/BusinessHours.tsx new file mode 100644 index 0000000..cafb916 --- /dev/null +++ b/src/components/BusinessHours.tsx @@ -0,0 +1,89 @@ +'use client' +import { useEffect, useState } from 'react' +import { useVulaContent } from '@/hooks/useVulaContent' +import { Clock } from 'lucide-react' + +const DAY_ORDER = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'] + +type HourRow = { day: string; open: string; close: string; closed: boolean; note: string } + +/** + * The weekday where the business is. Not the server's — it runs UTC, and for + * the first two hours of every South African day it would still say yesterday. + */ +export function todayIn(at: Date = new Date()): string { + return new Intl.DateTimeFormat('en-US', { weekday: 'long', timeZone: 'Africa/Johannesburg' }).format(at) +} + +/** + * The practice's opening hours, from the one place they are edited. + * + * `compact` is the footer form — no heading, tighter rows — so a footer can + * show real hours without anyone typing them in by hand again. + */ +export default function BusinessHours({ compact = false, title = 'Opening hours' }: { compact?: boolean; title?: string }) { + const { items, isLoading } = useVulaContent('business_hours') + // Chosen after the page arrives. Rendered on the server, "today" is whenever + // the page was built, not when a patient opens it. + const [today, setToday] = useState('') + useEffect(() => { setToday(todayIn()) }, []) + + const rows: HourRow[] = (items ?? []) + .map((item) => { + const d = (item.data ?? {}) as Record + return { + day: String(d.day_of_week ?? ''), + open: String(d.open_time ?? ''), + close: String(d.close_time ?? ''), + closed: Boolean(d.is_closed), + note: String(d.note ?? ''), + } + }) + .filter((r) => r.day) + .sort((a, b) => DAY_ORDER.indexOf(a.day) - DAY_ORDER.indexOf(b.day)) + + // Nothing rather than an empty frame. Seven rows reading "Day —" is what the + // AI-written version did, and it looks like a broken website rather than a + // business that has not filled its hours in yet. + if (isLoading || rows.length === 0) return null + + const line = (r: HourRow) => + r.closed ? 'Closed' : r.note ? r.note : r.open && r.close ? `${r.open} – ${r.close}` : '—' + + if (compact) { + return ( + + ) + } + + return ( +
+
+ + + +

{title}

+
+
    + {rows.map((r) => ( +
  • + + {r.day} + {r.day === today && ( + Today + )} + + {line(r)} +
  • + ))} +
+
+ ) +}