mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-06-09 15:07:51 +00:00
193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
import * as React from 'react';
|
|
import { useState } from 'react';
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Calendar as CalendarIcon,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/figma-make/components/base/button';
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/figma-make/components/base/popover';
|
|
import { Input } from '@/figma-make/components/base/input';
|
|
import { Label } from '@/figma-make/components/base/label';
|
|
|
|
interface DatePickerProps {
|
|
date: string;
|
|
onDateChange: (date: string) => void;
|
|
disabled?: boolean;
|
|
placeholder?: string;
|
|
formatDisplay?: (date: string) => string;
|
|
hasError?: boolean;
|
|
}
|
|
|
|
export function DatePicker({
|
|
date,
|
|
onDateChange,
|
|
disabled = false,
|
|
placeholder = 'Select date',
|
|
formatDisplay,
|
|
hasError = false,
|
|
}: DatePickerProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [currentMonth, setCurrentMonth] = useState(() => {
|
|
const d = date ? new Date(date) : new Date();
|
|
return { year: d.getFullYear(), month: d.getMonth() };
|
|
});
|
|
|
|
const defaultFormatDisplay = (dateStr: string) => {
|
|
if (!dateStr) return placeholder;
|
|
const d = new Date(dateStr + 'T00:00:00');
|
|
return d.toLocaleDateString('id-ID', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
// const formatDateInput = (dateStr: string) => {
|
|
// if (!dateStr) return '';
|
|
// const d = new Date(dateStr + 'T00:00:00');
|
|
// return d.toLocaleDateString('en-GB', {
|
|
// day: '2-digit',
|
|
// month: '2-digit',
|
|
// year: 'numeric',
|
|
// });
|
|
// };
|
|
|
|
const displayFormatter = formatDisplay || defaultFormatDisplay;
|
|
|
|
const navigateMonth = (direction: 'prev' | 'next') => {
|
|
const newDate = new Date(
|
|
currentMonth.year,
|
|
currentMonth.month + (direction === 'next' ? 1 : -1)
|
|
);
|
|
setCurrentMonth({ year: newDate.getFullYear(), month: newDate.getMonth() });
|
|
};
|
|
|
|
const handleDateSelect = (dateStr: string) => {
|
|
onDateChange(dateStr);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleManualInput = (value: string) => {
|
|
onDateChange(value);
|
|
setOpen(false);
|
|
};
|
|
|
|
const renderCalendar = () => {
|
|
const { year, month } = currentMonth;
|
|
const firstDay = new Date(year, month, 1).getDay();
|
|
const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1; // Monday = 0
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
const monthName = new Date(year, month).toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
});
|
|
|
|
const days = [];
|
|
|
|
// Empty cells before first day
|
|
for (let i = 0; i < adjustedFirstDay; i++) {
|
|
days.push(<div key={`empty-${i}`} className='h-9 w-9' />);
|
|
}
|
|
|
|
// Days of the month
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
const isSelected = dateStr === date;
|
|
const isToday = dateStr === new Date().toISOString().split('T')[0];
|
|
|
|
days.push(
|
|
<button
|
|
key={day}
|
|
onClick={() => handleDateSelect(dateStr)}
|
|
className={`
|
|
h-9 w-9 rounded-md text-sm font-medium transition-colors
|
|
${isSelected ? 'bg-[#0069e0] text-white hover:bg-[#0052b3]' : ''}
|
|
${!isSelected && isToday ? 'border border-[#0069e0] text-[#0069e0]' : ''}
|
|
${!isSelected && !isToday ? 'hover:bg-gray-100 text-gray-700' : ''}
|
|
`}
|
|
>
|
|
{day}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='p-3'>
|
|
<div className='flex items-center justify-between mb-3'>
|
|
<button
|
|
onClick={() => navigateMonth('prev')}
|
|
className='p-1 hover:bg-gray-100 rounded-md transition-colors'
|
|
>
|
|
<ChevronLeft className='w-4 h-4 text-gray-600' />
|
|
</button>
|
|
<div className='font-semibold text-sm text-gray-900'>
|
|
{monthName} {year}
|
|
</div>
|
|
<button
|
|
onClick={() => navigateMonth('next')}
|
|
className='p-1 hover:bg-gray-100 rounded-md transition-colors'
|
|
>
|
|
<ChevronRight className='w-4 h-4 text-gray-600' />
|
|
</button>
|
|
</div>
|
|
<div className='grid grid-cols-7 gap-1 mb-2'>
|
|
{['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].map((day) => (
|
|
<div
|
|
key={day}
|
|
className='h-9 w-9 flex items-center justify-center text-xs font-medium text-gray-500'
|
|
>
|
|
{day}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className='grid grid-cols-7 gap-1'>{days}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant='outline'
|
|
disabled={disabled}
|
|
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
|
|
>
|
|
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
|
{date ? (
|
|
<span className='text-gray-900'>{displayFormatter(date)}</span>
|
|
) : (
|
|
<span className='text-gray-500'>{placeholder}</span>
|
|
)}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className='w-auto p-0 bg-white shadow-lg rounded-xl'
|
|
align='start'
|
|
>
|
|
{renderCalendar()}
|
|
<div className='p-3 border-t border-gray-200'>
|
|
<Label
|
|
htmlFor='manual-date'
|
|
className='text-xs text-gray-600 mb-1.5 block'
|
|
>
|
|
Atau ketik manual (YYYY-MM-DD):
|
|
</Label>
|
|
<Input
|
|
id='manual-date'
|
|
type='date'
|
|
value={date}
|
|
onChange={(e) => handleManualInput(e.target.value)}
|
|
className='text-sm border-gray-200'
|
|
/>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|