178 lines
6.1 KiB
TypeScript
178 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Timer } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface ClockDurationPickerProps {
|
|
duration: number; // Duration in minutes
|
|
setDuration: (duration: number) => void;
|
|
label?: string;
|
|
isDark?: boolean;
|
|
options?: number[]; // Optional custom duration options
|
|
}
|
|
|
|
export function ClockDurationPicker({
|
|
duration,
|
|
setDuration,
|
|
label,
|
|
isDark = false,
|
|
options = [15, 30, 45, 60, 75, 90, 105, 120]
|
|
}: ClockDurationPickerProps) {
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
// Close picker when clicking outside
|
|
React.useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
// Handle duration selection
|
|
const handleDurationClick = (selectedDuration: number) => {
|
|
setDuration(selectedDuration);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
// Calculate position for clock numbers
|
|
const getClockPosition = (index: number, total: number, radius: number = 130) => {
|
|
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 duration display
|
|
const formatDuration = (minutes: number) => {
|
|
if (minutes < 60) {
|
|
return `${minutes}m`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
};
|
|
|
|
const displayDuration = duration ? formatDuration(duration) : 'Select duration';
|
|
|
|
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 w-full" ref={wrapperRef}>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={cn(
|
|
"w-full justify-start text-left font-normal h-12 text-base",
|
|
!duration && "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"
|
|
)}
|
|
>
|
|
<Timer className="mr-2 h-5 w-5" />
|
|
{displayDuration}
|
|
</Button>
|
|
{isOpen && (
|
|
<div className={cn(
|
|
"absolute z-[9999] top-full left-0 right-0 mt-2 rounded-lg shadow-xl border p-6 w-[420px] mx-auto overflow-visible",
|
|
isDark
|
|
? "bg-gray-800 border-gray-700"
|
|
: "bg-white border-gray-200"
|
|
)}>
|
|
{/* Clock face */}
|
|
<div className="relative w-[360px] h-[360px] mx-auto my-6 overflow-visible">
|
|
{/* 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"
|
|
)} />
|
|
|
|
{/* Duration options arranged in a circle */}
|
|
{options.map((option, index) => {
|
|
const { x, y } = getClockPosition(index, options.length, 130);
|
|
const isSelected = duration === option;
|
|
return (
|
|
<button
|
|
key={option}
|
|
type="button"
|
|
onClick={() => handleDurationClick(option)}
|
|
className={cn(
|
|
"absolute w-16 h-16 rounded-full flex items-center justify-center text-xs font-semibold transition-all z-20 whitespace-nowrap",
|
|
isSelected
|
|
? isDark
|
|
? "bg-blue-600 text-white scale-110 shadow-lg ring-2 ring-blue-400"
|
|
: "bg-blue-600 text-white scale-110 shadow-lg ring-2 ring-blue-400"
|
|
: 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%)',
|
|
}}
|
|
title={`${option} minutes`}
|
|
>
|
|
{formatDuration(option)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Quick select buttons for common durations */}
|
|
<div className="flex gap-2 mt-4 justify-center flex-wrap">
|
|
{[30, 60, 90, 120].map((quickDuration) => (
|
|
<button
|
|
key={quickDuration}
|
|
type="button"
|
|
onClick={() => handleDurationClick(quickDuration)}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded text-sm font-medium transition-colors",
|
|
duration === quickDuration
|
|
? 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"
|
|
)}
|
|
>
|
|
{formatDuration(quickDuration)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|