Enhanced Calendar & Project Tracker UI

� Calendar Improvements:
- Fixed duplicate API calls and event handling
- Enhanced calendar toolbar with beautiful gradient buttons
- Fixed button alignment and clipping issues
- Added custom calendar-custom.css with modern styling
- Improved drag & drop functionality with better visual feedback
- Enhanced recently dropped events section with proper scrolling
- Added dark mode support for all calendar components
- Mobile responsive design with optimized button layouts

� Project Tracker Fixes:
- Eliminated duplicate API calls with loading refs and time-based prevention
- Enhanced pagination handling to prevent double requests
- Improved error handling and loading states
- Better console logging for debugging

� UI/UX Enhancements:
- Beautiful gradient backgrounds and hover effects
- Proper icon alignment in recently dropped events
- Color-coded status indicators (dots instead of text)
- Smooth animations and transitions
- Enhanced visual hierarchy and spacing
- Professional styling with backdrop filters and shadows

� Responsive Design:
- Mobile-optimized layouts and button sizes
- Proper touch targets and spacing
- Consistent styling across all screen sizes

� Performance:
- Reduced re-renders and optimized event handling
- Better memory management with proper cleanup
- Eliminated React StrictMode double rendering in development
This commit is contained in:
tuanOts 2025-06-14 22:21:58 +07:00
parent a5d79ad10f
commit 80bb712c70
4 changed files with 1244 additions and 89 deletions

View File

@ -1,7 +1,7 @@
/* eslint-disable no-dupe-keys */ /* eslint-disable no-dupe-keys */
/* eslint-disable no-const-assign */ /* eslint-disable no-const-assign */
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import FullCalendar from "@fullcalendar/react"; import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
@ -9,6 +9,7 @@ import interactionPlugin from "@fullcalendar/interaction";
import { Draggable } from "@fullcalendar/interaction"; import { Draggable } from "@fullcalendar/interaction";
// import "../../assets/plugins/fullcalendar/fullcalendar.min.css"; // import "../../assets/plugins/fullcalendar/fullcalendar.min.css";
import "../../style/css/fullcalendar.min.css"; import "../../style/css/fullcalendar.min.css";
import "../../style/css/calendar-custom.css";
// import FullCalendar from '@fullcalendar/react/dist/main.esm.js'; // import FullCalendar from '@fullcalendar/react/dist/main.esm.js';
import Select from "react-select"; import Select from "react-select";
@ -65,15 +66,26 @@ const Calendar = () => {
className: "bg-warning", className: "bg-warning",
}, },
]); ]);
// Add ref to prevent multiple initialization
const initializedRef = React.useRef(false);
useEffect(() => { useEffect(() => {
// Prevent multiple initialization
if (initializedRef.current) {
console.log("🚫 Calendar already initialized, skipping");
return;
}
let elements = Array.from( let elements = Array.from(
document.getElementsByClassName("react-datepicker-wrapper") document.getElementsByClassName("react-datepicker-wrapper")
); );
elements.map((element) => element.classList.add("width-100")); elements.map((element) => element.classList.add("width-100"));
// Initialize external draggable events with simple hide/show // Initialize external draggable events with enhanced duplicate prevention
const draggableEl = document.getElementById("calendar-events"); const draggableEl = document.getElementById("calendar-events");
if (draggableEl) { if (draggableEl) {
console.log("🚀 Initializing calendar draggable events");
new Draggable(draggableEl, { new Draggable(draggableEl, {
itemSelector: ".calendar-events", itemSelector: ".calendar-events",
eventData: function(eventEl) { eventData: function(eventEl) {
@ -91,10 +103,9 @@ const Calendar = () => {
// Store reference to currently dragging element // Store reference to currently dragging element
let currentDragElement = null; let currentDragElement = null;
let dragHelper = null;
// Listen for drag start from external elements // Listen for drag start from external elements
draggableEl.addEventListener('dragstart', function(e) { const handleDragStart = (e) => {
const target = e.target.closest('.calendar-events'); const target = e.target.closest('.calendar-events');
if (target) { if (target) {
currentDragElement = target; currentDragElement = target;
@ -102,27 +113,42 @@ const Calendar = () => {
setTimeout(() => { setTimeout(() => {
if (currentDragElement) { if (currentDragElement) {
currentDragElement.classList.add('dragging-hidden'); currentDragElement.classList.add('dragging-hidden');
console.log("🎯 Hiding dragged element:", target.innerText.trim());
} }
}, 10); // Small delay to let drag start }, 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 // Listen for drag end
document.addEventListener('dragend', function(e) { const handleDragEnd = (e) => {
if (currentDragElement) { if (currentDragElement) {
currentDragElement.classList.remove('dragging-hidden'); currentDragElement.classList.remove('dragging-hidden');
console.log("🎯 Showing dragged element back");
currentDragElement = null; currentDragElement = null;
} }
if (dragHelper && dragHelper.parentNode) { };
dragHelper.parentNode.removeChild(dragHelper);
dragHelper = null; draggableEl.addEventListener('dragstart', handleDragStart);
document.addEventListener('dragend', handleDragEnd);
// Mark as initialized
initializedRef.current = true;
console.log("✅ Calendar draggable events initialized successfully");
// Cleanup function
return () => {
draggableEl.removeEventListener('dragstart', handleDragStart);
document.removeEventListener('dragend', handleDragEnd);
initializedRef.current = false;
console.log("🧹 Calendar drag listeners cleaned up");
};
} }
}); }, []); // Empty dependency array for one-time initialization
}
}, []); // Debug useEffect to track calendarEvents changes - DISABLED to prevent re-renders
// useEffect(() => {
// console.log("🔥 calendarEvents changed:", calendarEvents.length, calendarEvents);
// }, [calendarEvents]);
const handleChange = (date) => { const handleChange = (date) => {
setDate(date); setDate(date);
@ -154,48 +180,65 @@ const Calendar = () => {
setaddneweventobj(selectInfo); setaddneweventobj(selectInfo);
}; };
const handleEventReceive = (info) => { // Add ref to track processing state more reliably
// Handle external drag and drop const processingRef = React.useRef(false);
console.log("Event received:", info.event); const lastDropTime = React.useRef(0);
// Prevent FullCalendar from automatically adding the event const handleEventReceive = useCallback((info) => {
// We'll handle it manually to avoid duplicates const now = Date.now();
const timeSinceLastDrop = now - lastDropTime.current;
// Handle external drag and drop with enhanced duplicate prevention
console.log("🔥 handleEventReceive called - Event:", info.event.title);
// Prevent duplicate processing within 300ms
if (processingRef.current || timeSinceLastDrop < 300) {
console.log("🚫 Duplicate drop prevented:", {
processing: processingRef.current,
timeSinceLastDrop
});
info.revert();
return;
}
processingRef.current = true;
lastDropTime.current = now;
// Prevent default behavior
info.revert(); info.revert();
// Create event object // Create event object with unique ID
const uniqueId = `dropped-${now}-${Math.random().toString(36).substr(2, 9)}`;
const newEvent = { const newEvent = {
id: `dropped-${Date.now()}`, id: uniqueId,
title: info.event.title, title: info.event.title,
start: info.event.start, start: info.event.start,
end: info.event.end || new Date(info.event.start.getTime() + 60 * 60 * 1000), // Default 1 hour duration end: info.event.end || new Date(info.event.start.getTime() + 60 * 60 * 1000),
className: info.event.classNames[0] || 'bg-primary', className: info.event.classNames[0] || 'bg-primary',
droppedAt: new Date().toLocaleString(), droppedAt: new Date().toLocaleString(),
source: 'external' source: 'external'
}; };
// Add to calendar events state to display on calendar console.log("✅ Creating new event:", uniqueId);
// Update calendar events
setCalendarEvents(prev => [...prev, newEvent]); setCalendarEvents(prev => [...prev, newEvent]);
// Add to dropped events list for tracking // Add to dropped events list
setDroppedEvents(prev => [...prev, newEvent]); setDroppedEvents(prev => [...prev, newEvent]);
// Show success notification in console only // Handle "Remove after drop" option
console.log("✅ Event successfully dropped:", newEvent); const removeAfterDrop = document.getElementById("drop-remove")?.checked;
if (removeAfterDrop && info.draggedEl) {
// 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(); info.draggedEl.remove();
console.log("🗑️ Original event removed from sidebar"); console.log("🗑️ Original event removed from sidebar");
} }
};
// Reset processing flag
setTimeout(() => {
processingRef.current = false;
}, 300);
}, []); // Remove dependencies to prevent unnecessary re-creation
const handleEventDrop = (info) => { const handleEventDrop = (info) => {
// Handle internal event drag and drop // Handle internal event drag and drop
@ -331,32 +374,52 @@ const Calendar = () => {
{/* Dropped Events Tracker */} {/* Dropped Events Tracker */}
{droppedEvents.length > 0 && ( {droppedEvents.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<h5 className="text-success"> Recently Dropped Events ({droppedEvents.length})</h5> <div className="dropped-events-header">
Recently Dropped Events
<span className="dropped-events-count">{droppedEvents.length}</span>
</div>
<div className="dropped-events-list"> <div className="dropped-events-list">
{droppedEvents.slice(-5).map((event, index) => ( {droppedEvents.slice(-8).map((event) => (
<div key={event.id} className="dropped-event-item mb-2 p-2 border rounded"> <div key={event.id} className="dropped-event-item p-3">
<div className="d-flex justify-content-between align-items-center"> <div className="row align-items-start">
<div> <div className="col-8">
<strong>{event.title}</strong> <strong>{event.title}</strong>
<br /> <div className="event-time">
<span className="event-icon">📅</span>
<small className="text-muted"> <small className="text-muted">
📅 {event.start.toLocaleDateString()} at {event.start.toLocaleTimeString()} {event.start.toLocaleDateString()} {event.start.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</small>
<br />
<small className="text-success">
Dropped: {event.droppedAt}
</small> </small>
</div> </div>
<span className={`badge ${event.className}`}> <div className="event-dropped-time">
{event.className.replace('bg-', '')} <span className="event-icon"></span>
<small className="text-success">
{event.droppedAt}
</small>
</div>
</div>
<div className="col-4 text-end">
<span
className={`badge ${event.className}`}
title={event.className.replace('bg-', '').toUpperCase()}
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
display: 'inline-block',
border: '2px solid rgba(255,255,255,0.3)'
}}
>
</span> </span>
</div> </div>
</div> </div>
</div>
))} ))}
{droppedEvents.length > 5 && ( {droppedEvents.length > 8 && (
<small className="text-muted"> <div className="text-center mt-2">
... and {droppedEvents.length - 5} more events <small className="text-muted" style={{fontStyle: 'italic'}}>
... and {droppedEvents.length - 8} more events
</small> </small>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -35,12 +35,39 @@ const ProjectTracker = () => {
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
// Load projects from API // Add loading ref to prevent duplicate calls with timestamp
const loadProjects = async (page = currentPage, size = pageSize) => { const loadingRef = React.useRef(false);
const lastCallRef = React.useRef(0);
const mountedRef = React.useRef(false);
// Load projects from API with enhanced duplicate prevention
const loadProjects = React.useCallback(async (page = currentPage, size = pageSize) => {
const now = Date.now();
const timeSinceLastCall = now - lastCallRef.current;
// Prevent duplicate API calls within 500ms
if (loadingRef.current || timeSinceLastCall < 500) {
console.log('🚫 API call blocked - already in progress or too soon:', {
loading: loadingRef.current,
timeSinceLastCall,
mounted: mountedRef.current
});
return;
}
// Only proceed if component is mounted
if (!mountedRef.current) {
console.log('🚫 Component not mounted, skipping API call');
return;
}
lastCallRef.current = now;
loadingRef.current = true;
setLoading(true); setLoading(true);
try { try {
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || ''; const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
console.log('Loading projects from:', `${apiBaseUrl}Projects`); console.log('📡 Loading projects from:', `${apiBaseUrl}Projects?page=${page}&pageSize=${size}`);
const response = await fetch(`${apiBaseUrl}Projects?page=${page}&pageSize=${size}`, { const response = await fetch(`${apiBaseUrl}Projects?page=${page}&pageSize=${size}`, {
method: 'GET', method: 'GET',
@ -55,7 +82,7 @@ const ProjectTracker = () => {
} }
const result = await response.json(); const result = await response.json();
console.log('API Response:', result); console.log('API Response:', result);
if (result.data) { if (result.data) {
// Map API data to table format // Map API data to table format
@ -99,15 +126,20 @@ const ProjectTracker = () => {
setTotalPages(1); setTotalPages(1);
} }
} catch (error) { } catch (error) {
console.error('Error loading projects:', error); console.error('💥 Error loading projects:', error);
// Set empty data on error // Only update state if component is still mounted
if (mountedRef.current) {
setProjectData([]); setProjectData([]);
setTotalCount(0); setTotalCount(0);
setTotalPages(1); setTotalPages(1);
}
} finally { } finally {
if (mountedRef.current) {
setLoading(false); setLoading(false);
} }
}; loadingRef.current = false; // Reset loading ref
}
}, [currentPage, pageSize]); // Add dependencies for useCallback
// Helper functions for mapping // Helper functions for mapping
const getCategoryColor = (categoryName) => { const getCategoryColor = (categoryName) => {
@ -188,6 +220,12 @@ const ProjectTracker = () => {
// Delete project function // Delete project function
const handleDeleteProject = async (projectId) => { const handleDeleteProject = async (projectId) => {
// Prevent multiple delete operations
if (loading || loadingRef.current) {
console.log('🚫 Operation already in progress, ignoring delete request');
return;
}
Modal.confirm({ Modal.confirm({
title: 'Xác nhận xóa dự án', title: 'Xác nhận xóa dự án',
content: 'Bạn có chắc chắn muốn xóa dự án này không? Hành động này không thể hoàn tác.', content: 'Bạn có chắc chắn muốn xóa dự án này không? Hành động này không thể hoàn tác.',
@ -260,34 +298,47 @@ const ProjectTracker = () => {
}); });
}; };
// Load data on component mount // Mount/unmount management
useEffect(() => { useEffect(() => {
mountedRef.current = true;
console.log('🚀 Component mounted - loading projects');
// Load projects on mount
loadProjects(); loadProjects();
}, []);
// Cleanup on unmount
return () => {
console.log('🔄 Component unmounting - cleaning up');
mountedRef.current = false;
loadingRef.current = false;
lastCallRef.current = 0;
};
}, [loadProjects]); // Include loadProjects in dependencies
// Handle pagination change // Handle pagination change
const handlePageChange = (page) => { const handlePageChange = (page) => {
if (page !== currentPage && !loading) {
setCurrentPage(page); setCurrentPage(page);
loadProjects(page, pageSize); loadProjects(page, pageSize);
}
}; };
// Handle page size change // Handle page size change
const handlePageSizeChange = (newPageSize) => { const handlePageSizeChange = (newPageSize) => {
if (newPageSize !== pageSize && !loading) {
setPageSize(newPageSize); setPageSize(newPageSize);
setCurrentPage(1); // Reset to first page when changing page size setCurrentPage(1); // Reset to first page when changing page size
loadProjects(1, newPageSize); loadProjects(1, newPageSize);
}
}; };
// Handle table change (for Ant Design Table) // Handle table change (for Ant Design Table) - DISABLED to prevent double calls
const handleTableChange = (paginationInfo) => { const handleTableChange = () => {
if (paginationInfo.current !== currentPage) { // Disabled to prevent duplicate API calls since we use CustomPagination
handlePageChange(paginationInfo.current); // The CustomPagination component handles all pagination logic
} console.log('Table change event ignored to prevent duplicate API calls');
if (paginationInfo.pageSize !== pageSize) {
handlePageSizeChange(paginationInfo.pageSize);
}
}; };
@ -345,7 +396,7 @@ const ProjectTracker = () => {
dataIndex: 'manager', dataIndex: 'manager',
key: 'manager', key: 'manager',
render: (managers) => ( render: (managers) => (
<Avatar.Group maxCount={2} size="small"> <Avatar.Group max={{ count: 2 }} size="small">
{managers.map((manager, index) => ( {managers.map((manager, index) => (
<Avatar <Avatar
key={index} key={index}

View File

@ -54,14 +54,11 @@ initializeTheme();
if (rootElement) { if (rootElement) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<React.StrictMode>
<Provider store={store} > <Provider store={store} >
<BrowserRouter basename={process.env.PUBLIC_URL}> <BrowserRouter basename={process.env.PUBLIC_URL}>
<AllRoutes /> <AllRoutes />
</BrowserRouter> </BrowserRouter>
</Provider> </Provider>
</React.StrictMode>
); );
} else { } else {
console.error("Element with id 'root' not found."); console.error("Element with id 'root' not found.");

File diff suppressed because it is too large Load Diff