From 457eff44c4f4c4339c1302c915d924a25c401a38 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Mon, 6 Jul 2026 11:40:36 +0000 Subject: [PATCH] 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. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 8b01db58-6cde-464b-bd7d-46bd031e467f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE Replit-Helium-Checkpoint-Created: true --- .../tx-os/src/components/date-time-picker.tsx | 224 ++++++++++++++++++ .../tx-os/src/pages/protocol-request.tsx | 16 +- 2 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 artifacts/tx-os/src/components/date-time-picker.tsx diff --git a/artifacts/tx-os/src/components/date-time-picker.tsx b/artifacts/tx-os/src/components/date-time-picker.tsx new file mode 100644 index 00000000..59c1ed89 --- /dev/null +++ b/artifacts/tx-os/src/components/date-time-picker.tsx @@ -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(null); + React.useEffect(() => { + activeRef.current?.scrollIntoView({ block: "center" }); + }, [active]); + + return ( +
+
+ {values.map((v) => { + const isActive = v === active; + return ( + + ); + })} +
+
+ ); +} + +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 ( + + + + + + +
+
+ الوقت +
+
+ +
:
+ +
+ {(["am", "pm"] as const).map((p) => ( + + ))} +
+
+
+
+ +
+
+
+ ); +} diff --git a/artifacts/tx-os/src/pages/protocol-request.tsx b/artifacts/tx-os/src/pages/protocol-request.tsx index e52ba837..994d0b82 100644 --- a/artifacts/tx-os/src/pages/protocol-request.tsx +++ b/artifacts/tx-os/src/pages/protocol-request.tsx @@ -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() { - set("startsAt", e.target.value)} - required + onChange={(v) => set("startsAt", v)} + placeholder="اختر تاريخ ووقت البداية" />
- set("endsAt", e.target.value)} - required + onChange={(v) => set("endsAt", v)} + placeholder="اختر تاريخ ووقت النهاية" + minDate={form.startsAt ? new Date(form.startsAt) : undefined} />