Fix Calendar Drag & Drop Duplicate Events Issue

� Fixed Issues:
- Fixed duplicate events when dragging from sidebar to calendar
- Used info.revert() to prevent FullCalendar auto-adding events
- Manual event management through React state for better control
- Single event display after drag & drop (no more duplicates)

� Features Enhanced:
- Drag & Drop from sidebar to calendar (no duplicates)
- Internal calendar event dragging still works
- Event tracking in 'Recently Dropped Events' section
- Remove after drop option functionality
- Beautiful UI with 4 pre-existing events and 10 draggable events
- Dynamic dates based on current date for better visibility

 Technical Improvements:
- Better state management for calendar events
- Clean event handling with proper React patterns
- Console logging for debugging (no alert popups)
- Stable and reliable drag & drop experience
- Professional UX with smooth interactions

� UI/UX:
- Color-coded events with emoji icons
- Responsive calendar layout
- Event tracking with timestamps
- Clean and modern interface
- dayMaxEvents=3 for optimal display
This commit is contained in:
tuanOts 2025-06-14 21:35:02 +07:00
parent 71fd633073
commit a5d79ad10f
3 changed files with 1242 additions and 167 deletions

View File

@ -1,11 +1,12 @@
/* eslint-disable no-dupe-keys */
/* eslint-disable no-const-assign */
/* eslint-disable no-unused-vars */
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { Draggable } from "@fullcalendar/interaction";
// import "../../assets/plugins/fullcalendar/fullcalendar.min.css";
import "../../style/css/fullcalendar.min.css";
// import FullCalendar from '@fullcalendar/react/dist/main.esm.js';
@ -25,35 +26,102 @@ const Calendar = () => {
[category_color, setcategory_color] = useState(""),
[calenderevent, setcalenderevent] = useState(""),
[weekendsVisible, setweekendsVisible] = useState(true),
[currentEvents, setscurrentEvents] = useState([]),
defaultEvents = [
[currentEvents, setscurrentEvents] = useState([]);
const calendarRef = useRef(null);
// State to store dropped events temporarily
const [droppedEvents, setDroppedEvents] = useState([]);
// Combined events state for calendar display - using current dates
const today = new Date();
const [calendarEvents, setCalendarEvents] = useState([
{
title: "Event Name 4",
start: Date.now() + 148000000,
className: "bg-purple",
id: 'default-1',
title: "🎯 Existing Meeting",
start: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 0), // Today 10:00 AM
end: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 11, 0), // Today 11:00 AM
className: "bg-primary",
},
{
title: "Test Event 1",
start: Date.now(),
end: Date.now(),
id: 'default-2',
title: "📈 Weekly Review",
start: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 14, 0), // Tomorrow 2:00 PM
end: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 15, 30), // Tomorrow 3:30 PM
className: "bg-success",
},
{
title: "Test Event 2",
start: Date.now() + 168000000,
id: 'default-3',
title: "🚀 Product Demo",
start: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 9, 0), // Day after tomorrow 9:00 AM
end: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 10, 0), // Day after tomorrow 10:00 AM
className: "bg-info",
},
{
title: "Test Event 3",
start: Date.now() + 338000000,
className: "bg-primary",
id: 'default-4',
title: "🎨 Design Review",
start: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 16, 0), // 3 days from now 4:00 PM
end: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3, 17, 0), // 3 days from now 5:00 PM
className: "bg-warning",
},
];
]);
useEffect(() => {
let elements = Array.from(
document.getElementsByClassName("react-datepicker-wrapper")
);
elements.map((element) => element.classList.add("width-100"));
// Initialize external draggable events with simple hide/show
const draggableEl = document.getElementById("calendar-events");
if (draggableEl) {
new Draggable(draggableEl, {
itemSelector: ".calendar-events",
eventData: function(eventEl) {
const title = eventEl.innerText.trim();
const className = eventEl.getAttribute("data-class");
return {
title: title,
className: className,
duration: "01:00" // 1 hour default duration
};
},
longPressDelay: 0,
touchTimeoutDelay: 0
});
// Store reference to currently dragging element
let currentDragElement = null;
let dragHelper = null;
// Listen for drag start from external elements
draggableEl.addEventListener('dragstart', function(e) {
const target = e.target.closest('.calendar-events');
if (target) {
currentDragElement = target;
// Hide the original item when dragging starts
setTimeout(() => {
if (currentDragElement) {
currentDragElement.classList.add('dragging-hidden');
}
}, 10); // Small delay to let drag start
}
});
// Simple approach - just hide the original item during drag
// No custom helper, let FullCalendar handle the drag visual
// Listen for drag end
document.addEventListener('dragend', function(e) {
if (currentDragElement) {
currentDragElement.classList.remove('dragging-hidden');
currentDragElement = null;
}
if (dragHelper && dragHelper.parentNode) {
dragHelper.parentNode.removeChild(dragHelper);
dragHelper = null;
}
});
}
}, []);
const handleChange = (date) => {
@ -85,6 +153,54 @@ const Calendar = () => {
setisnewevent(true);
setaddneweventobj(selectInfo);
};
const handleEventReceive = (info) => {
// Handle external drag and drop
console.log("Event received:", info.event);
// Prevent FullCalendar from automatically adding the event
// We'll handle it manually to avoid duplicates
info.revert();
// Create event object
const newEvent = {
id: `dropped-${Date.now()}`,
title: info.event.title,
start: info.event.start,
end: info.event.end || new Date(info.event.start.getTime() + 60 * 60 * 1000), // Default 1 hour duration
className: info.event.classNames[0] || 'bg-primary',
droppedAt: new Date().toLocaleString(),
source: 'external'
};
// Add to calendar events state to display on calendar
setCalendarEvents(prev => [...prev, newEvent]);
// Add to dropped events list for tracking
setDroppedEvents(prev => [...prev, newEvent]);
// Show success notification in console only
console.log("✅ Event successfully dropped:", newEvent);
// Show the original item again (in case it was hidden)
const draggedEl = info.draggedEl;
if (draggedEl) {
draggedEl.classList.remove('dragging-hidden');
}
// Check if "Remove after drop" is checked
const removeAfterDrop = document.getElementById("drop-remove").checked;
if (removeAfterDrop) {
// Remove the dragged element from the external list
info.draggedEl.remove();
console.log("🗑️ Original event removed from sidebar");
}
};
const handleEventDrop = (info) => {
// Handle internal event drag and drop
console.log("Event dropped:", info.event);
};
const addnewevent = () => {
let calendarApi = addneweventobj.view.calendar;
@ -116,13 +232,13 @@ const Calendar = () => {
setiseditdelete(false);
};
const clickupdateevent = () => {
const newArray = defaultEvents;
const newArray = [...calendarEvents];
for (let i = 0; i < newArray.length; i++) {
if (newArray[i].id === parseInt(calenderevent.id)) {
newArray[i].title = event_title;
}
}
defaultEvents = newArray;
setCalendarEvents(newArray);
setiseditdelete(false);
};
@ -144,17 +260,17 @@ const Calendar = () => {
return (
<>
<div className="page-wrapper">
<div className="page-wrapper calendar-page-wrapper">
<div className="content">
<div className="page-header">
<div className="calendar-page-header">
<div className="row align-items-center w-100">
<div className="col-lg-10 col-sm-12">
<h3 className="page-title">Calendar</h3>
<div className="col-lg-8 col-sm-12">
<h3 className="page-title">📅 Beautiful Calendar</h3>
</div>
<div className="col-lg-2 col-sm-12">
<div className="col-lg-4 col-sm-12 text-end">
<a
to="#"
className="btn btn-primary"
href="#"
className="calendar-create-btn"
data-bs-toggle="modal"
data-bs-target="#add_event"
>
@ -165,19 +281,38 @@ const Calendar = () => {
</div>
<div className="row">
<div className="col-lg-3 col-md-4">
<h4 className="card-title">Drag &amp; Drop Event</h4>
<div className="calendar-sidebar">
<h4 className="card-title">🎯 Drag & Drop Events</h4>
<div id="calendar-events" className="mb-3">
<div className="calendar-events" data-class="bg-info">
<i className="fas fa-circle text-info" /> My Event One
<div className="calendar-events" data-class="bg-primary">
<i className="fas fa-circle" /> 👥 Team Meeting
</div>
<div className="calendar-events" data-class="bg-success">
<i className="fas fa-circle text-success" /> My Event Two
</div>
<div className="calendar-events" data-class="bg-danger">
<i className="fas fa-circle text-danger" /> My Event Three
<i className="fas fa-circle" /> 📊 Project Review
</div>
<div className="calendar-events" data-class="bg-warning">
<i className="fas fa-circle text-warning" /> My Event Four
<i className="fas fa-circle" /> 📞 Client Call
</div>
<div className="calendar-events" data-class="bg-danger">
<i className="fas fa-circle" /> 🎨 Design Workshop
</div>
<div className="calendar-events" data-class="bg-info">
<i className="fas fa-circle" /> 💻 Code Review
</div>
<div className="calendar-events" data-class="bg-secondary">
<i className="fas fa-circle" /> 🍽 Lunch Break
</div>
<div className="calendar-events" data-class="bg-purple">
<i className="fas fa-circle" /> 📚 Training Session
</div>
<div className="calendar-events" data-class="bg-success">
<i className="fas fa-circle" /> 🏃 Sprint Planning
</div>
<div className="calendar-events" data-class="bg-info">
<i className="fas fa-circle" /> 🔍 Bug Triage
</div>
<div className="calendar-events" data-class="bg-warning">
<i className="fas fa-circle" /> Coffee Chat
</div>
</div>
<div className="checkbox mb-3">
@ -185,18 +320,54 @@ const Calendar = () => {
<label htmlFor="drop-remove">Remove after drop</label>
</div>
<a
to="#"
href="#"
data-bs-toggle="modal"
data-bs-target="#add_new_event"
className="btn mb-3 btn-primary btn-block w-100"
className="calendar-add-category-btn"
>
<i className="fas fa-plus" /> Add Category
</a>
{/* Dropped Events Tracker */}
{droppedEvents.length > 0 && (
<div className="mt-4">
<h5 className="text-success"> Recently Dropped Events ({droppedEvents.length})</h5>
<div className="dropped-events-list">
{droppedEvents.slice(-5).map((event, index) => (
<div key={event.id} className="dropped-event-item mb-2 p-2 border rounded">
<div className="d-flex justify-content-between align-items-center">
<div>
<strong>{event.title}</strong>
<br />
<small className="text-muted">
📅 {event.start.toLocaleDateString()} at {event.start.toLocaleTimeString()}
</small>
<br />
<small className="text-success">
Dropped: {event.droppedAt}
</small>
</div>
<span className={`badge ${event.className}`}>
{event.className.replace('bg-', '')}
</span>
</div>
</div>
))}
{droppedEvents.length > 5 && (
<small className="text-muted">
... and {droppedEvents.length - 5} more events
</small>
)}
</div>
</div>
)}
</div>
</div>
<div className="col-lg-9 col-md-8">
<div className="card bg-white">
<div className="calendar-main-card">
<div className="card-body">
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
headerToolbar={{
left: "prev,next today",
@ -207,11 +378,15 @@ const Calendar = () => {
editable={true}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
dayMaxEvents={3} // Show max 3 events per day, then +more
weekends={weekendsVisible}
initialEvents={defaultEvents} // alternatively, use the `events` setting to fetch from a feed
droppable={true} // Enable dropping external events
dragScroll={true}
events={calendarEvents} // Use dynamic events state
select={handleDateSelect}
eventClick={(clickInfo) => handleEventClick(clickInfo)}
eventReceive={handleEventReceive} // Handle external drops
eventDrop={handleEventDrop} // Handle internal drops
/>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1434,3 +1434,152 @@ ul.ant-pagination.ant-table-pagination li button.ant-pagination-item-link {
.section-notes-slider .notes-slider .slick-list .slick-slide div{
margin-left: 12px !important;
}
// Calendar Dark Mode Integration
[data-layout-mode="dark_mode"] {
.calendar-page-wrapper {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
}
.calendar-page-header {
background: rgba(29, 29, 66, 0.95) !important;
border: 1px solid rgba(103, 116, 142, 0.3) !important;
h3 {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
}
}
.calendar-sidebar {
background: rgba(29, 29, 66, 0.95) !important;
border: 1px solid rgba(103, 116, 142, 0.3) !important;
h4 {
color: #ffffff !important;
}
}
.calendar-main-card {
background: rgba(29, 29, 66, 0.95) !important;
border: 1px solid rgba(103, 116, 142, 0.3) !important;
}
.fc-col-header-cell {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
color: #ffffff !important;
}
.fc-daygrid-day-number {
color: #ffffff !important;
&:hover {
color: #ff9f43 !important;
}
}
.fc-day-today {
background: linear-gradient(135deg, rgba(255, 159, 67, 0.1) 0%, rgba(254, 202, 87, 0.1) 100%) !important;
border: 2px solid rgba(255, 159, 67, 0.3) !important;
}
.fc-toolbar-title {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
}
.fc-button-primary {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
box-shadow: 0 4px 15px rgba(255, 159, 67, 0.3) !important;
&:hover {
background: linear-gradient(135deg, #feca57 0%, #ff9f43 100%) !important;
box-shadow: 0 6px 20px rgba(255, 159, 67, 0.4) !important;
}
&:not(:disabled):active,
&:not(:disabled).fc-button-active {
background: linear-gradient(135deg, #e8890a 0%, #ff9f43 100%) !important;
box-shadow: 0 2px 10px rgba(255, 159, 67, 0.5) !important;
}
}
.fc-daygrid-day:hover {
background: rgba(255, 159, 67, 0.05) !important;
}
.calendar-create-btn {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
box-shadow: 0 8px 25px rgba(255, 159, 67, 0.3) !important;
&:hover {
background: linear-gradient(135deg, #feca57 0%, #ff9f43 100%) !important;
box-shadow: 0 12px 35px rgba(255, 159, 67, 0.4) !important;
}
}
.calendar-add-category-btn {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
box-shadow: 0 8px 25px rgba(255, 159, 67, 0.3) !important;
&:hover {
background: linear-gradient(135deg, #feca57 0%, #ff9f43 100%) !important;
box-shadow: 0 12px 35px rgba(255, 159, 67, 0.4) !important;
}
}
// Calendar Modal Dark Mode
.modal.custom-modal {
.modal-content {
background: rgba(29, 29, 66, 0.95) !important;
border: 1px solid rgba(103, 116, 142, 0.3) !important;
}
.modal-header {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
}
.modal-body {
background: rgba(29, 29, 66, 0.95) !important;
.form-group {
label {
color: #ffffff !important;
}
.form-control {
background: rgba(29, 29, 66, 0.8) !important;
border-color: rgba(103, 116, 142, 0.5) !important;
color: #ffffff !important;
&:focus {
border-color: #ff9f43 !important;
box-shadow: 0 0 0 3px rgba(255, 159, 67, 0.1) !important;
}
&::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
}
}
}
.submit-btn {
background: linear-gradient(135deg, #ff9f43 0%, #feca57 100%) !important;
box-shadow: 0 8px 25px rgba(255, 159, 67, 0.3) !important;
&:hover {
background: linear-gradient(135deg, #feca57 0%, #ff9f43 100%) !important;
box-shadow: 0 12px 35px rgba(255, 159, 67, 0.4) !important;
}
}
}
.checkbox label {
color: #ffffff !important;
}
}