Browse Source

Drag to sides to move to next day on mobile

main
Kevin Mok 6 days ago
parent
commit
49152b6924
  1. 10
      src/app/globals.css
  2. 123
      src/components/Calendar.tsx
  3. 7
      src/types/index.ts
  4. 2
      tsconfig.json

10
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;
}
}

123
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<Event | null>(null);
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 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<HTMLDivElement>(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 (
//<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(() => {
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 (
<motion.div
ref={columnRef}
className={`flex-1 ${isMobile ? 'min-w-[90vw]' : ''}`}
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 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
@ -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',
}}
>
<h3 className="font-medium">{event.title}</h3>
<p className="text-sm text-gray-500">{event.time}</p>

7
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 };

2
tsconfig.json

@ -19,7 +19,7 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

Loading…
Cancel
Save