124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { ClockIcon } from "lucide-react"
|
|
|
|
import { cn } from "@/shared/lib/utils"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/shared/components/ui/popover"
|
|
import type { BaseFieldControlProps } from "../types"
|
|
|
|
export type TimePickerFieldProps = BaseFieldControlProps<string> & {
|
|
placeholder?: string
|
|
withSeconds?: boolean
|
|
}
|
|
|
|
function pad(n: number) {
|
|
return String(Math.max(0, n)).padStart(2, "0")
|
|
}
|
|
|
|
function parseTime(value: string) {
|
|
const parts = (value ?? "").split(":")
|
|
return {
|
|
hours: Math.min(23, Math.max(0, Number(parts[0]) || 0)),
|
|
minutes: Math.min(59, Math.max(0, Number(parts[1]) || 0)),
|
|
seconds: Math.min(59, Math.max(0, Number(parts[2]) || 0)),
|
|
}
|
|
}
|
|
|
|
export function TimePickerField({
|
|
value,
|
|
onChange,
|
|
onBlur,
|
|
disabled,
|
|
invalid,
|
|
placeholder = "Pick a time",
|
|
withSeconds = false,
|
|
}: TimePickerFieldProps) {
|
|
const { hours, minutes, seconds } = parseTime(value ?? "")
|
|
const hasValue = !!value
|
|
|
|
function update(h: number, m: number, s: number) {
|
|
const hh = Math.min(23, Math.max(0, h))
|
|
const mm = Math.min(59, Math.max(0, m))
|
|
const ss = Math.min(59, Math.max(0, s))
|
|
onChange(withSeconds ? `${pad(hh)}:${pad(mm)}:${pad(ss)}` : `${pad(hh)}:${pad(mm)}`)
|
|
}
|
|
|
|
const display = hasValue
|
|
? withSeconds
|
|
? `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
|
|
: `${pad(hours)}:${pad(minutes)}`
|
|
: null
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
type="button"
|
|
disabled={disabled}
|
|
onBlur={onBlur}
|
|
aria-invalid={invalid || undefined}
|
|
className={cn(
|
|
"w-full justify-start text-left font-normal",
|
|
!hasValue && "text-muted-foreground",
|
|
invalid && "border-destructive ring-3 ring-destructive/20",
|
|
)}
|
|
>
|
|
<ClockIcon className="mr-2 h-4 w-4" />
|
|
{display ?? <span>{placeholder}</span>}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-4" align="start">
|
|
<div className="flex items-end gap-2">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-xs text-muted-foreground">HH</span>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={23}
|
|
value={pad(hours)}
|
|
onChange={(e) => update(Number(e.target.value), minutes, seconds)}
|
|
className="w-16 text-center"
|
|
/>
|
|
</div>
|
|
<span className="mb-2 text-xl font-medium leading-none">:</span>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-xs text-muted-foreground">MM</span>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={59}
|
|
value={pad(minutes)}
|
|
onChange={(e) => update(hours, Number(e.target.value), seconds)}
|
|
className="w-16 text-center"
|
|
/>
|
|
</div>
|
|
{withSeconds && (
|
|
<>
|
|
<span className="mb-2 text-xl font-medium leading-none">:</span>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-xs text-muted-foreground">SS</span>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={59}
|
|
value={pad(seconds)}
|
|
onChange={(e) => update(hours, minutes, Number(e.target.value))}
|
|
className="w-16 text-center"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|