Files
stats/apps/start/src/modals/date-time-picker.tsx
Carl-Gerhard Lindesvärd 81a7e5d62e feat: dashboard v2, esm, upgrades (#211)
* esm

* wip

* wip

* wip

* wip

* wip

* wip

* subscription notice

* wip

* wip

* wip

* fix envs

* fix: update docker build

* fix

* esm/types

* delete dashboard :D

* add patches to dockerfiles

* update packages + catalogs + ts

* wip

* remove native libs

* ts

* improvements

* fix redirects and fetching session

* try fix favicon

* fixes

* fix

* order and resize reportds within a dashboard

* improvements

* wip

* added userjot to dashboard

* fix

* add op

* wip

* different cache key

* improve date picker

* fix table

* event details loading

* redo onboarding completely

* fix login

* fix

* fix

* extend session, billing and improve bars

* fix

* reduce price on 10M
2025-10-16 12:27:44 +02:00

194 lines
6.0 KiB
TypeScript

import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { format } from 'date-fns';
import { CalendarIcon, ClockIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
// Utility function to round date to nearest 5-minute interval
function roundToNearestFiveMinutes(date: Date): Date {
const roundedDate = new Date(date);
const minutes = roundedDate.getMinutes();
const remainder = minutes % 5;
if (remainder === 0) {
return roundedDate;
}
// Round to nearest 5-minute interval
if (remainder >= 2.5) {
// Round up
roundedDate.setMinutes(minutes + (5 - remainder));
} else {
// Round down
roundedDate.setMinutes(minutes - remainder);
}
// Reset seconds and milliseconds
roundedDate.setSeconds(0);
roundedDate.setMilliseconds(0);
return roundedDate;
}
type Props = {
onChange: (date: Date) => void;
initialDate?: Date;
title?: string;
};
export default function DateTimePicker({
onChange,
initialDate,
title = 'Select Date & Time',
}: Props) {
const scrollRef = useRef<HTMLDivElement>(null);
const [selectedDate, setSelectedDate] = useState<Date>(
roundToNearestFiveMinutes(initialDate || new Date()),
);
// Generate all time options with 5-minute intervals
const generateTimeOptions = () => {
const times = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 5) {
const timeString = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const displayTime = format(new Date(2000, 0, 1, hour, minute), 'HH:mm');
times.push({ value: timeString, label: displayTime });
}
}
return times;
};
const timeOptions = generateTimeOptions();
function handleDateSelect(date: Date | undefined) {
if (date) {
// Preserve the existing time when changing date
const newDate = new Date(date);
newDate.setHours(selectedDate.getHours());
newDate.setMinutes(selectedDate.getMinutes());
// Round to nearest 5-minute interval
setSelectedDate(roundToNearestFiveMinutes(newDate));
}
}
function handleTimeSelect(timeValue: string) {
const [hours, minutes] = timeValue.split(':').map(Number);
const newDate = new Date(selectedDate);
newDate.setHours(hours);
newDate.setMinutes(minutes);
// Ensure alignment to 5-minute intervals (safety measure)
setSelectedDate(roundToNearestFiveMinutes(newDate));
}
const currentTimeValue = `${selectedDate.getHours().toString().padStart(2, '0')}:${selectedDate.getMinutes().toString().padStart(2, '0')}`;
// Scroll to selected time when modal opens
useEffect(() => {
const buttonSize = 32;
const buttonMargin = 2;
const containerPadding = 4;
const scrollContainer = scrollRef.current;
const buttonIndex = timeOptions.findIndex(
(time) => time.value === currentTimeValue,
);
const calculatedScrollTo =
Math.max(0, buttonIndex - 4) * (buttonSize + buttonMargin) +
containerPadding;
if (scrollContainer) {
scrollContainer.scrollTo({
top: calculatedScrollTo,
behavior: 'instant',
});
}
}, []); // Empty dependency array to run only on mount
return (
<ModalContent className="max-w-[400px]">
<ModalHeader title={title} />
<div className="space-y-4">
{/* Selected Date/Time Display */}
<div className="rounded-lg border border-dashed bg-muted/50 p-4">
<div className="flex items-center justify-center space-x-2 text-sm">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">
{format(selectedDate, 'EEEE, MMMM d, yyyy')}
</span>
<div className="h-4 w-px bg-border" />
<ClockIcon className="h-4 w-4 text-muted-foreground" />
<span className="font-medium font-mono">
{format(selectedDate, 'HH:mm')}
</span>
</div>
</div>
{/* Calendar Section */}
<div className="row gap-2 h-[333px]">
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
initialFocus
className="[&_table]:mx-auto [&_table]:w-auto border rounded-lg"
/>
<ScrollArea
className="h-full w-full border rounded-lg bg-background/50"
ref={scrollRef}
>
<div className="flex flex-col p-1">
{timeOptions.map((time) => (
<Button
key={time.value}
size="sm"
data-value={time.value}
variant={
currentTimeValue === time.value ? 'default' : 'ghost'
}
className={cn(
'w-full mb-0.5 h-8 text-xs font-mono transition-all duration-200 justify-start',
currentTimeValue === time.value
? 'bg-primary text-primary-foreground shadow-sm'
: 'hover:bg-muted',
)}
onClick={() => handleTimeSelect(time.value)}
>
{time.label}
</Button>
))}
</div>
<ScrollBar orientation="vertical" />
</ScrollArea>
</div>
{/* Action Buttons */}
<div className="flex gap-3 pt-2">
<Button
variant="outline"
className="flex-1"
onClick={() => popModal()}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={() => {
popModal();
onChange(selectedDate);
}}
>
Confirm Selection
</Button>
</div>
</div>
</ModalContent>
);
}