From 49152b69247ae11c26d70fc1d5eea942f1d72a57 Mon Sep 17 00:00:00 2001 From: Kevin Mok Date: Fri, 28 Mar 2025 12:37:13 -0400 Subject: [PATCH] Drag to sides to move to next day on mobile --- src/app/globals.css | 10 +++ src/components/Calendar.tsx | 123 ++++++++++++++++++++++++++++++------ src/types/index.ts | 7 +- tsconfig.json | 2 +- 4 files changed, 121 insertions(+), 21 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..14436ae 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,3 +24,13 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +[data-draggable-event] { + user-select: none; +} + +@media (pointer: coarse) { + .touch-none { + touch-action: none; + } +} diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 63cf780..1631a99 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -4,11 +4,15 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { dropTargetForElements, monitorForElements, draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { motion, AnimatePresence } from 'framer-motion'; 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 EventModal from '@/components/EventModal'; 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 { width } = useWindowSize(); @@ -23,6 +27,16 @@ const Calendar = () => { const [selectedEvent, setSelectedEvent] = useState(null); const [events, setEvents] = useState(eventsData); + const eventsRef = useRef(eventsData); + eventsRef.current = events; + + useEffect(() => { + document.body.style.touchAction = 'manipulation'; + return () => { + document.body.style.touchAction = ''; + }; + }, []); + const getWeekDays = (date: Date) => { const start = startOfWeek(date); return Array.from({ length: 7 }, (_, i) => addDays(start, i)); @@ -53,39 +67,51 @@ const Calendar = () => { return () => window.removeEventListener('keydown', handleKeyDown); }, [handlePreviousWeek, handleNextWeek, selectedEvent]); - const handleDragEnd = (event: any) => { + const handleDragEnd = useCallback((event: any) => { const { source, location } = event; if (!location?.current?.dropTargets[0]) return; const dropTarget = location.current.dropTargets[0].data; - const movedEvent = Object.values(events) + const movedEvent = Object.values(eventsRef.current) .flat() .find(e => e.id === source.data.id); if (movedEvent) { - const newEvents = { ...events }; + const newEvents = { ...eventsRef.current }; const sourceDate = source.data.date; const targetDate = dropTarget.date; - // Remove from source date - newEvents[sourceDate] = newEvents[sourceDate].filter(e => e.id !== movedEvent.id); - // Add to target date - newEvents[targetDate] = [...(newEvents[targetDate] || []), movedEvent]; + // Update the event's date when moving between weeks + const updatedEvent = { ...movedEvent, date: targetDate }; + + newEvents[sourceDate] = (newEvents[sourceDate] || []).filter(e => e.id !== movedEvent.id); + newEvents[targetDate] = [...(newEvents[targetDate] || []), updatedEvent]; setEvents(newEvents); } - }; + }, []); useEffect(() => { const cleanup = monitorForElements({ onDrop: handleDragEnd, }); return () => cleanup(); - }, [events]); + }, [handleDragEnd]); const swipeHandlers = useSwipeable({ - onSwipedLeft: () => handleSwipe('left'), - onSwipedRight: () => handleSwipe('right'), + onSwipedLeft: (e) => { + 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') => { @@ -131,6 +157,7 @@ const Calendar = () => { }; const DayColumn = ({ date, events, index, activeDay, onEventClick }: any) => { + const columnRef = useRef(null); const { width } = useWindowSize(); 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 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 ( + // + //
+ //
+ //{format(date, 'EEE, MMM d')} + //
+ //{events.map((event: Event) => ( + // + //))} + //
+ //
+ //); + useEffect(() => { - const element = document.querySelector(`[data-day="${dayOffset}"]`); + const element = columnRef.current; if (!element) return; - return dropTargetForElements({ + const cleanup = dropTargetForElements({ element, getData: () => ({ date: format(date, 'yyyy-MM-dd') }), }); - }, [date, dayOffset]); + + return cleanup; + }, [date, activeDay]); // Add activeDay to dependencies return ( @@ -186,6 +254,21 @@ const DraggableEvent = ({ event, date, onEventClick }: { event: Event; date: str return draggable({ element, 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]); @@ -194,7 +277,11 @@ const DraggableEvent = ({ event, date, onEventClick }: { event: Event; date: str ref={ref} layoutId={event.id} 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', + }} >

{event.title}

{event.time}

diff --git a/src/types/index.ts b/src/types/index.ts index da8ae7a..e0f14e2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ -export interface Event { +console.log('Types module loaded'); +interface Event { id: string; title: string; description: string; @@ -6,6 +7,8 @@ export interface Event { time: string; } -export interface EventsByDate { +interface EventsByDate { [date: string]: Event[]; } + +export default { Event, EventsByDate }; diff --git a/tsconfig.json b/tsconfig.json index c133409..f6b33fe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],