grok2api / frontend /src /shared /components /date-time-picker.tsx
fromozuzhouzzz
Deploy grok2api v3.0.11 to HF Spaces
f1dd159
Raw
History Blame Contribute Delete
5.02 kB
import { format } from "date-fns"
import { enUS, zhCN } from "date-fns/locale"
import { CalendarIcon } from "lucide-react"
import { useRef } from "react"
import { useTranslation } from "react-i18next"
import { enUS as enUSDayPicker, zhCN as zhCNDayPicker } from "react-day-picker/locale"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/shared/lib/cn"
import { toDateTimeLocal } from "@/shared/lib/format"
type DateTimePickerProps = {
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
}
export function DateTimePicker({ value, onChange, placeholder, disabled = false }: DateTimePickerProps) {
const { t, i18n } = useTranslation()
const selected = parseLocalDateTime(value)
const isChinese = i18n.language.toLowerCase().startsWith("zh")
const timeParts = getTimeParts(selected)
const timeInputRefs = useRef<Array<HTMLInputElement | null>>([])
function enteredTimeParts(): TimeParts {
return timeParts.map((part, index) => timeInputRefs.current[index]?.value ?? part) as TimeParts
}
function selectDate(date: Date | undefined): void {
if (!date) {
onChange("")
return
}
const [hours, minutes, seconds] = normalizedTimeParts(enteredTimeParts())
date.setHours(hours, minutes, seconds, 0)
onChange(toDateTimeLocal(date.toISOString()))
}
function commitTime(): void {
const normalized = normalizedTimeParts(enteredTimeParts())
const date = selected ? new Date(selected) : new Date()
date.setHours(normalized[0], normalized[1], normalized[2], 0)
onChange(toDateTimeLocal(date.toISOString()))
}
return (
<Popover>
<PopoverTrigger asChild>
<Button type="button" variant="secondary" disabled={disabled} className={cn("h-8 w-full min-w-0 justify-start rounded-md bg-secondary/55 px-3 text-left font-normal", !selected && "text-muted-foreground")}>
<CalendarIcon />
<span className="truncate">
{selected ? format(selected, isChinese ? "yyyy年M月d日 HH:mm:ss" : "MMM d, yyyy HH:mm:ss", { locale: isChinese ? zhCN : enUS }) : (placeholder ?? t("keys.neverExpires"))}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={selected}
defaultMonth={selected}
onSelect={selectDate}
locale={isChinese ? zhCNDayPicker : enUSDayPicker}
/>
<div className="flex items-center gap-2 border-t px-3 py-2">
<span className="text-xs text-muted-foreground">{t("keys.expiryTime")}</span>
<div className="ml-auto flex items-center gap-1 text-xs">
{timeParts.map((part, index) => (
<div className="contents" key={index}>
{index > 0 ? <span className="text-muted-foreground">:</span> : null}
<Input
aria-label={`${t("keys.expiryTime")} ${index === 0 ? "HH" : index === 1 ? "MM" : "SS"}`}
className="h-7 w-9 bg-background px-1 text-center text-xs tabular-nums"
inputMode="numeric"
maxLength={2}
pattern="[0-9]*"
defaultValue={part}
key={`${value}-${index}`}
ref={(element) => {
timeInputRefs.current[index] = element
}}
onChange={(event) => {
event.currentTarget.value = event.currentTarget.value.replace(/\D/g, "").slice(0, 2)
}}
onBlur={commitTime}
onFocus={(event) => event.currentTarget.select()}
onKeyDown={(event) => {
if (event.key === "Enter") {
commitTime()
event.currentTarget.blur()
}
}}
/>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
)
}
type TimeParts = [string, string, string]
function getTimeParts(date: Date | undefined): TimeParts {
return date ? [format(date, "HH"), format(date, "mm"), format(date, "ss")] : ["23", "59", "59"]
}
function normalizedTimeParts(parts: TimeParts): [number, number, number] {
return [
clampTimePart(parts[0], 23),
clampTimePart(parts[1], 59),
clampTimePart(parts[2], 59),
]
}
function clampTimePart(value: string, maximum: number): number {
const parsed = Number.parseInt(value, 10)
return Number.isNaN(parsed) ? 0 : Math.min(Math.max(parsed, 0), maximum)
}
function parseLocalDateTime(value: string): Date | undefined {
if (!value) {
return undefined
}
const date = new Date(value)
return Number.isNaN(date.getTime()) ? undefined : date
}