cleanup
This commit is contained in:
109
apps/client/src/features/calendar/CalendarGrid.tsx
Normal file
109
apps/client/src/features/calendar/CalendarGrid.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
||||
eachDayOfInterval, isSameMonth, isToday, isSameDay, format,
|
||||
} from 'date-fns';
|
||||
import { motion } from 'framer-motion';
|
||||
import { clsx } from 'clsx';
|
||||
import type { CalendarEvent, Member } from '@/lib/api';
|
||||
|
||||
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
interface Props {
|
||||
month: Date;
|
||||
events: CalendarEvent[];
|
||||
members: Member[];
|
||||
onDayClick: (date: Date) => void;
|
||||
onEventClick: (event: CalendarEvent) => void;
|
||||
}
|
||||
|
||||
function eventColor(event: CalendarEvent, members: Member[]): string {
|
||||
if (event.color) return event.color;
|
||||
if (event.member_id) {
|
||||
return members.find((m) => m.id === event.member_id)?.color ?? '#6366f1';
|
||||
}
|
||||
return '#6366f1';
|
||||
}
|
||||
|
||||
export function CalendarGrid({ month, events, members, onDayClick, onEventClick }: Props) {
|
||||
const days = useMemo(() => {
|
||||
const start = startOfWeek(startOfMonth(month));
|
||||
const end = endOfWeek(endOfMonth(month));
|
||||
return eachDayOfInterval({ start, end });
|
||||
}, [month]);
|
||||
|
||||
function dayEvents(day: Date) {
|
||||
return events.filter((e) => {
|
||||
const start = new Date(e.start_at);
|
||||
const end = new Date(e.end_at);
|
||||
// All-day or events that touch this day
|
||||
return isSameDay(start, day) || (start <= day && end >= day);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Weekday headers */}
|
||||
<div className="grid grid-cols-7 border-b border-theme">
|
||||
{WEEKDAYS.map((d) => (
|
||||
<div key={d} className="py-2 text-center text-xs font-semibold text-muted uppercase tracking-wide">
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7 flex-1" style={{ gridTemplateRows: `repeat(${days.length / 7}, 1fr)` }}>
|
||||
{days.map((day, i) => {
|
||||
const de = dayEvents(day);
|
||||
const inMonth = isSameMonth(day, month);
|
||||
const today = isToday(day);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={i}
|
||||
whileHover={{ backgroundColor: 'var(--color-surface-raised)' }}
|
||||
onClick={() => onDayClick(day)}
|
||||
className={clsx(
|
||||
'relative min-h-[80px] border-b border-r border-theme p-1.5 cursor-pointer',
|
||||
'transition-colors duration-100',
|
||||
!inMonth && 'opacity-40',
|
||||
)}
|
||||
>
|
||||
{/* Date number */}
|
||||
<div className="flex justify-end mb-1">
|
||||
<span
|
||||
className={clsx(
|
||||
'h-6 w-6 flex items-center justify-center rounded-full text-xs font-semibold',
|
||||
today
|
||||
? 'bg-accent text-white'
|
||||
: 'text-primary hover:bg-surface-raised'
|
||||
)}
|
||||
>
|
||||
{format(day, 'd')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Event chips */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{de.slice(0, 3).map((ev) => (
|
||||
<button
|
||||
key={ev.id}
|
||||
onClick={(e) => { e.stopPropagation(); onEventClick(ev); }}
|
||||
className="w-full text-left px-1.5 py-0.5 rounded text-xs font-medium text-white truncate leading-5"
|
||||
style={{ backgroundColor: eventColor(ev, members) }}
|
||||
>
|
||||
{ev.all_day ? '' : `${format(new Date(ev.start_at), 'h:mm')} `}{ev.title}
|
||||
</button>
|
||||
))}
|
||||
{de.length > 3 && (
|
||||
<span className="text-xs text-muted px-1">+{de.length - 3} more</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user