feat(admin): add draw management features including create, update, delete, and batch delete functionalities
- Implemented API functions for creating, updating, and deleting draws. - Enhanced the admin draws console with UI components for managing draws. - Added internationalization support for new draw management actions and messages.
This commit is contained in:
184
src/components/admin/admin-datetime-field.tsx
Normal file
184
src/components/admin/admin-datetime-field.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { format, parse } from "date-fns";
|
||||
import { enUS } from "date-fns/locale";
|
||||
import { CalendarIcon, Clock } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** `yyyy-MM-dd HH:mm:ss` */
|
||||
export type AdminDateTimeValue = string;
|
||||
|
||||
function splitDateTime(value: AdminDateTimeValue): { date: string; time: string } {
|
||||
const trimmed = value.trim();
|
||||
const match = /^(\d{4}-\d{2}-\d{2})(?: (\d{2}:\d{2}(?::\d{2})?))?$/.exec(trimmed);
|
||||
if (!match) {
|
||||
return { date: "", time: "" };
|
||||
}
|
||||
const time = match[2] ?? "";
|
||||
if (/^\d{2}:\d{2}$/.test(time)) {
|
||||
return { date: match[1], time: `${time}:00` };
|
||||
}
|
||||
return { date: match[1], time };
|
||||
}
|
||||
|
||||
function normalizeTimeForInput(time: string): string {
|
||||
if (!time) {
|
||||
return "";
|
||||
}
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
||||
return time;
|
||||
}
|
||||
if (/^\d{2}:\d{2}$/.test(time)) {
|
||||
return `${time}:00`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeTimeForApi(time: string): string {
|
||||
if (!time) {
|
||||
return "00:00:00";
|
||||
}
|
||||
if (/^\d{2}:\d{2}$/.test(time)) {
|
||||
return `${time}:00`;
|
||||
}
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
||||
return time;
|
||||
}
|
||||
return "00:00:00";
|
||||
}
|
||||
|
||||
function joinDateTime(date: string, time: string): AdminDateTimeValue {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
return `${date} ${normalizeTimeForApi(time)}`;
|
||||
}
|
||||
|
||||
export function AdminDateTimeField({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
optional = false,
|
||||
required = false,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
/** `yyyy-MM-dd HH:mm:ss` or empty */
|
||||
value: AdminDateTimeValue;
|
||||
onChange: (next: AdminDateTimeValue) => void;
|
||||
/** 允许留空(开始/封盘等可选字段) */
|
||||
optional?: boolean;
|
||||
required?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { date, time } = splitDateTime(value);
|
||||
|
||||
const parsedDate = React.useMemo(() => {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return undefined;
|
||||
}
|
||||
const d = parse(date, "yyyy-MM-dd", new Date());
|
||||
return Number.isNaN(d.getTime()) ? undefined : d;
|
||||
}, [date]);
|
||||
|
||||
const dateSummary = parsedDate
|
||||
? format(parsedDate, "yyyy-MM-dd", { locale: enUS })
|
||||
: t("date.placeholder", { defaultValue: "Select date" });
|
||||
|
||||
const timeInputId = `${id}-time`;
|
||||
|
||||
return (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
{required ? <span className="text-destructive"> *</span> : null}
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Popover
|
||||
modal={false}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PopoverTrigger
|
||||
type="button"
|
||||
id={id}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "default" }),
|
||||
"h-8 w-full justify-start gap-2 px-2.5 font-normal sm:flex-1 md:text-sm",
|
||||
!parsedDate && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="pointer-events-none size-4 shrink-0 opacity-70" aria-hidden />
|
||||
<span className="min-w-0 flex-1 truncate text-left">{dateSummary}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" sideOffset={6} className="w-auto min-w-fit p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
locale={enUS}
|
||||
captionLayout="dropdown"
|
||||
selected={parsedDate}
|
||||
defaultMonth={parsedDate}
|
||||
onSelect={(d) => {
|
||||
if (!d) {
|
||||
onChange("");
|
||||
return;
|
||||
}
|
||||
onChange(joinDateTime(format(d, "yyyy-MM-dd"), time));
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
{optional ? (
|
||||
<div className="flex justify-end border-t px-2 py-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-7 px-2"
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("actions.clear", { defaultValue: "Clear" })}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<div className="relative w-full sm:w-[9.5rem] sm:shrink-0">
|
||||
<Clock
|
||||
className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 opacity-70"
|
||||
aria-hidden
|
||||
/>
|
||||
<Input
|
||||
id={timeInputId}
|
||||
type="time"
|
||||
step={1}
|
||||
disabled={!date}
|
||||
value={normalizeTimeForInput(time)}
|
||||
className="h-8 pl-8 font-mono text-sm tabular-nums"
|
||||
onChange={(e) => {
|
||||
onChange(joinDateTime(date, e.target.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{optional ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("datetime.optionalHint", { defaultValue: "Leave empty to auto-fill from server config." })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user