diff --git a/examples/interactive-calendar/README.md b/examples/interactive-calendar/README.md new file mode 100644 index 00000000..8bdaacf9 --- /dev/null +++ b/examples/interactive-calendar/README.md @@ -0,0 +1,90 @@ +# 📅 Interactive Calendar + +A lightweight, responsive calendar widget built with **vanilla JavaScript**. +This project demonstrates **DOM manipulation**, **Date object handling**, and **local state management** without external libraries. + +--- + +## 🚀 Features + +- **Dynamic Rendering:** Automatically generates the correct grid for any month and year. +- **Navigation:** Browse through past and future months. +- **Current Date Highlighting:** Visual indicator for today's date. +- **Event Management (Bonus):** Click any date to add, view, or delete notes. +- **Data Persistence:** Events are saved to the browser's `localStorage`, so they remain after refreshing the page. +- **Responsive Design:** Built with CSS Grid to adapt to different screen sizes. + +--- + +## 🛠️ Technologies Used + +- **HTML5:** Semantic structure +- **CSS3:** Flexbox and Grid layout, CSS Variables for theming +- **JavaScript (ES6+):** Date calculation and event handling logic + +--- + +## 📂 Project Structure + +```text +interactive-calendar/ +├── index.html # Main HTML structure +├── style.css # Styling and Grid layout +├── script.js # Calendar logic and event handling +└── README.md # Project documentation +``` + +--- + +## 💡 How It Works + +### 1. Date Calculation + +The calendar grid is calculated using the native `Date` object: + +- **Start Day:** + `new Date(year, month, 1).getDay()` + Determines which day of the week the month starts on + `(0 = Sunday, 1 = Monday, ...)` + +- **Total Days:** + `new Date(year, month + 1, 0).getDate()` + Retrieves the exact number of days in the current month + +--- + +### 2. Rendering the Grid + +- A loop generates `
` elements for the calendar. +- Empty placeholder divs are inserted to align the first day of the month correctly. +- Numbered day cells are then rendered dynamically. + +--- + +### 3. State Management + +Events are stored in a simple JSON object and persisted using `localStorage`. + +```javascript +// Data Structure Example +{ + "2023-10-25": "Meeting with team", + "2023-10-31": "Halloween Party" +} +``` + +--- + +## 🏃‍♂️ How to Run + +1. Clone the repository +2. Navigate to the `interactive-calendar` folder +3. Open `index.html` in your browser + +--- + +## 🔮 Future Improvements + +- Add drag-and-drop functionality for events +- Support multiple events per day +- Add specific time slots for events diff --git a/examples/interactive-calendar/index.html b/examples/interactive-calendar/index.html new file mode 100644 index 00000000..4dfc408f --- /dev/null +++ b/examples/interactive-calendar/index.html @@ -0,0 +1,38 @@ + + + + + + Interactive Calendar + + + +
+
+
+ < +

+ > +
+
+ +
+
Sun
Mon
Tue
Wed
Thu
Fri
Sat
+
+ +
+
+ + + + + + \ No newline at end of file diff --git a/examples/interactive-calendar/script.js b/examples/interactive-calendar/script.js new file mode 100644 index 00000000..cf534641 --- /dev/null +++ b/examples/interactive-calendar/script.js @@ -0,0 +1,138 @@ +const monthYearElement = document.getElementById('month-year'); +const datesElement = document.getElementById('calendar-dates'); +const prevBtn = document.getElementById('month-prev'); +const nextBtn = document.getElementById('month-next'); + +const modal = document.getElementById('event-modal'); +const closeModalBtn = document.querySelector('.close-btn'); +const selectedDateTitle = document.getElementById('selected-date'); +const eventInput = document.getElementById('event-input'); +const saveEventBtn = document.getElementById('save-event-btn'); +const deleteEventBtn = document.getElementById('delete-event-btn'); + +let currentDate = new Date(); +let clickedDate = null; +let events = JSON.parse(localStorage.getItem('events')) || {}; + +const months = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +]; + +// Helper: Zero-pad numbers (Fixed: Date formatting consistency) +const pad = (n) => (n < 10 ? '0' + n : n); + +function renderCalendar() { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + monthYearElement.innerText = `${months[month]} ${year}`; + datesElement.innerHTML = ''; + + const firstDay = new Date(year, month, 1).getDay(); + const totalDays = new Date(year, month + 1, 0).getDate(); + + for (let i = 0; i < firstDay; i++) { + const emptyDiv = document.createElement('div'); + datesElement.appendChild(emptyDiv); + } + + for (let day = 1; day <= totalDays; day++) { + const dayDiv = document.createElement('div'); + dayDiv.classList.add('day'); + dayDiv.innerText = day; + + const today = new Date(); + if (day === today.getDate() && month === today.getMonth() && year === today.getFullYear()) { + dayDiv.classList.add('current-date'); + } + + // Fixed: Use padded date format YYYY-MM-DD + const dateString = `${year}-${pad(month + 1)}-${pad(day)}`; + + // Store date in data attribute for Event Delegation + dayDiv.dataset.date = dateString; + + if (events[dateString]) { + dayDiv.classList.add('has-event'); + dayDiv.title = events[dateString]; + } + + // Removed individual event listeners here (Performance fix) + datesElement.appendChild(dayDiv); + } +} + +// Fixed: Event Delegation (One listener for all days) +datesElement.addEventListener('click', (e) => { + const target = e.target.closest('.day'); + if (target && target.dataset.date) { + openModal(target.dataset.date); + } +}); + +function openModal(dateStr) { + clickedDate = dateStr; + selectedDateTitle.innerText = `Event for: ${dateStr}`; + eventInput.value = events[dateStr] || ''; + modal.classList.remove('hidden'); + eventInput.focus(); +} + +function closeModal() { + modal.classList.add('hidden'); + clickedDate = null; +} + +// Fixed: Save logic handles whitespace and deletion +saveEventBtn.addEventListener('click', () => { + const eventText = eventInput.value.trim(); + + if (eventText) { + events[clickedDate] = eventText; + } else { + delete events[clickedDate]; // Remove event if input is cleared + } + + localStorage.setItem('events', JSON.stringify(events)); + closeModal(); + renderCalendar(); +}); + +deleteEventBtn.addEventListener('click', () => { + if (events[clickedDate]) { + delete events[clickedDate]; + localStorage.setItem('events', JSON.stringify(events)); + } + closeModal(); + renderCalendar(); +}); + +// Navigation & Close +prevBtn.addEventListener('click', () => { + currentDate.setMonth(currentDate.getMonth() - 1); + renderCalendar(); +}); + +nextBtn.addEventListener('click', () => { + currentDate.setMonth(currentDate.getMonth() + 1); + renderCalendar(); +}); + +closeModalBtn.addEventListener('click', closeModal); + +window.addEventListener('click', (e) => { + if (e.target === modal) closeModal(); +}); + +// Fixed: Keyboard Accessibility for Nav Buttons +[prevBtn, nextBtn, closeModalBtn].forEach(btn => { + btn.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + btn.click(); + } + }); +}); + +renderCalendar(); \ No newline at end of file diff --git a/examples/interactive-calendar/style.css b/examples/interactive-calendar/style.css new file mode 100644 index 00000000..c132b164 --- /dev/null +++ b/examples/interactive-calendar/style.css @@ -0,0 +1,147 @@ +:root { + --primary-color: #4a90e2; + --bg-color: #f4f4f9; + --text-color: #333; + --highlight-color: #e0f7fa; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: var(--bg-color); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; +} + +.calendar-container { + background: white; + width: 90%; + max-width: 400px; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0,0,0,0.1); + overflow: hidden; + padding: 20px; +} + +.calendar-header { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +.calendar-navigation { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.nav-btn { + cursor: pointer; + font-size: 1.5rem; + -webkit-user-select: none; + user-select: none; + padding: 0 10px; +} + +.nav-btn:hover { + color: var(--primary-color); +} + +/* Grid System */ +.calendar-weekdays, .calendar-dates { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; +} + +.calendar-weekdays div { + font-weight: bold; + color: #888; + margin-bottom: 10px; +} + +.day { + padding: 10px; + cursor: pointer; + border-radius: 5px; + transition: background 0.2s; + position: relative; +} + +.day:hover { + background-color: #f0f0f0; +} + +.day.current-date { + background-color: var(--primary-color); + color: white; + font-weight: bold; +} + +.day.has-event::after { + content: ''; + position: absolute; + bottom: 5px; + left: 50%; + transform: translateX(-50%); + width: 5px; + height: 5px; + background-color: red; + border-radius: 50%; +} + +.day.inactive { + color: #ccc; + pointer-events: none; +} + +/* Modal Styles */ +.modal { + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.hidden { display: none; } + +.modal-content { + background: white; + padding: 20px; + border-radius: 8px; + width: 300px; + position: relative; +} + +.close-btn { + position: absolute; + top: 10px; right: 15px; + cursor: pointer; + font-size: 1.2rem; +} + +textarea { + width: 100%; + height: 60px; + margin: 10px 0; + padding: 5px; + box-sizing: border-box; /* Fixed: prevents width overflow */ + resize: vertical; /* Optional: improves UX */ +} + +button { + background: var(--primary-color); + color: white; + border: none; + padding: 8px 15px; + cursor: pointer; + border-radius: 4px; +} + +button.secondary { background: #ccc; } \ No newline at end of file