Skip to content

Commit

Permalink
Enhance calendar functionality with drag-and-drop support, dynamic ev…
Browse files Browse the repository at this point in the history
…ent handling, and improved state management. Update event positioning and collision detection in both day and week views. Refactor appointment creation and event updates for better user experience.
  • Loading branch information
salvinoto committed Dec 2, 2024
1 parent 3d4717e commit eee613f
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 71 deletions.
55 changes: 30 additions & 25 deletions examples/basic/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,63 @@ import 'kabob-cal/dist/globals.css';
import { ChevronLeft, ChevronRight } from 'lucide-react';

const App = () => {
// Sample people data
// Define people (e.g., doctors)
const people: Person[] = [
{
id: 'doctor1',
name: 'Dr. Smith',
color: 'blue' // indigo color
color: 'blue'
},
{
id: 'doctor2',
name: 'Dr. Johnson',
color: 'pink' // red color
color: 'pink'
}
];

// Sample appointments/events
const events: CalendarEvent[] = [
const today = new Date();
const [currentDate, setCurrentDate] = useState(today);
const [events, setEvents] = useState<CalendarEvent[]>([
{
id: '1',
title: 'Annual Checkup',
start: new Date(2024, 0, 15, 10, 0), // Jan 15, 2024, 10:00 AM
end: new Date(2024, 0, 15, 11, 0), // Jan 15, 2024, 11:00 AM
start: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 0),
end: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 11, 0),
personId: 'doctor1'
},
{
id: '2',
title: 'Dental Cleaning',
start: new Date(2024, 0, 15, 14, 30), // Jan 15, 2024, 2:30 PM
end: new Date(2024, 0, 15, 15, 30), // Jan 15, 2024, 3:30 PM
start: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 14, 30),
end: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 15, 30),
personId: 'doctor2'
},
{
id: '3',
title: 'Follow-up Visit',
start: new Date(2024, 0, 16, 9, 0), // Jan 16, 2024, 9:00 AM
end: new Date(2024, 0, 16, 9, 30), // Jan 16, 2024, 9:30 AM
personId: 'doctor1'
}
];
]);

const handleEventClick = (event: CalendarEvent) => {
console.log('Appointment clicked:', event);
};

const handleAddAppointment = (date: Date) => {
console.log('Add appointment clicked for date:', date);
};
const newEvent: CalendarEvent = {
id: String(Date.now()),
title: 'New Appointment',
start: date,
end: new Date(date.getTime() + 60 * 60 * 1000), // 1 hour duration
personId: people[0].id
};

const [currentDate, setCurrentDate] = useState(new Date(2024, 0, 1)); // Initial fixed date
setEvents(prevEvents => [...prevEvents, newEvent]);
};

useEffect(() => {
setCurrentDate(new Date()); // Update to current date after mount
}, []);
const handleUpdateEvent = (updatedEvent: CalendarEvent) => {
console.log('Updating event:', updatedEvent);
setEvents(prevEvents =>
prevEvents.map(event =>
event.id === updatedEvent.id ? updatedEvent : event
)
);
};

return (
<div className="h-screen w-full p-4 bg-white">
Expand All @@ -65,10 +70,11 @@ const App = () => {
people={people}
defaultSelectedPersonIds={['doctor1', 'doctor2']}
onAddAppointment={handleAddAppointment}
onEventClick={handleEventClick}
onUpdateEvent={handleUpdateEvent}
>
<div className="flex h-full flex-col space-y-4">
<div className="flex flex-wrap items-center justify-between gap-4">
{/* Person selector and view controls */}
<div className="flex flex-wrap justify-center items-center gap-2 w-full sm:w-auto">
<CalendarPersonSelector className="w-full sm:w-auto" />
<div className="flex flex-wrap justify-center items-center gap-1">
Expand Down Expand Up @@ -99,7 +105,6 @@ const App = () => {
</div>
</div>

{/* Navigation controls */}
<div className="flex flex-wrap justify-center items-center gap-2 w-full sm:w-auto sm:justify-end">
<CalendarCurrentDate className="text-center sm:text-right min-w-24" />
<div className="flex items-center gap-1">
Expand Down
17 changes: 14 additions & 3 deletions src/components/DraggableEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ export function DraggableEvent({ event, person, view, style }: DraggableEventPro
} = useSortable({
id: event.id,
data: { event },
strategy: undefined
});

const dragStyle: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
transition: isDragging ? undefined : transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
zIndex: isDragging ? 50 : 1,
touchAction: 'none',
userSelect: 'none',
...style,
};

Expand All @@ -40,7 +44,9 @@ export function DraggableEvent({ event, person, view, style }: DraggableEventPro
style={dragStyle}
{...attributes}
{...listeners}
className="px-1 rounded text-sm flex items-center gap-1"
className="px-1 rounded text-sm flex items-center gap-1 select-none touch-none"
data-id={event.id}
data-type="event"
>
<div
className={cn(
Expand All @@ -64,12 +70,17 @@ export function DraggableEvent({ event, person, view, style }: DraggableEventPro
{...attributes}
{...listeners}
className={cn(
'relative',
'relative select-none touch-none',
dayEventVariants({ variant: person?.color || event.color })
)}
data-id={event.id}
data-type="event"
>
<div className="text-xs font-semibold">{event.title}</div>
<div className="text-xs">{person?.name}</div>
<time className="text-xs opacity-75">
{format(event.start, 'HH:mm')} - {format(event.end, 'HH:mm')}
</time>
</div>
);
}
103 changes: 82 additions & 21 deletions src/components/views/CalendarDayView.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCalendar } from '../../context/CalendarContext';
import { TimeTable } from '../TimeTable';
import { setHours, isSameHour, differenceInMinutes } from 'date-fns';
import { setHours, isSameHour, differenceInMinutes, setMinutes, startOfDay, isSameDay } from 'date-fns';
import { cn } from '../../lib/utils';
import { DraggableEvent } from '../DraggableEvent';
import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core';
import { DndContext, DragEndEvent, closestCenter, DragStartEvent, DragOverEvent } from '@dnd-kit/core';
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable';
import { CalendarEvent } from '../../types';

Expand All @@ -15,48 +15,104 @@ export const CalendarDayView = () => {
people,
selectedPersonIds,
onAddAppointment,
onUpdateEvent
onUpdateEvent,
setEvents
} = useCalendar();

const handleTimeClick = (hour: Date) => {
onAddAppointment?.(hour);
};

const calculateTimeFromYPosition = (y: number, containerRect: DOMRect) => {
const hourHeight = 80; // height of each hour block in pixels
const scrollTop = containerRect.top;
const relativeY = y - scrollTop;
const totalMinutes = (relativeY / hourHeight) * 60;
return Math.max(0, Math.min(totalMinutes, 24 * 60 - 1)); // Clamp between 0 and 23:59
};

const handleDragStart = (event: DragStartEvent) => {
console.log('Drag start:', event);
};

const handleDragOver = (event: DragOverEvent) => {
console.log('Drag over:', event);
};

const handleDragEnd = (event: DragEndEvent) => {
console.log('Drag end:', event);
const { active, over } = event;

if (!over || !active.data.current) return;

const draggedEvent = active.data.current.event as CalendarEvent;
const dropDate = new Date(over.id);

if (isSameHour(draggedEvent.start, dropDate)) return;
const hourElement = document.getElementById(over.id as string);
if (!hourElement) return;

const containerRect = hourElement.getBoundingClientRect();
const minutes = calculateTimeFromYPosition(event.over?.rect.top ?? 0, containerRect);

// Calculate time difference between start and end
// Calculate new times
const duration = draggedEvent.end.getTime() - draggedEvent.start.getTime();

// Create new start date
const newStart = new Date(dropDate);

// Create new end date
const newStart = setMinutes(setHours(new Date(over.id), 0), minutes);
const newEnd = new Date(newStart.getTime() + duration);

const updatedEvent = {
...draggedEvent,
start: newStart,
end: newEnd,
};

onUpdateEvent?.(updatedEvent);
// Check for collisions
const collidingEvents = events.filter(e =>
e.id !== draggedEvent.id &&
e.start.getTime() < newEnd.getTime() &&
e.end.getTime() > newStart.getTime()
);

if (collidingEvents.length > 0) {
// Push colliding events down
const updatedEvents = events.map(e => {
if (collidingEvents.find(ce => ce.id === e.id)) {
const pushDuration = newEnd.getTime() - e.start.getTime();
return {
...e,
start: new Date(e.start.getTime() + pushDuration),
end: new Date(e.end.getTime() + pushDuration)
};
}
return e;
});

// Update dragged event
const finalEvents = updatedEvents.map(e =>
e.id === draggedEvent.id
? { ...draggedEvent, start: newStart, end: newEnd }
: e
);

setEvents(finalEvents);
finalEvents.forEach(e => {
if (e.id === draggedEvent.id || collidingEvents.find(ce => ce.id === e.id)) {
onUpdateEvent?.(e);
}
});
} else {
// No collisions, just update the dragged event
const updatedEvent = {
...draggedEvent,
start: newStart,
end: newEnd,
};

onUpdateEvent?.(updatedEvent);
}
};

if (view !== 'day') return null;

const hours = [...Array(24)].map((_, i) => setHours(date, i));
const dayStart = startOfDay(date);

return (
<DndContext
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex relative pt-2 overflow-auto h-full">
Expand All @@ -71,16 +127,21 @@ export const CalendarDayView = () => {
role="button"
tabIndex={0}
id={hour.toISOString()}
data-type="hour"
>
<div className="h-20 border-t last:border-b">
{events
.filter(
(event) =>
isSameDay(event.start, dayStart) &&
isSameHour(event.start, hour) &&
selectedPersonIds.includes(event.personId)
)
.map((event) => {
const person = people.find((p) => p.id === event.personId);
const startMinutes = event.start.getMinutes();
const duration = (event.end.getTime() - event.start.getTime()) / (60 * 60 * 1000);

return (
<DraggableEvent
key={event.id}
Expand All @@ -89,10 +150,10 @@ export const CalendarDayView = () => {
view="day"
style={{
position: 'absolute',
top: 0,
top: `${(startMinutes / 60) * 100}%`,
left: 0,
right: 0,
height: `${(event.end.getTime() - event.start.getTime()) / (60 * 60 * 1000) * 100}%`,
height: `${duration * 100}%`,
}}
/>
);
Expand Down
Loading

0 comments on commit eee613f

Please sign in to comment.