|
|
|
@@ -4,11 +4,15 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
import { dropTargetForElements, monitorForElements, draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
|
|
import { dropTargetForElements, monitorForElements, draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
import { useSwipeable } from 'react-swipeable';
|
|
|
|
import { useSwipeable } from 'react-swipeable';
|
|
|
|
import { format, addDays, startOfWeek, differenceInDays } from 'date-fns';
|
|
|
|
import { format, addDays, startOfWeek, differenceInDays, parseISO } from 'date-fns';
|
|
|
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
|
|
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
|
|
|
import EventModal from '@/components/EventModal';
|
|
|
|
import EventModal from '@/components/EventModal';
|
|
|
|
import eventsData from '@/data/events';
|
|
|
|
import eventsData from '@/data/events';
|
|
|
|
import { Event, EventsByDate } from '@/types';
|
|
|
|
//import { Event, EventsByDate } from '@/types';
|
|
|
|
|
|
|
|
import types from '@/types';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Event = types.Event;
|
|
|
|
|
|
|
|
type EventsByDate = types.EventsByDate;
|
|
|
|
|
|
|
|
|
|
|
|
const Calendar = () => {
|
|
|
|
const Calendar = () => {
|
|
|
|
const { width } = useWindowSize();
|
|
|
|
const { width } = useWindowSize();
|
|
|
|
@@ -23,6 +27,16 @@ const Calendar = () => {
|
|
|
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
|
|
|
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
|
|
|
const [events, setEvents] = useState<EventsByDate>(eventsData);
|
|
|
|
const [events, setEvents] = useState<EventsByDate>(eventsData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const eventsRef = useRef(eventsData);
|
|
|
|
|
|
|
|
eventsRef.current = events;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
document.body.style.touchAction = 'manipulation';
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
|
|
document.body.style.touchAction = '';
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const getWeekDays = (date: Date) => {
|
|
|
|
const getWeekDays = (date: Date) => {
|
|
|
|
const start = startOfWeek(date);
|
|
|
|
const start = startOfWeek(date);
|
|
|
|
return Array.from({ length: 7 }, (_, i) => addDays(start, i));
|
|
|
|
return Array.from({ length: 7 }, (_, i) => addDays(start, i));
|
|
|
|
@@ -53,39 +67,51 @@ const Calendar = () => {
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
}, [handlePreviousWeek, handleNextWeek, selectedEvent]);
|
|
|
|
}, [handlePreviousWeek, handleNextWeek, selectedEvent]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = (event: any) => {
|
|
|
|
const handleDragEnd = useCallback((event: any) => {
|
|
|
|
const { source, location } = event;
|
|
|
|
const { source, location } = event;
|
|
|
|
if (!location?.current?.dropTargets[0]) return;
|
|
|
|
if (!location?.current?.dropTargets[0]) return;
|
|
|
|
|
|
|
|
|
|
|
|
const dropTarget = location.current.dropTargets[0].data;
|
|
|
|
const dropTarget = location.current.dropTargets[0].data;
|
|
|
|
const movedEvent = Object.values(events)
|
|
|
|
const movedEvent = Object.values(eventsRef.current)
|
|
|
|
.flat()
|
|
|
|
.flat()
|
|
|
|
.find(e => e.id === source.data.id);
|
|
|
|
.find(e => e.id === source.data.id);
|
|
|
|
|
|
|
|
|
|
|
|
if (movedEvent) {
|
|
|
|
if (movedEvent) {
|
|
|
|
const newEvents = { ...events };
|
|
|
|
const newEvents = { ...eventsRef.current };
|
|
|
|
const sourceDate = source.data.date;
|
|
|
|
const sourceDate = source.data.date;
|
|
|
|
const targetDate = dropTarget.date;
|
|
|
|
const targetDate = dropTarget.date;
|
|
|
|
|
|
|
|
|
|
|
|
// Remove from source date
|
|
|
|
// Update the event's date when moving between weeks
|
|
|
|
newEvents[sourceDate] = newEvents[sourceDate].filter(e => e.id !== movedEvent.id);
|
|
|
|
const updatedEvent = { ...movedEvent, date: targetDate };
|
|
|
|
// Add to target date
|
|
|
|
|
|
|
|
newEvents[targetDate] = [...(newEvents[targetDate] || []), movedEvent];
|
|
|
|
newEvents[sourceDate] = (newEvents[sourceDate] || []).filter(e => e.id !== movedEvent.id);
|
|
|
|
|
|
|
|
newEvents[targetDate] = [...(newEvents[targetDate] || []), updatedEvent];
|
|
|
|
|
|
|
|
|
|
|
|
setEvents(newEvents);
|
|
|
|
setEvents(newEvents);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
const cleanup = monitorForElements({
|
|
|
|
const cleanup = monitorForElements({
|
|
|
|
onDrop: handleDragEnd,
|
|
|
|
onDrop: handleDragEnd,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return () => cleanup();
|
|
|
|
return () => cleanup();
|
|
|
|
}, [events]);
|
|
|
|
}, [handleDragEnd]);
|
|
|
|
|
|
|
|
|
|
|
|
const swipeHandlers = useSwipeable({
|
|
|
|
const swipeHandlers = useSwipeable({
|
|
|
|
onSwipedLeft: () => handleSwipe('left'),
|
|
|
|
onSwipedLeft: (e) => {
|
|
|
|
onSwipedRight: () => handleSwipe('right'),
|
|
|
|
if (!e.event.target?.closest?.('[data-draggable-event]')) {
|
|
|
|
|
|
|
|
setActiveDay(prev => Math.min(6, prev + 1));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
onSwipedRight: (e) => {
|
|
|
|
|
|
|
|
if (!e.event.target?.closest?.('[data-draggable-event]')) {
|
|
|
|
|
|
|
|
setActiveDay(prev => Math.max(0, prev - 1));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
trackTouch: true,
|
|
|
|
|
|
|
|
delta: 50, // Minimum swipe distance
|
|
|
|
|
|
|
|
preventScrollOnSwipe: true
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const handleSwipe = (dir: 'left' | 'right') => {
|
|
|
|
const handleSwipe = (dir: 'left' | 'right') => {
|
|
|
|
@@ -131,6 +157,7 @@ const Calendar = () => {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const DayColumn = ({ date, events, index, activeDay, onEventClick }: any) => {
|
|
|
|
const DayColumn = ({ date, events, index, activeDay, onEventClick }: any) => {
|
|
|
|
|
|
|
|
const columnRef = useRef<HTMLDivElement>(null);
|
|
|
|
const { width } = useWindowSize();
|
|
|
|
const { width } = useWindowSize();
|
|
|
|
const [isMobile, setIsMobile] = useState(false); // Default to false for SSR
|
|
|
|
const [isMobile, setIsMobile] = useState(false); // Default to false for SSR
|
|
|
|
|
|
|
|
|
|
|
|
@@ -141,21 +168,62 @@ const DayColumn = ({ date, events, index, activeDay, onEventClick }: any) => {
|
|
|
|
const dayOffset = differenceInDays(date, startOfWeek(date));
|
|
|
|
const dayOffset = differenceInDays(date, startOfWeek(date));
|
|
|
|
const isActive = isMobile ? activeDay === dayOffset : true;
|
|
|
|
const isActive = isMobile ? activeDay === dayOffset : true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//useEffect(() => {
|
|
|
|
|
|
|
|
//const element = document.querySelector(`[data-day="${dayOffset}"]`);
|
|
|
|
|
|
|
|
//if (!element) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//return dropTargetForElements({
|
|
|
|
|
|
|
|
//element,
|
|
|
|
|
|
|
|
//getData: () => ({ date: format(date, 'yyyy-MM-dd') }),
|
|
|
|
|
|
|
|
//});
|
|
|
|
|
|
|
|
//}, [date, dayOffset]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//return (
|
|
|
|
|
|
|
|
//<motion.div
|
|
|
|
|
|
|
|
//className={`flex-1 ${isMobile ? 'min-w-[90vw]' : ''}`}
|
|
|
|
|
|
|
|
//data-day={dayOffset}
|
|
|
|
|
|
|
|
//style={{ transform: `translateX(-${activeDay * 100}%)` }}
|
|
|
|
|
|
|
|
//animate={{ x: isMobile ? -activeDay * 100 + '%' : 0 }}
|
|
|
|
|
|
|
|
//transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
|
|
|
|
|
|
//>
|
|
|
|
|
|
|
|
//<div className="h-full bg-gray-50 rounded-lg p-2">
|
|
|
|
|
|
|
|
//<div className="font-bold mb-2">
|
|
|
|
|
|
|
|
//{format(date, 'EEE, MMM d')}
|
|
|
|
|
|
|
|
//</div>
|
|
|
|
|
|
|
|
//{events.map((event: Event) => (
|
|
|
|
|
|
|
|
//<DraggableEvent
|
|
|
|
|
|
|
|
//key={event.id}
|
|
|
|
|
|
|
|
//event={event}
|
|
|
|
|
|
|
|
//date={format(date, 'yyyy-MM-dd')}
|
|
|
|
|
|
|
|
//onEventClick={onEventClick}
|
|
|
|
|
|
|
|
///>
|
|
|
|
|
|
|
|
//))}
|
|
|
|
|
|
|
|
//</div>
|
|
|
|
|
|
|
|
//</motion.div>
|
|
|
|
|
|
|
|
//);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
const element = document.querySelector(`[data-day="${dayOffset}"]`);
|
|
|
|
const element = columnRef.current;
|
|
|
|
if (!element) return;
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
|
|
|
|
return dropTargetForElements({
|
|
|
|
const cleanup = dropTargetForElements({
|
|
|
|
element,
|
|
|
|
element,
|
|
|
|
getData: () => ({ date: format(date, 'yyyy-MM-dd') }),
|
|
|
|
getData: () => ({ date: format(date, 'yyyy-MM-dd') }),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}, [date, dayOffset]);
|
|
|
|
|
|
|
|
|
|
|
|
return cleanup;
|
|
|
|
|
|
|
|
}, [date, activeDay]); // Add activeDay to dependencies
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<motion.div
|
|
|
|
<motion.div
|
|
|
|
|
|
|
|
ref={columnRef}
|
|
|
|
className={`flex-1 ${isMobile ? 'min-w-[90vw]' : ''}`}
|
|
|
|
className={`flex-1 ${isMobile ? 'min-w-[90vw]' : ''}`}
|
|
|
|
data-day={dayOffset}
|
|
|
|
data-day={dayOffset}
|
|
|
|
style={{ transform: `translateX(-${activeDay * 100}%)` }}
|
|
|
|
style={{
|
|
|
|
|
|
|
|
transform: `translateX(-${activeDay * 100}%)`,
|
|
|
|
|
|
|
|
// Ensure all columns are rendered in DOM for desktop
|
|
|
|
|
|
|
|
display: isMobile ? undefined : 'block'
|
|
|
|
|
|
|
|
}}
|
|
|
|
animate={{ x: isMobile ? -activeDay * 100 + '%' : 0 }}
|
|
|
|
animate={{ x: isMobile ? -activeDay * 100 + '%' : 0 }}
|
|
|
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
|
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
@@ -186,6 +254,21 @@ const DraggableEvent = ({ event, date, onEventClick }: { event: Event; date: str
|
|
|
|
return draggable({
|
|
|
|
return draggable({
|
|
|
|
element,
|
|
|
|
element,
|
|
|
|
getInitialData: () => ({ id: event.id, date }),
|
|
|
|
getInitialData: () => ({ id: event.id, date }),
|
|
|
|
|
|
|
|
onDragStart: () => {
|
|
|
|
|
|
|
|
element.style.zIndex = '9999';
|
|
|
|
|
|
|
|
element.style.transform = 'scale(1.05)';
|
|
|
|
|
|
|
|
element.style.boxShadow = '0 8px 16px rgba(0,0,0,0.2)';
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
onDrop: ({ location }) => {
|
|
|
|
|
|
|
|
element.style.zIndex = '';
|
|
|
|
|
|
|
|
element.style.transform = '';
|
|
|
|
|
|
|
|
element.style.boxShadow = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Force reflow to ensure drop targets update
|
|
|
|
|
|
|
|
if (!location?.current?.dropTargets[0]) {
|
|
|
|
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}, [event.id, date]);
|
|
|
|
}, [event.id, date]);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -194,7 +277,11 @@ const DraggableEvent = ({ event, date, onEventClick }: { event: Event; date: str
|
|
|
|
ref={ref}
|
|
|
|
ref={ref}
|
|
|
|
layoutId={event.id}
|
|
|
|
layoutId={event.id}
|
|
|
|
onClick={() => onEventClick(event)}
|
|
|
|
onClick={() => onEventClick(event)}
|
|
|
|
className="bg-white p-4 rounded shadow mb-2"
|
|
|
|
className="bg-white p-4 rounded shadow mb-2 select-none"
|
|
|
|
|
|
|
|
style={{
|
|
|
|
|
|
|
|
touchAction: 'none',
|
|
|
|
|
|
|
|
cursor: 'grab',
|
|
|
|
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<h3 className="font-medium">{event.title}</h3>
|
|
|
|
<h3 className="font-medium">{event.title}</h3>
|
|
|
|
<p className="text-sm text-gray-500">{event.time}</p>
|
|
|
|
<p className="text-sm text-gray-500">{event.time}</p>
|
|
|
|
|