Add professional Arabic date and time picker to booking form

Introduce a custom DateTimePicker component for improved date and time selection in the protocol request form, replacing native inputs with an Arabic-localized, RTL-compatible interface.
This commit is contained in:
Riyadh
2026-07-06 11:40:36 +00:00
parent 74327027c3
commit c676f51f13
2 changed files with 232 additions and 8 deletions
@@ -0,0 +1,224 @@
import * as React from "react";
import { CalendarClock } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
const AR_LATN = "ar-u-nu-latn";
function pad(n: number): string {
return String(n).padStart(2, "0");
}
// Value contract: local wall-clock string "YYYY-MM-DDTHH:mm" (same shape a
// native datetime-local input produces) or "" when nothing is selected.
function parseValue(value: string): Date | undefined {
if (!value) return undefined;
const d = new Date(value);
return Number.isNaN(d.getTime()) ? undefined : d;
}
function toValue(d: Date): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(
d.getHours(),
)}:${pad(d.getMinutes())}`;
}
function to24(hour12: number, period: "am" | "pm"): number {
if (period === "am") return hour12 === 12 ? 0 : hour12;
return hour12 === 12 ? 12 : hour12 + 12;
}
const HOURS_12 = Array.from({ length: 12 }, (_, i) => i + 1);
const MINUTES = Array.from({ length: 12 }, (_, i) => i * 5);
function TimeColumn({
values,
active,
onSelect,
format,
ariaLabel,
}: {
values: number[];
active: number;
onSelect: (v: number) => void;
format: (v: number) => string;
ariaLabel: string;
}) {
const activeRef = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
activeRef.current?.scrollIntoView({ block: "center" });
}, [active]);
return (
<div
role="listbox"
aria-label={ariaLabel}
className="h-40 w-14 overflow-y-auto rounded-md border p-1"
>
<div className="flex flex-col gap-1">
{values.map((v) => {
const isActive = v === active;
return (
<Button
key={v}
ref={isActive ? activeRef : undefined}
type="button"
size="sm"
variant={isActive ? "default" : "ghost"}
className="h-8 w-full shrink-0"
onClick={() => onSelect(v)}
>
{format(v)}
</Button>
);
})}
</div>
</div>
);
}
export function DateTimePicker({
id,
value,
onChange,
placeholder = "اختر التاريخ والوقت",
disabled,
minDate,
}: {
id?: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
disabled?: boolean;
minDate?: Date;
}) {
const [open, setOpen] = React.useState(false);
const selected = parseValue(value);
const hour12 = selected ? selected.getHours() % 12 || 12 : 9;
const minute = selected ? selected.getMinutes() : 0;
const period: "am" | "pm" = selected
? selected.getHours() < 12
? "am"
: "pm"
: "am";
const baseDate = (): Date => {
if (selected) return new Date(selected);
const d = minDate ? new Date(minDate) : new Date();
d.setHours(9, 0, 0, 0);
return d;
};
const commit = (next: Date) => onChange(toValue(next));
const handleDay = (day: Date | undefined) => {
if (!day) return;
const base = baseDate();
const next = new Date(day);
next.setHours(base.getHours(), base.getMinutes(), 0, 0);
commit(next);
};
const handleHour = (h: number) => {
const next = baseDate();
next.setHours(to24(h, period), next.getMinutes(), 0, 0);
commit(next);
};
const handleMinute = (m: number) => {
const next = baseDate();
next.setMinutes(m, 0, 0);
commit(next);
};
const handlePeriod = (p: "am" | "pm") => {
const next = baseDate();
next.setHours(to24(hour12, p), next.getMinutes(), 0, 0);
commit(next);
};
const label = selected
? new Intl.DateTimeFormat(AR_LATN, {
dateStyle: "medium",
timeStyle: "short",
}).format(selected)
: placeholder;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={id}
type="button"
variant="outline"
disabled={disabled}
className={cn(
"w-full justify-between font-normal",
!selected && "text-muted-foreground",
)}
>
<span className="truncate">{label}</span>
<CalendarClock className="size-4 shrink-0 opacity-60" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selected}
onSelect={handleDay}
defaultMonth={selected ?? minDate ?? new Date()}
disabled={minDate ? { before: minDate } : undefined}
autoFocus
/>
<div className="border-t p-3">
<div className="mb-2 text-center text-xs font-medium text-muted-foreground">
الوقت
</div>
<div className="flex items-stretch justify-center gap-2">
<TimeColumn
values={HOURS_12}
active={hour12}
onSelect={handleHour}
format={pad}
ariaLabel="الساعة"
/>
<div className="flex items-center text-muted-foreground">:</div>
<TimeColumn
values={MINUTES}
active={minute}
onSelect={handleMinute}
format={pad}
ariaLabel="الدقيقة"
/>
<div className="flex flex-col justify-center gap-1">
{(["am", "pm"] as const).map((p) => (
<Button
key={p}
type="button"
size="sm"
variant={period === p ? "default" : "outline"}
className="h-8 w-12"
onClick={() => handlePeriod(p)}
>
{p === "am" ? "ص" : "م"}
</Button>
))}
</div>
</div>
</div>
<div className="flex justify-end border-t p-2">
<Button type="button" size="sm" onClick={() => setOpen(false)}>
تم
</Button>
</div>
</PopoverContent>
</Popover>
);
}
@@ -12,6 +12,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DateTimePicker } from "@/components/date-time-picker";
const API = `${import.meta.env.BASE_URL}api`;
@@ -303,24 +304,23 @@ export default function ProtocolRequestPage() {
<Label htmlFor="startsAt">
من <span className="text-rose-500">*</span>
</Label>
<Input
<DateTimePicker
id="startsAt"
type="datetime-local"
value={form.startsAt}
onChange={(e) => set("startsAt", e.target.value)}
required
onChange={(v) => set("startsAt", v)}
placeholder="اختر تاريخ ووقت البداية"
/>
</div>
<div className="space-y-2">
<Label htmlFor="endsAt">
إلى <span className="text-rose-500">*</span>
</Label>
<Input
<DateTimePicker
id="endsAt"
type="datetime-local"
value={form.endsAt}
onChange={(e) => set("endsAt", e.target.value)}
required
onChange={(v) => set("endsAt", v)}
placeholder="اختر تاريخ ووقت النهاية"
minDate={form.startsAt ? new Date(form.startsAt) : undefined}
/>
</div>
</div>