Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Follow-Up Calendar</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .calendar-day:hover { | |
| transform: scale(1.05); | |
| transition: transform 0.2s ease; | |
| } | |
| .event-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| } | |
| .modal-overlay { | |
| background-color: rgba(0, 0, 0, 0.5); | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.3s ease-in-out; | |
| } | |
| .event-card { | |
| transition: all 0.2s ease; | |
| } | |
| .event-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .section-toggle { | |
| transition: all 0.3s ease; | |
| } | |
| .section-toggle:hover { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| @media (max-width: 640px) { | |
| .month-grid { | |
| grid-template-columns: repeat(7, minmax(0, 1fr)); | |
| } | |
| .week-grid { | |
| grid-template-columns: repeat(1, minmax(0, 1fr)); | |
| } | |
| .week-day-header { | |
| display: none; | |
| } | |
| .week-day-mobile { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0.5rem; | |
| border-bottom: 1px solid #e5e7eb; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect } = React; | |
| const FollowUpCalendar = () => { | |
| const [currentDate, setCurrentDate] = useState(new Date()); | |
| const [events, setEvents] = useState([]); | |
| const [showModal, setShowModal] = useState(false); | |
| const [selectedDate, setSelectedDate] = useState(null); | |
| const [newEvent, setNewEvent] = useState({ | |
| title: '', | |
| description: '', | |
| time: '09:00', | |
| type: 'meeting' | |
| }); | |
| const [viewMode, setViewMode] = useState('month'); // 'month' or 'week' | |
| const [selectedEvent, setSelectedEvent] = useState(null); | |
| const [showCompleted, setShowCompleted] = useState(false); | |
| const [showAll, setShowAll] = useState(false); | |
| // Sample initial events | |
| useEffect(() => { | |
| const sampleEvents = [ | |
| { | |
| id: 1, | |
| title: 'Client Meeting', | |
| description: 'Discuss project requirements with ABC Corp', | |
| date: new Date(new Date().setDate(new Date().getDate() + 1)), | |
| time: '10:30', | |
| type: 'meeting', | |
| completed: false | |
| }, | |
| { | |
| id: 2, | |
| title: 'Follow-up Call', | |
| description: 'Check on product delivery status', | |
| date: new Date(new Date().setDate(new Date().getDate() + 3)), | |
| time: '14:00', | |
| type: 'call', | |
| completed: false | |
| }, | |
| { | |
| id: 3, | |
| title: 'Email Reminder', | |
| description: 'Send proposal to XYZ Inc', | |
| date: new Date(new Date().setDate(new Date().getDate() - 2)), | |
| time: '11:15', | |
| type: 'email', | |
| completed: true | |
| }, | |
| { | |
| id: 4, | |
| title: 'Project Review', | |
| description: 'Review project milestones with team', | |
| date: new Date(new Date().setDate(new Date().getDate() - 5)), | |
| time: '15:30', | |
| type: 'meeting', | |
| completed: true | |
| } | |
| ]; | |
| setEvents(sampleEvents); | |
| }, []); | |
| const navigateMonth = (direction) => { | |
| const newDate = new Date(currentDate); | |
| newDate.setMonth(newDate.getMonth() + direction); | |
| setCurrentDate(newDate); | |
| }; | |
| const navigateWeek = (direction) => { | |
| const newDate = new Date(currentDate); | |
| newDate.setDate(newDate.getDate() + (direction * 7)); | |
| setCurrentDate(newDate); | |
| }; | |
| const getDaysInMonth = () => { | |
| const year = currentDate.getFullYear(); | |
| const month = currentDate.getMonth(); | |
| const firstDay = new Date(year, month, 1); | |
| const lastDay = new Date(year, month + 1, 0); | |
| const daysInMonth = lastDay.getDate(); | |
| const startingDay = firstDay.getDay(); | |
| const days = []; | |
| // Add empty cells for days before the first day of the month | |
| for (let i = 0; i < startingDay; i++) { | |
| days.push(null); | |
| } | |
| // Add days of the month | |
| for (let i = 1; i <= daysInMonth; i++) { | |
| const date = new Date(year, month, i); | |
| days.push(date); | |
| } | |
| return days; | |
| }; | |
| const getWeekDays = () => { | |
| const weekStart = new Date(currentDate); | |
| weekStart.setDate(weekStart.getDate() - weekStart.getDay()); | |
| const weekDays = []; | |
| for (let i = 0; i < 7; i++) { | |
| const day = new Date(weekStart); | |
| day.setDate(day.getDate() + i); | |
| weekDays.push(day); | |
| } | |
| return weekDays; | |
| }; | |
| const getEventsForDate = (date) => { | |
| if (!date) return []; | |
| return events.filter(event => | |
| event.date.getDate() === date.getDate() && | |
| event.date.getMonth() === date.getMonth() && | |
| event.date.getFullYear() === date.getFullYear() | |
| ); | |
| }; | |
| const handleDateClick = (date) => { | |
| if (!date) return; | |
| setSelectedDate(date); | |
| setShowModal(true); | |
| setNewEvent({ | |
| title: '', | |
| description: '', | |
| time: '09:00', | |
| type: 'meeting' | |
| }); | |
| }; | |
| const handleEventClick = (event, e) => { | |
| e.stopPropagation(); | |
| setSelectedEvent(event); | |
| }; | |
| const handleAddEvent = () => { | |
| if (!newEvent.title || !selectedDate) return; | |
| const event = { | |
| id: events.length + 1, | |
| title: newEvent.title, | |
| description: newEvent.description, | |
| date: new Date(selectedDate), | |
| time: newEvent.time, | |
| type: newEvent.type, | |
| completed: false | |
| }; | |
| setEvents([...events, event]); | |
| setShowModal(false); | |
| }; | |
| const handleDeleteEvent = (id) => { | |
| setEvents(events.filter(event => event.id !== id)); | |
| setSelectedEvent(null); | |
| }; | |
| const toggleEventCompletion = (id) => { | |
| setEvents(events.map(event => | |
| event.id === id ? { ...event, completed: !event.completed } : event | |
| )); | |
| }; | |
| const isToday = (date) => { | |
| if (!date) return false; | |
| const today = new Date(); | |
| return date.getDate() === today.getDate() && | |
| date.getMonth() === today.getMonth() && | |
| date.getFullYear() === today.getFullYear(); | |
| }; | |
| const getEventTypeColor = (type) => { | |
| switch (type) { | |
| case 'meeting': return 'bg-blue-500'; | |
| case 'call': return 'bg-green-500'; | |
| case 'email': return 'bg-purple-500'; | |
| case 'task': return 'bg-yellow-500'; | |
| default: return 'bg-gray-500'; | |
| } | |
| }; | |
| const getEventTypeIcon = (type) => { | |
| switch (type) { | |
| case 'meeting': return 'fa-users'; | |
| case 'call': return 'fa-phone'; | |
| case 'email': return 'fa-envelope'; | |
| case 'task': return 'fa-check-circle'; | |
| default: return 'fa-calendar'; | |
| } | |
| }; | |
| const renderMonthView = () => { | |
| const days = getDaysInMonth(); | |
| const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; | |
| return ( | |
| <div className="bg-white rounded-lg shadow overflow-hidden"> | |
| <div className="grid grid-cols-7 gap-1 p-2 bg-gray-100"> | |
| {weekdays.map(day => ( | |
| <div key={day} className="text-center font-medium text-gray-600 py-2 text-xs sm:text-sm"> | |
| {day} | |
| </div> | |
| ))} | |
| </div> | |
| <div className="grid month-grid grid-cols-7 gap-1 p-2"> | |
| {days.map((date, index) => { | |
| const dateEvents = getEventsForDate(date); | |
| return ( | |
| <div | |
| key={index} | |
| onClick={() => handleDateClick(date)} | |
| className={`calendar-day min-h-16 sm:min-h-24 p-1 sm:p-2 border rounded-lg cursor-pointer transition-all ${date ? 'hover:bg-gray-50' : 'bg-gray-50'} ${isToday(date) ? 'border-blue-400 border-2' : 'border-gray-200'}`} | |
| > | |
| {date && ( | |
| <> | |
| <div className="flex justify-between items-center mb-1"> | |
| <span className={`font-medium text-xs sm:text-sm ${isToday(date) ? 'text-blue-600' : 'text-gray-700'}`}> | |
| {date.getDate()} | |
| </span> | |
| {dateEvents.length > 0 && ( | |
| <div className="flex space-x-1"> | |
| {dateEvents.slice(0, 2).map(event => ( | |
| <div | |
| key={event.id} | |
| className={`event-dot ${getEventTypeColor(event.type)}`} | |
| /> | |
| ))} | |
| {dateEvents.length > 2 && <span className="text-xs text-gray-500 hidden sm:inline">+{dateEvents.length - 2}</span>} | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-1 overflow-hidden hidden sm:block"> | |
| {dateEvents.slice(0, 2).map(event => ( | |
| <div | |
| key={event.id} | |
| onClick={(e) => handleEventClick(event, e)} | |
| className={`text-xs p-1 rounded truncate ${getEventTypeColor(event.type)} text-white`} | |
| > | |
| {event.time} {event.title} | |
| </div> | |
| ))} | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const renderWeekView = () => { | |
| const weekDays = getWeekDays(); | |
| return ( | |
| <div className="bg-white rounded-lg shadow overflow-hidden"> | |
| <div className="grid week-day-header grid-cols-7 gap-1 p-2 bg-gray-100"> | |
| {weekDays.map(day => { | |
| const dateEvents = getEventsForDate(day); | |
| return ( | |
| <div key={day} className="text-center"> | |
| <div className="font-medium text-gray-600 text-xs sm:text-sm"> | |
| {day.toLocaleDateString('en-US', { weekday: 'short' })} | |
| </div> | |
| <div | |
| className={`mx-auto w-6 h-6 sm:w-8 sm:h-8 flex items-center justify-center rounded-full text-xs sm:text-sm ${isToday(day) ? 'bg-blue-500 text-white' : 'text-gray-700'}`} | |
| > | |
| {day.getDate()} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div className="grid week-grid grid-cols-1 sm:grid-cols-7 gap-2 sm:gap-4 p-2 sm:p-4 min-h-96"> | |
| {weekDays.map(day => { | |
| const dateEvents = getEventsForDate(day); | |
| return ( | |
| <div key={day} className="border rounded-lg"> | |
| <div | |
| onClick={() => handleDateClick(day)} | |
| className={`week-day-mobile sm:hidden ${isToday(day) ? 'bg-blue-50' : ''}`} | |
| > | |
| <span className="font-medium"> | |
| {day.toLocaleDateString('en-US', { weekday: 'short' })}, {day.getDate()} | |
| </span> | |
| {dateEvents.length > 0 && ( | |
| <span className="text-xs bg-gray-200 px-2 py-1 rounded-full"> | |
| {dateEvents.length} event{dateEvents.length !== 1 ? 's' : ''} | |
| </span> | |
| )} | |
| </div> | |
| <div | |
| onClick={() => handleDateClick(day)} | |
| className={`p-2 cursor-pointer ${isToday(day) ? 'border-blue-400' : 'border-gray-200'}`} | |
| > | |
| <div className="space-y-2"> | |
| {dateEvents.length > 0 ? ( | |
| dateEvents.map(event => ( | |
| <div | |
| key={event.id} | |
| onClick={(e) => handleEventClick(event, e)} | |
| className={`p-2 rounded-lg ${getEventTypeColor(event.type)} text-white`} | |
| > | |
| <div className="flex items-center"> | |
| <i className={`fas ${getEventTypeIcon(event.type)} mr-2 text-xs sm:text-sm`}></i> | |
| <div className="truncate"> | |
| <div className="font-medium text-xs sm:text-sm">{event.title}</div> | |
| <div className="text-xs">{event.time}</div> | |
| </div> | |
| </div> | |
| </div> | |
| )) | |
| ) : ( | |
| <div className="text-gray-400 text-center py-4 text-xs sm:text-sm">No events</div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const renderEventCard = (event) => { | |
| return ( | |
| <div | |
| key={event.id} | |
| onClick={() => setSelectedEvent(event)} | |
| className={`event-card p-3 sm:p-4 rounded-lg cursor-pointer ${getEventTypeColor(event.type)} text-white mb-2 sm:mb-3`} | |
| > | |
| <div className="flex justify-between items-start"> | |
| <div className="flex-1"> | |
| <div className="font-medium text-sm sm:text-base mb-1">{event.title}</div> | |
| <div className="text-xs sm:text-sm mb-1">{event.date.toLocaleDateString()} at {event.time}</div> | |
| {event.description && ( | |
| <div className="text-xs opacity-90 truncate">{event.description}</div> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <i className={`fas ${getEventTypeIcon(event.type)} text-sm sm:text-base`}></i> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| toggleEventCompletion(event.id); | |
| }} | |
| className={`w-5 h-5 rounded-full flex items-center justify-center ${event.completed ? 'bg-white text-green-500' : 'bg-white bg-opacity-30'}`} | |
| > | |
| {event.completed && <i className="fas fa-check text-xs"></i>} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gray-100 p-2 sm:p-4 md:p-8"> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="flex flex-col sm:flex-row justify-between items-center mb-4 sm:mb-6"> | |
| <h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-800 mb-2 sm:mb-0"> | |
| Follow-Up Calendar | |
| </h1> | |
| <div className="flex space-x-2 sm:space-x-4"> | |
| <button | |
| onClick={() => setViewMode('month')} | |
| className={`px-3 py-1 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm ${viewMode === 'month' ? 'bg-blue-500 text-white' : 'bg-white text-gray-700'}`} | |
| > | |
| Month | |
| </button> | |
| <button | |
| onClick={() => setViewMode('week')} | |
| className={`px-3 py-1 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm ${viewMode === 'week' ? 'bg-blue-500 text-white' : 'bg-white text-gray-700'}`} | |
| > | |
| Week | |
| </button> | |
| </div> | |
| </div> | |
| <div className="bg-white rounded-lg shadow p-2 sm:p-4 mb-4 sm:mb-6"> | |
| <div className="flex flex-col sm:flex-row justify-between items-center mb-3 sm:mb-4"> | |
| <div className="flex items-center space-x-2 sm:space-x-4 mb-2 sm:mb-0"> | |
| <button | |
| onClick={viewMode === 'month' ? () => navigateMonth(-1) : () => navigateWeek(-1)} | |
| className="p-1 sm:p-2 rounded-full hover:bg-gray-100" | |
| > | |
| <i className="fas fa-chevron-left text-gray-600 text-sm sm:text-base"></i> | |
| </button> | |
| <h2 className="text-lg sm:text-xl font-semibold text-gray-800"> | |
| {currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} | |
| </h2> | |
| <button | |
| onClick={viewMode === 'month' ? () => navigateMonth(1) : () => navigateWeek(1)} | |
| className="p-1 sm:p-2 rounded-full hover:bg-gray-100" | |
| > | |
| <i className="fas fa-chevron-right text-gray-600 text-sm sm:text-base"></i> | |
| </button> | |
| <button | |
| onClick={() => setCurrentDate(new Date())} | |
| className="px-2 py-1 sm:px-3 sm:py-1 text-xs sm:text-sm bg-gray-200 rounded-lg hover:bg-gray-300" | |
| > | |
| Today | |
| </button> | |
| </div> | |
| <button | |
| onClick={() => { | |
| setSelectedDate(new Date()); | |
| setShowModal(true); | |
| }} | |
| className="px-3 py-1 sm:px-4 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center text-xs sm:text-sm" | |
| > | |
| <i className="fas fa-plus mr-1 sm:mr-2 text-xs sm:text-sm"></i> | |
| Add Event | |
| </button> | |
| </div> | |
| {viewMode === 'month' ? renderMonthView() : renderWeekView()} | |
| </div> | |
| {/* Upcoming Follow-Ups */} | |
| <div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-4 sm:mb-6"> | |
| <h3 className="text-base sm:text-lg font-semibold text-gray-800 mb-3 sm:mb-4">Upcoming Follow-Ups</h3> | |
| <div className="space-y-3 sm:space-y-4"> | |
| {events | |
| .filter(event => !event.completed && event.date >= new Date()) | |
| .sort((a, b) => a.date - b.date) | |
| .slice(0, 5) | |
| .map(event => renderEventCard(event))} | |
| {events.filter(event => !event.completed && event.date >= new Date()).length === 0 && ( | |
| <div className="text-center py-4 text-gray-500 text-sm">No upcoming follow-ups</div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Completed Follow-Ups */} | |
| <div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-4 sm:mb-6"> | |
| <div | |
| className="flex justify-between items-center cursor-pointer mb-3 sm:mb-4 section-toggle p-2 rounded-lg" | |
| onClick={() => setShowCompleted(!showCompleted)} | |
| > | |
| <h3 className="text-base sm:text-lg font-semibold text-gray-800">Completed Follow-Ups</h3> | |
| <i className={`fas ${showCompleted ? 'fa-chevron-up' : 'fa-chevron-down'} text-gray-500`}></i> | |
| </div> | |
| {showCompleted && ( | |
| <div className="space-y-3 sm:space-y-4"> | |
| {events | |
| .filter(event => event.completed) | |
| .sort((a, b) => b.date - a.date) | |
| .slice(0, 5) | |
| .map(event => renderEventCard(event))} | |
| {events.filter(event => event.completed).length === 0 && ( | |
| <div className="text-center py-4 text-gray-500 text-sm">No completed follow-ups</div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| {/* All Follow-Ups */} | |
| <div className="bg-white rounded-lg shadow p-4 sm:p-6"> | |
| <div | |
| className="flex justify-between items-center cursor-pointer mb-3 sm:mb-4 section-toggle p-2 rounded-lg" | |
| onClick={() => setShowAll(!showAll)} | |
| > | |
| <h3 className="text-base sm:text-lg font-semibold text-gray-800">All Follow-Ups</h3> | |
| <i className={`fas ${showAll ? 'fa-chevron-up' : 'fa-chevron-down'} text-gray-500`}></i> | |
| </div> | |
| {showAll && ( | |
| <div className="space-y-3 sm:space-y-4"> | |
| {events | |
| .sort((a, b) => a.date - b.date) | |
| .map(event => renderEventCard(event))} | |
| {events.length === 0 && ( | |
| <div className="text-center py-4 text-gray-500 text-sm">No follow-ups created yet</div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Add Event Modal */} | |
| {showModal && ( | |
| <div className="fixed inset-0 flex items-center justify-center z-50 modal-overlay fade-in p-2 sm:p-4"> | |
| <div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-2 sm:mx-4"> | |
| <div className="p-4 sm:p-6"> | |
| <div className="flex justify-between items-center mb-3 sm:mb-4"> | |
| <h3 className="text-lg sm:text-xl font-semibold text-gray-800"> | |
| {selectedDate?.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} | |
| </h3> | |
| <button | |
| onClick={() => setShowModal(false)} | |
| className="text-gray-500 hover:text-gray-700" | |
| > | |
| <i className="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div className="space-y-3 sm:space-y-4"> | |
| <div> | |
| <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Event Type</label> | |
| <div className="grid grid-cols-4 gap-1 sm:gap-2"> | |
| {['meeting', 'call', 'email', 'task'].map(type => ( | |
| <button | |
| key={type} | |
| onClick={() => setNewEvent({...newEvent, type})} | |
| className={`p-1 sm:p-2 rounded-lg flex flex-col items-center text-xs ${newEvent.type === type ? getEventTypeColor(type) + ' text-white' : 'bg-gray-100 text-gray-700'}`} | |
| > | |
| <i className={`fas ${getEventTypeIcon(type)} mb-1 text-xs sm:text-sm`}></i> | |
| <span className="capitalize">{type}</span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div> | |
| <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Title</label> | |
| <input | |
| type="text" | |
| value={newEvent.title} | |
| onChange={(e) => setNewEvent({...newEvent, title: e.target.value})} | |
| className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm" | |
| placeholder="Enter event title" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Time</label> | |
| <input | |
| type="time" | |
| value={newEvent.time} | |
| onChange={(e) => setNewEvent({...newEvent, time: e.target.value})} | |
| className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Description</label> | |
| <textarea | |
| value={newEvent.description} | |
| onChange={(e) => setNewEvent({...newEvent, description: e.target.value})} | |
| className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm" | |
| rows="3" | |
| placeholder="Enter event details" | |
| ></textarea> | |
| </div> | |
| <div className="flex justify-end space-x-2 sm:space-x-3 pt-3 sm:pt-4"> | |
| <button | |
| onClick={() => setShowModal(false)} | |
| className="px-3 sm:px-4 py-1 sm:py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-xs sm:text-sm" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleAddEvent} | |
| className="px-3 sm:px-4 py-1 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-xs sm:text-sm" | |
| > | |
| Add Event | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Event Detail Modal */} | |
| {selectedEvent && ( | |
| <div className="fixed inset-0 flex items-center justify-center z-50 modal-overlay fade-in p-2 sm:p-4"> | |
| <div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-2 sm:mx-4"> | |
| <div className="p-4 sm:p-6"> | |
| <div className="flex justify-between items-center mb-3 sm:mb-4"> | |
| <h3 className="text-lg sm:text-xl font-semibold text-gray-800"> | |
| {selectedEvent.title} | |
| </h3> | |
| <button | |
| onClick={() => setSelectedEvent(null)} | |
| className="text-gray-500 hover:text-gray-700" | |
| > | |
| <i className="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div className={`p-3 sm:p-4 rounded-lg mb-3 sm:mb-4 ${getEventTypeColor(selectedEvent.type)} text-white`}> | |
| <div className="flex items-center mb-1 sm:mb-2"> | |
| <i className={`fas ${getEventTypeIcon(selectedEvent.type)} mr-2 text-sm sm:text-xl`}></i> | |
| <span className="capitalize font-medium text-xs sm:text-sm">{selectedEvent.type}</span> | |
| </div> | |
| <div className="text-xs sm:text-sm"> | |
| {selectedEvent.date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} at {selectedEvent.time} | |
| </div> | |
| </div> | |
| <div className="mb-3 sm:mb-4"> | |
| <h4 className="text-xs sm:text-sm font-medium text-gray-700 mb-1">Description</h4> | |
| <p className="text-gray-800 text-xs sm:text-sm">{selectedEvent.description || 'No description provided'}</p> | |
| </div> | |
| <div className="flex items-center mb-4 sm:mb-5"> | |
| <span className="text-xs sm:text-sm font-medium text-gray-700 mr-2">Status:</span> | |
| <button | |
| onClick={() => toggleEventCompletion(selectedEvent.id)} | |
| className={`px-3 py-1 rounded-lg text-xs sm:text-sm ${selectedEvent.completed ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`} | |
| > | |
| {selectedEvent.completed ? 'Completed' : 'Pending'} | |
| </button> | |
| </div> | |
| <div className="flex justify-end space-x-2 sm:space-x-3 pt-3 sm:pt-4"> | |
| <button | |
| onClick={() => handleDeleteEvent(selectedEvent.id)} | |
| className="px-3 sm:px-4 py-1 sm:py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 text-xs sm:text-sm" | |
| > | |
| Delete | |
| </button> | |
| <button | |
| onClick={() => { | |
| setSelectedDate(selectedEvent.date); | |
| setNewEvent({ | |
| title: selectedEvent.title, | |
| description: selectedEvent.description, | |
| time: selectedEvent.time, | |
| type: selectedEvent.type | |
| }); | |
| setSelectedEvent(null); | |
| setShowModal(true); | |
| }} | |
| className="px-3 sm:px-4 py-1 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-xs sm:text-sm" | |
| > | |
| Edit | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const root = ReactDOM.createRoot(document.getElementById('root')); | |
| root.render(<FollowUpCalendar />); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=localhost-llm/follow-up" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |