90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
'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<string, unknown>
|
||
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 (
|
||
<ul className="space-y-1 text-sm">
|
||
{rows.map((r) => (
|
||
<li key={r.day} className={'flex justify-between gap-4' + (r.day === today ? ' font-semibold' : '')}>
|
||
<span className={r.day === today ? 'text-foreground' : 'text-muted-foreground'}>{r.day.slice(0, 3)}</span>
|
||
<span className={r.closed ? 'text-muted-foreground' : ''}>{line(r)}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div data-vula-cms="business_hours" className="rounded-2xl border border-border bg-card p-6">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||
<Clock className="h-5 w-5" />
|
||
</span>
|
||
<h3 className="text-lg font-semibold">{title}</h3>
|
||
</div>
|
||
<ul className="divide-y divide-border">
|
||
{rows.map((r) => (
|
||
<li key={r.day} className={'flex items-center justify-between gap-4 py-2.5 text-sm' + (r.day === today ? ' -mx-3 rounded-lg bg-muted px-3' : '')}>
|
||
<span className="font-medium">
|
||
{r.day}
|
||
{r.day === today && (
|
||
<span className="ml-2 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">Today</span>
|
||
)}
|
||
</span>
|
||
<span className={r.closed ? 'text-muted-foreground' : 'text-foreground'}>{line(r)}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)
|
||
}
|