-
Notifications
You must be signed in to change notification settings - Fork 186
feat: add interactive calendar with event support #366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 📅 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+):** Logic for date calculation and event handling. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## 📂 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ##💡 How It Works | |
| ## 💡 How It Works |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect markdown heading syntax. The "Date Calculation" and subsequent numbered sections should use proper markdown headings (### ) rather than plain text followed by descriptions.
| ##💡 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 | |
| We use a loop to generate <div> elements. | |
| First, we insert empty placeholder divs to align the 1st of the month with the correct weekday column. | |
| Then, we generate the actual numbered days. | |
| 3. State Management | |
| ## 💡 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 | |
| We use a loop to generate <div> elements. | |
| First, we insert empty placeholder divs to align the 1st of the month with the correct weekday column. | |
| Then, we generate the actual numbered days. | |
| ### 3. State Management |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Markdown formatting issue. The code block for JavaScript data structure example is incomplete. It starts with "JavaScript" as a language identifier but lacks proper code fencing (triple backticks). The example should be wrapped in proper markdown code fences.
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent heading level. This should be "## 🔮 Future Improvements" (level 2) to match the other main sections in the document, not "# 🔮 Future Improvements" (level 1).
| # 🔮 Future Improvements | |
| ## 🔮 Future Improvements |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||||||
| <!DOCTYPE html> | ||||||||||||||
| <html lang="en"> | ||||||||||||||
| <head> | ||||||||||||||
| <meta charset="UTF-8"> | ||||||||||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||||||||||
| <title>Interactive Calendar</title> | ||||||||||||||
| <link rel="stylesheet" href="style.css"> | ||||||||||||||
|
||||||||||||||
| </head> | ||||||||||||||
| <body> | ||||||||||||||
|
|
||||||||||||||
| <div class="calendar-container"> | ||||||||||||||
| <header class="calendar-header"> | ||||||||||||||
| <div class="calendar-navigation"> | ||||||||||||||
| <span id="month-prev" class="nav-btn"><</span> | ||||||||||||||
| <h2 id="month-year"></h2> | ||||||||||||||
| <span id="month-next" class="nav-btn">></span> | ||||||||||||||
|
Comment on lines
+14
to
+16
|
||||||||||||||
| <span id="month-prev" class="nav-btn"><</span> | |
| <h2 id="month-year"></h2> | |
| <span id="month-next" class="nav-btn">></span> | |
| <span id="month-prev" class="nav-btn" aria-label="Previous month"><</span> | |
| <h2 id="month-year"></h2> | |
| <span id="month-next" class="nav-btn" aria-label="Next month">></span> |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility issue: Interactive elements (navigation buttons and day cells) lack proper keyboard accessibility. The .nav-btn spans should be buttons or have role="button" with tabindex="0" and keyboard event handlers. Similarly, day cells should be keyboard navigable. Users who cannot use a mouse will not be able to interact with the calendar.
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility issue: The modal lacks proper ARIA attributes. The modal should have role="dialog", aria-modal="true", and aria-labelledby pointing to the heading element. This helps screen readers understand the modal's purpose and behavior.
| <div id="event-modal" class="modal hidden"> | |
| <div id="event-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="selected-date"> |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility issue: The close button uses the × symbol without an accessible label. Add an aria-label="Close" attribute to help screen reader users understand the button's purpose.
| <span class="close-btn">×</span> | |
| <span class="close-btn" aria-label="Close">×</span> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,121 @@ | ||||||
| const monthYearElement = document.getElementById('month-year'); | ||||||
| const datesElement = document.getElementById('calendar-dates'); | ||||||
| const prevBtn = document.getElementById('month-prev'); | ||||||
| const nextBtn = document.getElementById('month-next'); | ||||||
|
|
||||||
| // Modal Elements | ||||||
| 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" | ||||||
| ]; | ||||||
|
|
||||||
| // --- Core Functions --- | ||||||
|
|
||||||
| function renderCalendar() { | ||||||
| const year = currentDate.getFullYear(); | ||||||
| const month = currentDate.getMonth(); | ||||||
|
|
||||||
| monthYearElement.innerText = `${months[month]} ${year}`; | ||||||
| datesElement.innerHTML = ''; | ||||||
|
|
||||||
| // First day of the month (0 = Sunday, 1 = Monday, etc.) | ||||||
| const firstDay = new Date(year, month, 1).getDay(); | ||||||
| // Total days in the current month | ||||||
| const totalDays = new Date(year, month + 1, 0).getDate(); | ||||||
|
|
||||||
| // Add empty divs for days before the first day of the month | ||||||
| for (let i = 0; i < firstDay; i++) { | ||||||
| const emptyDiv = document.createElement('div'); | ||||||
| datesElement.appendChild(emptyDiv); | ||||||
| } | ||||||
|
|
||||||
| // Render days | ||||||
| for (let day = 1; day <= totalDays; day++) { | ||||||
| const dayDiv = document.createElement('div'); | ||||||
| dayDiv.classList.add('day'); | ||||||
| dayDiv.innerText = day; | ||||||
|
|
||||||
| // Check if this day is "Today" | ||||||
| const today = new Date(); | ||||||
| if (day === today.getDate() && month === today.getMonth() && year === today.getFullYear()) { | ||||||
| dayDiv.classList.add('current-date'); | ||||||
| } | ||||||
|
|
||||||
| // Check for Events | ||||||
| const dateString = `${year}-${month + 1}-${day}`; | ||||||
|
||||||
| const dateString = `${year}-${month + 1}-${day}`; | |
| const dateString = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; |
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security consideration: User input from eventInput is stored in localStorage and later displayed as a tooltip (title attribute) without sanitization. While title attributes are generally safe from XSS, this could be a concern if the data is used elsewhere in the future. Consider documenting this or adding a comment about input handling if event display methods change.
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The event listener is attached to each day cell individually, which creates many event listeners. For better performance, especially with larger calendars, consider using event delegation by attaching a single listener to the parent datesElement and checking the event target. This reduces memory usage and improves performance.
Copilot
AI
Jan 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential data loss issue. When saving an event, if the user enters only whitespace and then clicks save, the event is not saved (due to the trim check), but the modal closes and the calendar re-renders. However, if an existing event is present and the user clears it to whitespace and clicks save, the old event remains. Consider also deleting the event when the trimmed value is empty to provide consistent behavior.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,145 @@ | ||||||||
| :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; | ||||||||
|
||||||||
| padding: 5px; | |
| padding: 5px; | |
| box-sizing: border-box; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lowercase sentence beginning. The sentence should start with a capital letter: "Visual indicator for today's date."