website/components/ClockTimePicker.tsx

279 lines
10 KiB
TypeScript
Raw Permalink Normal View History

'use client';
import * as React from 'react';
import { Clock } from 'lucide-react';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface ClockTimePickerProps {
time: string; // HH:mm format (e.g., "09:00")
setTime: (time: string) => void;
label?: string;
isDark?: boolean;
}
export function ClockTimePicker({ time, setTime, label, isDark = false }: ClockTimePickerProps) {
const [isOpen, setIsOpen] = React.useState(false);
const [mode, setMode] = React.useState<'hour' | 'minute'>('hour');
const wrapperRef = React.useRef<HTMLDivElement>(null);
// Parse time string to hours and minutes
const [hours, minutes] = React.useMemo(() => {
if (!time) return [9, 0];
const parts = time.split(':').map(Number);
return [parts[0] || 9, parts[1] || 0];
}, [time]);
// Convert to 12-hour format for display
const displayHours = hours % 12 || 12;
const ampm = hours >= 12 ? 'PM' : 'AM';
// Close picker when clicking outside
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setIsOpen(false);
setMode('hour');
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// Handle hour selection
const handleHourClick = (selectedHour: number) => {
const newHours = ampm === 'PM' && selectedHour !== 12
? selectedHour + 12
: ampm === 'AM' && selectedHour === 12
? 0
: selectedHour;
setTime(`${newHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
setMode('minute');
};
// Handle minute selection
const handleMinuteClick = (selectedMinute: number) => {
setTime(`${hours.toString().padStart(2, '0')}:${selectedMinute.toString().padStart(2, '0')}`);
setIsOpen(false);
setMode('hour');
};
// Generate hour numbers (1-12)
const hourNumbers = Array.from({ length: 12 }, (_, i) => i + 1);
// Generate minute numbers (0, 15, 30, 45 or 0-59)
const minuteNumbers = Array.from({ length: 12 }, (_, i) => i * 5); // 0, 5, 10, 15, ..., 55
// Calculate position for clock numbers
const getClockPosition = (index: number, total: number, radius: number = 90) => {
const angle = (index * 360) / total - 90; // Start from top (-90 degrees)
const radian = (angle * Math.PI) / 180;
const x = Math.cos(radian) * radius;
const y = Math.sin(radian) * radius;
return { x, y };
};
// Format display time
const displayTime = time
? `${displayHours}:${minutes.toString().padStart(2, '0')} ${ampm}`
: 'Select time';
return (
<div className="space-y-2">
{label && (
<label className={cn(
"text-sm font-semibold",
isDark ? "text-gray-300" : "text-gray-700"
)}>
{label}
</label>
)}
<div className="relative" ref={wrapperRef}>
<Button
type="button"
variant="outline"
onClick={() => setIsOpen(!isOpen)}
className={cn(
"w-full justify-start text-left font-normal h-12 text-base",
!time && "text-muted-foreground",
isDark
? "bg-gray-800 border-gray-600 text-white hover:bg-gray-700"
: "bg-white border-gray-300 text-gray-900 hover:bg-gray-50"
)}
>
<Clock className="mr-2 h-5 w-5" />
{displayTime}
</Button>
{isOpen && (
<div className={cn(
"absolute z-[9999] mt-1 rounded-lg shadow-lg border p-4 -translate-y-1",
isDark
? "bg-gray-800 border-gray-700"
: "bg-white border-gray-200"
)}>
{/* Mode selector */}
<div className="flex gap-2 mb-4">
<button
onClick={() => setMode('hour')}
className={cn(
"px-3 py-1.5 rounded text-sm font-medium transition-colors",
mode === 'hour'
? isDark
? "bg-blue-600 text-white"
: "bg-blue-600 text-white"
: isDark
? "bg-gray-700 text-gray-300 hover:bg-gray-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
)}
>
Hour
</button>
<button
onClick={() => setMode('minute')}
className={cn(
"px-3 py-1.5 rounded text-sm font-medium transition-colors",
mode === 'minute'
? isDark
? "bg-blue-600 text-white"
: "bg-blue-600 text-white"
: isDark
? "bg-gray-700 text-gray-300 hover:bg-gray-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
)}
>
Minute
</button>
</div>
{/* Clock face */}
<div className="relative w-64 h-64 mx-auto my-4">
{/* Clock circle */}
<div className={cn(
"absolute inset-0 rounded-full border-2",
isDark ? "border-gray-600" : "border-gray-300"
)} />
{/* Center dot */}
<div className={cn(
"absolute top-1/2 left-1/2 w-2 h-2 rounded-full -translate-x-1/2 -translate-y-1/2 z-10",
isDark ? "bg-gray-400" : "bg-gray-600"
)} />
{/* Hour numbers */}
{mode === 'hour' && hourNumbers.map((hour, index) => {
const { x, y } = getClockPosition(index, 12, 90);
const isSelected = displayHours === hour;
return (
<button
key={hour}
type="button"
onClick={() => handleHourClick(hour)}
className={cn(
"absolute w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold transition-all z-20",
isSelected
? isDark
? "bg-blue-600 text-white scale-110 shadow-lg"
: "bg-blue-600 text-white scale-110 shadow-lg"
: isDark
? "bg-gray-700 text-gray-200 hover:bg-gray-600 hover:scale-105"
: "bg-gray-100 text-gray-700 hover:bg-gray-200 hover:scale-105"
)}
style={{
left: `calc(50% + ${x}px)`,
top: `calc(50% + ${y}px)`,
transform: 'translate(-50%, -50%)',
}}
>
{hour}
</button>
);
})}
{/* Minute numbers */}
{mode === 'minute' && minuteNumbers.map((minute, index) => {
const { x, y } = getClockPosition(index, 12, 90);
const isSelected = minutes === minute;
return (
<button
key={minute}
type="button"
onClick={() => handleMinuteClick(minute)}
className={cn(
"absolute w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold transition-all z-20",
isSelected
? isDark
? "bg-blue-600 text-white scale-110 shadow-lg"
: "bg-blue-600 text-white scale-110 shadow-lg"
: isDark
? "bg-gray-700 text-gray-200 hover:bg-gray-600 hover:scale-105"
: "bg-gray-100 text-gray-700 hover:bg-gray-200 hover:scale-105"
)}
style={{
left: `calc(50% + ${x}px)`,
top: `calc(50% + ${y}px)`,
transform: 'translate(-50%, -50%)',
}}
>
{minute}
</button>
);
})}
</div>
{/* AM/PM toggle */}
<div className="flex gap-2 mt-4 justify-center">
<button
type="button"
onClick={() => {
const newHours = ampm === 'PM' ? hours - 12 : hours;
setTime(`${newHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
}}
className={cn(
"px-4 py-2 rounded text-sm font-medium transition-colors",
ampm === 'AM'
? isDark
? "bg-blue-600 text-white"
: "bg-blue-600 text-white"
: isDark
? "bg-gray-700 text-gray-300 hover:bg-gray-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
)}
>
AM
</button>
<button
type="button"
onClick={() => {
const newHours = ampm === 'AM' ? hours + 12 : hours;
setTime(`${newHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
}}
className={cn(
"px-4 py-2 rounded text-sm font-medium transition-colors",
ampm === 'PM'
? isDark
? "bg-blue-600 text-white"
: "bg-blue-600 text-white"
: isDark
? "bg-gray-700 text-gray-300 hover:bg-gray-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
)}
>
PM
</button>
</div>
</div>
)}
</div>
</div>
);
}