mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
128 lines
5.8 KiB
React
128 lines
5.8 KiB
React
/***********************************************************************************************************************************************************************
|
|
* File Name : DeadlinePicker.jsx
|
|
* Type : Reusable Component
|
|
* Description : Combined date + time picker for deadline fields.
|
|
* Controlled via a single ISO datetime string (value / onChange).
|
|
* Calendar + time input live inside a Card, matching shadcn's
|
|
* "Date and Time Picker" pattern.
|
|
*
|
|
* Props:
|
|
* value : string | null — ISO datetime string e.g. "2026-06-01T10:30"
|
|
* onChange : (iso: string | null) => void
|
|
* disabled?: boolean
|
|
***********************************************************************************************************************************************************************/
|
|
import { useState } from 'react';
|
|
import { format, parseISO, isValid } from 'date-fns';
|
|
import { ChevronDownIcon, Clock2Icon } from 'lucide-react';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Calendar } from '@/components/ui/calendar';
|
|
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
|
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field';
|
|
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
function toDate(iso) {
|
|
if (!iso) return undefined;
|
|
const d = parseISO(iso);
|
|
return isValid(d) ? d : undefined;
|
|
}
|
|
|
|
function toTimeString(iso) {
|
|
if (!iso) return '00:00';
|
|
const d = parseISO(iso);
|
|
if (!isValid(d)) return '00:00';
|
|
return format(d, 'HH:mm');
|
|
}
|
|
|
|
function buildISO(date, timeStr) {
|
|
if (!date) return null;
|
|
const [h = '00', m = '00'] = (timeStr ?? '00:00').split(':');
|
|
const d = new Date(date);
|
|
d.setHours(Number(h), Number(m), 0, 0);
|
|
return d.toISOString();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
export default function DeadlinePicker({ value, onChange, disabled = false }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const selectedDate = toDate(value);
|
|
const timeStr = toTimeString(value);
|
|
|
|
const handleDateSelect = (date) => {
|
|
onChange(buildISO(date, timeStr));
|
|
};
|
|
|
|
const handleTimeChange = (e) => {
|
|
onChange(buildISO(selectedDate ?? new Date(), e.target.value));
|
|
};
|
|
|
|
const handleClear = () => onChange(null);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
disabled={disabled}
|
|
className="w-56 justify-between font-normal text-sm"
|
|
>
|
|
{selectedDate
|
|
? `${format(selectedDate, 'MMM d, yyyy')} ${format(parseISO(buildISO(selectedDate, timeStr)), 'h:mm a')}`
|
|
: 'Select date'}
|
|
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto border-0 bg-transparent p-0 shadow-none ring-0" align="start">
|
|
<Card size="sm" className="w-fit">
|
|
<CardContent>
|
|
<Calendar
|
|
mode="single"
|
|
selected={selectedDate}
|
|
captionLayout="dropdown"
|
|
defaultMonth={selectedDate}
|
|
onSelect={handleDateSelect}
|
|
disabled={disabled}
|
|
className="p-0"
|
|
/>
|
|
</CardContent>
|
|
<CardFooter className="flex-col items-stretch gap-3 border-t bg-card">
|
|
<FieldGroup>
|
|
<Field orientation="horizontal">
|
|
<FieldLabel htmlFor="deadline-time">Time</FieldLabel>
|
|
<InputGroup>
|
|
<InputGroupInput
|
|
id="deadline-time"
|
|
type="time"
|
|
step="60"
|
|
value={timeStr}
|
|
onChange={handleTimeChange}
|
|
disabled={disabled || !selectedDate}
|
|
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
|
/>
|
|
<InputGroupAddon>
|
|
<Clock2Icon className="text-muted-foreground" />
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</Field>
|
|
</FieldGroup>
|
|
{value && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={disabled}
|
|
onClick={handleClear}
|
|
>
|
|
Clear
|
|
</Button>
|
|
)}
|
|
</CardFooter>
|
|
</Card>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
} |