From 684cf33360d3a8e61b78e7a582089582a92976d7 Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Fri, 28 Mar 2025 13:36:10 -0400 Subject: [PATCH] Week header on mobile --- src/components/Calendar.tsx | 241 +++++++++++++++++----------------- src/components/EventModal.tsx | 87 ++++++++---- src/data/events.ts | 6 +- src/types.ts | 2 + 4 files changed, 193 insertions(+), 143 deletions(-) diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 673519c..3d81769 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -13,7 +13,7 @@ import { } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { motion, AnimatePresence } from 'framer-motion'; import { useSwipe } from '@/hooks/useSwipe'; -import { format, addDays, startOfWeek, differenceInDays, parseISO } from 'date-fns'; +import { format, addDays, startOfWeek, differenceInDays, parseISO, isSameDay } from 'date-fns'; import { useWindowSize } from '@/hooks/useWindowSize'; import EventModal from '@/components/EventModal'; import eventsData from '@/data/events'; @@ -35,13 +35,17 @@ const Calendar = () => { const [selectedEvent, setSelectedEvent] = useState(null); const [events, setEvents] = useState(eventsData); - const [dragPreview, setDragPreview] = useState<{ - event: Event; - targetDate: string; - } | null>(null); const containerRef = useRef(null); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + const screenWidth = useRef(isClient ? window.innerWidth : 0); + const handleDragEnd = useCallback((event: any) => { const { source, location } = event; if (!location?.current?.dropTargets[0]) return; @@ -56,7 +60,6 @@ const Calendar = () => { const sourceDate = source.data.date; const targetDate = dropTarget.date; - // Update the event's date when moving between weeks const updatedEvent = { ...movedEvent, date: targetDate }; newEvents[sourceDate] = (newEvents[sourceDate] || []).filter((e: Event) => e.id !== movedEvent.id); @@ -64,42 +67,15 @@ const Calendar = () => { setEvents(newEvents); } - setDragPreview(null); }, []); useEffect(() => { const cleanup = monitorForElements({ onDrop: handleDragEnd, - onDrag: ({ source, location }) => { - //console.log('Drag source data:', source?.data); - //console.log('Current drop targets:', location?.current?.dropTargets); - - if (!source?.data) { - console.warn('No drag data found. Source:', source); - setDragPreview(null); - return; - } - - const dropTarget = location?.current?.dropTargets[0]?.data; - if (dropTarget?.date) { - const movedEvent = Object.values(events) - .flat() - .find((e: Event) => e.id === source.data.id); - - if (movedEvent) { - setDragPreview({ - event: movedEvent, - targetDate: dropTarget.date - }); - } - } else { - setDragPreview(null); - } - } }); return () => cleanup(); - }, [events, handleDragEnd]); + }, [handleDragEnd]); const eventsRef = useRef(eventsData); eventsRef.current = events; @@ -118,7 +94,7 @@ const Calendar = () => { }, [width]); const getWeekDays = (date: Date) => { - const start = startOfWeek(date); + const start = startOfWeek(date, { weekStartsOn: 0 }); return Array.from({ length: 7 }, (_, i) => addDays(start, i)); }; @@ -147,13 +123,6 @@ const Calendar = () => { return () => window.removeEventListener('keydown', handleKeyDown); }, [handlePreviousWeek, handleNextWeek, selectedEvent]); - useEffect(() => { - const cleanup = monitorForElements({ - onDrop: handleDragEnd, - }); - return () => cleanup(); - }, [handleDragEnd]); - const handleSwipe = (dir: 'left' | 'right') => { if (isMobile) { setDirection(dir); @@ -187,11 +156,10 @@ const Calendar = () => { setIsSwiping(false); onTouchEnd(e); - const containerWidth = containerRef.current?.offsetWidth || window.innerWidth; + const containerWidth = isClient ? (containerRef.current?.offsetWidth || window.innerWidth) : 0; const targetIndex = Math.round(-offset / containerWidth); const newDate = addDays(currentDate, targetIndex); - // Animate to the new center position setCurrentDate(newDate); setOffset(0); setTempDate(null); @@ -230,14 +198,10 @@ const Calendar = () => { const event = newEvents[oldDateKey].find((e: Event) => e.id === eventId); if (event) { const newDateKey = format(newDate, 'yyyy-MM-dd'); - - // Remove from old date newEvents[oldDateKey] = newEvents[oldDateKey].filter((e: Event) => e.id !== eventId); - - // Add to new date newEvents[newDateKey] = [ ...(newEvents[newDateKey] || []), - { ...event, date: newDateKey } + event ]; } } @@ -245,12 +209,31 @@ const Calendar = () => { }); }; + const mobileViewStyle = { + x: offset - (isClient ? (containerRef.current?.offsetWidth || window.innerWidth) : 0), + width: '300%', + transition: isSwiping ? 'none' : 'transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)' + }; + + const motionDivStyle = { + opacity: 1 - Math.abs(offset)/(isClient ? (containerRef.current?.offsetWidth || 1) * 0.5 : 1), + scale: 1 - Math.abs(offset)/(isClient ? (containerRef.current?.offsetWidth || 1) * 2 : 1), + }; + + const handleEventClick = useCallback(({ event, date }: { event: Event; date: string }) => { + setSelectedEvent({ + ...event, + date + }); + }, []); + return (
+ {isMobile && }
{ {isMobile ? ( {[addDays(currentDate, -1), currentDate, addDays(currentDate, 1)].map((date, index) => ( setIsEventDragging(true)} onDragEnd={() => setIsEventDragging(false)} handleEventMove={handleEventMove} onDayChange={handleDayChange} + isClient={isClient} /> ))} @@ -301,12 +277,12 @@ const Calendar = () => { events={events[format(date, 'yyyy-MM-dd')] || []} isMobile={isMobile} index={index} - onEventClick={setSelectedEvent} - dragPreview={dragPreview?.targetDate === format(date, 'yyyy-MM-dd') ? dragPreview.event : null} + onEventClick={handleEventClick} onDragStart={() => setIsEventDragging(true)} onDragEnd={() => setIsEventDragging(false)} handleEventMove={handleEventMove} onDayChange={handleDayChange} + isClient={isClient} /> )) )} @@ -330,39 +306,27 @@ interface DayColumnProps { events: Event[]; isMobile: boolean; index: number; - onEventClick: (event: Event) => void; - dragPreview: Event | null; + onEventClick: (eventData: { event: Event; date: string }) => void; onDragStart: () => void; onDragEnd: () => void; handleEventMove: (eventId: string, newDate: Date) => void; onDayChange: (direction: 'left' | 'right') => void; + isClient: boolean; } -const PreviewComponent = ({ event }: { event: Event }) => ( - -

{event.title}

-

{event.time}

-
-); - const DayColumn = ({ date, events, - dragPreview, onDragStart, onDragEnd, onEventClick, handleEventMove, - onDayChange + onDayChange, + isClient }: DayColumnProps) => { const columnRef = useRef(null); const { width } = useWindowSize(); - const [isMobile, setIsMobile] = useState(false); // Default to false for SSR + const [isMobile, setIsMobile] = useState(false); const parseTimeToMinutes = (time: string) => { const [timePart, modifier] = time.split(' '); @@ -385,7 +349,6 @@ const DayColumn = ({ [events] ); - useEffect(() => { setIsMobile(width < 768); // Update isMobile after hydration }, [width]); @@ -405,17 +368,6 @@ const DayColumn = ({ return cleanup; }, [date]); - const previewPosition = useMemo(() => { - if (!dragPreview) return -1; - - const previewMinutes = parseTimeToMinutes(dragPreview.time); - - return sortedEvents.findIndex(event => { - const eventMinutes = parseTimeToMinutes(event.time); - return eventMinutes >= previewMinutes; - }); - }, [sortedEvents, dragPreview]); - return (
- {sortedEvents.map((event, index) => ( - { - // Update calendar view first - onDayChange(dir); - // Then move the event to the new date - const newDate = addDays(date, dir === 'left' ? -1 : 1); - handleEventMove(event.id, newDate); - }} - /> - ))} + {sortedEvents + .filter(event => event && event.id) // Filter out invalid events + .map((event, index) => ( + { + onDayChange(dir); + const newDate = addDays(date, dir === 'left' ? -1 : 1); + handleEventMove(event.id, newDate); + }} + isClient={isClient} + /> + ))}
); @@ -455,18 +408,26 @@ const DraggableEvent = ({ onEventClick, onDragStart, onDragEnd, - onDayChange + onDayChange, + isClient }: { event: Event; date: string; - onEventClick: (event: Event) => void; + onEventClick: (eventData: { event: Event; date: string }) => void; onDragStart: () => void; onDragEnd: () => void; onDayChange: (direction: 'left' | 'right') => void; + isClient: boolean; }) => { const ref = useRef(null); const [isDragging, setIsDragging] = useState(false); - const screenWidth = useRef(window.innerWidth); + const screenWidth = useRef(isClient ? window.innerWidth : 0); + const lastChangeTime = useRef(0); + + if (!event || typeof event !== 'object' || !event.id) { + console.error('Invalid event object:', event); + return null; + } useEffect(() => { const element = ref.current; @@ -478,15 +439,20 @@ const DraggableEvent = ({ screenWidth.current = window.innerWidth; setIsDragging(true); onDragStart(); + lastChangeTime.current = Date.now(); }, onDrag: ({ location }) => { const currentX = location.current.input.clientX; + const now = Date.now(); - // Simple edge detection without delta checks - if (currentX < 50) { - onDayChange('left'); - } else if (currentX > screenWidth.current - 50) { - onDayChange('right'); + if (now - lastChangeTime.current > 500) { + if (currentX < 50) { + lastChangeTime.current = now; + onDayChange('left'); + } else if (currentX > screenWidth.current - 50) { + lastChangeTime.current = now; + onDayChange('right'); + } } }, onDrop: () => { @@ -504,7 +470,7 @@ const DraggableEvent = ({ !isDragging && onEventClick(event)} + onClick={() => !isDragging && onEventClick({ event, date })} className="bg-white p-4 rounded shadow mb-2 cursor-grab active:cursor-grabbing transition-all relative select-none" whileHover={{ scale: 1.01 }} style={{ @@ -527,15 +493,54 @@ const Header = ({ currentDate: Date; isMobile: boolean; }) => { + const weekRange = useMemo(() => { + if (!isMobile) return ''; + const start = startOfWeek(currentDate, { weekStartsOn: 0 }); + const end = addDays(start, 6); + return `${format(start, 'MMM d')} - ${format(end, 'MMM d')}`; + }, [currentDate, isMobile]); + return (

- {format(currentDate, 'MMMM yyyy')} + {isMobile ? weekRange : format(currentDate, 'MMMM yyyy')}

); }; +const WeekHeader = ({ currentDate }: { currentDate: Date }) => { + const weekDays = useMemo(() => { + const start = startOfWeek(currentDate, { weekStartsOn: 0 }); // Start week on Sunday + return Array.from({ length: 7 }, (_, i) => addDays(start, i)); + }, [currentDate]); + + return ( +
+ {weekDays.map((day, index) => ( + +
+ {format(day, 'EEE')} +
+
+ {format(day, 'd')} +
+
+ ))} +
+ ); +}; + export default Calendar; diff --git a/src/components/EventModal.tsx b/src/components/EventModal.tsx index 907e5c4..ee0924c 100644 --- a/src/components/EventModal.tsx +++ b/src/components/EventModal.tsx @@ -1,31 +1,74 @@ -import { motion } from 'framer-motion'; -import { Event } from '@/types'; +import { motion, AnimatePresence } from 'framer-motion'; +import { format, parseISO, isValid } from 'date-fns'; + +interface EventWithDate { + id: string; + title: string; + time: string; + date: string; // Now required + description?: string; + imageUrl?: string; +} + +const EventModal = ({ event, onClose }: { event: EventWithDate; onClose: () => void }) => { + // Now we can safely use event.date + const eventDate = parseISO(event.date); + const formattedDate = isValid(eventDate) ? format(eventDate, 'MMM d, yyyy') : 'Invalid date'; -const EventModal = ({ event, onClose }: { event: Event; onClose: () => void }) => { return ( - + - {event.title} - {event.title} - {event.description} - - {event.time} - + e.stopPropagation()} + initial={{ scale: 0.95 }} + animate={{ scale: 1 }} + exit={{ scale: 0.95 }} + > + + + {event.imageUrl && ( + {event.title} + )} + {event.title} + Date: {formattedDate} + Time: {event.time} + {event.description && ( + + Description: {event.description} + + )} + - + ); }; diff --git a/src/data/events.ts b/src/data/events.ts index 734fa39..6af8e37 100644 --- a/src/data/events.ts +++ b/src/data/events.ts @@ -1,7 +1,7 @@ import { EventsByDate } from '@/types'; const events: EventsByDate = { - "2025-03-20": [ + "2025-03-21": [ { id: "event-1", title: "Coffee with Alex", @@ -21,7 +21,7 @@ const events: EventsByDate = { time: "02:00 PM", }, ], - "2025-03-21": [ + "2025-03-22": [ { id: "event-3", title: "Yoga Session", @@ -41,7 +41,7 @@ const events: EventsByDate = { time: "03:30 PM", }, ], - "2025-03-22": [ + "2025-03-23": [ { id: "event-5", title: "Client Meeting", diff --git a/src/types.ts b/src/types.ts index caccbcd..11ae375 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,8 @@ export interface Event { title: string; time: string; date: string; + description?: string; + imageUrl?: string; // add other properties as needed }