Exploring different calendar grid examples can significantly enhance your UI design, making it more intuitive and visually appealing. Whether you're building a scheduling app or a project management tool, the right calendar layout can make all the difference.
In this article, we'll dive into ten diverse calendar grid examples that showcase various styles and functionalities. From minimalistic designs to feature-rich layouts, these examples will inspire you to create a calendar that meets your specific needs.
CODE1
Here's the code:
CODETEXT1
CODE2
Here's the code:
CODETEXT2
CODE3
Here's the code:
CODETEXT3
CODE4
Here's the code:
CODETEXT4
CODE5
Here's the code:
CODETEXT5
Designers and developers, elevate your calendar grid designs with Subframe's drag-and-drop interface. Its intuitive, responsive canvas ensures pixel-perfect UI every time, making it a favorite among professionals.
Ready to transform your design process? Start for free today!
CODE6
Here's the code:
CODETEXT6
CODE7
Here's the code:
CODETEXT7
CODE8
Here's the code:
CODETEXT8
CODE9
Here's the code:
CODETEXT9
CODE10
Here's the code:
CODETEXT10
Unlock the power of Subframe to design stunning UIs, including Calendar Grids, with unmatched efficiency. Create pixel-perfect interfaces instantly using our drag-and-drop editor.
Ready to elevate your design game? Start for free and begin creating right away!
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Enterprise Calendar Grid</title> <style> :root { --primary: #2c3e50; --secondary: #34495e; --accent: #3498db; --accent-hover: #2980b9; --light: #ecf0f1; --text: #2c3e50; --text-light: #7f8c8d; --border: #bdc3c7; --success: #27ae60; --warning: #f39c12; --error: #e74c3c; --shadow: rgba(0, 0, 0, 0.1); } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } body { background-color: #f9fafb; color: var(--text); display: flex; justify-content: center; align-items: center; min-height: 100vh; font-size: 14px; line-height: 1.5; } .calendar-container { width: 100%; max-width: 700px; height: 680px; background-color: white; border-radius: 12px; box-shadow: 0 8px 30px var(--shadow); overflow: hidden; display: flex; flex-direction: column; position: relative; transition: all 0.3s ease; } .calendar-header { padding: 20px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); background-color: white; position: relative; z-index: 10; } .calendar-title { font-size: 20px; font-weight: 600; color: var(--primary); } .calendar-controls { display: flex; align-items: center; gap: 16px; } .calendar-btn { background: none; border: none; color: var(--text); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.2s ease; } .calendar-btn:hover { background-color: var(--light); } .calendar-btn.active { background-color: var(--accent); color: white; } .timezone-selector { position: relative; margin-left: 8px; } .timezone-btn { display: flex; align-items: center; gap: 4px; background-color: var(--light); padding: 8px 12px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .timezone-btn:hover { background-color: #dfe6e9; } .timezone-dropdown { position: absolute; top: 100%; right: 0; width: 240px; background-color: white; border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); padding: 8px 0; margin-top: 8px; z-index: 100; display: none; max-height: 300px; overflow-y: auto; } .timezone-dropdown.active { display: block; animation: fadeIn 0.2s ease; } .timezone-option { padding: 8px 16px; cursor: pointer; transition: all 0.2s ease; } .timezone-option:hover { background-color: var(--light); } .timezone-option.selected { font-weight: 500; color: var(--accent); } .calendar-navigation { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; border-bottom: 1px solid var(--border); } .calendar-month { font-size: 18px; font-weight: 600; } .nav-buttons { display: flex; gap: 8px; } .nav-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: none; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .nav-btn:hover { background-color: var(--light); border-color: var(--text-light); } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; overflow-y: auto; padding-bottom: 16px; } .calendar-weekday { text-align: center; padding: 12px 0; font-weight: 500; color: var(--text-light); border-bottom: 1px solid var(--border); position: sticky; top: 0; background-color: white; z-index: 1; } .calendar-day { min-height: 130px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: 8px; position: relative; overflow: hidden; } .calendar-day:nth-child(7n) { border-right: none; } .current-day .day-number { background-color: var(--accent); color: white; } .day-number { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; font-weight: 500; border-radius: 50%; margin-bottom: 4px; } .other-month { color: var(--text-light); background-color: #f9fafb; } .events-container { margin-top: 4px; } .event { padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; position: relative; transition: all 0.2s ease; border-left: 3px solid; } .event:hover { transform: translateY(-1px); box-shadow: 0 2px 5px var(--shadow); } .event-meeting { background-color: rgba(52, 152, 219, 0.1); border-left-color: var(--accent); } .event-deadline { background-color: rgba(231, 76, 60, 0.1); border-left-color: var(--error); } .event-reminder { background-color: rgba(241, 196, 15, 0.1); border-left-color: var(--warning); } .event-completed { background-color: rgba(46, 204, 113, 0.1); border-left-color: var(--success); } .event-modal { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: none; justify-content: center; align-items: center; z-index: 1000; } .event-modal.active { display: flex; animation: fadeIn 0.2s ease; } .event-details { background-color: white; border-radius: 12px; width: 90%; max-width: 480px; padding: 24px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); position: relative; } .event-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; } .event-title { font-size: 18px; font-weight: 600; color: var(--primary); } .close-modal { background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text-light); padding: 4px; } .event-info { display: grid; grid-template-columns: auto 1fr; gap: 12px 16px; margin-bottom: 20px; } .info-label { color: var(--text-light); font-weight: 500; } .info-value { color: var(--text); } .event-footer { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; } .action-btn { padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .btn-primary { background-color: var(--accent); color: white; } .btn-primary:hover { background-color: var(--accent-hover); } .btn-secondary { background-color: var(--light); color: var(--text); } .btn-secondary:hover { background-color: #dfe6e9; } .participants { display: flex; flex-wrap: wrap; gap: 8px; } .participant { display: flex; align-items: center; gap: 6px; font-size: 13px; } .participant-avatar { width: 24px; height: 24px; border-radius: 50%; background-color: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; } .toast { position: fixed; bottom: 20px; right: 20px; background-color: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border-radius: 8px; padding: 12px 16px; display: flex; align-items: center; gap: 12px; transform: translateY(100px); opacity: 0; transition: all 0.3s ease; z-index: 1000; } .toast.active { transform: translateY(0); opacity: 1; } .toast-icon { width: 24px; height: 24px; border-radius: 50%; background-color: var(--success); color: white; display: flex; align-items: center; justify-content: center; } .toast-message { font-size: 14px; font-weight: 500; } .toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 24px; background-color: white; border-top: 1px solid var(--border); } .more-indicator { font-size: 11px; color: var(--text-light); text-align: center; margin-top: 2px; } .loading-overlay { position: absolute; inset: 0; background-color: rgba(255, 255, 255, 0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .loading-overlay.active { opacity: 1; visibility: visible; } .spinner { width: 40px; height: 40px; border: 3px solid var(--light); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; } .time-indicator { position: absolute; left: 0; width: 100%; height: 2px; background-color: var(--accent); z-index: 1; } .time-indicator::after { content: ''; position: absolute; left: 0; top: -4px; width: 10px; height: 10px; border-radius: 50%; background-color: var(--accent); } .meeting-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; } .status-active { background-color: var(--success); animation: pulse 1.5s infinite; } .status-upcoming { background-color: var(--warning); } .timezone-pill { background-color: var(--light); padding: 2px 8px; border-radius: 12px; font-size: 12px; margin-left: 8px; color: var(--text-light); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes spin { to { transform: rotate(360deg); } } @keyframes pulse { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.2); } 100% { opacity: 1; transform: scale(1); } } .click-ripple { position: absolute; border-radius: 50%; background-color: rgba(52, 152, 219, 0.3); transform: scale(0); animation: ripple 0.5s linear; pointer-events: none; } @keyframes ripple { to { transform: scale(2); opacity: 0; } } @media (max-width: 480px) { .calendar-grid { grid-template-columns: repeat(1, 1fr); } .calendar-weekday:not(.current-weekday) { display: none; } .calendar-header { flex-direction: column; align-items: flex-start; gap: 16px; } .calendar-controls { width: 100%; justify-content: space-between; } .event-info { grid-template-columns: 1fr; gap: 8px; } } </style> </head> <body> <div class="calendar-container"> <div class="calendar-header"> <h1 class="calendar-title">Enterprise Calendar</h1> <div class="calendar-controls"> <button class="calendar-btn active" data-view="month">Month</button> <button class="calendar-btn" data-view="week">Week</button> <button class="calendar-btn" data-view="day">Day</button> <div class="timezone-selector"> <div class="timezone-btn"> <span class="current-timezone">GMT+1</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <polyline points="6 9 12 15 18 9"></polyline> </svg> </div> <div class="timezone-dropdown"> <div class="timezone-option selected" data-value="GMT+1">GMT+1 (London)</div> <div class="timezone-option" data-value="GMT-5">GMT-5 (New York)</div> <div class="timezone-option" data-value="GMT-8">GMT-8 (San Francisco)</div> <div class="timezone-option" data-value="GMT+9">GMT+9 (Tokyo)</div> <div class="timezone-option" data-value="GMT+5:30">GMT+5:30 (New Delhi)</div> <div class="timezone-option" data-value="GMT+8">GMT+8 (Singapore)</div> </div> </div> </div> </div> <div class="calendar-navigation"> <div class="calendar-month">November 2023</div> <div class="nav-buttons"> <button class="nav-btn" id="prev-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <polyline points="15 18 9 12 15 6"></polyline> </svg> </button> <button class="nav-btn" id="next-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <polyline points="9 18 15 12 9 6"></polyline> </svg> </button> <button class="nav-btn" id="today-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12" y2="16"></line> </svg> </button> </div> </div> <div class="calendar-grid"> <!-- Weekday headers will be generated dynamically --> </div> <div class="toolbar"> <button class="calendar-btn" id="refresh-btn"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/> </svg> Refresh </button> </div> <div class="event-modal"> <div class="event-details"> <div class="event-header"> <h2 class="event-title">Q4 Strategy Meeting</h2> <button class="close-modal">×</button> </div> <div class="event-info"> <div class="info-label">Date:</div> <div class="info-value">November 15, 2023</div> <div class="info-label">Time:</div> <div class="info-value">10:00 AM - 11:30 AM <span class="timezone-pill">GMT+1</span></div> <div class="info-label">Location:</div> <div class="info-value">Conference Room A / Zoom</div> <div class="info-label">Status:</div> <div class="info-value"><span class="meeting-status status-upcoming"></span> Upcoming</div> <div class="info-label">Organizer:</div> <div class="info-value">Sarah Martinez, Product Director</div> <div class="info-label">Participants:</div> <div class="info-value"> <div class="participants"> <div class="participant"> <div class="participant-avatar" style="background-color: #3498db;">JD</div> <span>James Davis</span> </div> <div class="participant"> <div class="participant-avatar" style="background-color: #9b59b6;">AP</div> <span>Alice Porter</span> </div> <div class="participant"> <div class="participant-avatar" style="background-color: #e74c3c;">RK</div> <span>Robert Kim</span> </div> <div class="participant"> <div class="participant-avatar" style="background-color: #f39c12;">LN</div> <span>Lisa Nguyen</span> </div> <div class="participant"> <div class="participant-avatar" style="background-color: #27ae60;">+3</div> </div> </div> </div> <div class="info-label">Agenda:</div> <div class="info-value">1. Q4 Performance Review<br>2. 2024 Strategic Initiatives<br>3. Budget Planning</div> </div> <div class="event-footer"> <button class="action-btn btn-secondary" id="cancel-event">Cancel</button> <button class="action-btn btn-primary" id="join-meeting">Join Meeting</button> </div> </div> </div> <div class="loading-overlay"> <div class="spinner"></div> </div> </div> <div class="toast" id="notification-toast"> <div class="toast-icon"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <polyline points="20 6 9 17 4 12"></polyline> </svg> </div> <div class="toast-message">Meeting joined successfully!</div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Calendar data const today = new Date(); let currentMonth = today.getMonth(); let currentYear = today.getFullYear(); // Sample events data const events = [ { id: 1, title: 'Q4 Strategy Meeting', date: new Date(2023, 10, 15), startTime: '10:00', endTime: '11:30', type: 'meeting', location: 'Conference Room A / Zoom', organizer: 'Sarah Martinez, Product Director', status: 'upcoming', participants: [ { name: 'James Davis', initials: 'JD', color: '#3498db' }, { name: 'Alice Porter', initials: 'AP', color: '#9b59b6' }, { name: 'Robert Kim', initials: 'RK', color: '#e74c3c' }, { name: 'Lisa Nguyen', initials: 'LN', color: '#f39c12' } ], agenda: '1. Q4 Performance Review\n2. 2024 Strategic Initiatives\n3. Budget Planning' }, { id: 2, title: 'Product Launch Preparation', date: new Date(2023, 10, 10), startTime: '14:00', endTime: '15:30', type: 'meeting', location: 'Conference Room B', status: 'upcoming' }, { id: 3, title: 'Marketing Budget Deadline', date: new Date(2023, 10, 20), startTime: '12:00', endTime: '12:00', type: 'deadline', status: 'upcoming' }, { id: 4, title: 'Client Presentation: Acme Corp', date: new Date(2023, 10, 16), startTime: '09:00', endTime: '10:00', type: 'meeting', location: 'Zoom', status: 'upcoming' }, { id: 5, title: 'Weekly Team Sync', date: new Date(2023, 10, 13), startTime: '11:00', endTime: '12:00', type: 'meeting', location: 'Conference Room A', status: 'upcoming' }, { id: 6, title: 'Quarterly Report Submission', date: new Date(2023, 10, 25), startTime: '18:00', endTime: '18:00', type: 'deadline', status: 'upcoming' }, { id: 7, title: 'HR Training: Leadership Skills', date: new Date(2023, 10, 8), startTime: '13:00', endTime: '16:00', type: 'meeting', location: 'Training Room', status: 'completed' }, { id: 8, title: 'IT System Maintenance', date: new Date(2023, 10, 19), startTime: '22:00', endTime: '23:59', type: 'reminder', status: 'upcoming' }, { id: 9, title: 'Board Meeting', date: new Date(2023, 10, 28), startTime: '15:00', endTime: '17:00', type: 'meeting', location: 'Executive Boardroom', status: 'upcoming' }, { id: 10, title: 'Design Review', date: new Date(2023, 10, 14), startTime: '14:00', endTime: '15:00', type: 'meeting', location: 'Zoom', status: 'upcoming' }, { id: 11, title: 'Performance Reviews Due', date: new Date(2023, 10, 30), startTime: '17:00', endTime: '17:00', type: 'deadline', status: 'upcoming' }, { id: 12, title: 'Team Building Activity', date: new Date(2023, 10, 17), startTime: '16:00', endTime: '18:00', type: 'reminder', location: 'City Park', status: 'upcoming' }, { id: 13, title: 'Budget Planning Session', date: new Date(2023, 10, 22), startTime: '10:00', endTime: '12:00', type: 'meeting', location: 'Conference Room C', status: 'upcoming' } ]; // DOM elements const calendarGrid = document.querySelector('.calendar-grid'); const calendarMonth = document.querySelector('.calendar-month'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); const todayBtn = document.getElementById('today-btn'); const refreshBtn = document.getElementById('refresh-btn'); const eventModal = document.querySelector('.event-modal'); const closeModal = document.querySelector('.close-modal'); const loadingOverlay = document.querySelector('.loading-overlay'); const joinMeetingBtn = document.getElementById('join-meeting'); const cancelEventBtn = document.getElementById('cancel-event'); const toast = document.getElementById('notification-toast'); const timezoneBtn = document.querySelector('.timezone-btn'); const timezoneDropdown = document.querySelector('.timezone-dropdown'); const timezoneOptions = document.querySelectorAll('.timezone-option'); const currentTimezone = document.querySelector('.current-timezone'); const viewButtons = document.querySelectorAll('.calendar-btn[data-view]'); // Initialize calendar renderCalendar(); // Event listeners prevBtn.addEventListener('click', goToPreviousMonth); nextBtn.addEventListener('click', goToNextMonth); todayBtn.addEventListener('click', goToToday); refreshBtn.addEventListener('click', refreshCalendar); closeModal.addEventListener('click', closeEventModal); eventModal.addEventListener('click', function(e) { if (e.target === eventModal) { closeEventModal(); } }); joinMeetingBtn.addEventListener('click', joinMeeting); cancelEventBtn.addEventListener('click', closeEventModal); timezoneBtn.addEventListener('click', toggleTimezoneDropdown); document.addEventListener('click', function(e) { if (!timezoneBtn.contains(e.target) && !timezoneDropdown.contains(e.target)) { timezoneDropdown.classList.remove('active'); } }); timezoneOptions.forEach(option => { option.addEventListener('click', function() { const value = this.getAttribute('data-value'); currentTimezone.textContent = value; // Update selected option timezoneOptions.forEach(opt => opt.classList.remove('selected')); this.classList.add('selected'); // Close dropdown and show notification timezoneDropdown.classList.remove('active'); showToast(`Timezone updated to ${value}`); // Update timezone pills in the UI document.querySelectorAll('.timezone-pill').forEach(pill => { pill.textContent = value; }); }); }); viewButtons.forEach(button => { button.addEventListener('click', function() { viewButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); const view = this.getAttribute('data-view'); showToast(`Switched to ${view} view`); }); }); // Functions function renderCalendar() { // Clear grid calendarGrid.innerHTML = ''; // Update month and year display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; calendarMonth.textContent = `${monthNames[currentMonth]} ${currentYear}`; // Add weekday headers const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; weekdays.forEach(day => { const weekdayHeader = document.createElement('div'); weekdayHeader.className = 'calendar-weekday'; weekdayHeader.textContent = day; calendarGrid.appendChild(weekdayHeader); }); // Get first day of the month and last day of the month const firstDay = new Date(currentYear, currentMonth, 1); const lastDay = new Date(currentYear, currentMonth + 1, 0); // Get day of week of first day (0-6, 0 is Sunday) const firstDayIndex = firstDay.getDay(); // Get number of days in current month const daysInMonth = lastDay.getDate(); // Get number of days in previous month const prevMonthLastDay = new Date(currentYear, currentMonth, 0).getDate(); // Calculate cells needed for previous and next month days const prevMonthDays = firstDayIndex; const nextMonthDays = 42 - (prevMonthDays + daysInMonth); // 6 rows × 7 days = 42 cells // Add prev month days for (let i = prevMonthDays - 1; i >= 0; i--) { const day = document.createElement('div'); day.className = 'calendar-day other-month'; const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = prevMonthLastDay - i; const eventsContainer = document.createElement('div'); eventsContainer.className = 'events-container'; day.appendChild(dayNumber); day.appendChild(eventsContainer); calendarGrid.appendChild(day); } // Add current month days for (let i = 1; i <= daysInMonth; i++) { const day = document.createElement('div'); day.className = 'calendar-day'; // Check if this day is today const isToday = (i === today.getDate() && currentMonth === today.getMonth() && currentYear === today.getFullYear()); if (isToday) { day.classList.add('current-day'); } const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = i; const eventsContainer = document.createElement('div'); eventsContainer.className = 'events-container'; // Add events for this day const dayEvents = events.filter(event => { return event.date.getDate() === i && event.date.getMonth() === currentMonth && event.date.getFullYear() === currentYear; }); // Sort events by start time dayEvents.sort((a, b) => { return a.startTime.localeCompare(b.startTime); }); // Show only 3 events max, with a "more" indicator if there are more const eventsToShow = dayEvents.slice(0, 3); eventsToShow.forEach(event => { const eventEl = document.createElement('div'); eventEl.className = `event event-${event.type}`; eventEl.textContent = `${event.startTime} ${event.title}`; eventEl.setAttribute('data-event-id', event.id); // Add ripple effect on click eventEl.addEventListener('click', function(e) { createRipple(e); openEventModal(event); }); eventsContainer.appendChild(eventEl); }); // Add "more" indicator if needed if (dayEvents.length > 3) { const moreEl = document.createElement('div'); moreEl.className = 'more-indicator'; moreEl.textContent = `+ ${dayEvents.length - 3} more`; eventsContainer.appendChild(moreEl); } day.appendChild(dayNumber); day.appendChild(eventsContainer); calendarGrid.appendChild(day); } // Add next month days for (let i = 1; i <= nextMonthDays; i++) { const day = document.createElement('div'); day.className = 'calendar-day other-month'; const dayNumber = document.createElement('div'); dayNumber.
<html> <head> <title>Medical Appointment Calendar</title> <style> :root { --primary: #2b6cb0; --primary-light: #3182ce; --primary-dark: #2c5282; --secondary: #48bb78; --warning: #ed8936; --danger: #e53e3e; --background: #f7fafc; --text: #2d3748; --text-light: #718096; --border: #e2e8f0; --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --calendar-cell-size: 90px; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--background); color: var(--text); max-width: 700px; max-height: 700px; overflow: hidden; margin: 0 auto; padding: 20px; height: 100vh; display: flex; flex-direction: column; } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } h1 { font-size: 1.5rem; color: var(--primary-dark); font-weight: 600; } .month-nav { display: flex; align-items: center; gap: 15px; } .month-nav button { background: none; border: none; cursor: pointer; color: var(--primary); font-size: 1.2rem; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .month-nav button:hover { background-color: var(--primary-light); color: white; } .month-title { font-size: 1.2rem; font-weight: 500; min-width: 130px; text-align: center; } .view-options { display: flex; gap: 8px; } .view-options button { background-color: var(--background); border: 1px solid var(--border); padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s ease; } .view-options button.active { background-color: var(--primary); color: white; border-color: var(--primary); } .view-options button:hover:not(.active) { background-color: var(--border); } .legend { display: flex; gap: 15px; margin-bottom: 10px; font-size: 0.85rem; } .legend-item { display: flex; align-items: center; gap: 5px; } .legend-color { width: 12px; height: 12px; border-radius: 2px; } .color-regular { background-color: var(--primary-light); } .color-urgent { background-color: var(--danger); } .color-telehealth { background-color: var(--secondary); } .color-pending { background-color: var(--warning); } .calendar-container { flex: 1; overflow: auto; box-shadow: var(--shadow); border-radius: 8px; background-color: white; } .calendar { width: 100%; border-collapse: collapse; height: 100%; } .calendar th { background-color: var(--primary-dark); color: white; padding: 10px; font-weight: 500; position: sticky; top: 0; z-index: 10; } .calendar td { border: 1px solid var(--border); vertical-align: top; height: var(--calendar-cell-size); width: calc(100% / 7); padding: 5px; position: relative; } .date-number { font-size: 0.9rem; font-weight: 500; margin-bottom: 5px; } .other-month .date-number { color: var(--text-light); opacity: 0.5; } .today { background-color: rgba(49, 130, 206, 0.1); } .today .date-number { background-color: var(--primary); color: white; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } .appointment { font-size: 0.8rem; padding: 3px 5px; border-radius: 3px; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: transform 0.15s ease; color: white; } .appointment:hover { transform: translateY(-2px); box-shadow: var(--shadow); z-index: 5; } .appointment.regular { background-color: var(--primary-light); } .appointment.urgent { background-color: var(--danger); } .appointment.telehealth { background-color: var(--secondary); } .appointment.pending { background-color: var(--warning); } .appointment-time { font-weight: 500; } .add-appointment { position: absolute; right: 5px; top: 5px; width: 18px; height: 18px; border-radius: 50%; background-color: var(--primary-light); color: white; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; cursor: pointer; opacity: 0; transition: opacity 0.2s ease; } td:hover .add-appointment { opacity: 1; } .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 100; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } .modal.active { opacity: 1; pointer-events: all; } .modal-content { background-color: white; padding: 25px; border-radius: 8px; width: 90%; max-width: 450px; transform: translateY(20px); transition: transform 0.3s ease; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); } .modal.active .modal-content { transform: translateY(0); } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .modal-title { font-size: 1.2rem; font-weight: 600; color: var(--primary-dark); } .close-modal { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--text-light); } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-size: 0.9rem; color: var(--text); } input, select, textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 4px; font-size: 0.9rem; } input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary-light); box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.2); } .appointment-actions { display: flex; justify-content: space-between; margin-top: 20px; } .appointment-actions button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; } .btn-cancel { background-color: var(--background); color: var(--text); } .btn-cancel:hover { background-color: var(--border); } .btn-save { background-color: var(--primary); color: white; } .btn-save:hover { background-color: var(--primary-dark); } .btn-delete { background-color: var(--danger); color: white; } .btn-delete:hover { background-color: #c53030; } .status-badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 8px; } .badge-confirmed { background-color: #e6fffa; color: #2c7a7b; } .badge-pending { background-color: #fffaf0; color: #c05621; } .more-appointments { background-color: var(--primary-dark); color: white; padding: 2px 5px; border-radius: 3px; font-size: 0.75rem; text-align: center; cursor: pointer; } .quick-actions { position: fixed; bottom: 20px; right: 20px; z-index: 50; } .quick-add { width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary); color: white; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; box-shadow: var(--shadow); cursor: pointer; transition: transform 0.2s ease, background-color 0.2s ease; border: none; } .quick-add:hover { transform: translateY(-3px); background-color: var(--primary-dark); } @media (max-width: 600px) { body { padding: 10px; } h1 { font-size: 1.2rem; } .month-title { font-size: 1rem; min-width: 100px; } .view-options { display: none; } .legend { flex-wrap: wrap; font-size: 0.75rem; } .calendar th { padding: 5px; font-size: 0.8rem; } .date-number { font-size: 0.8rem; } .appointment { font-size: 0.7rem; padding: 2px 3px; } .quick-add { width: 40px; height: 40px; } } /* Tooltip */ .tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 5px 10px; border-radius: 4px; font-size: 0.8rem; z-index: 20; pointer-events: none; opacity: 0; transition: opacity 0.2s; width: max-content; max-width: 200px; } /* Loading animation */ .loading-spinner { width: 30px; height: 30px; border: 3px solid rgba(49, 130, 206, 0.3); border-radius: 50%; border-top-color: var(--primary); animation: spin 1s ease-in-out infinite; margin: 0 auto; } @keyframes spin { to { transform: rotate(360deg); } } /* Small pulse animation for new appointments */ @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .pulse { animation: pulse 2s infinite; } /* Day hover effect */ .calendar td:hover { background-color: rgba(49, 130, 206, 0.05); } /* Scroll handle styling */ .calendar-container::-webkit-scrollbar { width: 8px; height: 8px; } .calendar-container::-webkit-scrollbar-track { background: var(--border); border-radius: 4px; } .calendar-container::-webkit-scrollbar-thumb { background: var(--primary-light); border-radius: 4px; } .calendar-container::-webkit-scrollbar-thumb:hover { background: var(--primary); } /* Animation for modal */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .fade-in { animation: fadeIn 0.3s forwards; } </style> </head> <body> <header> <h1>Dr. Sara Mitchell's Schedule</h1> <div class="month-nav"> <button id="prevMonth">❮</button> <div class="month-title" id="currentMonth">September 2023</div> <button id="nextMonth">❯</button> </div> </header> <div class="view-options"> <button class="active">Month</button> <button>Week</button> <button>Day</button> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color color-regular"></div> <span>Regular</span> </div> <div class="legend-item"> <div class="legend-color color-urgent"></div> <span>Urgent</span> </div> <div class="legend-item"> <div class="legend-color color-telehealth"></div> <span>Telehealth</span> </div> <div class="legend-item"> <div class="legend-color color-pending"></div> <span>Pending</span> </div> </div> <div class="calendar-container"> <table class="calendar" id="calendar"> <thead> <tr> <th>Sun</th> <th>Mon</th> <th>Tue</th> <th>Wed</th> <th>Thu</th> <th>Fri</th> <th>Sat</th> </tr> </thead> <tbody id="calendarBody"> <!-- Calendar cells will be generated here --> </tbody> </table> </div> <div class="quick-actions"> <button class="quick-add" id="quickAdd">+</button> </div> <!-- Add/Edit Appointment Modal --> <div class="modal" id="appointmentModal"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title" id="modalTitle">New Appointment</h3> <button class="close-modal" id="closeModal">×</button> </div> <form id="appointmentForm"> <input type="hidden" id="appointmentId"> <div class="form-group"> <label for="patientName">Patient Name</label> <input type="text" id="patientName" required> </div> <div class="form-group"> <label for="appointmentDate">Date</label> <input type="date" id="appointmentDate" required> </div> <div class="form-group"> <label for="appointmentTime">Time</label> <input type="time" id="appointmentTime" required> </div> <div class="form-group"> <label for="appointmentType">Appointment Type</label> <select id="appointmentType" required> <option value="regular">Regular Check-up</option> <option value="urgent">Urgent Care</option> <option value="telehealth">Telehealth</option> <option value="pending">Pending Confirmation</option> </select> </div> <div class="form-group"> <label for="appointmentNotes">Notes</label> <textarea id="appointmentNotes" rows="3"></textarea> </div> <div class="appointment-actions"> <button type="button" class="btn-cancel" id="cancelAppointment">Cancel</button> <button type="button" class="btn-delete" id="deleteAppointment" style="display: none;">Delete</button> <button type="submit" class="btn-save">Save Appointment</button> </div> </form> </div> </div> <!-- View Appointment Modal --> <div class="modal" id="viewAppointmentModal"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title">Appointment Details</h3> <button class="close-modal" id="closeViewModal">×</button> </div> <div id="appointmentDetails"> <h4 id="viewPatientName">John Doe</h4> <p> <span id="viewAppointmentDate">September 15, 2023</span> at <span id="viewAppointmentTime">10:30 AM</span> <span class="status-badge badge-confirmed" id="viewAppointmentStatus">Confirmed</span> </p> <p id="viewAppointmentType">Regular Check-up</p> <p><strong>Notes:</strong></p> <p id="viewAppointmentNotes">Patient is coming for their annual check-up. Please have their lab results ready.</p> <div class="appointment-actions"> <button type="button" class="btn-cancel" id="closeAppointmentView">Close</button> <button type="button" class="btn-save" id="editAppointment">Edit</button> </div> </div> </div> </div> <div class="tooltip" id="tooltip"></div> <script> document.addEventListener('DOMContentLoaded', function() { // Current date and view state let currentDate = new Date(); let selectedDate = null; let selectedAppointment = null; let appointments = [ { id: 1, patientName: "Maria Garcia", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), time: "09:00", type: "regular", notes: "Annual wellness check. Review blood pressure medication." }, { id: 2, patientName: "James Wilson", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), time: "10:30", type: "telehealth", notes: "Follow-up on recent lab results. Patient has reported improved energy levels." }, { id: 3, patientName: "Sarah Johnson", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16), time: "14:15", type: "urgent", notes: "Persistent chest pain, shortness of breath. Prioritize for immediate assessment." }, { id: 4, patientName: "Michael Chen", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 17), time: "11:00", type: "regular", notes: "Routine physical examination. Patient requested discussion about weight management." }, { id: 5, patientName: "Emma Davis", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 18), time: "15:30", type: "pending", notes: "New patient consultation. Needs to confirm insurance details before appointment." }, { id: 6, patientName: "Robert Taylor", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 20), time: "08:45", type: "telehealth", notes: "Virtual consultation for medication adjustment. Patient reports side effects." }, { id: 7, patientName: "Lisa Rodriguez", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), 22), time: "13:00", type: "regular", notes: "Six-month follow-up for diabetes management. Review HbA1c test results." }, { id: 8, patientName: "Thomas Brown", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()), time: "09:30", type: "regular", notes: "Annual physical examination. Update vaccination record." }, { id: 9, patientName: "Olivia Martinez", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()), time: "11:45", type: "urgent", notes: "Acute migraine symptoms. Patient requested earliest available appointment." }, { id: 10, patientName: "David Lee", date: new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()+1), time: "14:00", type: "telehealth", notes: "Post-surgery follow-up via video call. Check wound healing progress." } ]; // DOM Elements const calendarBody = document.getElementById('calendarBody'); const currentMonthElement = document.getElementById('currentMonth'); const prevMonthButton = document.getElementById('prevMonth'); const nextMonthButton = document.getElementById('nextMonth'); const appointmentModal = document.getElementById('appointmentModal'); const viewAppointmentModal = document.getElementById('viewAppointmentModal'); const appointmentForm = document.getElementById('appointmentForm'); const modalTitle = document.getElementById('modalTitle'); const appointmentIdInput = document.getElementById('appointmentId'); const patientNameInput = document.getElementById('patientName'); const appointmentDateInput = document.getElementById('appointmentDate'); const appointmentTimeInput = document.getElementById('appointmentTime'); const appointmentTypeInput = document.getElementById('appointmentType'); const appointmentNotesInput = document.getElementById('appointmentNotes'); const deleteAppointmentButton = document.getElementById('deleteAppointment'); const quickAddButton = document.getElementById('quickAdd'); const tooltip = document.getElementById('tooltip'); // Event Listeners prevMonthButton.addEventListener('click', goToPreviousMonth); nextMonthButton.addEventListener('click', goToNextMonth); document.getElementById('closeModal').addEventListener('click', closeAppointmentModal); document.getElementById('cancelAppointment').addEventListener('click', closeAppointmentModal); document.getElementById('closeViewModal').addEventListener('click', closeViewAppointmentModal); document.getElementById('closeAppointmentView').addEventListener('click', closeViewAppointmentModal); document.getElementById('editAppointment').addEventListener('click', editCurrentAppointment); appointmentForm.addEventListener('submit', saveAppointment); deleteAppointmentButton.addEventListener('click', deleteAppointment); quickAddButton.addEventListener('click', () => openAppointmentModal()); // View options document.querySelectorAll('.view-options button').forEach(button => { button.addEventListener('click', function() { document.querySelector('.view-options button.active').classList.remove('active'); this.classList.add('active'); // In a real app, we would change the view here renderCalendar(); // Re-render for demo purposes }); }); // Initialize calendar renderCalendar(); // Functions function renderCalendar() { // Clear existing calendar calendarBody.innerHTML = ''; // Set current month display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; currentMonthElement.textContent = `${monthNames[currentDate.getMonth()]} ${currentDate.getFullYear()}`; // Get first day of month and total days const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); const lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); const totalDays = lastDay.getDate(); const startingDay = firstDay.getDay(); // 0 = Sunday // Calculate previous month's days to show const prevMonthLastDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0).getDate(); // Build the calendar grid let date = 1; let nextMonthDate = 1; // Create calendar rows for (let i = 0; i < 6; i++) { // If we've already rendered all days of current month and at least one day of next month, // we can stop generating more rows if (date > totalDays && i > 0) break; const row = document.createElement('tr'); // Create 7 cells for each day of the week for (let j = 0; j < 7; j++) { const cell = document.createElement('td'); if (i === 0 && j < startingDay) { // Previous month's days const prevDate = prevMonthLastDay - startingDay + j + 1; const dateElement = document.createElement('div'); dateElement.className = 'date-number'; dateElement.textContent = prevDate; cell.className = 'other-month'; cell.appendChild(dateElement); // Add previous month's date for adding appointments const prevMonthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, prevDate); cell.dataset.date = formatDate(prevMonthDate); // Add "+" button for adding appointments const addButton = createAddAppointmentButton(prevMonthDate); cell.appendChild(addButton); } else if (date > totalDays) { // Next month's days const dateElement = document.createElement('div'); dateElement.className = 'date-number'; dateElement.textContent = nextMonthDate; cell.className = 'other-month'; cell.appendChild(dateElement); // Add next month's date for adding appointments const nextMonthDateObj = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, nextMonthDate); cell.dataset.date = formatDate(nextMonthDateObj); // Add "+" button for adding appointments const addButton = createAddAppointmentButton(nextMonthDateObj); cell.appendChild(addButton); nextMonthDate++; } else { // Current month's days const dateElement = document.createElement('div'); dateElement.className = 'date-number'; dateElement.textContent = date; // Check if it's today const currentMonthDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), date); cell.dataset.date = formatDate(currentMonthDate); if (isToday(currentMonthDate)) { cell.className = 'today'; } cell.appendChild(dateElement); // Add appointments for this day const dayAppointments = appointments.filter(appointment => appointment.date.getDate() === date && appointment.date.getMonth() === currentDate.getMonth() && appointment.date.getFullYear() === currentDate.getFullYear() ); // Sort appointments by time dayAppointments.sort((a, b) => a.time.localeCompare(b.time)); // Display up to 3 appointments, with a "more" link if there are more const maxVisible = 3; const displayCount = Math.min(dayAppointments.length, maxVisible); for (let k = 0; k < displayCount; k++) { const appt = dayAppointments[k]; const apptElement = document.createElement('div'); apptElement.className = `appointment ${appt.type}`; apptElement.dataset.id = appt.id; // Format the time for display const timeDisplay = formatTime(appt.time); apptElement.innerHTML = `<span class="appointment-time">${timeDisplay}</span> ${appt.patientName}`; // Add pulse animation to today's appointments if (isToday(currentMonthDate)) { apptElement.classList.add('pulse'); } // Add tooltip functionality apptElement.addEventListener('mouseover', (e) => { showTooltip(e, `${appt.patientName} - ${timeDisplay}\n${appt.type.charAt(0).toUpperCase() + appt.type.slice(1)} appointment`); }); apptElement.addEventListener('mouseout', hideTooltip); // Add click event to view appointment details apptElement.addEventListener('click', () => viewAppointment(appt.id)); cell.appendChild(apptElement); } // Add "more" link if there are more appointments if (dayAppointments.length > maxVisible) { const moreElement = document.createElement('div'); moreElement.className = 'more-appointments'; moreElement.textContent = `+ ${dayAppointments.length - maxVisible} more`; // Add tooltip with additional appointments moreElement.addEventListener('mouseover', (e) => { const additionalAppts = dayAppointments.slice(maxVisible) .map(a => `${formatTime(a.time)} - ${a.patientName}`) .join('\n'); showTooltip(e, additionalAppts); }); moreElement.addEventListener('mouseout', hideTooltip); cell.appendChild(moreElement); } // Add "+" button for adding appointments const addButton = createAddAppointmentButton(currentMonthDate); cell.appendChild(addButton); date++; } row.appendChild(cell); } calendarBody.appendChild(row); } } function createAddAppointmentButton(date) { const addButton = document.createElement('div'); addButton.className = 'add-appointment'; addButton.textContent = '+'; addButton.addEventListener('click', (e) => { e.stopPropagation(); openAppointmentModal(date); }); addButton.addEventListener('mouseover', (e) => { showTooltip(e, 'Add new appointment'); }); addButton.addEventListener('mouseout', hideTooltip); return addButton; } function isToday(date) { const today = new Date(); return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); } function formatDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function formatTime(timeString) { // Convert 24-hour format to 12-hour format with AM/PM const [hours, minutes] = timeString.split(':'); const hour = parseInt(hours); const ampm = hour >= 12 ? 'PM' : 'AM'; const hour12 = hour % 12 || 12; return `${hour12}:${minutes} ${ampm}`; } function parseTimeToStandard(timeString) { // Convert 12-hour format with AM/PM to 24-hour format if (timeString.includes('AM') || timeString.includes('PM')) { const [time, period] = timeString.split(' '); let [hours, minutes] = time.split(':'); hours = parseInt(hours); if (period === 'PM' && hours < 12) { hours += 12; } else if (period === 'AM' && hours === 12) { hours = 0; } return `${String(hours).padStart(2, '0')}:${minutes}`; } return timeString; // Already in 24-hour format } function goToPreviousMonth() { currentDate.setMonth(currentDate.getMonth() - 1); renderCalendar(); } function goToNextMonth() { currentDate.setMonth(currentDate.getMonth() + 1); renderCalendar(); } function openAppointmentModal(date = null) { // Reset form appointmentForm.reset(); modalTitle.textContent = 'New Appointment'; appointmentIdInput.value = ''; deleteAppointmentButton.style.display = 'none'; // Set date if provided if (date) { appointmentDateInput.value = formatDate(date); } else { // Default to today appointmentDateInput.value = formatDate(new Date()); } // Default time to current hour + 30 minutes, rounded to nearest 15 min const now = new Date(); const minutes = Math.ceil(now.getMinutes() / 15) * 15; now.setMinutes(minutes); const timeString = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; appointmentTimeInput.value = timeString; // Open modal with animation appointmentModal.classList.add('active'); setTimeout(() => patientNameInput.focus(), 300); } function closeAppointmentModal() { appointmentModal.classList.remove('active'); } function closeViewAppointmentModal() { viewAppointmentModal.classList.remove('active'); } function saveAppointment(e) { e.preventDefault(); // Validate form (basic validation) if (!patientNameInput.value.trim()) { // Simple validation feedback patientNameInput.style.borderColor = '
<html> <head> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Poppins', sans-serif; } :root { --primary: #5b62f4; --secondary: #fd7e50; --accent: #2ce5a7; --light: #f9f7ff; --dark: #292639; --gray: #e0e0e6; --warning: #ffcb45; --danger: #ff6b6b; } @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); body { background-color: var(--light); color: var(--dark); height: 700px; width: 700px; max-width: 100%; max-height: 100%; overflow: hidden; padding: 20px; display: flex; flex-direction: column; } .container { display: flex; flex-direction: column; height: 100%; width: 100%; background-color: white; border-radius: 20px; box-shadow: 0 10px 30px rgba(91, 98, 244, 0.1); overflow: hidden; position: relative; } .header { display: flex; justify-content: space-between; align-items: center; padding: 20px 30px; background-color: var(--primary); color: white; } .header h1 { font-size: 1.8rem; font-weight: 600; position: relative; } .header h1::after { content: ''; position: absolute; bottom: -5px; left: 0; width: 40px; height: 3px; background-color: var(--accent); border-radius: 3px; } .semester-selector { display: flex; gap: 10px; align-items: center; } .semester-btn { background-color: rgba(255, 255, 255, 0.2); border: none; border-radius: 8px; padding: 8px 15px; color: white; cursor: pointer; transition: all 0.3s ease; font-weight: 500; } .semester-btn.active { background-color: var(--accent); color: var(--dark); } .semester-btn:hover { background-color: rgba(255, 255, 255, 0.3); } .semester-btn.active:hover { background-color: var(--accent); opacity: 0.9; } .nav { display: flex; justify-content: space-between; align-items: center; padding: 15px 30px; border-bottom: 1px solid var(--gray); } .month-nav { display: flex; align-items: center; gap: 20px; } .month-display { font-size: 1.2rem; font-weight: 600; min-width: 150px; text-align: center; } .nav-btn { background: none; border: none; cursor: pointer; width: 35px; height: 35px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; color: var(--dark); } .nav-btn:hover { background-color: var(--gray); } .view-toggle { display: flex; background-color: var(--gray); border-radius: 8px; overflow: hidden; } .view-btn { background: none; border: none; padding: 8px 15px; cursor: pointer; transition: all 0.3s ease; } .view-btn.active { background-color: var(--primary); color: white; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); flex-grow: 1; overflow-y: auto; position: relative; } .day-header { text-align: center; padding: 10px; font-weight: 600; background-color: var(--light); position: sticky; top: 0; z-index: 1; } .calendar-day { border: 1px solid var(--gray); min-height: 100px; padding: 8px; position: relative; transition: all 0.2s ease; } .calendar-day:hover { background-color: var(--light); } .day-number { font-size: 1rem; font-weight: 600; margin-bottom: 5px; } .other-month { opacity: 0.4; } .today { background-color: rgba(91, 98, 244, 0.05); } .today .day-number { background-color: var(--primary); color: white; border-radius: 50%; width: 25px; height: 25px; display: flex; align-items: center; justify-content: center; } .class-event { padding: 5px 8px; border-radius: 5px; font-size: 0.7rem; margin-bottom: 5px; cursor: grab; position: relative; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; transition: all 0.2s ease; } .class-event:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .class-event.math { background-color: rgba(91, 98, 244, 0.2); border-left: 3px solid var(--primary); } .class-event.science { background-color: rgba(44, 229, 167, 0.2); border-left: 3px solid var(--accent); } .class-event.history { background-color: rgba(253, 126, 80, 0.2); border-left: 3px solid var(--secondary); } .class-event.english { background-color: rgba(255, 203, 69, 0.2); border-left: 3px solid var(--warning); } .class-event.exam { background-color: rgba(255, 107, 107, 0.2); border-left: 3px solid var(--danger); } .add-event { position: absolute; bottom: 5px; right: 5px; width: 20px; height: 20px; border-radius: 50%; background-color: var(--gray); display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transition: all 0.3s ease; } .calendar-day:hover .add-event { opacity: 1; } .add-event:hover { background-color: var(--primary); color: white; } .modal { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(41, 38, 57, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10; opacity: 0; pointer-events: none; transition: all 0.3s ease; } .modal.active { opacity: 1; pointer-events: all; } .modal-content { background-color: white; border-radius: 15px; padding: 25px; width: 80%; max-width: 500px; transform: translateY(20px); transition: all 0.3s ease; } .modal.active .modal-content { transform: translateY(0); } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .modal-title { font-size: 1.3rem; font-weight: 600; } .close-modal { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--dark); } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .form-control { width: 100%; padding: 10px; border: 1px solid var(--gray); border-radius: 8px; font-size: 1rem; } .form-control:focus { outline: none; border-color: var(--primary); } .course-type { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; } .type-option { padding: 8px 15px; border: 1px solid var(--gray); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; } .type-option:hover { background-color: var(--light); } .type-option.active { background-color: var(--primary); color: white; border-color: var(--primary); } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .modal-btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; } .cancel-btn { background-color: var(--gray); color: var(--dark); } .save-btn { background-color: var(--primary); color: white; } .cancel-btn:hover { background-color: #d0d0d6; } .save-btn:hover { background-color: #4a50d8; } .assignment-reminder { position: absolute; bottom: 20px; right: 20px; background-color: white; border-radius: 15px; padding: 15px; width: 300px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); transform: translateX(350px); transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 5; } .assignment-reminder.active { transform: translateX(0); } .reminder-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .reminder-title { font-weight: 600; color: var(--primary); display: flex; align-items: center; gap: 5px; } .close-reminder { background: none; border: none; cursor: pointer; font-size: 1.2rem; color: var(--dark); } .reminder-content { display: flex; flex-direction: column; gap: 10px; } .reminder-item { padding: 8px; border-radius: 8px; font-size: 0.8rem; display: flex; align-items: center; justify-content: space-between; } .reminder-item.urgent { background-color: rgba(255, 107, 107, 0.1); border-left: 3px solid var(--danger); } .reminder-item.upcoming { background-color: rgba(255, 203, 69, 0.1); border-left: 3px solid var(--warning); } .droppable-active { background-color: rgba(44, 229, 167, 0.1); border: 2px dashed var(--accent); } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 20px; color: #a0a0a8; text-align: center; } .empty-state-icon { font-size: 3rem; margin-bottom: 15px; color: var(--gray); } .loading-spinner { width: 40px; height: 40px; border: 4px solid rgba(91, 98, 244, 0.1); border-radius: 50%; border-left-color: var(--primary); animation: spin 1s ease infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .tooltip { position: absolute; background-color: var(--dark); color: white; padding: 5px 10px; border-radius: 5px; font-size: 0.7rem; z-index: 100; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; } .tooltip.active { opacity: 1; } .drag-ghost { position: absolute; z-index: 1000; pointer-events: none; opacity: 0.8; transform: rotate(3deg); width: auto; } .week-view { display: none; flex-direction: column; flex-grow: 1; overflow-y: auto; } .week-header { display: grid; grid-template-columns: 60px repeat(7, 1fr); background-color: var(--light); position: sticky; top: 0; z-index: 1; } .week-day { text-align: center; padding: 10px; font-weight: 600; } .week-times { display: grid; grid-template-columns: 60px repeat(7, 1fr); grid-auto-rows: 60px; } .time-label { font-size: 0.75rem; color: #a0a0a8; text-align: right; padding-right: 10px; padding-top: 5px; font-weight: 500; } .week-cell { border: 1px solid var(--gray); padding: 2px; position: relative; } .confetti { position: absolute; width: 10px; height: 10px; background-color: var(--primary); border-radius: 2px; opacity: 0; z-index: 1000; animation: fall 3s ease forwards; } @keyframes fall { 0% { transform: translateY(-100px) rotate(0deg); opacity: 1; } 100% { transform: translateY(500px) rotate(180deg); opacity: 0; } } @media (max-width: 600px) { .header { flex-direction: column; align-items: flex-start; gap: 10px; } .semester-selector { width: 100%; justify-content: space-between; } .nav { flex-direction: column; gap: 15px; } .month-nav { width: 100%; justify-content: space-between; } .day-header { font-size: 0.8rem; padding: 5px; } .calendar-day { padding: 5px; min-height: 80px; } .day-number { font-size: 0.9rem; } .class-event { font-size: 0.6rem; padding: 3px 5px; } .week-header { grid-template-columns: 40px repeat(7, 1fr); } .week-times { grid-template-columns: 40px repeat(7, 1fr); } .time-label { font-size: 0.6rem; } } /* Animation for appearing calendar */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .calendar-grid { animation: fadeIn 0.5s ease forwards; } /* Pulse animation for today */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(91, 98, 244, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(91, 98, 244, 0); } 100% { box-shadow: 0 0 0 0 rgba(91, 98, 244, 0); } } .today { animation: pulse 2s infinite; } </style> </head> <body> <div class="container"> <div class="header"> <h1>StudyPlanner</h1> <div class="semester-selector"> <button class="semester-btn" data-semester="spring">Spring '23</button> <button class="semester-btn active" data-semester="fall">Fall '23</button> <button class="semester-btn" data-semester="winter">Winter '24</button> </div> </div> <div class="nav"> <div class="month-nav"> <button class="nav-btn prev-month">←</button> <div class="month-display">November 2023</div> <button class="nav-btn next-month">→</button> </div> <div class="view-toggle"> <button class="view-btn active" data-view="month">Month</button> <button class="view-btn" data-view="week">Week</button> </div> </div> <div class="calendar-grid"> <div class="day-header">Sun</div> <div class="day-header">Mon</div> <div class="day-header">Tue</div> <div class="day-header">Wed</div> <div class="day-header">Thu</div> <div class="day-header">Fri</div> <div class="day-header">Sat</div> <!-- Week 1 --> <div class="calendar-day other-month" data-date="2023-10-29"> <div class="day-number">29</div> <div class="add-event">+</div> </div> <div class="calendar-day other-month" data-date="2023-10-30"> <div class="day-number">30</div> <div class="add-event">+</div> </div> <div class="calendar-day other-month" data-date="2023-10-31"> <div class="day-number">31</div> <div class="class-event math" draggable="true" data-type="math"> MATH301 - 9:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-01"> <div class="day-number">1</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 Lab - 2:30PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-02"> <div class="day-number">2</div> <div class="class-event history" draggable="true" data-type="history"> HIST145 - 11:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-03"> <div class="day-number">3</div> <div class="class-event english" draggable="true" data-type="english"> ENG202 Workshop </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-04"> <div class="day-number">4</div> <div class="add-event">+</div> </div> <!-- Week 2 --> <div class="calendar-day" data-date="2023-11-05"> <div class="day-number">5</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-06"> <div class="day-number">6</div> <div class="class-event math" draggable="true" data-type="math"> MATH301 - 9:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-07"> <div class="day-number">7</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 - 1:00PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-08"> <div class="day-number">8</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 Lab - 2:30PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-09"> <div class="day-number">9</div> <div class="class-event history" draggable="true" data-type="history"> HIST145 - 11:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-10"> <div class="day-number">10</div> <div class="class-event english" draggable="true" data-type="english"> ENG202 - 10:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-11"> <div class="day-number">11</div> <div class="add-event">+</div> </div> <!-- Week 3 --> <div class="calendar-day" data-date="2023-11-12"> <div class="day-number">12</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-13"> <div class="day-number">13</div> <div class="class-event math" draggable="true" data-type="math"> MATH301 - 9:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-14"> <div class="day-number">14</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 - 1:00PM </div> <div class="class-event exam" draggable="true" data-type="exam"> MATH301 Midterm </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-15"> <div class="day-number">15</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 Lab - 2:30PM </div> <div class="add-event">+</div> </div> <div class="calendar-day today" data-date="2023-11-16"> <div class="day-number">16</div> <div class="class-event history" draggable="true" data-type="history"> HIST145 - 11:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-17"> <div class="day-number">17</div> <div class="class-event english" draggable="true" data-type="english"> ENG202 - 10:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-18"> <div class="day-number">18</div> <div class="add-event">+</div> </div> <!-- Week 4 --> <div class="calendar-day" data-date="2023-11-19"> <div class="day-number">19</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-20"> <div class="day-number">20</div> <div class="class-event math" draggable="true" data-type="math"> MATH301 - 9:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-21"> <div class="day-number">21</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 - 1:00PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-22"> <div class="day-number">22</div> <div class="class-event exam" draggable="true" data-type="exam"> HIST145 Paper Due </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-23"> <div class="day-number">23</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-24"> <div class="day-number">24</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-25"> <div class="day-number">25</div> <div class="add-event">+</div> </div> <!-- Week 5 --> <div class="calendar-day" data-date="2023-11-26"> <div class="day-number">26</div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-27"> <div class="day-number">27</div> <div class="class-event math" draggable="true" data-type="math"> MATH301 - 9:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-28"> <div class="day-number">28</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 - 1:00PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-29"> <div class="day-number">29</div> <div class="class-event science" draggable="true" data-type="science"> BIO210 Lab - 2:30PM </div> <div class="add-event">+</div> </div> <div class="calendar-day" data-date="2023-11-30"> <div class="day-number">30</div> <div class="class-event history" draggable="true" data-type="history"> HIST145 - 11:00AM </div> <div class="class-event exam" draggable="true" data-type="exam"> ENG202 Presentation </div> <div class="add-event">+</div> </div> <div class="calendar-day other-month" data-date="2023-12-01"> <div class="day-number">1</div> <div class="class-event english" draggable="true" data-type="english"> ENG202 - 10:00AM </div> <div class="add-event">+</div> </div> <div class="calendar-day other-month" data-date="2023-12-02"> <div class="day-number">2</div> <div class="add-event">+</div> </div> </div> <div class="week-view"> <div class="week-header"> <div></div> <div class="week-day">Sun 12</div> <div class="week-day">Mon 13</div> <div class="week-day">Tue 14</div> <div class="week-day">Wed 15</div> <div class="week-day">Thu 16</div> <div class="week-day">Fri 17</div> <div class="week-day">Sat 18</div> </div> <div class="week-times"> <!-- Time slots: 8am to 6pm --> <div class="time-label">8:00</div> <div class="week-cell" data-day="sun" data-time="8:00"></div> <div class="week-cell" data-day="mon" data-time="8:00"></div> <div class="week-cell" data-day="tue" data-time="8:00"></div> <div class="week-cell" data-day="wed" data-time="8:00"></div> <div class="week-cell" data-day="thu" data-time="8:00"></div> <div class="week-cell" data-day="fri" data-time="8:00"></div> <div class="week-cell" data-day="sat" data-time="8:00"></div> <div class="time-label">9:00</div> <div class="week-cell" data-day="sun" data-time="9:00"></div> <div class="week-cell" data-day="mon" data-time="9:00"> <div class="class-event math" draggable="true">MATH301</div> </div> <div class="week-cell" data-day="tue" data-time="9:00"></div> <div class="week-cell" data-day="wed" data-time="9:00"></div> <div class="week-cell" data-day="thu" data-time="9:00"></div> <div class="week-cell" data-day="fri" data-time="9:00"></div> <div class="week-cell" data-day="sat" data-time="9:00"></div> <div class="time-label">10:00</div> <div class="week-cell" data-day="sun" data-time="10:00"></div> <div class="week-cell" data-day="mon" data-time="10:00"></div> <div class="week-cell" data-day="tue" data-time="10:00"></div> <div class="week-cell" data-day="wed" data-time="10:00"></div> <div class="week-cell" data-day="thu" data-time="10:00"></div> <div class="week-cell" data-day="fri" data-time="10:00"> <div class="class-event english" draggable="true">ENG202</div> </div> <div class="week-cell" data-day="sat" data-time="10:00"></div> <div class="time-label">11:00</div> <div class="week-cell" data-day="sun" data-time="11:00"></div> <div class="week-cell" data-day="mon" data-time="11:00"></div> <div class="week-cell" data-day="tue" data-time="11:00"></div> <div class="week-cell" data-day="wed" data-time="11:00"></div> <div class="week-cell" data-day="thu" data-time="11:00"> <div class="class-event history" draggable="true">HIST145</div> </div> <div class="week-cell" data-day="fri" data-time="11:00"></div> <div class="week-cell" data-day="sat" data-time="11:00"></div> <div class="time-label">12:00</div> <div class="week-cell" data-day="sun" data-time="12:00"></div> <div class="week-cell" data-day="mon" data-time="12:00"></div> <div class="week-cell" data-day="tue" data-time="12:00"></div> <div class="week-cell" data-day="wed" data-time="12:00"></div> <div class="week-cell" data-day="thu" data-time="12:00"></div> <div class="week-cell" data-day="fri" data-time="12:00"></div> <div class="week-cell" data-day="sat" data-time="12:00"></div> <div class="time-label">1:00</div> <div class="week-cell" data-day="sun" data-time="13:00"></div> <div class="week-cell" data-day="mon" data-time="13:00"></div> <div class="week-cell" data-day="tue" data-time="13:00"> <div class="class-event science" draggable="true">BIO210</div> </div> <div class="week-cell" data-day="wed" data-time="13:00"></div> <div class="week-cell" data-day="thu" data-time="13:00"></div> <div class="week-cell" data-day="fri" data-time="13:00"></div> <div class="week-cell" data-day="sat" data-time="13:00"></div> <div class="time-label">2:00</div> <div class="week-cell" data-day="sun" data-time="14:00"></div> <div class="week-cell" data-day="mon" data-time="14:00"></div> <div class="week-cell" data-day="tue" data-time="14:00"> <div class="class-event exam" draggable="true">MATH301 Midterm</div> </div> <div class="week-cell" data-day="wed" data-time="14:00"></div> <div class="week-cell" data-day="thu" data-time="14:00"></div> <div class="week-cell" data-day="fri" data-time="14:00"></div> <div class="week-cell" data-day="sat" data-time="14:00"></div> <div class="time-label">3:00</div> <div class="week-cell" data-day="sun" data-time="15:00"></div>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Luxury Hotel Booking Calendar</title> <style> @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Montserrat:wght@300;400;500&display=swap'); :root { --primary: #b18e5e; --primary-light: #d9c6a5; --secondary: #34495e; --tertiary: #2c3e50; --light: #f5f5f5; --dark: #212121; --available: #2ecc71; --limited: #f39c12; --unavailable: #e74c3c; --shadow: 0 10px 30px rgba(0, 0, 0, 0.1); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Montserrat', sans-serif; background-color: var(--light); color: var(--dark); height: 100vh; display: flex; align-items: center; justify-content: center; overflow-x: hidden; } .booking-container { width: 100%; max-width: 700px; height: 700px; background: white; border-radius: 12px; box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: hidden; position: relative; } .header { background-color: var(--secondary); color: white; padding: 20px; text-align: center; position: relative; } h1 { font-family: 'Playfair Display', serif; font-size: 28px; margin-bottom: 5px; letter-spacing: 1px; } .subheading { font-weight: 300; margin-bottom: 10px; font-size: 14px; opacity: 0.8; } .booking-body { display: flex; flex-direction: column; height: calc(100% - 140px); overflow-y: auto; padding: 20px; } .room-select { margin-bottom: 20px; } .select-wrapper { position: relative; margin-bottom: 15px; } .select-wrapper select { width: 100%; padding: 12px 15px; border: 1px solid var(--primary-light); border-radius: 5px; background-color: transparent; font-family: 'Montserrat', sans-serif; font-size: 14px; appearance: none; outline: none; cursor: pointer; transition: var(--transition); } .select-wrapper:after { content: '▼'; position: absolute; top: 50%; right: 15px; transform: translateY(-50%); pointer-events: none; color: var(--primary); font-size: 12px; } .select-wrapper select:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(177, 142, 94, 0.2); } .calendar-navigation { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .month-display { font-family: 'Playfair Display', serif; font-size: 18px; color: var(--tertiary); } .nav-button { background: transparent; border: none; cursor: pointer; font-size: 16px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: var(--transition); color: var(--primary); } .nav-button:hover { background-color: var(--primary-light); color: white; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-bottom: 20px; } .weekday { text-align: center; font-weight: 500; padding: 8px 0; font-size: 14px; color: var(--tertiary); } .day { position: relative; height: 0; padding-bottom: 100%; border-radius: 5px; background-color: var(--light); transition: var(--transition); cursor: pointer; overflow: hidden; } .day-content { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; } .day.empty { background-color: transparent; cursor: default; } .day.available { background-color: rgba(46, 204, 113, 0.1); } .day.available:hover { background-color: rgba(46, 204, 113, 0.3); transform: translateY(-2px); } .day.limited { background-color: rgba(243, 156, 18, 0.1); } .day.limited:hover { background-color: rgba(243, 156, 18, 0.3); transform: translateY(-2px); } .day.unavailable { background-color: rgba(231, 76, 60, 0.1); cursor: not-allowed; } .day.selected { background-color: var(--primary); color: white; transform: scale(0.95); box-shadow: 0 5px 15px rgba(177, 142, 94, 0.3); } .day.selected:hover { transform: scale(0.95) translateY(-2px); } .availability-indicator { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; } .available .availability-indicator { background-color: var(--available); } .limited .availability-indicator { background-color: var(--limited); } .unavailable .availability-indicator { background-color: var(--unavailable); } .legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; margin-bottom: 20px; } .legend-item { display: flex; align-items: center; font-size: 12px; } .legend-color { width: 12px; height: 12px; border-radius: 50%; margin-right: 5px; } .selected-dates { padding: 15px; background-color: var(--light); border-radius: 8px; margin-bottom: 20px; } .selected-dates h3 { font-family: 'Playfair Display', serif; font-size: 16px; margin-bottom: 10px; color: var(--tertiary); } .date-range { display: flex; justify-content: space-between; font-size: 14px; } .date-box { flex: 1; padding: 10px; background-color: white; border-radius: 5px; border: 1px solid var(--primary-light); display: flex; align-items: center; justify-content: center; } .date-separator { margin: 0 10px; color: var(--primary); } .night-count { margin-top: 10px; text-align: center; font-size: 13px; color: var(--tertiary); } .footer { padding: 20px; background-color: white; border-top: 1px solid rgba(0, 0, 0, 0.05); display: flex; justify-content: space-between; align-items: center; gap: 15px; } .price-info { font-size: 14px; } .price { font-family: 'Playfair Display', serif; font-size: 24px; color: var(--primary); margin-top: 5px; } .book-button { background-color: var(--primary); color: white; border: none; border-radius: 5px; padding: 12px 25px; font-family: 'Montserrat', sans-serif; font-size: 16px; cursor: pointer; transition: var(--transition); position: relative; overflow: hidden; } .book-button:hover { background-color: var(--secondary); } .book-button:before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient( to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0) 100% ); transition: all 0.8s ease; } .book-button:hover:before { left: 100%; } .toast { position: absolute; bottom: -60px; left: 50%; transform: translateX(-50%); background-color: var(--secondary); color: white; padding: 12px 25px; border-radius: 5px; font-size: 14px; transition: var(--transition); opacity: 0; z-index: 1000; } .toast.show { bottom: 20px; opacity: 1; } .room-preview { position: relative; height: 120px; overflow: hidden; border-radius: 8px; margin-bottom: 20px; transition: var(--transition); } .room-preview:hover { transform: translateY(-5px); } .room-preview img { width: 100%; height: 100%; object-fit: cover; transition: var(--transition); } .room-preview:hover img { transform: scale(1.05); } .room-preview-info { position: absolute; bottom: 0; left: 0; right: 0; padding: 10px 15px; background: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0)); color: white; } .room-preview-info h3 { font-family: 'Playfair Display', serif; font-size: 16px; margin-bottom: 3px; } .room-features { display: flex; font-size: 12px; gap: 10px; } @media (max-width: 600px) { .booking-container { height: auto; min-height: 700px; } h1 { font-size: 24px; } .weekday, .day-content { font-size: 12px; } .footer { flex-direction: column; text-align: center; } .book-button { width: 100%; } .room-preview { height: 100px; } } </style> </head> <body> <div class="booking-container"> <div class="header"> <h1>Riviera Grand Hotel</h1> <p class="subheading">Select your stay dates for the perfect escape</p> </div> <div class="booking-body"> <div class="room-select"> <div class="select-wrapper"> <select id="room-type"> <option value="deluxe">Deluxe Ocean View Suite</option> <option value="premium">Premium Harbor View Room</option> <option value="executive">Executive Penthouse Suite</option> <option value="garden">Garden Villa with Private Pool</option> </select> </div> <div class="room-preview"> <img id="room-image" src="https://images.unsplash.com/photo-1590490360182-c33d57733427?ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80" alt="Room preview"> <div class="room-preview-info"> <h3>Deluxe Ocean View Suite</h3> <div class="room-features"> <span>King Bed</span> <span>•</span> <span>Ocean View</span> <span>•</span> <span>65 m²</span> </div> </div> </div> </div> <div class="calendar-section"> <div class="calendar-navigation"> <button class="nav-button" id="prev-month">←</button> <div class="month-display" id="month-display">August 2023</div> <button class="nav-button" id="next-month">→</button> </div> <div class="calendar-grid" id="calendar-grid"> <!-- Calendar will be generated here --> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color" style="background-color: var(--available);"></div> <span>Available</span> </div> <div class="legend-item"> <div class="legend-color" style="background-color: var(--limited);"></div> <span>Limited</span> </div> <div class="legend-item"> <div class="legend-color" style="background-color: var(--unavailable);"></div> <span>Unavailable</span> </div> </div> <div class="selected-dates"> <h3>Your Stay</h3> <div class="date-range"> <div class="date-box" id="check-in">Select check-in</div> <div class="date-separator">→</div> <div class="date-box" id="check-out">Select check-out</div> </div> <div class="night-count" id="night-count"></div> </div> </div> </div> <div class="footer"> <div class="price-info"> <div>Total for selected stay</div> <div class="price" id="total-price">-</div> </div> <button class="book-button" id="book-button">Reserve Now</button> </div> <div class="toast" id="toast">Your booking has been confirmed!</div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const calendarGrid = document.getElementById('calendar-grid'); const monthDisplay = document.getElementById('month-display'); const prevMonthBtn = document.getElementById('prev-month'); const nextMonthBtn = document.getElementById('next-month'); const checkInDisplay = document.getElementById('check-in'); const checkOutDisplay = document.getElementById('check-out'); const nightCountDisplay = document.getElementById('night-count'); const totalPriceDisplay = document.getElementById('total-price'); const bookButton = document.getElementById('book-button'); const toast = document.getElementById('toast'); const roomTypeSelect = document.getElementById('room-type'); const roomImage = document.getElementById('room-image'); const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; let currentDate = new Date(); let currentMonth = currentDate.getMonth(); let currentYear = currentDate.getFullYear(); let checkInDate = null; let checkOutDate = null; // Room data with pricing and images const roomData = { deluxe: { name: 'Deluxe Ocean View Suite', features: ['King Bed', 'Ocean View', '65 m²'], price: 425, image: 'https://images.unsplash.com/photo-1590490360182-c33d57733427?ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80' }, premium: { name: 'Premium Harbor View Room', features: ['Queen Bed', 'Harbor View', '45 m²'], price: 350, image: 'https://images.unsplash.com/photo-1566665797739-1674de7a421a?ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80' }, executive: { name: 'Executive Penthouse Suite', features: ['King Bed', 'Panoramic View', '120 m²'], price: 750, image: 'https://images.unsplash.com/photo-1578683010236-d716f9a3f461?ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80' }, garden: { name: 'Garden Villa with Private Pool', features: ['King Bed', 'Garden View', '90 m²'], price: 600, image: 'https://images.unsplash.com/photo-1540541338287-41700207dee6?ixlib=rb-1.2.1&auto=format&fit=crop&w=1050&q=80' } }; // Generate random availability for days function getAvailabilityForDate(date) { // Past dates are unavailable if (date < new Date(currentDate.setHours(0, 0, 0, 0))) { return 'unavailable'; } const randomValue = Math.random(); if (randomValue < 0.6) { return 'available'; } else if (randomValue < 0.8) { return 'limited'; } else { return 'unavailable'; } } // Generate the calendar for the current month function generateCalendar() { calendarGrid.innerHTML = ''; // Add weekday headers weekdays.forEach(weekday => { const weekdayEl = document.createElement('div'); weekdayEl.className = 'weekday'; weekdayEl.textContent = weekday; calendarGrid.appendChild(weekdayEl); }); // Update month display const monthName = new Date(currentYear, currentMonth).toLocaleString('default', { month: 'long' }); monthDisplay.textContent = `${monthName} ${currentYear}`; // Calculate the first day of the month and how many days in the month const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay(); const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); // Add empty days for the days before the 1st of the month for (let i = 0; i < firstDayOfMonth; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'day empty'; calendarGrid.appendChild(emptyDay); } // Add days of the month for (let day = 1; day <= daysInMonth; day++) { const date = new Date(currentYear, currentMonth, day); const availability = getAvailabilityForDate(date); const dayEl = document.createElement('div'); dayEl.className = `day ${availability}`; if (checkInDate && date.getTime() === checkInDate.getTime()) { dayEl.classList.add('selected'); } if (checkOutDate && date.getTime() === checkOutDate.getTime()) { dayEl.classList.add('selected'); } const dayContent = document.createElement('div'); dayContent.className = 'day-content'; dayContent.textContent = day; const indicator = document.createElement('div'); indicator.className = 'availability-indicator'; dayContent.appendChild(indicator); dayEl.appendChild(dayContent); // Add click event for date selection if (availability !== 'unavailable') { dayEl.addEventListener('click', () => selectDate(date)); } calendarGrid.appendChild(dayEl); } } // Handle date selection function selectDate(date) { if (!checkInDate || (checkInDate && checkOutDate) || date < checkInDate) { // Start a new selection or reset if out of order checkInDate = date; checkOutDate = null; checkInDisplay.textContent = formatDate(checkInDate); checkOutDisplay.textContent = 'Select check-out'; nightCountDisplay.textContent = ''; totalPriceDisplay.textContent = '-'; } else { // Complete the selection checkOutDate = date; checkOutDisplay.textContent = formatDate(checkOutDate); // Calculate nights and total const nightCount = calculateNights(checkInDate, checkOutDate); nightCountDisplay.textContent = `${nightCount} night${nightCount !== 1 ? 's' : ''}`; // Update total price const selectedRoom = roomTypeSelect.value; const roomPrice = roomData[selectedRoom].price; const total = nightCount * roomPrice; totalPriceDisplay.textContent = `$${total}`; } generateCalendar(); // Redraw calendar to show selection } // Format date for display function formatDate(date) { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } // Calculate number of nights between two dates function calculateNights(startDate, endDate) { const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds const diffDays = Math.round(Math.abs((startDate - endDate) / oneDay)); return diffDays; } // Update room preview when selection changes roomTypeSelect.addEventListener('change', function() { const selectedRoom = this.value; const room = roomData[selectedRoom]; roomImage.src = room.image; const roomInfoContainer = roomImage.nextElementSibling; roomInfoContainer.querySelector('h3').textContent = room.name; roomInfoContainer.querySelector('.room-features').innerHTML = room.features.join('<span>•</span>'); // Update price if dates are selected if (checkInDate && checkOutDate) { const nightCount = calculateNights(checkInDate, checkOutDate); const total = nightCount * room.price; totalPriceDisplay.textContent = `$${total}`; } }); // Navigation buttons prevMonthBtn.addEventListener('click', () => { currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } generateCalendar(); }); nextMonthBtn.addEventListener('click', () => { currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } generateCalendar(); }); // Book button bookButton.addEventListener('click', () => { if (checkInDate && checkOutDate) { toast.textContent = 'Booking confirmed! Check your email for details.'; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); // Reset selection after booking setTimeout(() => { checkInDate = null; checkOutDate = null; checkInDisplay.textContent = 'Select check-in'; checkOutDisplay.textContent = 'Select check-out'; nightCountDisplay.textContent = ''; totalPriceDisplay.textContent = '-'; generateCalendar(); }, 500); }, 3000); } else { toast.textContent = 'Please select check-in and check-out dates'; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } }); // Initialize calendar generateCalendar(); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Event Planner Calendar Grid</title> <style> :root { --primary: #2e4dd4; --primary-light: #4a69ff; --secondary: #ff6b6b; --background: #f8f9fb; --light: #ffffff; --dark: #333342; --gray: #8e92a4; --light-gray: #e8eaf1; --success: #4caf50; --warning: #ff9800; --info: #2196f3; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--background); color: var(--dark); height: 100%; overflow: hidden; } .container { max-width: 700px; height: 700px; margin: 0 auto; padding: 15px; overflow: hidden; display: flex; flex-direction: column; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .logo { display: flex; align-items: center; gap: 10px; } .logo-icon { width: 40px; height: 40px; background-color: var(--primary); border-radius: 10px; display: flex; align-items: center; justify-content: center; color: white; font-size: 20px; } .logo-text { font-weight: bold; font-size: 18px; } .view-toggle { display: flex; align-items: center; background-color: var(--light-gray); border-radius: 20px; padding: 3px; } .view-toggle button { padding: 6px 12px; border: none; background: none; border-radius: 20px; cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.3s ease; color: var(--gray); } .view-toggle button.active { background-color: var(--light); color: var(--primary); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); } .month-selector { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .month-title { font-size: 24px; font-weight: bold; } .month-nav { display: flex; gap: 10px; } .month-nav button { width: 36px; height: 36px; border-radius: 50%; border: none; background-color: var(--light); cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; color: var(--dark); } .month-nav button:hover { background-color: var(--primary-light); color: white; } .filters { display: flex; gap: 10px; margin-bottom: 15px; overflow-x: auto; padding-bottom: 5px; } .filter-chip { padding: 6px 14px; border-radius: 20px; border: 1px solid var(--light-gray); background-color: var(--light); font-size: 12px; cursor: pointer; transition: all 0.3s ease; white-space: nowrap; } .filter-chip.selected { background-color: var(--primary); color: white; border-color: var(--primary); } .calendar-container { flex: 1; display: flex; flex-direction: column; background: var(--light); border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); } .days-header { display: grid; grid-template-columns: repeat(7, 1fr); padding: 10px 5px; font-weight: 500; background-color: var(--primary); color: white; text-align: center; } .days-header span { font-size: 14px; } .calendar-body { flex: 1; display: grid; grid-template-columns: repeat(7, 1fr); grid-template-rows: repeat(6, 1fr); overflow-y: auto; } .date-cell { border: 1px solid var(--light-gray); padding: 5px; position: relative; min-height: 90px; transition: all 0.3s ease; } .date-cell:hover { background-color: #f0f4ff; } .date-cell.today { background-color: rgba(46, 77, 212, 0.05); } .date-cell.other-month { opacity: 0.4; } .date-number { font-size: 14px; font-weight: 500; margin-bottom: 5px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } .date-cell.today .date-number { background-color: var(--primary); color: white; border-radius: 50%; } .event { padding: 2px 4px; border-radius: 4px; font-size: 10px; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: all 0.2s ease; } .event:hover { transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .event.conference { background-color: rgba(33, 150, 243, 0.15); border-left: 2px solid var(--info); color: #0d6efd; } .event.workshop { background-color: rgba(76, 175, 80, 0.15); border-left: 2px solid var(--success); color: #2e7d32; } .event.exhibition { background-color: rgba(255, 152, 0, 0.15); border-left: 2px solid var(--warning); color: #ef6c00; } .event.keynote { background-color: rgba(255, 107, 107, 0.15); border-left: 2px solid var(--secondary); color: #d32f2f; } .event-indicator { position: absolute; bottom: 2px; right: 2px; font-size: 9px; color: var(--gray); } .tooltip { position: absolute; background-color: var(--light); border-radius: 8px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); padding: 12px; z-index: 10; width: 250px; pointer-events: none; opacity: 0; transition: opacity 0.3s ease; transform: translateY(10px); } .tooltip.active { opacity: 1; transform: translateY(0); } .tooltip-header { font-weight: bold; margin-bottom: 5px; color: var(--primary); } .tooltip-time { font-size: 12px; color: var(--gray); margin-bottom: 5px; } .tooltip-description { font-size: 12px; margin-bottom: 10px; } .tooltip-attendees { display: flex; gap: 2px; } .tooltip-attendee { width: 22px; height: 22px; border-radius: 50%; background-color: var(--primary-light); color: white; display: flex; align-items: center; justify-content: center; font-size: 10px; } .status-indicator { display: flex; align-items: center; gap: 5px; margin-top: 10px; } .status-dot { width: 8px; height: 8px; border-radius: 50%; background-color: var(--success); } .status-text { font-size: 11px; color: var(--success); } .live-indicator { position: absolute; top: 5px; right: 5px; background-color: var(--secondary); color: white; font-size: 8px; padding: 2px 5px; border-radius: 10px; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } .add-event-button { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary); color: white; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); transition: all 0.3s ease; z-index: 100; } .add-event-button:hover { background-color: var(--primary-light); transform: scale(1.05); } @media (max-width: 600px) { .month-title { font-size: 20px; } .days-header span { font-size: 12px; } .date-cell { min-height: 70px; } } /* Loader animation */ .loader { width: 100%; height: 3px; position: absolute; top: 0; left: 0; overflow: hidden; } .loader:before { content: ""; position: absolute; top: 0; left: -50%; width: 50%; height: 100%; background: linear-gradient(to right, transparent, var(--primary), transparent); animation: loading 1.5s infinite; } @keyframes loading { 0% { left: -50%; } 100% { left: 150%; } } /* Notification badge */ .notification-badge { position: absolute; top: -5px; right: -5px; width: 16px; height: 16px; border-radius: 50%; background-color: var(--secondary); color: white; font-size: 10px; display: flex; align-items: center; justify-content: center; } </style> </head> <body> <div class="container"> <div class="loader" id="loader"></div> <div class="header"> <div class="logo"> <div class="logo-icon"> <i>E</i> </div> <div class="logo-text">EventMaster</div> </div> <div class="view-toggle"> <button class="active">Month</button> <button>Week</button> <button>Day</button> </div> </div> <div class="month-selector"> <div class="month-title" id="month-title">October 2023</div> <div class="month-nav"> <button id="prev-month">←</button> <button id="next-month">→</button> </div> </div> <div class="filters"> <div class="filter-chip selected">All Events</div> <div class="filter-chip">Conferences</div> <div class="filter-chip">Workshops</div> <div class="filter-chip">Exhibitions</div> <div class="filter-chip">Keynotes</div> </div> <div class="calendar-container"> <div class="days-header"> <span>Sun</span> <span>Mon</span> <span>Tue</span> <span>Wed</span> <span>Thu</span> <span>Fri</span> <span>Sat</span> </div> <div class="calendar-body" id="calendar-body"> <!-- Calendar cells will be generated by JS --> </div> </div> <div id="tooltip" class="tooltip"> <div class="tooltip-header">Event Title</div> <div class="tooltip-time">Time</div> <div class="tooltip-description">Description</div> <div class="tooltip-attendees"> <!-- Attendees will be populated by JS --> </div> <div class="status-indicator"> <div class="status-dot"></div> <div class="status-text">Confirmed</div> </div> </div> <div class="add-event-button" id="add-event">+</div> </div> <script> // Event data const events = [ { id: 1, title: "TechConf 2023", date: "2023-10-05", time: "09:00 - 18:00", type: "conference", description: "Annual tech conference featuring the latest in cloud computing and AI innovations.", attendees: 450, status: "confirmed", location: "Main Hall A" }, { id: 2, title: "Product Design Workshop", date: "2023-10-08", time: "13:00 - 16:00", type: "workshop", description: "Hands-on workshop exploring modern UI/UX design patterns.", attendees: 28, status: "confirmed", location: "Design Lab" }, { id: 3, title: "Startup Exhibition", date: "2023-10-12", time: "10:00 - 17:00", type: "exhibition", description: "Showcase of 50+ innovative startups from around the globe.", attendees: 350, status: "confirmed", location: "Exhibition Hall" }, { id: 4, title: "Future of AI Keynote", date: "2023-10-05", time: "10:30 - 12:00", type: "keynote", description: "Featured talk by Dr. Sarah Chen on AI ethics and future implications.", attendees: 220, status: "live", location: "Auditorium" }, { id: 5, title: "Networking Mixer", date: "2023-10-15", time: "18:00 - 21:00", type: "conference", description: "Evening networking event with industry leaders and refreshments.", attendees: 180, status: "confirmed", location: "Rooftop Garden" }, { id: 6, title: "Data Science Workshop", date: "2023-10-18", time: "09:30 - 15:30", type: "workshop", description: "Practical workshop on implementing machine learning models.", attendees: 32, status: "confirmed", location: "Tech Lab B" }, { id: 7, title: "DevOps Summit", date: "2023-10-25", time: "09:00 - 17:00", type: "conference", description: "Full-day summit on DevOps practices and continuous integration.", attendees: 175, status: "confirmed", location: "Conference Center" }, { id: 8, title: "Product Launch", date: "2023-10-20", time: "11:00 - 13:00", type: "keynote", description: "Unveiling of the new ProductX platform with live demonstrations.", attendees: 260, status: "confirmed", location: "Main Stage" }, { id: 9, title: "Tech Art Exhibition", date: "2023-10-22", time: "10:00 - 19:00", type: "exhibition", description: "Interactive exhibition exploring the intersection of art and technology.", attendees: 320, status: "confirmed", location: "Gallery Space" } ]; // DOM elements const calendarBody = document.getElementById('calendar-body'); const monthTitle = document.getElementById('month-title'); const prevMonthBtn = document.getElementById('prev-month'); const nextMonthBtn = document.getElementById('next-month'); const tooltip = document.getElementById('tooltip'); const addEventBtn = document.getElementById('add-event'); const loader = document.getElementById('loader'); // Current date tracking let currentDate = new Date(); let currentMonth = currentDate.getMonth(); let currentYear = currentDate.getFullYear(); // Initialize the calendar generateCalendar(currentMonth, currentYear); // Event listeners prevMonthBtn.addEventListener('click', () => { showLoader(); currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } generateCalendar(currentMonth, currentYear); hideLoader(); }); nextMonthBtn.addEventListener('click', () => { showLoader(); currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } generateCalendar(currentMonth, currentYear); hideLoader(); }); document.querySelectorAll('.filter-chip').forEach(chip => { chip.addEventListener('click', () => { document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('selected')); chip.classList.add('selected'); showLoader(); setTimeout(() => { generateCalendar(currentMonth, currentYear); hideLoader(); }, 300); }); }); document.querySelectorAll('.view-toggle button').forEach(button => { button.addEventListener('click', () => { document.querySelectorAll('.view-toggle button').forEach(b => b.classList.remove('active')); button.classList.add('active'); showLoader(); setTimeout(hideLoader, 500); }); }); addEventBtn.addEventListener('click', () => { addEventBtn.style.transform = 'scale(0.9)'; setTimeout(() => { addEventBtn.style.transform = 'scale(1)'; }, 100); }); // Calendar generation function function generateCalendar(month, year) { calendarBody.innerHTML = ''; // Update month title const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; monthTitle.textContent = `${monthNames[month]} ${year}`; // Get first day of month const firstDay = new Date(year, month, 1).getDay(); // Get number of days in month const daysInMonth = new Date(year, month + 1, 0).getDate(); // Get previous month's days to fill starting gaps const prevMonthDays = new Date(year, month, 0).getDate(); // Calculate the total number of cells needed const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7; let date = 1; let prevMonthDate = prevMonthDays - firstDay + 1; let nextMonthDate = 1; // Create cells for (let i = 0; i < totalCells; i++) { const cell = document.createElement('div'); cell.className = 'date-cell'; if (i < firstDay) { // Previous month cell.innerHTML = `<div class="date-number">${prevMonthDate}</div>`; cell.classList.add('other-month'); prevMonthDate++; } else if (date > daysInMonth) { // Next month cell.innerHTML = `<div class="date-number">${nextMonthDate}</div>`; cell.classList.add('other-month'); nextMonthDate++; } else { // Current month cell.innerHTML = `<div class="date-number">${date}</div>`; // Check if it's today const today = new Date(); if (date === today.getDate() && month === today.getMonth() && year === today.getFullYear()) { cell.classList.add('today'); } // Check for events on this day const cellDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`; const selectedFilter = document.querySelector('.filter-chip.selected').textContent.toLowerCase(); // Filter events based on the selected filter let cellEvents = events.filter(event => event.date === cellDate); if (selectedFilter !== 'all events') { const filterType = selectedFilter.slice(0, -1).toLowerCase(); // Remove 's' at the end cellEvents = cellEvents.filter(event => event.type.includes(filterType)); } // Add events to the cell cellEvents.forEach(event => { const eventElement = document.createElement('div'); eventElement.className = `event ${event.type}`; eventElement.textContent = event.title; eventElement.dataset.eventId = event.id; // Add live indicator for ongoing events if (event.status === 'live') { const liveIndicator = document.createElement('div'); liveIndicator.className = 'live-indicator'; liveIndicator.textContent = 'LIVE'; eventElement.appendChild(liveIndicator); } // Event hover for tooltip eventElement.addEventListener('mouseenter', (e) => { showEventTooltip(event, e); }); eventElement.addEventListener('mouseleave', () => { hideEventTooltip(); }); cell.appendChild(eventElement); }); // Show event count if there are more events than can fit if (cellEvents.length > 3) { const visibleEvents = Math.min(cellEvents.length, 3); const hiddenEvents = cellEvents.length - visibleEvents; if (hiddenEvents > 0) { const indicator = document.createElement('div'); indicator.className = 'event-indicator'; indicator.textContent = `+${hiddenEvents} more`; cell.appendChild(indicator); } } date++; } calendarBody.appendChild(cell); } } // Tooltip functions function showEventTooltip(event, e) { const tooltipHeader = tooltip.querySelector('.tooltip-header'); const tooltipTime = tooltip.querySelector('.tooltip-time'); const tooltipDescription = tooltip.querySelector('.tooltip-description'); const tooltipAttendees = tooltip.querySelector('.tooltip-attendees'); const statusDot = tooltip.querySelector('.status-dot'); const statusText = tooltip.querySelector('.status-text'); tooltipHeader.textContent = event.title; tooltipTime.textContent = `${event.time} • ${event.location}`; tooltipDescription.textContent = event.description; // Generate attendee avatars tooltipAttendees.innerHTML = ''; const maxAvatars = 5; const shownAvatars = Math.min(event.attendees, maxAvatars); for (let i = 0; i < shownAvatars; i++) { const attendee = document.createElement('div'); attendee.className = 'tooltip-attendee'; // Using initials as placeholder attendee.textContent = String.fromCharCode(65 + i % 26); tooltipAttendees.appendChild(attendee); } if (event.attendees > maxAvatars) { const moreAttendees = document.createElement('div'); moreAttendees.className = 'tooltip-attendee'; moreAttendees.textContent = `+${event.attendees - maxAvatars}`; tooltipAttendees.appendChild(moreAttendees); } // Set status if (event.status === 'live') { statusDot.style.backgroundColor = 'var(--secondary)'; statusText.textContent = 'Live Now'; statusText.style.color = 'var(--secondary)'; } else { statusDot.style.backgroundColor = 'var(--success)'; statusText.textContent = 'Confirmed'; statusText.style.color = 'var(--success)'; } // Position tooltip const rect = e.target.getBoundingClientRect(); const containerRect = document.querySelector('.container').getBoundingClientRect(); let left = rect.left - containerRect.left; let top = rect.top - containerRect.top; // Adjust position to keep tooltip in view if (left + 250 > containerRect.width) { left = containerRect.width - 270; } if (top + 150 > containerRect.height) { top = rect.top - 160; } tooltip.style.left = `${left}px`; tooltip.style.top = `${top + 25}px`; tooltip.classList.add('active'); } function hideEventTooltip() { tooltip.classList.remove('active'); } // Loader functions function showLoader() { loader.style.display = 'block'; } function hideLoader() { setTimeout(() => { loader.style.display = 'none'; }, 500); } // Initial load animation showLoader(); setTimeout(hideLoader, 800); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tech Project Management Calendar</title> <style> :root { --primary: #2c3e50; --secondary: #3498db; --accent: #1abc9c; --light: #ecf0f1; --medium: #bdc3c7; --dark: #34495e; --warning: #e74c3c; --success: #2ecc71; --info: #9b59b6; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--light); color: var(--primary); height: 700px; width: 700px; overflow: hidden; } .container { max-width: 700px; height: 700px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; background-color: white; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); border-radius: 10px; position: relative; overflow: hidden; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid var(--medium); position: relative; z-index: 10; } .logo { display: flex; align-items: center; } .logo-icon { width: 30px; height: 30px; background-color: var(--secondary); border-radius: 6px; margin-right: 10px; position: relative; overflow: hidden; } .logo-icon::before, .logo-icon::after { content: ''; position: absolute; background-color: white; } .logo-icon::before { width: 60%; height: 2px; top: 50%; left: 20%; transform: translateY(-50%); } .logo-icon::after { width: 2px; height: 60%; left: 50%; top: 20%; transform: translateX(-50%); } .logo-text { font-weight: 600; font-size: 18px; color: var(--primary); } .controls { display: flex; gap: 15px; } .btn { padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 5px; } .btn-primary { background-color: var(--secondary); color: white; } .btn-primary:hover { background-color: #2980b9; transform: translateY(-2px); } .btn-outline { background-color: transparent; color: var(--primary); border: 1px solid var(--medium); } .btn-outline:hover { background-color: var(--light); transform: translateY(-2px); } .btn-icon { width: 14px; height: 14px; display: inline-block; position: relative; } .view-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; z-index: 5; } .date-navigation { display: flex; align-items: center; gap: 15px; } .current-date { font-size: 20px; font-weight: 600; } .date-controls button { background: none; border: none; cursor: pointer; font-size: 18px; color: var(--primary); transition: all 0.2s ease; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .date-controls button:hover { background-color: var(--light); } .view-selector { display: flex; gap: 5px; background-color: var(--light); padding: 3px; border-radius: 5px; } .view-selector button { padding: 5px 10px; border: none; background: none; border-radius: 4px; cursor: pointer; font-size: 14px; color: var(--primary); transition: all 0.2s ease; position: relative; } .view-selector button.active { background-color: white; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .view-selector button:not(.active):hover { background-color: rgba(255, 255, 255, 0.5); } .calendar-container { flex: 1; overflow: auto; scrollbar-width: thin; scrollbar-color: var(--medium) var(--light); position: relative; z-index: 1; } .calendar-container::-webkit-scrollbar { width: 6px; height: 6px; } .calendar-container::-webkit-scrollbar-track { background: var(--light); } .calendar-container::-webkit-scrollbar-thumb { background-color: var(--medium); border-radius: 3px; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); grid-auto-rows: minmax(80px, auto); gap: 5px; height: 100%; } .day-header { text-align: center; padding: 10px; font-weight: 600; color: var(--dark); border-bottom: 1px solid var(--medium); background-color: var(--light); position: sticky; top: 0; z-index: 2; } .calendar-day { border: 1px solid var(--medium); border-radius: 5px; padding: 5px; overflow: hidden; background-color: white; transition: all 0.2s ease; position: relative; } .calendar-day.today { border-color: var(--secondary); box-shadow: 0 0 0 1px var(--secondary); } .calendar-day:hover { box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .day-number { font-size: 14px; font-weight: 600; text-align: right; padding: 2px 5px; background-color: var(--light); border-radius: 3px; margin-bottom: 5px; } .today .day-number { background-color: var(--secondary); color: white; } .task { padding: 5px; margin-bottom: 5px; border-radius: 3px; font-size: 12px; cursor: pointer; position: relative; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; animation: fadeIn 0.3s ease; color: white; } .task-dev { background-color: var(--secondary); } .task-design { background-color: var(--info); } .task-meeting { background-color: var(--warning); } .task-milestone { background-color: var(--success); } .task-deadline { background-color: var(--dark); } .task::before { content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background-color: rgba(255, 255, 255, 0.4); } .task:hover { transform: translateX(2px); } .milestones { display: flex; gap: 3px; margin-top: 5px; flex-wrap: wrap; } .milestone { width: 8px; height: 8px; border-radius: 50%; cursor: pointer; } .popup { position: absolute; z-index: 100; background-color: white; border-radius: 5px; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15); padding: 15px; width: 300px; max-width: calc(100% - 40px); animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border-left: 4px solid var(--secondary); pointer-events: none; opacity: 0; transform: translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; } .popup.active { opacity: 1; transform: translateY(0); pointer-events: all; } .popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--medium); } .popup-title { font-weight: 600; color: var(--primary); } .popup-close { background: none; border: none; font-size: 18px; cursor: pointer; color: var(--primary); } .popup-body { margin-bottom: 15px; } .popup-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .meta-item { font-size: 12px; display: flex; align-items: center; gap: 5px; color: var(--dark); } .tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; color: white; margin-right: 5px; margin-bottom: 5px; } .tag-dev { background-color: var(--secondary); } .tag-design { background-color: var(--info); } .tag-high { background-color: var(--warning); } .tag-medium { background-color: var(--accent); } .tag-low { background-color: var(--success); } .quick-add { position: absolute; right: 30px; bottom: 30px; width: 56px; height: 56px; border-radius: 50%; background-color: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); cursor: pointer; transition: all 0.3s ease; z-index: 10; } .quick-add:hover { background-color: #16a085; transform: translateY(-3px) rotate(90deg); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2); } .zoom-controls { position: absolute; left: 30px; bottom: 30px; display: flex; flex-direction: column; gap: 10px; z-index: 10; } .zoom-btn { width: 40px; height: 40px; border-radius: 50%; background-color: white; border: 1px solid var(--medium); color: var(--primary); display: flex; align-items: center; justify-content: center; font-size: 18px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: all 0.2s ease; } .zoom-btn:hover { background-color: var(--light); transform: scale(1.1); } @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } @keyframes popIn { 0% { opacity: 0; transform: scale(0.8) translateY(10px); } 100% { opacity: 1; transform: scale(1) translateY(0); } } /* Responsive adjustments */ @media (max-width: 600px) { .container { padding: 10px; } .header { flex-direction: column; align-items: flex-start; gap: 10px; } .controls { width: 100%; justify-content: space-between; } .view-controls { flex-direction: column; align-items: flex-start; gap: 10px; } .view-selector { width: 100%; justify-content: space-between; } .calendar-grid { grid-template-columns: repeat(1, 1fr); } .day-header { display: none; } .calendar-day { margin-bottom: 10px; min-height: 100px; } .quick-add { right: 20px; bottom: 20px; } .zoom-controls { left: 20px; bottom: 20px; } } /* Custom Animation */ .pulse { animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(26, 188, 156, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(26, 188, 156, 0); } 100% { box-shadow: 0 0 0 0 rgba(26, 188, 156, 0); } } /* Task count indicator */ .day-task-count { position: absolute; top: 5px; left: 5px; background: var(--dark); color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; } /* Empty state */ .empty-day { height: 100%; display: flex; align-items: center; justify-content: center; color: var(--medium); font-size: 12px; font-style: italic; } /* Color theme toggle */ .theme-toggle { position: absolute; top: 20px; right: 20px; width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, var(--light) 50%, var(--dark) 50%); cursor: pointer; border: 2px solid var(--medium); transition: transform 0.5s ease; z-index: 100; } .theme-toggle:hover { transform: rotate(180deg); } /* Zoom transition */ .calendar-grid.zoomed-in { grid-template-columns: repeat(3, 1fr); grid-auto-rows: minmax(120px, auto); transition: all 0.5s ease; } .calendar-grid.zoomed-out { grid-template-columns: repeat(7, 1fr); grid-auto-rows: minmax(60px, auto); transition: all 0.5s ease; } </style> </head> <body> <div class="container"> <div class="header"> <div class="logo"> <div class="logo-icon"></div> <div class="logo-text">TechPM Calendar</div> </div> <div class="controls"> <button class="btn btn-outline" id="filter-btn"> <span class="btn-icon">⚙️</span> Filter </button> <button class="btn btn-primary" id="add-task-btn"> <span class="btn-icon">+</span> New Task </button> </div> </div> <div class="view-controls"> <div class="date-navigation"> <div class="date-controls"> <button id="prev-month">←</button> </div> <div class="current-date" id="current-date">October 2023</div> <div class="date-controls"> <button id="next-month">→</button> </div> </div> <div class="view-selector"> <button class="active" data-view="month">Month</button> <button data-view="week">Week</button> <button data-view="day">Day</button> </div> </div> <div class="calendar-container" id="calendar-container"> <div class="calendar-grid" id="calendar-grid"> <!-- Calendar will be populated by JavaScript --> </div> </div> <div class="zoom-controls"> <button class="zoom-btn" id="zoom-in">+</button> <button class="zoom-btn" id="zoom-out">-</button> </div> <div class="quick-add pulse" id="quick-add">+</div> <div class="popup" id="task-popup"> <div class="popup-header"> <div class="popup-title" id="popup-title">Task Details</div> <button class="popup-close" id="popup-close">×</button> </div> <div class="popup-body" id="popup-body"> <p>Implement the new authentication flow to support multi-factor authentication and single sign-on integration.</p> <div class="popup-meta"> <div class="meta-item"> <span>⏱️</span> <span>Oct 12, 2023 (2h)</span> </div> <div class="meta-item"> <span>👤</span> <span>Alex Chen</span> </div> </div> <div class="popup-tags" style="margin-top: 10px;"> <span class="tag tag-dev">Development</span> <span class="tag tag-high">High Priority</span> </div> </div> </div> <div class="theme-toggle" id="theme-toggle"></div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Initialize variables const calendarGrid = document.getElementById('calendar-grid'); const currentDateEl = document.getElementById('current-date'); const prevMonthBtn = document.getElementById('prev-month'); const nextMonthBtn = document.getElementById('next-month'); const viewButtons = document.querySelectorAll('.view-selector button'); const taskPopup = document.getElementById('task-popup'); const popupClose = document.getElementById('popup-close'); const quickAddBtn = document.getElementById('quick-add'); const zoomInBtn = document.getElementById('zoom-in'); const zoomOutBtn = document.getElementById('zoom-out'); const themeToggle = document.getElementById('theme-toggle'); let currentDate = new Date(); let currentView = 'month'; let zoomLevel = 1; // Default zoom level // Sample data - Tasks for the calendar const tasks = [ { id: 1, title: 'API Integration', description: 'Integrate payment gateway API with the checkout flow and implement error handling.', date: new Date(2023, 9, 5), // October 5, 2023 type: 'dev', priority: 'high', assignee: 'Alex Chen', duration: '3h' }, { id: 2, title: 'UX Review', description: 'Conduct user experience review for the new dashboard interface with the design team.', date: new Date(2023, 9, 10), // October 10, 2023 type: 'design', priority: 'medium', assignee: 'Sarah Kim', duration: '2h' }, { id: 3, title: 'Sprint Planning', description: 'Q4 sprint planning meeting with all team leads to outline project milestones.', date: new Date(2023, 9, 7), // October 7, 2023 type: 'meeting', priority: 'high', assignee: 'Team', duration: '1.5h' }, { id: 4, title: 'Beta Release', description: 'Beta release of the customer feedback module with A/B testing features.', date: new Date(2023, 9, 15), // October 15, 2023 type: 'milestone', priority: 'high', assignee: 'Dev Team', duration: 'All day' }, { id: 5, title: 'Security Audit', description: 'Comprehensive security audit of all API endpoints and data storage practices.', date: new Date(2023, 9, 12), // October 12, 2023 type: 'dev', priority: 'high', assignee: 'Michael Wong', duration: '4h' }, { id: 6, title: 'Client Demo', description: 'Demonstration of new analytics dashboard to key stakeholders and clients.', date: new Date(2023, 9, 18), // October 18, 2023 type: 'meeting', priority: 'medium', assignee: 'Laura Evans', duration: '1h' }, { id: 7, title: 'UI Redesign', description: 'Complete the responsive redesign for mobile application interface.', date: new Date(2023, 9, 8), // October 8, 2023 type: 'design', priority: 'medium', assignee: 'Javier Rodriguez', duration: '6h' }, { id: 8, title: 'Database Migration', description: 'Migrate user data to the new cloud database architecture with zero downtime.', date: new Date(2023, 9, 20), // October 20, 2023 type: 'dev', priority: 'high', assignee: 'Devon Taylor', duration: '8h' }, { id: 9, title: 'Final Deadline', description: 'Project delivery deadline for the Q4 feature roadmap implementation.', date: new Date(2023, 9, 31), // October 31, 2023 type: 'deadline', priority: 'high', assignee: 'Team', duration: 'All day' }, { id: 10, title: 'Team Retrospective', description: 'Monthly team retrospective to discuss improvements and challenges.', date: new Date(2023, 9, 25), // October 25, 2023 type: 'meeting', priority: 'low', assignee: 'Team', duration: '2h' } ]; // Initialize calendar function renderCalendar() { calendarGrid.innerHTML = ''; if (currentView === 'month') { renderMonthView(); } else if (currentView === 'week') { renderWeekView(); } else if (currentView === 'day') { renderDayView(); } updateCurrentDateDisplay(); } // Render month view function renderMonthView() { // Add day headers const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'day-header'; dayHeader.textContent = day; calendarGrid.appendChild(dayHeader); }); // Get the first day of the month const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); const startDayOfWeek = firstDayOfMonth.getDay(); // Get the last day of the month const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); const daysInMonth = lastDayOfMonth.getDate(); // Add cells for days of previous month for (let i = 0; i < startDayOfWeek; i++) { const dayCell = document.createElement('div'); dayCell.className = 'calendar-day prev-month'; const prevMonthLastDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0).getDate(); const day = prevMonthLastDay - startDayOfWeek + i + 1; const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = day; dayCell.appendChild(dayNumber); dayCell.appendChild(createEmptyDayMessage()); calendarGrid.appendChild(dayCell); } // Add cells for days of current month for (let day = 1; day <= daysInMonth; day++) { const currentCellDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day); const dayCell = document.createElement('div'); dayCell.className = 'calendar-day'; // Check if this is today const today = new Date(); if (today.getDate() === day && today.getMonth() === currentDate.getMonth() && today.getFullYear() === currentDate.getFullYear()) { dayCell.classList.add('today'); } const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = day; dayCell.appendChild(dayNumber); // Add tasks for this day const dayTasks = tasks.filter(task => { return task.date.getDate() === day && task.date.getMonth() === currentDate.getMonth() && task.date.getFullYear() === currentDate.getFullYear(); }); if (dayTasks.length > 0) { // Add task count indicator const taskCount = document.createElement('div'); taskCount.className = 'day-task-count'; taskCount.textContent = dayTasks.length; dayCell.appendChild(taskCount); // Add tasks dayTasks.forEach(task => { const taskEl = document.createElement('div'); taskEl.className = `task task-${task.type}`; taskEl.textContent = task.title; taskEl.dataset.taskId = task.id; taskEl.addEventListener('click', (e) => { e.stopPropagation(); showTaskPopup(task, e); }); dayCell.appendChild(taskEl); }); } else { dayCell.appendChild(createEmptyDayMessage()); } calendarGrid.appendChild(dayCell); } // Calculate remaining cells needed to complete the grid const totalCells = 42; // 6 rows of 7 days const remainingCells = totalCells - (startDayOfWeek + daysInMonth); // Add cells for days of next month for (let day = 1; day <= remainingCells; day++) { const dayCell = document.createElement('div'); dayCell.className = 'calendar-day next-month'; const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = day; dayCell.appendChild(dayNumber); dayCell.appendChild(createEmptyDayMessage()); calendarGrid.appendChild(dayCell); } // Apply zoom level applyZoomLevel(); } // Create empty day message function createEmptyDayMessage() { const emptyDay = document.createElement('div'); emptyDay.className = 'empty-day'; emptyDay.textContent = 'No tasks'; return emptyDay; } // Render week view function renderWeekView() { // Find the first day of the week containing the current date const currentDay = currentDate.getDay(); // 0-6, 0 = Sunday const firstDayOfWeek = new Date(currentDate); firstDayOfWeek.setDate(currentDate.getDate() - currentDay); // Add day headers for the week const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; daysOfWeek.forEach((day, index) => { const dayDate = new Date(firstDayOfWeek); dayDate.setDate(firstDayOfWeek.getDate() + index); const dayHeader = document.createElement('div'); dayHeader.className = 'day-header'; dayHeader.textContent = `${day} (${dayDate.getDate()})`; calendarGrid.appendChild(dayHeader); const dayCell = document.createElement('div'); dayCell.className = 'calendar-day'; dayCell.style.height = '200px'; // Check if this is today const today = new Date(); if (today.getDate() === dayDate.getDate() && today.getMonth() === dayDate.getMonth() && today.getFullYear() === dayDate.getFullYear()) { dayCell.classList.add('today'); } // Add tasks for this day const dayTasks = tasks.filter(task => { return task.date.getDate() === dayDate.getDate() && task.date.getMonth() === dayDate.getMonth() && task.date.getFullYear() === dayDate.getFullYear(); }); if (dayTasks.length > 0) { // Add task count indicator const taskCount = document.createElement('div'); taskCount.className = 'day-task-count'; taskCount.textContent = dayTasks.length; dayCell.appendChild(taskCount); // Add tasks dayTasks.forEach(task => { const taskEl = document.createElement('div'); taskEl.className = `task task-${task.type}`; taskEl.textContent = task.title; taskEl.dataset.taskId = task.id; taskEl.addEventListener('click', (e) => { e.stopPropagation(); showTaskPopup(task, e); }); dayCell.appendChild(taskEl); }); } else { dayCell.appendChild(createEmptyDayMessage()); } calendarGrid.appendChild(dayCell); }); } // Render day view function renderDayView() { // Create a single full-day cell const dayCell = document.createElement('div'); dayCell.className = 'calendar-day'; dayCell.style.height = '500px'; const dayHeader = document.createElement('div'); dayHeader.className = 'day-header'; const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; dayHeader.textContent = currentDate.toLocaleDateString(undefined, options); calendarGrid.appendChild(dayHeader); // Add tasks for this day const dayTasks = tasks.filter(task => { return task.date.getDate() === currentDate.getDate() && task.date.getMonth() === currentDate.getMonth() && task.date.getFullYear() === currentDate.getFullYear(); }); if (dayTasks.length > 0) { // Add task count indicator const taskCount = document.createElement('div'); taskCount.className = 'day-task-count'; taskCount.textContent = dayTasks.length; dayCell.appendChild(taskCount); // Add tasks with more details dayTasks.forEach(task => { const taskEl = document.createElement('div'); taskEl.className = `task task-${task.type}`; taskEl.style.padding = '10px'; taskEl.style.marginBottom = '10px'; taskEl.style.whiteSpace = 'normal'; const taskTitle = document.createElement('div'); taskTitle.style.fontWeight = 'bold'; taskTitle.style.marginBottom = '5px'; taskTitle.textContent = task.title; taskEl.appendChild(taskTitle); const taskDetails = document.createElement('div'); taskDetails.style.fontSize = '11px'; taskDetails.textContent = `${task.assignee} | ${task.duration}`; taskEl.appendChild(taskDetails); taskEl.dataset.taskId = task.id; taskEl.addEventListener('click', (e) => { e.stopPropagation(); showTaskPopup(task, e); }); dayCell.appendChild(taskEl); }); } else { const emptyDay = createEmptyDayMessage(); emptyDay.style.height = '100%'; dayCell.appendChild(emptyDay); } calendarGrid.appendChild(dayCell); } // Update the current date display function updateCurrentDateDisplay() { const options = { month: 'long', year: 'numeric' }; currentDateEl.textContent = currentDate.toLocaleDateString(undefined, options); } // Event Listeners prevMonthBtn.addEventListener('click', () => { if (currentView === 'month') { currentDate.setMonth(currentDate.getMonth() - 1); } else if (currentView === 'week') { currentDate.setDate(currentDate.getDate() - 7); } else if (currentView === 'day') { currentDate.setDate(currentDate.getDate() - 1); } renderCalendar(); }); nextMonthBtn.addEventListener('click', () => { if (currentView === 'month') { currentDate.setMonth(currentDate.getMonth() + 1); } else if (currentView === 'week') { currentDate.setDate(currentDate.getDate() + 7); } else if (currentView === 'day') { currentDate.setDate(currentDate.getDate() + 1); } renderCalendar(); }); // View selector buttons viewButtons.forEach(button => { button.addEventListener('click', () => { viewButtons.forEach(btn => btn.classList
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Travel Itinerary Calendar</title> <style> :root { --primary: #3498db; --secondary: #2ecc71; --accent: #e74c3c; --dark: #34495e; --light: #f5f7fa; --text: #2c3e50; --shadow: 0 4px 12px rgba(0, 0, 0, 0.1); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; -webkit-tap-highlight-color: transparent; user-select: none; } body { background-color: var(--light); color: var(--text); height: 700px; width: 700px; max-width: 100%; max-height: 100%; overflow: hidden; position: relative; margin: 0 auto; } .container { height: 100%; width: 100%; display: flex; flex-direction: column; overflow: hidden; position: relative; background: linear-gradient(135deg, #ffffff, #f5f7fa); } .calendar-header { padding: 20px; display: flex; justify-content: space-between; align-items: center; background-color: var(--primary); color: white; border-radius: 12px 12px 0 0; box-shadow: var(--shadow); z-index: 10; } .month-nav { display: flex; align-items: center; gap: 15px; } .nav-btn { background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; transition: var(--transition); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .nav-btn:hover, .nav-btn:active { background: rgba(255, 255, 255, 0.35); transform: translateY(-2px); } .month-year { font-size: 20px; font-weight: 600; } .view-toggle { padding: 8px 12px; background: rgba(255, 255, 255, 0.2); border: none; border-radius: 20px; color: white; font-weight: 500; cursor: pointer; transition: var(--transition); } .view-toggle:hover, .view-toggle:active { background: rgba(255, 255, 255, 0.35); } .trip-info { padding: 15px 20px; background-color: white; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .trip-location { font-weight: 600; display: flex; align-items: center; gap: 8px; } .trip-location .icon { color: var(--accent); } .trip-dates { color: var(--dark); font-size: 14px; opacity: 0.8; } .calendar-grid { display: flex; flex-direction: column; flex: 1; overflow: hidden; } .days-header { display: grid; grid-template-columns: repeat(7, 1fr); padding: 10px 0; text-align: center; background-color: white; font-weight: 500; color: var(--dark); border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); grid-auto-rows: minmax(60px, 1fr); flex: 1; overflow-y: auto; transition: transform 0.35s ease; background-color: white; gap: 1px; padding: 1px; perspective: 1000px; } .day-cell { position: relative; border-radius: 8px; padding: 5px; display: flex; flex-direction: column; transition: var(--transition); cursor: pointer; background-color: rgba(245, 247, 250, 0.8); overflow: hidden; transform: scale(0.98); height: 100%; } .day-cell.active { background-color: #e8f4fc; transform: scale(1); box-shadow: 0 2px 10px rgba(52, 152, 219, 0.15); } .day-cell.has-event { background-color: rgba(46, 204, 113, 0.15); } .day-cell.trip-start { background: linear-gradient(135deg, rgba(231, 76, 60, 0.2), rgba(46, 204, 113, 0.15)); } .day-cell.trip-end { background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(231, 76, 60, 0.2)); } .day-cell.other-month { opacity: 0.4; } .day-number { font-weight: 600; margin-bottom: 4px; display: flex; justify-content: center; align-items: center; width: 25px; height: 25px; border-radius: 50%; } .day-cell.today .day-number { background-color: var(--primary); color: white; } .event-list { display: flex; flex-direction: column; gap: 3px; font-size: 11px; flex: 1; overflow: hidden; } .event-item { padding: 2px 4px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 3px; font-weight: 500; animation: fadeIn 0.3s ease; } .event-item.flight { background-color: rgba(52, 152, 219, 0.2); color: #2980b9; } .event-item.hotel { background-color: rgba(155, 89, 182, 0.2); color: #8e44ad; } .event-item.activity { background-color: rgba(46, 204, 113, 0.2); color: #27ae60; } .event-item .icon { font-size: 10px; } .bottom-nav { display: flex; justify-content: space-around; padding: 15px 10px; background-color: white; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); border-radius: 0 0 12px 12px; } .nav-item { display: flex; flex-direction: column; align-items: center; color: var(--dark); opacity: 0.7; transition: var(--transition); cursor: pointer; } .nav-item.active { color: var(--primary); opacity: 1; } .nav-item .icon { font-size: 20px; margin-bottom: 5px; } .nav-item .label { font-size: 12px; font-weight: 500; } .event-details { position: absolute; bottom: 0; left: 0; right: 0; background-color: white; border-radius: 20px 20px 0 0; padding: 20px; box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.1); z-index: 100; transform: translateY(100%); transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1); max-height: 60%; overflow-y: auto; } .event-details.active { transform: translateY(0); } .event-details-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .event-date { font-weight: 600; color: var(--dark); } .close-btn { background: transparent; border: none; font-size: 20px; color: var(--dark); cursor: pointer; } .event-details-list { display: flex; flex-direction: column; gap: 15px; } .detail-item { display: flex; gap: 12px; padding: 12px; border-radius: 10px; background-color: var(--light); transition: var(--transition); } .detail-item:hover { transform: translateY(-2px); box-shadow: var(--shadow); } .detail-icon { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 8px; color: white; } .detail-icon.flight { background-color: var(--primary); } .detail-icon.hotel { background-color: #9b59b6; } .detail-icon.activity { background-color: var(--secondary); } .detail-content { flex: 1; } .detail-title { font-weight: 600; margin-bottom: 3px; } .detail-time, .detail-location { font-size: 13px; color: var(--dark); opacity: 0.8; display: flex; align-items: center; gap: 5px; } .detail-time .icon, .detail-location .icon { font-size: 12px; } /* Swipe effect */ .swipe-container { position: relative; overflow: hidden; flex: 1; width: 100%; display: flex; } .month-slider { display: flex; transition: transform 0.35s ease; height: 100%; width: 300%; } .month-item { width: calc(100% / 3); height: 100%; display: flex; flex-direction: column; } .drag-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(255, 255, 255, 0.9); padding: 15px 20px; border-radius: 30px; box-shadow: var(--shadow); display: flex; align-items: center; gap: 10px; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 5; } .drag-hint.show { animation: fadeInOut 2s ease forwards; } .drag-hint .icon { color: var(--primary); } /* Backdrop overlay for modal */ .backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); z-index: 50; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } .backdrop.active { opacity: 1; pointer-events: all; } /* Animations */ @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } } /* Pulse animation for today indicator */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(52, 152, 219, 0); } 100% { box-shadow: 0 0 0 0 rgba(52, 152, 219, 0); } } .day-cell.today { animation: pulse 2s infinite; } /* Responsive adjustments */ @media (max-width: 500px) { .calendar-header { padding: 15px; } .month-year { font-size: 18px; } .nav-btn { width: 36px; height: 36px; } .day-cell { padding: 3px; } .event-item { font-size: 9px; padding: 1px 3px; } } /* Loading effect */ .loading-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.9); display: flex; justify-content: center; align-items: center; z-index: 1000; transition: opacity 0.5s ease, visibility 0.5s ease; } .spinner { width: 40px; height: 40px; border: 4px solid rgba(52, 152, 219, 0.2); border-radius: 50%; border-top-color: var(--primary); animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .hidden { opacity: 0; visibility: hidden; } /* Tooltip */ .tooltip { position: absolute; background-color: var(--dark); color: white; padding: 5px 10px; border-radius: 4px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 200; white-space: nowrap; } .tooltip.show { opacity: 1; } </style> </head> <body> <div class="container"> <div class="loading-overlay"> <div class="spinner"></div> </div> <div class="calendar-header"> <div class="month-nav"> <button class="nav-btn prev-month"> <i class="icon">←</i> </button> <div class="month-year">September 2023</div> <button class="nav-btn next-month"> <i class="icon">→</i> </button> </div> <button class="view-toggle">Month View</button> </div> <div class="trip-info"> <div class="trip-location"> <i class="icon">📍</i> <span>Kyoto, Japan</span> </div> <div class="trip-dates">Sep 15 - Sep 23, 2023</div> </div> <div class="swipe-container"> <div class="drag-hint"> <i class="icon">👆</i> <span>Swipe to navigate months</span> </div> <div class="month-slider"> <div class="month-item" id="prev-month"> <div class="days-header"> <div>Sun</div> <div>Mon</div> <div>Tue</div> <div>Wed</div> <div>Thu</div> <div>Fri</div> <div>Sat</div> </div> <div class="calendar-days" id="prev-month-days"> <!-- Will be filled by JS --> </div> </div> <div class="month-item" id="current-month"> <div class="days-header"> <div>Sun</div> <div>Mon</div> <div>Tue</div> <div>Wed</div> <div>Thu</div> <div>Fri</div> <div>Sat</div> </div> <div class="calendar-days" id="current-month-days"> <!-- Will be filled by JS --> </div> </div> <div class="month-item" id="next-month"> <div class="days-header"> <div>Sun</div> <div>Mon</div> <div>Tue</div> <div>Wed</div> <div>Thu</div> <div>Fri</div> <div>Sat</div> </div> <div class="calendar-days" id="next-month-days"> <!-- Will be filled by JS --> </div> </div> </div> </div> <div class="bottom-nav"> <div class="nav-item active"> <i class="icon">📅</i> <span class="label">Calendar</span> </div> <div class="nav-item"> <i class="icon">✈️</i> <span class="label">Flights</span> </div> <div class="nav-item"> <i class="icon">🏨</i> <span class="label">Hotels</span> </div> <div class="nav-item"> <i class="icon">🎭</i> <span class="label">Activities</span> </div> <div class="nav-item"> <i class="icon">⚙️</i> <span class="label">Settings</span> </div> </div> <div class="backdrop"></div> <div class="event-details"> <div class="event-details-header"> <div class="event-date">September 16, 2023</div> <button class="close-btn">×</button> </div> <div class="event-details-list"> <div class="detail-item"> <div class="detail-icon flight">✈️</div> <div class="detail-content"> <div class="detail-title">Flight JL123 to Kyoto</div> <div class="detail-time"><i class="icon">🕒</i> 08:30 - 12:15</div> <div class="detail-location"><i class="icon">🛫</i> Tokyo Haneda → Kyoto</div> </div> </div> <div class="detail-item"> <div class="detail-icon hotel">🏨</div> <div class="detail-content"> <div class="detail-title">Check-in: Kyoto Ryokan</div> <div class="detail-time"><i class="icon">🕒</i> 14:00</div> <div class="detail-location"><i class="icon">📍</i> Gion District, Kyoto</div> </div> </div> <div class="detail-item"> <div class="detail-icon activity">🍵</div> <div class="detail-content"> <div class="detail-title">Tea Ceremony Experience</div> <div class="detail-time"><i class="icon">🕒</i> 16:30 - 18:00</div> <div class="detail-location"><i class="icon">📍</i> Camellia Tea House</div> </div> </div> </div> </div> <div class="tooltip"></div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Global variables const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const TODAY = new Date(); let currentDate = new Date(2023, 8, 15); // Sept 15, 2023 for demo purposes let currentMonthIndex = 1; // Middle position in slider let startX = 0; let isDragging = false; // Elements const monthSlider = document.querySelector('.month-slider'); const monthYearDisplay = document.querySelector('.month-year'); const prevMonthBtn = document.querySelector('.prev-month'); const nextMonthBtn = document.querySelector('.next-month'); const loadingOverlay = document.querySelector('.loading-overlay'); const eventDetails = document.querySelector('.event-details'); const closeBtn = document.querySelector('.close-btn'); const backdrop = document.querySelector('.backdrop'); const dragHint = document.querySelector('.drag-hint'); const tooltip = document.querySelector('.tooltip'); const bottomNavItems = document.querySelectorAll('.nav-item'); // Sample event data const events = [ { date: '2023-09-15', type: 'flight', title: 'Flight to Tokyo' }, { date: '2023-09-16', type: 'flight', title: 'Flight to Kyoto' }, { date: '2023-09-16', type: 'hotel', title: 'Check-in: Kyoto Ryokan' }, { date: '2023-09-16', type: 'activity', title: 'Tea Ceremony' }, { date: '2023-09-17', type: 'activity', title: 'Fushimi Inari Shrine' }, { date: '2023-09-18', type: 'activity', title: 'Arashiyama Bamboo Grove' }, { date: '2023-09-19', type: 'activity', title: 'Kinkaku-ji Temple' }, { date: '2023-09-20', type: 'activity', title: 'Gion District Tour' }, { date: '2023-09-21', type: 'activity', title: 'Cooking Class' }, { date: '2023-09-22', type: 'activity', title: 'Nara Day Trip' }, { date: '2023-09-23', type: 'flight', title: 'Flight back home' }, { date: '2023-09-23', type: 'hotel', title: 'Check-out' } ]; // Functions function initCalendar() { updateMonthDisplay(); renderAllMonths(); // Hide loading overlay setTimeout(() => { loadingOverlay.classList.add('hidden'); // Show the drag hint after a short delay setTimeout(() => { dragHint.classList.add('show'); }, 1000); }, 800); } function updateMonthDisplay() { const monthYear = `${MONTHS[currentDate.getMonth()]} ${currentDate.getFullYear()}`; monthYearDisplay.textContent = monthYear; } function renderAllMonths() { // Previous month const prevDate = new Date(currentDate); prevDate.setMonth(prevDate.getMonth() - 1); renderMonth(prevDate, 'prev-month-days'); // Current month renderMonth(currentDate, 'current-month-days'); // Next month const nextDate = new Date(currentDate); nextDate.setMonth(nextDate.getMonth() + 1); renderMonth(nextDate, 'next-month-days'); } function renderMonth(date, containerId) { const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startingDayOfWeek = firstDay.getDay(); const daysInMonth = lastDay.getDate(); const container = document.getElementById(containerId); container.innerHTML = ''; // Add days from previous month to fill the first week const prevMonthLastDay = new Date(year, month, 0).getDate(); for (let i = 0; i < startingDayOfWeek; i++) { const dayNum = prevMonthLastDay - startingDayOfWeek + i + 1; const dayCell = createDayCell(dayNum, true); container.appendChild(dayCell); } // Add days of current month for (let day = 1; day <= daysInMonth; day++) { const currentDayDate = new Date(year, month, day); const dayCell = createDayCell(day, false, currentDayDate); // Check if this is today if (year === TODAY.getFullYear() && month === TODAY.getMonth() && day === TODAY.getDate()) { dayCell.classList.add('today'); } // Check if this is within the trip dates const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; if (dateStr === '2023-09-15') { dayCell.classList.add('trip-start'); } else if (dateStr === '2023-09-23') { dayCell.classList.add('trip-end'); } else if (dateStr >= '2023-09-15' && dateStr <= '2023-09-23') { dayCell.classList.add('active'); } // Add events for this day const dayEvents = events.filter(event => event.date === dateStr); if (dayEvents.length > 0) { dayCell.classList.add('has-event'); const eventList = document.createElement('div'); eventList.className = 'event-list'; // Only show up to 3 events const visibleEvents = dayEvents.slice(0, 3); visibleEvents.forEach(event => { const eventItem = document.createElement('div'); eventItem.className = `event-item ${event.type}`; let icon = '📝'; if (event.type === 'flight') icon = '✈️'; if (event.type === 'hotel') icon = '🏨'; if (event.type === 'activity') icon = '🎭'; eventItem.innerHTML = `<span class="icon">${icon}</span> ${event.title}`; eventList.appendChild(eventItem); // Add tooltip functionality eventItem.addEventListener('mouseenter', (e) => { showTooltip(e, event.title); }); eventItem.addEventListener('mouseleave', () => { hideTooltip(); }); }); // Add a "+X more" indicator if needed if (dayEvents.length > 3) { const moreItem = document.createElement('div'); moreItem.className = 'event-item'; moreItem.textContent = `+${dayEvents.length - 3} more`; eventList.appendChild(moreItem); } dayCell.appendChild(eventList); } // Add click event to show details dayCell.addEventListener('click', function() { if (dayEvents.length > 0) { showEventDetails(new Date(year, month, day)); } }); container.appendChild(dayCell); } // Add days from next month if needed const cellsAdded = startingDayOfWeek + daysInMonth; const cellsNeeded = Math.ceil(cellsAdded / 7) * 7; for (let i = 0; i < cellsNeeded - cellsAdded; i++) { const dayCell = createDayCell(i + 1, true); container.appendChild(dayCell); } } function createDayCell(dayNumber, isOtherMonth, dateObj) { const cell = document.createElement('div'); cell.className = 'day-cell'; if (isOtherMonth) { cell.classList.add('other-month'); } const dayNumberElement = document.createElement('div'); dayNumberElement.className = 'day-number'; dayNumberElement.textContent = dayNumber; cell.appendChild(dayNumberElement); // Apply ripple effect on tap/click cell.addEventListener('mousedown', function(e) { createRippleEffect(e, cell); }); cell.addEventListener('touchstart', function(e) { createRippleEffect(e, cell); }); return cell; } function createRippleEffect(e, element) { const circle = document.createElement('div'); const d = Math.max(element.clientWidth, element.clientHeight); circle.style.width = circle.style.height = d + 'px'; const rect = element.getBoundingClientRect(); let x = e.clientX - rect.left; let y = e.clientY - rect.top; // For touch events if (e.touches && e.touches[0]) { x = e.touches[0].clientX - rect.left; y = e.touches[0].clientY - rect.top; } circle.style.left = x - (d/2) + 'px'; circle.style.top = y - (d/2) + 'px'; circle.style.position = 'absolute'; circle.style.borderRadius = '50%'; circle.style.backgroundColor = 'rgba(255, 255, 255, 0.5)'; circle.style.pointerEvents = 'none'; circle.style.transform = 'scale(0)'; circle.style.transition = 'transform 0.4s ease-out, opacity 0.4s ease-out'; element.appendChild(circle); setTimeout(function() { circle.style.transform = 'scale(1)'; circle.style.opacity = '0'; }, 10); setTimeout(function() { circle.remove(); }, 400); } function goToPrevMonth() { currentDate.setMonth(currentDate.getMonth() - 1); updateMonthDisplay(); animateMonthChange('prev'); } function goToNextMonth() { currentDate.setMonth(currentDate.getMonth() + 1); updateMonthDisplay(); animateMonthChange('next'); } function animateMonthChange(direction) { monthSlider.style.transition = 'transform 0.35s ease'; if (direction === 'prev') { currentMonthIndex = 0; // First position monthSlider.style.transform = `translateX(0)`; } else if (direction === 'next') { currentMonthIndex = 2; // Last position monthSlider.style.transform = `translateX(-${(2/3) * 100}%)`; } // After animation, reset position and render new months setTimeout(() => { monthSlider.style.transition = 'none'; currentMonthIndex = 1; // Reset to middle monthSlider.style.transform = `translateX(-${(1/3) * 100}%)`; renderAllMonths(); }, 350); } function handleDragStart(e) { isDragging = true; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; monthSlider.style.transition = 'none'; document.addEventListener('mousemove', handleDragMove); document.addEventListener('touchmove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); document.addEventListener('touchend', handleDragEnd); } function handleDragMove(e) { if (!isDragging) return; const x = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; const diff = x - startX; const percentage = (diff / monthSlider.offsetWidth) * 100; // Calculate new position const baseTranslation = -(currentMonthIndex / 3) * 100; let newTranslation = baseTranslation + (percentage / 3); // Limit the drag to one month in each direction if (newTranslation > 0) newTranslation = 0; if (newTranslation < -((3-1)/3) * 100) newTranslation = -((3-1)/3) * 100; monthSlider.style.transform = `translateX(${newTranslation}%)`; } function handleDragEnd(e) { if (!isDragging) return; isDragging = false; const x = e.type.includes('mouse') ? e.clientX : (e.changedTouches ? e.changedTouches[0].clientX : startX); const diff = x - startX; // Determine if we should change months if (Math.abs(diff) > 50) { // Threshold for month change if (diff > 0) { goToPrevMonth(); } else { goToNextMonth(); } } else { // Return to original position monthSlider.style.transition = 'transform 0.25s ease'; monthSlider.style.transform = `translateX(-${(currentMonthIndex/3) * 100}%)`; } document.removeEventListener('mousemove', handleDragMove); document.removeEventListener('touchmove', handleDragMove); document.removeEventListener('mouseup', handleDragEnd); document.removeEventListener('touchend', handleDragEnd); } function showEventDetails(date) { const formattedDate = `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; document.querySelector('.event-date').textContent = formattedDate; // Show the backdrop and details panel backdrop.classList.add('active'); eventDetails.classList.add('active'); // Prevent body scrolling document.body.style.overflow = 'hidden'; } function hideEventDetails() { backdrop.classList.remove('active'); eventDetails.classList.remove('active'); document.body.style.overflow = 'auto'; } function showTooltip(e, text) { tooltip.textContent = text; tooltip.classList.add('show'); // Position tooltip const rect = e.target.getBoundingClientRect(); tooltip.style.top = `${rect.top - 30}px`; tooltip.style.left = `${rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2)}px`; } function hideTooltip() { tooltip.classList.remove('show'); }
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Bistro Reservations</title> <style> :root { --primary: #a73437; --primary-dark: #8a292c; --secondary: #f8f1e9; --accent: #5c9b72; --text-dark: #2e2e2e; --text-light: #f8f1e9; --shadow: rgba(0, 0, 0, 0.1); --available: #5c9b72; --booked: #a73437; --pending: #e6a44e; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Outfit', sans-serif; } body { background-color: var(--secondary); color: var(--text-dark); font-size: 16px; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; width: 100%; height: 100vh; overflow: hidden; } @font-face { font-family: 'Outfit'; src: url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); } .container { width: 100%; max-width: 700px; padding: 0 1rem; height: 700px; display: flex; flex-direction: column; } header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 0; border-bottom: 2px solid var(--primary); margin-bottom: 1rem; position: relative; } h1 { font-size: 1.8rem; font-weight: 700; color: var(--primary); margin: 0; letter-spacing: 0.5px; } .restaurant-icon { width: 2.2rem; height: 2.2rem; display: flex; align-items: center; justify-content: center; color: var(--primary); } .month-selector { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.8rem; } .month-display { font-size: 1.2rem; font-weight: 600; display: flex; gap: 0.5rem; align-items: center; } .month-name { min-width: 130px; text-align: center; } .nav-btn { background: none; border: none; color: var(--primary); font-size: 1.2rem; cursor: pointer; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s ease; } .nav-btn:hover { background-color: var(--primary); color: var(--text-light); } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.5rem; flex-grow: 1; } .day-header { text-align: center; font-weight: 600; font-size: 0.85rem; padding: 0.5rem 0; border-bottom: 1px solid var(--shadow); } .day { aspect-ratio: 1/1; padding: 0.5rem; border-radius: 8px; background-color: white; box-shadow: 0 2px 5px var(--shadow); cursor: pointer; display: flex; flex-direction: column; transition: transform 0.2s ease, box-shadow 0.2s ease; overflow: hidden; position: relative; } .day:hover { transform: translateY(-2px); box-shadow: 0 4px 10px var(--shadow); } .day.empty { background-color: transparent; box-shadow: none; cursor: default; } .day.empty:hover { transform: none; } .date-number { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.3rem; } .availability { display: flex; flex-direction: column; font-size: 0.75rem; gap: 3px; z-index: 1; } .time-slot { padding: 2px 4px; border-radius: 4px; font-size: 0.7rem; text-align: center; color: var(--text-light); font-weight: 500; transition: all 0.3s ease; } .time-slot.available { background-color: var(--available); } .time-slot.booked { background-color: var(--booked); } .time-slot.pending { background-color: var(--pending); } .controls { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-top: 1px solid rgba(0, 0, 0, 0.1); margin-top: 1rem; } .today-btn { background-color: var(--primary); color: var(--text-light); border: none; padding: 0.5rem 1rem; border-radius: 4px; font-weight: 500; cursor: pointer; transition: background-color 0.2s ease; } .today-btn:hover { background-color: var(--primary-dark); } .legend { display: flex; gap: 1rem; font-size: 0.8rem; } .legend-item { display: flex; align-items: center; gap: 0.3rem; } .color-box { width: 12px; height: 12px; border-radius: 3px; } .color-box.available { background-color: var(--available); } .color-box.booked { background-color: var(--booked); } .color-box.pending { background-color: var(--pending); } .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } .modal.active { opacity: 1; pointer-events: all; } .modal-content { background-color: white; padding: 1.5rem; border-radius: 8px; width: 90%; max-width: 450px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); transform: translateY(-20px); transition: transform 0.3s ease; } .modal.active .modal-content { transform: translateY(0); } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .modal-title { font-size: 1.3rem; font-weight: 600; color: var(--primary); } .close-modal { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-dark); } .reservation-details { margin-bottom: 1rem; } .detail-row { display: flex; margin-bottom: 0.5rem; } .detail-label { font-weight: 500; width: 100px; } .time-slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; margin-top: 1rem; } .time-option { padding: 0.5rem; text-align: center; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; } .time-option.available { background-color: var(--available); color: white; } .time-option.booked { background-color: var(--booked); color: white; cursor: not-allowed; opacity: 0.7; } .time-option.pending { background-color: var(--pending); color: white; } .time-option.available:hover { transform: translateY(-2px); box-shadow: 0 2px 5px var(--shadow); } .reservation-info { margin-bottom: 1rem; } .action-buttons { display: flex; justify-content: flex-end; gap: 0.8rem; margin-top: 1.5rem; } .btn { padding: 0.6rem 1.2rem; border-radius: 4px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; border: none; } .btn-primary { background-color: var(--primary); color: white; } .btn-primary:hover { background-color: var(--primary-dark); } .btn-secondary { background-color: #e2e2e2; color: var(--text-dark); } .btn-secondary:hover { background-color: #d1d1d1; } .day-status { position: absolute; top: 0; right: 0; width: 0; height: 0; border-style: solid; border-width: 0 20px 20px 0; border-color: transparent var(--available) transparent transparent; } .day.fully-booked .day-status { border-color: transparent var(--booked) transparent transparent; } .day.partially-booked .day-status { border-color: transparent var(--pending) transparent transparent; } .booking-toast { position: fixed; bottom: -100px; left: 50%; transform: translateX(-50%); background-color: var(--accent); color: white; padding: 1rem 2rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); display: flex; align-items: center; gap: 0.5rem; transition: bottom 0.3s ease; z-index: 100; } .booking-toast.show { bottom: 20px; } .status-indicator { position: absolute; top: 0; right: 0; width: 10px; height: 10px; border-radius: 50%; } .status-indicator.open { background-color: var(--available); } .spinner { display: none; width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; margin-left: 10px; } @keyframes spin { to { transform: rotate(360deg); } } .showing { display: inline-block; } @media (max-width: 600px) { .container { padding: 0 0.5rem; } h1 { font-size: 1.5rem; } .day { padding: 0.3rem; } .date-number { font-size: 0.85rem; } .time-slot { font-size: 0.65rem; padding: 1px 2px; } .legend { flex-wrap: wrap; } .time-slots { grid-template-columns: repeat(2, 1fr); } } /* Cool water ripple effect on booking */ .ripple { position: absolute; border-radius: 50%; background-color: rgba(255, 255, 255, 0.5); transform: scale(0); animation: ripple 0.8s linear; pointer-events: none; } @keyframes ripple { to { transform: scale(2.5); opacity: 0; } } /* Food pattern background for visual interest */ .day::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23a73437' fill-opacity='0.03' fill-rule='evenodd'%3E%3Ccircle cx='3' cy='3' r='1'/%3E%3Ccircle cx='3' cy='17' r='1'/%3E%3Ccircle cx='17' cy='3' r='1'/%3E%3Ccircle cx='17' cy='17' r='1'/%3E%3C/g%3E%3C/svg%3E"); opacity: 0.3; z-index: 0; } /* Highlight today */ .day.today { border: 2px solid var(--primary); } /* Add a subtle animation for selected date */ .day.selected { animation: pulse 1.5s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(167, 52, 55, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(167, 52, 55, 0); } 100% { box-shadow: 0 0 0 0 rgba(167, 52, 55, 0); } } </style> </head> <body> <div class="container"> <header> <h1>Bistro Nouveau</h1> <div class="restaurant-icon"> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16"> <path d="M3.75 0a1 1 0 0 0-.8.4L.1 4.2a.5.5 0 0 0-.1.3V15a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V4.5a.5.5 0 0 0-.1-.3L13.05.4a1 1 0 0 0-.8-.4h-8.5ZM8.5 4h6l.5.667V5H1v-.333L1.5 4h6V1h1v3ZM8 7.993c1.664-1.711 5.825 1.283 0 5.132-5.825-3.85-1.664-6.843 0-5.132Z"/> </svg> </div> <div class="status-indicator open"></div> </header> <div class="month-selector"> <button class="nav-btn prev-month"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/> </svg> </button> <div class="month-display"> <span class="month-name">August 2023</span> </div> <button class="nav-btn next-month"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/> </svg> </button> </div> <div class="calendar" id="calendar"> <!-- Day headers --> <div class="day-header">Sun</div> <div class="day-header">Mon</div> <div class="day-header">Tue</div> <div class="day-header">Wed</div> <div class="day-header">Thu</div> <div class="day-header">Fri</div> <div class="day-header">Sat</div> <!-- Calendar days will be dynamically generated --> </div> <div class="controls"> <button class="today-btn">Today</button> <div class="legend"> <div class="legend-item"> <div class="color-box available"></div> <span>Available</span> </div> <div class="legend-item"> <div class="color-box pending"></div> <span>Limited</span> </div> <div class="legend-item"> <div class="color-box booked"></div> <span>Booked</span> </div> </div> </div> </div> <div class="modal" id="reservationModal"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title">Reservation Details</h3> <button class="close-modal">×</button> </div> <div class="reservation-details"> <div class="detail-row"> <span class="detail-label">Date:</span> <span class="detail-value" id="reservationDate">August 15, 2023</span> </div> <div class="detail-row"> <span class="detail-label">Status:</span> <span class="detail-value" id="reservationStatus">Available for booking</span> </div> </div> <div class="reservation-info"> <h4>Available Time Slots</h4> <div class="time-slots" id="timeSlots"> <!-- Time slots will be dynamically generated --> </div> </div> <div class="action-buttons"> <button class="btn btn-secondary" id="cancelBtn">Cancel</button> <button class="btn btn-primary" id="confirmBtn">Book Table <span class="spinner" id="spinner"></span></button> </div> </div> </div> <div class="booking-toast" id="bookingToast"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/> </svg> <span>Reservation confirmed!</span> </div> <script> document.addEventListener('DOMContentLoaded', function() { // DOM elements const calendarEl = document.getElementById('calendar'); const monthNameEl = document.querySelector('.month-name'); const prevMonthBtn = document.querySelector('.prev-month'); const nextMonthBtn = document.querySelector('.next-month'); const todayBtn = document.querySelector('.today-btn'); const modal = document.getElementById('reservationModal'); const closeModalBtn = document.querySelector('.close-modal'); const cancelBtn = document.getElementById('cancelBtn'); const confirmBtn = document.getElementById('confirmBtn'); const reservationDateEl = document.getElementById('reservationDate'); const reservationStatusEl = document.getElementById('reservationStatus'); const timeSlotsEl = document.getElementById('timeSlots'); const bookingToast = document.getElementById('bookingToast'); const spinner = document.getElementById('spinner'); // Current date let currentDate = new Date(); let selectedDate = null; let selectedDay = null; // Restaurant data - in a real app this would come from a backend const restaurantData = { openingHours: { 0: [{ start: '17:00', end: '22:00' }], // Sunday 1: [{ start: '17:00', end: '22:00' }], // Monday 2: [{ start: '17:00', end: '22:00' }], // Tuesday 3: [{ start: '17:00', end: '22:00' }], // Wednesday 4: [{ start: '17:00', end: '23:00' }], // Thursday 5: [{ start: '17:00', end: '23:00' }], // Friday 6: [{ start: '12:00', end: '23:00' }] // Saturday }, tables: 20, seatsPerTable: 4 }; // Sample reservation data (in a real app, this would come from a database) let reservations = generateSampleReservations(); // Initialize calendar renderCalendar(currentDate); // Event Listeners prevMonthBtn.addEventListener('click', () => { currentDate.setMonth(currentDate.getMonth() - 1); renderCalendar(currentDate); }); nextMonthBtn.addEventListener('click', () => { currentDate.setMonth(currentDate.getMonth() + 1); renderCalendar(currentDate); }); todayBtn.addEventListener('click', () => { currentDate = new Date(); renderCalendar(currentDate); }); closeModalBtn.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); confirmBtn.addEventListener('click', function() { const selectedTimeSlot = document.querySelector('.time-option.selected'); if (selectedTimeSlot) { // Show spinner spinner.classList.add('showing'); // Simulate API call with delay setTimeout(() => { // Update reservation data const time = selectedTimeSlot.dataset.time; const newReservation = { date: selectedDate.toISOString().split('T')[0], time: time, tables: 1, status: 'confirmed' }; reservations.push(newReservation); // Update UI renderCalendar(currentDate); closeModal(); showBookingToast(); // Hide spinner spinner.classList.remove('showing'); }, 1500); } else { alert('Please select a time slot first'); } }); // Functions function renderCalendar(date) { const year = date.getFullYear(); const month = date.getMonth(); // Update month display const monthName = new Date(year, month, 1).toLocaleString('default', { month: 'long' }); monthNameEl.textContent = `${monthName} ${year}`; // Clear previous days except headers const dayHeaders = Array.from(document.querySelectorAll('.day-header')); calendarEl.innerHTML = ''; dayHeaders.forEach(header => calendarEl.appendChild(header)); // Get first day of month and total days const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); // Add empty cells for days before the 1st for (let i = 0; i < firstDay; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'day empty'; calendarEl.appendChild(emptyDay); } // Add days const today = new Date(); for (let i = 1; i <= daysInMonth; i++) { const dayEl = document.createElement('div'); dayEl.className = 'day'; // Check if this is today const currentDateCheck = new Date(year, month, i); if (currentDateCheck.getDate() === today.getDate() && currentDateCheck.getMonth() === today.getMonth() && currentDateCheck.getFullYear() === today.getFullYear()) { dayEl.classList.add('today'); } // Date info const dateNumber = document.createElement('div'); dateNumber.className = 'date-number'; dateNumber.textContent = i; dayEl.appendChild(dateNumber); // Availability status const availabilityEl = document.createElement('div'); availabilityEl.className = 'availability'; // Get availability for this day const dateString = `${year}-${(month + 1).toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`; const dayAvailability = getAvailabilityForDay(currentDateCheck, dateString); // Add day status indicator const dayStatusEl = document.createElement('div'); dayStatusEl.className = 'day-status'; dayEl.appendChild(dayStatusEl); // Mark day as fully booked, partially booked, or available if (dayAvailability.fullyBooked) { dayEl.classList.add('fully-booked'); } else if (dayAvailability.partiallyBooked) { dayEl.classList.add('partially-booked'); } // Add time slots summary (for display in the calendar) const timeSlots = dayAvailability.timeSlots; // Only show first 3 slots in calendar view const displaySlots = timeSlots.slice(0, 3); displaySlots.forEach(slot => { const timeSlotEl = document.createElement('div'); timeSlotEl.className = `time-slot ${slot.status}`; timeSlotEl.textContent = slot.time; availabilityEl.appendChild(timeSlotEl); }); dayEl.appendChild(availabilityEl); // Add event listener to open modal dayEl.addEventListener('click', () => { if (!dayEl.classList.contains('empty')) { selectedDate = new Date(year, month, i); selectedDay = dayEl; openReservationModal(selectedDate, dayAvailability); // Remove selected class from all days document.querySelectorAll('.day.selected').forEach(el => { el.classList.remove('selected'); }); // Add selected class to clicked day dayEl.classList.add('selected'); // Add ripple effect createRippleEffect(dayEl); } }); calendarEl.appendChild(dayEl); } } function getAvailabilityForDay(date, dateString) { const dayOfWeek = date.getDay(); // Check if we have opening hours for this day if (!restaurantData.openingHours[dayOfWeek]) { return { fullyBooked: true, partiallyBooked: false, timeSlots: [] }; } const openingHours = restaurantData.openingHours[dayOfWeek]; const timeSlots = []; // Create time slots every 1 hour within opening hours openingHours.forEach(hours => { const startHour = parseInt(hours.start.split(':')[0]); const endHour = parseInt(hours.end.split(':')[0]); for (let hour = startHour; hour < endHour; hour++) { const time = `${hour.toString().padStart(2, '0')}:00`; const existingReservations = reservations.filter(r => r.date === dateString && r.time === time ); let status = 'available'; // Determine status based on existing reservations if (existingReservations.length >= restaurantData.tables) { status = 'booked'; } else if (existingReservations.length >= restaurantData.tables * 0.7) { status = 'pending'; } timeSlots.push({ time: time, status: status }); } }); // Check if all slots are booked const fullyBooked = timeSlots.every(slot => slot.status === 'booked'); // Check if some slots are booked const partiallyBooked = !fullyBooked && timeSlots.some(slot => slot.status !== 'available'); return { fullyBooked, partiallyBooked, timeSlots }; } function openReservationModal(date, availability) { // Format date for display const formattedDate = date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); reservationDateEl.textContent = formattedDate; // Set status text if (availability.fullyBooked) { reservationStatusEl.textContent = 'Fully booked'; reservationStatusEl.style.color = 'var(--booked)'; } else if (availability.partiallyBooked) { reservationStatusEl.textContent = 'Limited availability'; reservationStatusEl.style.color = 'var(--pending)'; } else { reservationStatusEl.textContent = 'Available for booking'; reservationStatusEl.style.color = 'var(--available)'; } // Clear existing time slots timeSlotsEl.innerHTML = ''; // Add time slots availability.timeSlots.forEach(slot => { const timeOptionEl = document.createElement('div'); timeOptionEl.className = `time-option ${slot.status}`; timeOptionEl.textContent = slot.time; timeOptionEl.dataset.time = slot.time; if (slot.status === 'available') { timeOptionEl.addEventListener('click', () => { // Remove selection from all time options document.querySelectorAll('.time-option.selected').forEach(el => { el.classList.remove('selected'); }); // Add selection to clicked time option timeOptionEl.classList.add('selected'); }); } timeSlotsEl.appendChild(timeOptionEl); }); // Show modal modal.classList.add('active'); } function closeModal() { modal.classList.remove('active'); } function showBookingToast() { bookingToast.classList.add('show'); setTimeout(() => { bookingToast.classList.remove('show'); }, 3000); } function generateSampleReservations() { const reservations = []; const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 7); const endDate = new Date(today); endDate.setDate(today.getDate() + 30); // Loop through dates for (let date = startDate; date <= endDate; date.setDate(date.getDate() + 1)) { const dayOfWeek = date.getDay(); // Skip days when restaurant is closed if (!restaurantData.openingHours[dayOfWeek]) continue; const openingHours = restaurantData.openingHours[dayOfWeek]; const dateString = date.toISOString().split('T')[0]; // Create time slots openingHours.forEach(hours => { const startHour = parseInt(hours.start.split(':')[0]); const endHour = parseInt(hours.end.split(':')[0]); for (let hour = startHour; hour < endHour; hour++) { const time = `${hour.toString().padStart(2, '0')}:00`; // Randomly decide how many tables are booked // More likely to be booked for weekends and prime dining hours let bookedTables = 0; const isPrimeTime = hour >= 18 && hour <= 20; const isWeekend = dayOfWeek === 5 || dayOfWeek === 6; const baseBookingChance = 0.4; const primeTimeMultiplier = isPrimeTime ? 1.5 : 1; const weekendMultiplier = isWeekend ? 1.3 : 1; // Calculate booking chance const bookingChance = Math.min(baseBookingChance * primeTimeMultiplier * weekendMultiplier, 0.95); // Determine how many tables are booked for (let table = 0; table < restaurantData.tables; table++) { if (Math.random() < bookingChance) { bookedTables++; } } // Add reservations for booked tables if (bookedTables > 0) { reservations.push({ date: dateString, time: time, tables: bookedTables, status: 'confirmed' }); } } }); } return reservations; } function createRippleEffect(element) { const ripple = document.createElement('span'); ripple.className = 'ripple'; element.appendChild(ripple); const rect = element.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); ripple.style.width = ripple.style.height = `${size}px`; // Center the ripple ripple.style.left = `${rect.width / 2 - size / 2}px`; ripple.style.top = `${rect.height / 2 - size / 2}px`; // Remove ripple after animation setTimeout(() => { ripple.remove(); }, 800); } }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sports Events Calendar</title> <style> :root { --primary: #ff5722; --secondary: #2196f3; --accent: #ffeb3b; --dark: #263238; --light: #eceff1; --success: #4caf50; --playoff: #9c27b0; --playoff-light: #e1bee7; --regular-light: #bbdefb; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--light); color: var(--dark); width: 100%; height: 100vh; display: flex; flex-direction: column; overflow-x: hidden; } .container { max-width: 700px; width: 100%; height: 700px; margin: 0 auto; padding: 1rem; display: flex; flex-direction: column; background-color: white; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); border-radius: 12px; overflow: hidden; } header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 3px solid var(--primary); } .team-selector { position: relative; } .selected-team { display: flex; align-items: center; cursor: pointer; padding: 0.5rem 1rem; border-radius: 25px; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3); transition: all 0.3s ease; } .selected-team:hover { transform: translateY(-2px); box-shadow: 0 6px 14px rgba(33, 150, 243, 0.4); } .selected-team img { width: 28px; height: 28px; margin-right: 8px; border-radius: 50%; background: white; padding: 2px; } .team-dropdown { position: absolute; top: 100%; left: 0; right: 0; margin-top: 0.5rem; background-color: white; border-radius: 8px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); overflow: hidden; transform-origin: top center; transform: scaleY(0); opacity: 0; transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); z-index: 100; } .team-dropdown.active { transform: scaleY(1); opacity: 1; } .team-option { display: flex; align-items: center; padding: 0.5rem 1rem; cursor: pointer; transition: all 0.2s ease; } .team-option:hover { background-color: var(--light); } .team-option img { width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; } .calendar-nav { display: flex; justify-content: space-between; align-items: center; margin: 1rem 0; } .month-display { font-size: 1.5rem; font-weight: 700; letter-spacing: 0.5px; color: var(--dark); position: relative; padding-bottom: 0.25rem; } .month-display::after { content: ''; position: absolute; bottom: 0; left: 0; width: 40%; height: 3px; background: var(--primary); border-radius: 2px; } .nav-btn { background: none; border: none; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--dark); font-size: 1.25rem; transition: all 0.3s ease; background-color: var(--light); } .nav-btn:hover { background-color: var(--primary); color: white; transform: scale(1.05); } .toggle-container { display: flex; align-items: center; margin-bottom: 1rem; } .toggle-label { margin: 0 1rem; font-weight: 600; font-size: 0.9rem; color: var(--dark); opacity: 0.7; transition: all 0.3s ease; cursor: pointer; } .toggle-label.active { opacity: 1; color: var(--primary); transform: scale(1.05); } .toggle-switch { position: relative; width: 60px; height: 30px; background-color: var(--secondary); border-radius: 30px; cursor: pointer; transition: all 0.3s ease; } .toggle-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; background-color: white; border-radius: 50%; transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .toggle-switch.playoff-mode { background-color: var(--playoff); } .toggle-switch.playoff-mode::after { left: calc(100% - 27px); } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; flex-grow: 1; margin-bottom: 1rem; } .day-label { text-align: center; font-weight: 700; font-size: 0.85rem; text-transform: uppercase; padding: 0.5rem 0; color: var(--dark); letter-spacing: 1px; } .calendar-day { position: relative; aspect-ratio: 1/1; border-radius: 8px; overflow: hidden; cursor: pointer; display: flex; flex-direction: column; align-items: center; background-color: var(--light); transition: all 0.3s ease; border: 2px solid transparent; } .calendar-day:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); border-color: var(--primary); } .calendar-day.has-event { background-color: var(--regular-light); } .calendar-day.has-playoff-event { background-color: var(--playoff-light); } .calendar-day.today { border: 2px solid var(--primary); } .calendar-day.other-month { opacity: 0.4; pointer-events: none; } .day-number { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-top: 5px; font-weight: 600; font-size: 0.9rem; } .today .day-number { background-color: var(--primary); color: white; border-radius: 50%; } .event-indicator { width: 10px; height: 10px; border-radius: 50%; margin-top: 3px; background-color: var(--secondary); } .playoff-indicator { background-color: var(--playoff); } .events-list { flex-grow: 1; padding: 0.5rem; margin-top: 1rem; border-radius: 8px; background-color: var(--light); overflow-y: auto; max-height: 180px; transition: all 0.3s ease; } .event-item { padding: 0.75rem; margin-bottom: 0.5rem; border-radius: 8px; background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transform-origin: left center; animation: slideIn 0.3s ease forwards; opacity: 0; display: flex; align-items: center; border-left: 4px solid var(--secondary); } .event-item.playoff-event { border-left-color: var(--playoff); } .event-time { min-width: 70px; font-weight: 700; padding-right: 0.5rem; color: var(--dark); } .event-info { flex-grow: 1; } .event-title { font-weight: 600; margin-bottom: 0.25rem; color: var(--dark); } .event-location { font-size: 0.8rem; color: rgba(0, 0, 0, 0.6); display: flex; align-items: center; } .event-location::before { content: '📍'; margin-right: 4px; } .event-teams { display: flex; align-items: center; font-size: 0.85rem; margin-top: 4px; } .event-team { display: flex; align-items: center; } .event-team img { width: 16px; height: 16px; margin-right: 4px; border-radius: 50%; } .versus { margin: 0 6px; font-weight: 700; color: var(--primary); } .no-events { text-align: center; padding: 2rem; color: rgba(0, 0, 0, 0.4); font-weight: 500; } @keyframes slideIn { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @media (max-width: 480px) { .calendar-day { font-size: 0.8rem; } .event-item { flex-direction: column; align-items: flex-start; } .event-time { margin-bottom: 0.5rem; } .month-display { font-size: 1.2rem; } } </style> </head> <body> <div class="container"> <header> <h1>Sports Calendar</h1> <div class="team-selector"> <div class="selected-team" id="selected-team"> <img src="https://via.placeholder.com/40/FF5722/FFFFFF?text=BC" alt="Team logo"> <span>Boston Comets</span> </div> <div class="team-dropdown" id="team-dropdown"> <div class="team-option" data-team="Boston Comets"> <img src="https://via.placeholder.com/40/FF5722/FFFFFF?text=BC" alt="Boston Comets"> <span>Boston Comets</span> </div> <div class="team-option" data-team="Miami Thunder"> <img src="https://via.placeholder.com/40/2196F3/FFFFFF?text=MT" alt="Miami Thunder"> <span>Miami Thunder</span> </div> <div class="team-option" data-team="Chicago Stars"> <img src="https://via.placeholder.com/40/4CAF50/FFFFFF?text=CS" alt="Chicago Stars"> <span>Chicago Stars</span> </div> </div> </div> </header> <div class="calendar-nav"> <button class="nav-btn" id="prev-month">←</button> <div class="month-display" id="month-display">May 2024</div> <button class="nav-btn" id="next-month">→</button> </div> <div class="toggle-container"> <span class="toggle-label active" id="regular-label">Regular Season</span> <div class="toggle-switch" id="season-toggle"></div> <span class="toggle-label" id="playoff-label">Playoff Season</span> </div> <div class="calendar-grid" id="calendar-grid"> <!-- Day labels --> <div class="day-label">Sun</div> <div class="day-label">Mon</div> <div class="day-label">Tue</div> <div class="day-label">Wed</div> <div class="day-label">Thu</div> <div class="day-label">Fri</div> <div class="day-label">Sat</div> <!-- Calendar days will be generated by JS --> </div> <div class="events-list" id="events-list"> <div class="no-events">Select a date to view events</div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // State variables let currentDate = new Date(); let selectedDate = new Date(); let isPlayoffMode = false; let selectedTeam = "Boston Comets"; // DOM elements const calendarGrid = document.getElementById('calendar-grid'); const monthDisplay = document.getElementById('month-display'); const prevMonthBtn = document.getElementById('prev-month'); const nextMonthBtn = document.getElementById('next-month'); const seasonToggle = document.getElementById('season-toggle'); const regularLabel = document.getElementById('regular-label'); const playoffLabel = document.getElementById('playoff-label'); const eventsList = document.getElementById('events-list'); const selectedTeamEl = document.getElementById('selected-team'); const teamDropdown = document.getElementById('team-dropdown'); const teamOptions = document.querySelectorAll('.team-option'); // Sample data - Regular season events const regularEvents = [ { date: new Date(2024, 4, 5), time: '7:30 PM', title: 'Boston Comets vs Miami Thunder', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Miami Thunder', logo: 'https://via.placeholder.com/40/2196F3/FFFFFF?text=MT'} ] }, { date: new Date(2024, 4, 8), time: '6:00 PM', title: 'Chicago Stars vs Boston Comets', location: 'Stellar Stadium, Chicago', teams: [ {name: 'Chicago Stars', logo: 'https://via.placeholder.com/40/4CAF50/FFFFFF?text=CS'}, {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'} ] }, { date: new Date(2024, 4, 15), time: '7:00 PM', title: 'Boston Comets vs Dallas Rockets', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Dallas Rockets', logo: 'https://via.placeholder.com/40/9C27B0/FFFFFF?text=DR'} ] }, { date: new Date(2024, 4, 19), time: '8:30 PM', title: 'Miami Thunder vs Chicago Stars', location: 'Thunder Dome, Miami', teams: [ {name: 'Miami Thunder', logo: 'https://via.placeholder.com/40/2196F3/FFFFFF?text=MT'}, {name: 'Chicago Stars', logo: 'https://via.placeholder.com/40/4CAF50/FFFFFF?text=CS'} ] }, { date: new Date(2024, 4, 22), time: '7:00 PM', title: 'Boston Comets vs LA Waves', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'LA Waves', logo: 'https://via.placeholder.com/40/9E9E9E/FFFFFF?text=LW'} ] }, { date: new Date(2024, 4, 28), time: '6:30 PM', title: 'New York Blitz vs Boston Comets', location: 'Lightning Field, New York', teams: [ {name: 'New York Blitz', logo: 'https://via.placeholder.com/40/FFEB3B/000000?text=NY'}, {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'} ] } ]; // Sample data - Playoff events const playoffEvents = [ { date: new Date(2024, 4, 10), time: '8:00 PM', title: 'Quarterfinals Game 1', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Dallas Rockets', logo: 'https://via.placeholder.com/40/9C27B0/FFFFFF?text=DR'} ] }, { date: new Date(2024, 4, 13), time: '7:30 PM', title: 'Quarterfinals Game 2', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Dallas Rockets', logo: 'https://via.placeholder.com/40/9C27B0/FFFFFF?text=DR'} ] }, { date: new Date(2024, 4, 18), time: '7:00 PM', title: 'Semifinals Game 1', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Chicago Stars', logo: 'https://via.placeholder.com/40/4CAF50/FFFFFF?text=CS'} ] }, { date: new Date(2024, 4, 21), time: '8:00 PM', title: 'Semifinals Game 2', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Chicago Stars', logo: 'https://via.placeholder.com/40/4CAF50/FFFFFF?text=CS'} ] }, { date: new Date(2024, 4, 26), time: '8:30 PM', title: 'Finals Game 1', location: 'Thunder Dome, Miami', teams: [ {name: 'Miami Thunder', logo: 'https://via.placeholder.com/40/2196F3/FFFFFF?text=MT'}, {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'} ] }, { date: new Date(2024, 4, 30), time: '7:30 PM', title: 'Finals Game 2', location: 'Comets Arena, Boston', teams: [ {name: 'Boston Comets', logo: 'https://via.placeholder.com/40/FF5722/FFFFFF?text=BC'}, {name: 'Miami Thunder', logo: 'https://via.placeholder.com/40/2196F3/FFFFFF?text=MT'} ] } ]; // Initialize calendar function initCalendar() { renderCalendar(); addEventListeners(); } // Render calendar grid function renderCalendar() { // Clear existing days (except day labels) const dayElements = document.querySelectorAll('.calendar-day'); dayElements.forEach(day => day.remove()); // Update month/year display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; monthDisplay.textContent = `${monthNames[currentDate.getMonth()]} ${currentDate.getFullYear()}`; // Get first day of month and number of days const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); const lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); const numDays = lastDay.getDate(); const startDayIndex = firstDay.getDay(); // 0 = Sunday // Get the last few days of previous month const prevMonthLastDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0).getDate(); // Generate previous month days for (let i = 0; i < startDayIndex; i++) { const dayNum = prevMonthLastDay - startDayIndex + i + 1; const dayEl = createDayElement(dayNum, true); calendarGrid.appendChild(dayEl); } // Generate current month days const today = new Date(); for (let i = 1; i <= numDays; i++) { const isToday = today.getDate() === i && today.getMonth() === currentDate.getMonth() && today.getFullYear() === currentDate.getFullYear(); const checkDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), i); const hasRegularEvent = regularEvents.some(e => isSameDay(e.date, checkDate) && (e.teams[0].name === selectedTeam || e.teams[1].name === selectedTeam)); const hasPlayoffEvent = playoffEvents.some(e => isSameDay(e.date, checkDate) && (e.teams[0].name === selectedTeam || e.teams[1].name === selectedTeam)); const dayEl = createDayElement(i, false, isToday, hasRegularEvent && !isPlayoffMode, hasPlayoffEvent && isPlayoffMode); dayEl.addEventListener('click', () => { selectDate(new Date(currentDate.getFullYear(), currentDate.getMonth(), i)); }); calendarGrid.appendChild(dayEl); } // Fill in the rest with next month's days const totalCells = 42; // 6 rows of 7 days const remainingCells = totalCells - (startDayIndex + numDays); for (let i = 1; i <= remainingCells; i++) { if (calendarGrid.children.length >= totalCells) break; const dayEl = createDayElement(i, true); calendarGrid.appendChild(dayEl); } } // Create a day element function createDayElement(dayNum, isOtherMonth, isToday = false, hasEvent = false, hasPlayoffEvent = false) { const dayEl = document.createElement('div'); dayEl.classList.add('calendar-day'); if (isOtherMonth) { dayEl.classList.add('other-month'); } if (isToday) { dayEl.classList.add('today'); } if (hasEvent) { dayEl.classList.add('has-event'); } if (hasPlayoffEvent) { dayEl.classList.add('has-playoff-event'); } const dayNumEl = document.createElement('div'); dayNumEl.classList.add('day-number'); dayNumEl.textContent = dayNum; dayEl.appendChild(dayNumEl); if (hasEvent) { const indicator = document.createElement('div'); indicator.classList.add('event-indicator'); dayEl.appendChild(indicator); } if (hasPlayoffEvent) { const indicator = document.createElement('div'); indicator.classList.add('event-indicator', 'playoff-indicator'); dayEl.appendChild(indicator); } return dayEl; } // Select a specific date function selectDate(date) { selectedDate = date; renderEvents(); // Highlight selected date document.querySelectorAll('.calendar-day').forEach((day, index) => { // Remove previously selected day.style.borderColor = ''; // First 7 elements are day labels if (index >= 7) { const dayNumber = parseInt(day.querySelector('.day-number').textContent); const isCurrentMonth = !day.classList.contains('other-month'); if (isCurrentMonth && dayNumber === selectedDate.getDate()) { day.style.borderColor = isPlayoffMode ? 'var(--playoff)' : 'var(--primary)'; } } }); } // Render events for selected date function renderEvents() { const eventsToDisplay = isPlayoffMode ? playoffEvents.filter(e => isSameDay(e.date, selectedDate) && (e.teams[0].name === selectedTeam || e.teams[1].name === selectedTeam)) : regularEvents.filter(e => isSameDay(e.date, selectedDate) && (e.teams[0].name === selectedTeam || e.teams[1].name === selectedTeam)); eventsList.innerHTML = ''; if (eventsToDisplay.length === 0) { const noEvents = document.createElement('div'); noEvents.classList.add('no-events'); noEvents.textContent = `No ${isPlayoffMode ? 'playoff' : 'regular season'} events scheduled for this date`; eventsList.appendChild(noEvents); return; } eventsToDisplay.forEach((event, index) => { const eventEl = document.createElement('div'); eventEl.classList.add('event-item'); if (isPlayoffMode) eventEl.classList.add('playoff-event'); // Staggered animation setTimeout(() => { eventEl.style.animation = `slideIn 0.3s ease forwards`; }, index * 100); eventEl.innerHTML = ` <div class="event-time">${event.time}</div> <div class="event-info"> <div class="event-title">${event.title}</div> <div class="event-location">${event.location}</div> <div class="event-teams"> <div class="event-team"> <img src="${event.teams[0].logo}" alt="${event.teams[0].name}"> <span>${event.teams[0].name}</span> </div> <span class="versus">vs</span> <div class="event-team"> <img src="${event.teams[1].logo}" alt="${event.teams[1].name}"> <span>${event.teams[1].name}</span> </div> </div> </div> `; eventsList.appendChild(eventEl); }); } // Helper function to check if two dates are the same day function isSameDay(date1, date2) { return date1.getDate() === date2.getDate() && date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear(); } // Add event listeners function addEventListeners() { // Month navigation prevMonthBtn.addEventListener('click', () => { currentDate.setMonth(currentDate.getMonth() - 1); renderCalendar(); selectDate(new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)); }); nextMonthBtn.addEventListener('click', () => { currentDate.setMonth(currentDate.getMonth() + 1); renderCalendar(); selectDate(new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)); }); // Season toggle seasonToggle.addEventListener('click', () => { isPlayoffMode = !isPlayoffMode; seasonToggle.classList.toggle('playoff-mode', isPlayoffMode); regularLabel.classList.toggle('active', !isPlayoffMode); playoffLabel.classList.toggle('active', isPlayoffMode); renderCalendar(); renderEvents(); }); // Team selector selectedTeamEl.addEventListener('click', () => { teamDropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (!selectedTeamEl.contains(e.target) && !teamDropdown.contains(e.target)) { teamDropdown.classList.remove('active'); } }); teamOptions.forEach(option => { option.addEventListener('click', () => { selectedTeam = option.dataset.team; selectedTeamEl.querySelector('img').src = option.querySelector('img').src; selectedTeamEl.querySelector('span').textContent = selectedTeam; teamDropdown.classList.remove('active'); renderCalendar(); renderEvents(); }); }); } // Initialize initCalendar(); selectDate(new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Freelancer Availability Calendar</title> <style> :root { --primary: #6c5ce7; --primary-light: #a29bfe; --secondary: #fd79a8; --tertiary: #00cec9; --background: #f8f9fa; --text: #2d3436; --text-light: #636e72; --available: #55efc4; --booked: #fab1a0; --tentative: #ffeaa7; --unavailable: #dfe6e9; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Quicksand', sans-serif; } body { background-color: var(--background); color: var(--text); display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-height: 700px; max-width: 700px; margin: 0 auto; padding: 20px; transition: all 0.3s ease; overflow-x: hidden; } h1 { margin-bottom: 10px; color: var(--primary); font-weight: 700; font-size: 1.8rem; text-align: center; position: relative; } h1::after { content: ''; position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%); width: 60px; height: 3px; background: linear-gradient(to right, var(--primary-light), var(--tertiary)); border-radius: 3px; } p.subtitle { color: var(--text-light); margin-bottom: 25px; text-align: center; font-size: 0.9rem; } .controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; width: 100%; } .month-selector { display: flex; align-items: center; gap: 15px; } .month-display { font-weight: 600; color: var(--primary); min-width: 150px; text-align: center; } .month-nav { background: none; border: none; color: var(--primary); font-size: 1.2rem; cursor: pointer; height: 30px; width: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.2s ease; } .month-nav:hover { background-color: var(--primary-light); color: white; transform: scale(1.1); } .legend { display: flex; gap: 12px; font-size: 0.8rem; } .legend-item { display: flex; align-items: center; gap: 5px; } .legend-color { width: 12px; height: 12px; border-radius: 3px; } .legend-available { background-color: var(--available); } .legend-booked { background-color: var(--booked); } .legend-tentative { background-color: var(--tentative); } .legend-unavailable { background-color: var(--unavailable); } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); width: 100%; gap: 10px; margin-bottom: 25px; } .day-header { text-align: center; font-weight: 600; color: var(--text); padding: 5px 0; font-size: 0.9rem; } .calendar-day { aspect-ratio: 1/1; padding: 5px; border-radius: 10px; display: flex; flex-direction: column; cursor: pointer; background-color: var(--unavailable); position: relative; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; } .calendar-day:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } .day-number { font-weight: 600; font-size: 1rem; padding: 2px 5px; } .status-indicator { flex-grow: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; transition: all 0.3s ease; } .status-icon { font-size: 1.2rem; margin-bottom: 2px; } .status-text { font-size: 0.7rem; text-align: center; } .available { background-color: var(--available); } .booked { background-color: var(--booked); } .tentative { background-color: var(--tentative); } .unavailable { background-color: var(--unavailable); } .outside-month { opacity: 0.4; cursor: default; } .outside-month:hover { transform: none; box-shadow: none; } .edit-form { background: white; border-radius: 15px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); padding: 20px; width: 100%; max-width: 500px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 100; opacity: 0; pointer-events: none; transition: all 0.3s ease; } .edit-form.active { opacity: 1; pointer-events: all; } .form-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .form-title { font-weight: 600; color: var(--primary); } .close-btn { background: none; border: none; color: var(--text-light); font-size: 1.2rem; cursor: pointer; transition: color 0.2s ease; } .close-btn:hover { color: var(--secondary); } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; font-size: 0.9rem; color: var(--text); } .status-selector { display: flex; gap: 10px; margin-bottom: 15px; } .status-option { flex: 1; text-align: center; padding: 10px; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; border: 2px solid transparent; } .status-option.selected { border-color: var(--primary); font-weight: 600; } .status-option:hover { transform: translateY(-2px); } .available-option { background-color: var(--available); } .booked-option { background-color: var(--booked); } .tentative-option { background-color: var(--tentative); } .unavailable-option { background-color: var(--unavailable); } .notes-input { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #ddd; resize: none; font-size: 0.9rem; min-height: 80px; transition: border-color 0.2s ease; } .notes-input:focus { outline: none; border-color: var(--primary-light); } .time-slots { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 15px; } .time-slot { padding: 8px; border-radius: 5px; text-align: center; background-color: #f1f1f1; cursor: pointer; transition: all 0.2s ease; font-size: 0.8rem; } .time-slot.selected { background-color: var(--primary-light); color: white; } .save-btn { background: linear-gradient(to right, var(--primary), var(--primary-light)); color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.2s ease; } .save-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(108, 92, 231, 0.3); } .overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } .overlay.active { opacity: 1; pointer-events: all; } .stats { display: flex; justify-content: space-around; background: white; border-radius: 15px; padding: 15px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); width: 100%; margin-bottom: 20px; } .stat-item { text-align: center; } .stat-value { font-size: 1.5rem; font-weight: 700; margin-bottom: 5px; background: linear-gradient(to right, var(--primary), var(--tertiary)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .stat-label { font-size: 0.8rem; color: var(--text-light); } .tooltip { position: absolute; background: white; border-radius: 5px; padding: 8px 12px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); font-size: 0.8rem; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 10; max-width: 200px; white-space: normal; } .tooltip.visible { opacity: 1; } .quick-actions { display: flex; gap: 10px; margin-bottom: 20px; width: 100%; } .action-btn { flex: 1; background: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; color: var(--text); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } .action-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } .action-btn i { font-size: 1rem; } .mark-available { border-bottom: 3px solid var(--available); } .mark-unavailable { border-bottom: 3px solid var(--unavailable); } .mark-tentative { border-bottom: 3px solid var(--tentative); } @media (max-width: 600px) { .calendar { gap: 5px; } .day-number { font-size: 0.8rem; } .status-icon { font-size: 1rem; } .status-text { font-size: 0.6rem; } .legend { flex-wrap: wrap; justify-content: center; } .quick-actions { flex-direction: column; gap: 5px; } .stats { flex-wrap: wrap; } .stat-item { width: 50%; margin-bottom: 10px; } } /* Animation for day toggles */ @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .pulse-animation { animation: pulse 0.3s ease-in-out; } /* Loading spinner */ .loader { width: 48px; height: 48px; border: 5px solid var(--primary-light); border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: none; } @keyframes rotation { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } .loading .loader { display: inline-block; } .loading .calendar, .loading .controls, .loading .stats, .loading .quick-actions { opacity: 0.5; pointer-events: none; } </style> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> </head> <body> <h1>Freelance Focus Calendar</h1> <p class="subtitle">Toggle your availability, manage bookings, and stay organized with ease</p> <div class="stats"> <div class="stat-item"> <div class="stat-value" id="available-days">14</div> <div class="stat-label">Available Days</div> </div> <div class="stat-item"> <div class="stat-value" id="booked-days">8</div> <div class="stat-label">Booked Days</div> </div> <div class="stat-item"> <div class="stat-value" id="booking-rate">62%</div> <div class="stat-label">Booking Rate</div> </div> </div> <div class="quick-actions"> <button class="action-btn mark-available" id="bulk-available"> <i class="fas fa-check-circle"></i> Mark Week Available </button> <button class="action-btn mark-unavailable" id="bulk-unavailable"> <i class="fas fa-times-circle"></i> Mark Week Unavailable </button> <button class="action-btn mark-tentative" id="bulk-tentative"> <i class="fas fa-question-circle"></i> Mark Week Tentative </button> </div> <div class="controls"> <div class="month-selector"> <button class="month-nav" id="prev-month"><i class="fas fa-chevron-left"></i></button> <div class="month-display" id="month-year">September 2023</div> <button class="month-nav" id="next-month"><i class="fas fa-chevron-right"></i></button> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color legend-available"></div> <span>Available</span> </div> <div class="legend-item"> <div class="legend-color legend-booked"></div> <span>Booked</span> </div> <div class="legend-item"> <div class="legend-color legend-tentative"></div> <span>Tentative</span> </div> </div> </div> <div class="calendar" id="calendar"> <!-- Days will be generated here --> </div> <div class="overlay" id="overlay"></div> <div class="edit-form" id="edit-form"> <div class="form-header"> <h3 class="form-title" id="form-date-display">September 15, 2023</h3> <button class="close-btn" id="close-form"><i class="fas fa-times"></i></button> </div> <div class="form-group"> <label>Status</label> <div class="status-selector"> <div class="status-option available-option" data-status="available">Available</div> <div class="status-option booked-option" data-status="booked">Booked</div> <div class="status-option tentative-option" data-status="tentative">Tentative</div> <div class="status-option unavailable-option" data-status="unavailable">Unavailable</div> </div> </div> <div class="form-group"> <label>Available Time Slots</label> <div class="time-slots"> <div class="time-slot" data-time="8:00 AM">8:00 AM</div> <div class="time-slot" data-time="9:00 AM">9:00 AM</div> <div class="time-slot" data-time="10:00 AM">10:00 AM</div> <div class="time-slot" data-time="11:00 AM">11:00 AM</div> <div class="time-slot" data-time="12:00 PM">12:00 PM</div> <div class="time-slot" data-time="1:00 PM">1:00 PM</div> <div class="time-slot" data-time="2:00 PM">2:00 PM</div> <div class="time-slot" data-time="3:00 PM">3:00 PM</div> <div class="time-slot" data-time="4:00 PM">4:00 PM</div> <div class="time-slot" data-time="5:00 PM">5:00 PM</div> </div> </div> <div class="form-group"> <label>Notes</label> <textarea class="notes-input" id="day-notes" placeholder="Add any details about this day..."></textarea> </div> <button class="save-btn" id="save-day">Save Changes</button> </div> <div class="tooltip" id="tooltip"></div> <span class="loader"></span> <script> document.addEventListener('DOMContentLoaded', function() { // Current date trackers let currentDate = new Date(); let currentMonth = currentDate.getMonth(); let currentYear = currentDate.getFullYear(); let selectedDay = null; let selectedDate = null; let calendarData = {}; // DOM elements const calendar = document.getElementById('calendar'); const monthYearDisplay = document.getElementById('month-year'); const prevMonthBtn = document.getElementById('prev-month'); const nextMonthBtn = document.getElementById('next-month'); const editForm = document.getElementById('edit-form'); const overlay = document.getElementById('overlay'); const closeFormBtn = document.getElementById('close-form'); const formDateDisplay = document.getElementById('form-date-display'); const dayNotesInput = document.getElementById('day-notes'); const saveDayBtn = document.getElementById('save-day'); const statusOptions = document.querySelectorAll('.status-option'); const timeSlots = document.querySelectorAll('.time-slot'); const tooltip = document.getElementById('tooltip'); const bulkAvailableBtn = document.getElementById('bulk-available'); const bulkUnavailableBtn = document.getElementById('bulk-unavailable'); const bulkTentativeBtn = document.getElementById('bulk-tentative'); // Statistics elements const availableDaysEl = document.getElementById('available-days'); const bookedDaysEl = document.getElementById('booked-days'); const bookingRateEl = document.getElementById('booking-rate'); // Initial calendar setup initCalendar(); renderCalendar(); // Initialize with some sample data function initCalendar() { // Generate some sample data for the current month const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); for (let i = 1; i <= daysInMonth; i++) { const dateKey = `${currentYear}-${currentMonth + 1}-${i}`; // Randomly assign statuses const statusOptions = ['available', 'booked', 'tentative', 'unavailable']; const randomStatus = statusOptions[Math.floor(Math.random() * statusOptions.length)]; // Generate random time slots const possibleTimeSlots = [ '8:00 AM', '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', '1:00 PM', '2:00 PM', '3:00 PM', '4:00 PM', '5:00 PM' ]; const selectedTimeSlots = []; const slotCount = Math.floor(Math.random() * 5); // 0-4 slots for (let j = 0; j < slotCount; j++) { const randomSlot = possibleTimeSlots[Math.floor(Math.random() * possibleTimeSlots.length)]; if (!selectedTimeSlots.includes(randomSlot)) { selectedTimeSlots.push(randomSlot); } } // Random notes for some days let notes = ''; if (Math.random() > 0.7) { const sampleNotes = [ 'Client meeting with ABC Corp', 'Design workshop for XYZ project', 'Deadline for website mockups', 'Conference call with international team', 'Working on-site with client' ]; notes = sampleNotes[Math.floor(Math.random() * sampleNotes.length)]; } // Store the data calendarData[dateKey] = { status: randomStatus, timeSlots: selectedTimeSlots, notes: notes }; } updateStats(); } // Render the calendar for the current month function renderCalendar() { calendar.innerHTML = ''; // Add day headers const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Get first day of month and total days const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay(); const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); const daysInPrevMonth = new Date(currentYear, currentMonth, 0).getDate(); // Update month/year display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; monthYearDisplay.textContent = `${monthNames[currentMonth]} ${currentYear}`; // Add previous month's trailing days for (let i = firstDayOfMonth - 1; i >= 0; i--) { const prevMonthDay = daysInPrevMonth - i; const dayEl = createDayElement(prevMonthDay, true, currentMonth === 0 ? 11 : currentMonth - 1, currentMonth === 0 ? currentYear - 1 : currentYear); calendar.appendChild(dayEl); } // Add current month's days for (let i = 1; i <= daysInMonth; i++) { const dayEl = createDayElement(i, false, currentMonth, currentYear); calendar.appendChild(dayEl); } // Add next month's leading days const totalCells = 42; // 6 rows * 7 days const remainingCells = totalCells - (firstDayOfMonth + daysInMonth); for (let i = 1; i <= remainingCells; i++) { const dayEl = createDayElement(i, true, currentMonth === 11 ? 0 : currentMonth + 1, currentMonth === 11 ? currentYear + 1 : currentYear); calendar.appendChild(dayEl); } updateStats(); } // Create a day element for the calendar function createDayElement(day, isOutsideMonth, month, year) { const dayEl = document.createElement('div'); dayEl.className = isOutsideMonth ? 'calendar-day outside-month' : 'calendar-day'; const dayNumber = document.createElement('div'); dayNumber.className = 'day-number'; dayNumber.textContent = day; dayEl.appendChild(dayNumber); const dateKey = `${year}-${month + 1}-${day}`; const dayData = calendarData[dateKey]; const statusIndicator = document.createElement('div'); statusIndicator.className = 'status-indicator'; // Default to unavailable if no data exists let status = 'unavailable'; let statusText = 'Unavailable'; let statusIcon = 'fa-ban'; if (dayData) { status = dayData.status; switch (status) { case 'available': statusText = 'Available'; statusIcon = 'fa-check'; break; case 'booked': statusText = 'Booked'; statusIcon = 'fa-calendar-check'; break; case 'tentative': statusText = 'Tentative'; statusIcon = 'fa-clock'; break; default: statusText = 'Unavailable'; statusIcon = 'fa-ban'; } } dayEl.classList.add(status); const icon = document.createElement('div'); icon.className = 'status-icon'; icon.innerHTML = `<i class="fas ${statusIcon}"></i>`; statusIndicator.appendChild(icon); const text = document.createElement('div'); text.className = 'status-text'; text.textContent = statusText; statusIndicator.appendChild(text); dayEl.appendChild(statusIndicator); // Add day data as data attributes dayEl.dataset.date = dateKey; dayEl.dataset.status = status; // Add event listeners if (!isOutsideMonth) { dayEl.addEventListener('click', () => openDayEditor(dateKey, day, month, year)); // Add tooltip for days with notes if (dayData && dayData.notes) { dayEl.addEventListener('mouseenter', (e) => showTooltip(e, dayData.notes)); dayEl.addEventListener('mouseleave', hideTooltip); } } return dayEl; } // Open the day editor function openDayEditor(dateKey, day, month, year) { selectedDay = day; selectedDate = dateKey; // Format the date for display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; formDateDisplay.textContent = `${monthNames[month]} ${day}, ${year}`; // Set form values based on existing data const dayData = calendarData[dateKey] || { status: 'unavailable', timeSlots: [], notes: '' }; // Reset all status options statusOptions.forEach(option => { option.classList.remove('selected'); if (option.dataset.status === dayData.status) { option.classList.add('selected'); } }); // Reset all time slots timeSlots.forEach(slot => { slot.classList.remove('selected'); if (dayData.timeSlots && dayData.timeSlots.includes(slot.dataset.time)) { slot.classList.add('selected'); } }); // Set notes dayNotesInput.value = dayData.notes || ''; // Show the form editForm.classList.add('active'); overlay.classList.add('active'); // Focus on notes field setTimeout(() => dayNotesInput.focus(), 300); } // Save day data function saveDayData() { // Simulate loading document.body.classList.add('loading'); setTimeout(() => { // Get the selected status const selectedStatusOption = document.querySelector('.status-option.selected'); const status = selectedStatusOption ? selectedStatusOption.dataset.status : 'unavailable'; // Get selected time slots const selectedSlots = []; document.querySelectorAll('.time-slot.selected').forEach(slot => { selectedSlots.push(slot.dataset.time); }); // Get notes const notes = dayNotesInput.value.trim(); // Update data calendarData[selectedDate] = { status: status, timeSlots: selectedSlots, notes: notes }; // Close the form closeForm(); // Update the calendar renderCalendar(); // Remove loading state document.body.classList.remove('loading'); }, 300); } // Close the form function closeForm() { editForm.classList.remove('active'); overlay.classList.remove('active'); } // Show tooltip function showTooltip(event, content) { tooltip.textContent = content; tooltip.classList.add('visible'); // Position the tooltip const rect = event.target.getBoundingClientRect(); tooltip.style.top = `${rect.bottom + 10}px`; tooltip.style.left = `${rect.left + rect.width / 2}px`; tooltip.style.transform = 'translateX(-50%)'; } // Hide tooltip function hideTooltip() { tooltip.classList.remove('visible'); } // Update statistics function updateStats() { const currentMonthData = Object.entries(calendarData).filter(([key]) => { const [year, month] = key.split('-'); return parseInt(year) === currentYear && parseInt(month) === currentMonth + 1; }); const availableDays = currentMonthData.filter(([_, data]) => data.status === 'available').length; const bookedDays = currentMonthData.filter(([_, data]) => data.status === 'booked').length; const bookingRate = currentMonthData.length > 0 ? Math.round((bookedDays / (availableDays + bookedDays)) * 100) : 0; availableDaysEl.textContent = availableDays; bookedDaysEl.textContent = bookedDays; bookingRateEl.textContent = `${bookingRate}%`; } // Bulk update for a week function bulkUpdateWeek(status) { document.body.classList.add('loading'); setTimeout(() => { // Get the current week's Monday date const today = new Date(currentYear, currentMonth, currentDate.getDate()); const dayOfWeek = today.getDay(); const diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust for Sunday const monday = new Date(today.setDate(diff)); // Update each day of the week for (let i = 0; i < 7; i++) { const date = new Date(monday); date.setDate(monday.getDate() + i); const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); const dateKey = `${year}-${month + 1}-${day}`; // Only update if it's the current month if (month === currentMonth) { // Get existing data or create new const dayData = calendarData[dateKey] || { timeSlots: [], notes: '' }; // Update status dayData.status = status; // Save calendarData[dateKey] = dayData; } } // Update the calendar renderCalendar(); // Remove loading state document.body.classList.remove('loading'); // Add animation to the affected days document.querySelectorAll('.calendar-day').forEach(day => { if (!day.classList.contains('outside-month')) { day.classList.add('pulse-animation'); setTimeout(() => day.classList.remove('pulse-animation'), 300); } }); }, 300); } // Event listeners prevMonthBtn.addEventListener('click', () => { currentMonth--; if (currentMonth < 0) { currentMonth = 11; currentYear--; } renderCalendar(); }); nextMonthBtn.addEventListener('click', () => { currentMonth++; if (currentMonth > 11) { currentMonth = 0; currentYear++; } renderCalendar(); }); closeFormBtn.addEventListener('click', closeForm); overlay.addEventListener('click', closeForm); saveDayBtn.addEventListener('click', saveDayData); statusOptions.forEach(option => { option.addEventListener('click', () => { statusOptions.forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); }); }); timeSlots.forEach(slot => { slot.addEventListener('click', () => { slot.classList.toggle('selected'); }); }); bulkAvailableBtn.addEventListener('click', () => bulkUpdateWeek('available')); bulkUnavailableBtn.addEventListener('click', () => bulkUpdateWeek('unavailable')); bulkTentativeBtn.addEventListener('click', () => bulkUpdateWeek('tentative')); }); </script> </body> </html>