Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Anniversary Reminder</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| } | |
| .pulse-animation { | |
| animation: pulse 2s infinite; | |
| } | |
| .anniversary-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| } | |
| .anniversary-card { | |
| transition: all 0.3s ease; | |
| } | |
| .notification-badge { | |
| position: absolute; | |
| top: -8px; | |
| right: -8px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-indigo-50 to-purple-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="text-center mb-12"> | |
| <h1 class="text-4xl font-bold text-indigo-800 mb-2">Anniversary Reminder</h1> | |
| <p class="text-lg text-gray-600">Never forget important dates again</p> | |
| <div class="w-24 h-1 bg-indigo-400 mx-auto mt-4 rounded-full"></div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Add Anniversary Form --> | |
| <div class="bg-white rounded-xl shadow-lg p-6 lg:col-span-1 h-fit sticky top-8"> | |
| <div class="flex items-center mb-6"> | |
| <i class="fas fa-calendar-plus text-2xl text-indigo-600 mr-3"></i> | |
| <h2 class="text-2xl font-semibold text-gray-800">Add New Anniversary</h2> | |
| </div> | |
| <form id="anniversaryForm" class="space-y-4"> | |
| <div> | |
| <label for="name" class="block text-sm font-medium text-gray-700 mb-1">Name/Title</label> | |
| <input type="text" id="name" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="date" class="block text-sm font-medium text-gray-700 mb-1">Date</label> | |
| <input type="date" id="date" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="notes" class="block text-sm font-medium text-gray-700 mb-1">Notes (Optional)</label> | |
| <textarea id="notes" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="recurring" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> | |
| <label for="recurring" class="ml-2 block text-sm text-gray-700">Recurring annually</label> | |
| </div> | |
| <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center"> | |
| <i class="fas fa-save mr-2"></i> Save Anniversary | |
| </button> | |
| </form> | |
| <div class="mt-6 pt-6 border-t border-gray-200"> | |
| <h3 class="text-lg font-medium text-gray-800 mb-3">Notification Settings</h3> | |
| <div class="flex items-center mb-2"> | |
| <input type="checkbox" id="enableNotifications" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> | |
| <label for="enableNotifications" class="ml-2 block text-sm text-gray-700">Enable browser notifications</label> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="number" id="daysBefore" min="0" max="30" value="1" class="w-16 px-2 py-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| <label for="daysBefore" class="ml-2 block text-sm text-gray-700">days before anniversary</label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Upcoming Anniversaries --> | |
| <div class="lg:col-span-2"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-bell text-2xl text-indigo-600 mr-3"></i> | |
| <h2 class="text-2xl font-semibold text-gray-800">Upcoming Anniversaries</h2> | |
| </div> | |
| <div class="relative"> | |
| <button id="filterBtn" class="flex items-center text-indigo-600 hover:text-indigo-800"> | |
| <i class="fas fa-filter mr-1"></i> Filter | |
| </button> | |
| <div id="filterDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10"> | |
| <div class="py-1"> | |
| <a href="#" class="filter-option block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50" data-days="7">Next 7 days</a> | |
| <a href="#" class="filter-option block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50" data-days="30">Next 30 days</a> | |
| <a href="#" class="filter-option block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50" data-days="all">All anniversaries</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Anniversary Cards --> | |
| <div id="anniversaryList" class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <!-- Cards will be dynamically inserted here --> | |
| <div class="text-center py-10 text-gray-500" id="emptyState"> | |
| <i class="fas fa-calendar-check text-4xl mb-3"></i> | |
| <p>No anniversaries added yet. Add one to get started!</p> | |
| </div> | |
| </div> | |
| <!-- Today's Anniversaries (only shown if there are any) --> | |
| <div id="todaySection" class="mt-12 hidden"> | |
| <div class="flex items-center mb-6"> | |
| <i class="fas fa-gift text-2xl text-indigo-600 mr-3"></i> | |
| <h2 class="text-2xl font-semibold text-gray-800">Today's Anniversaries</h2> | |
| </div> | |
| <div id="todayList" class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <!-- Today's cards will be inserted here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Edit Modal --> | |
| <div id="editModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-xl shadow-xl p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold text-gray-800">Edit Anniversary</h3> | |
| <button id="closeModal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <form id="editForm" class="space-y-4"> | |
| <input type="hidden" id="editId"> | |
| <div> | |
| <label for="editName" class="block text-sm font-medium text-gray-700 mb-1">Name/Title</label> | |
| <input type="text" id="editName" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="editDate" class="block text-sm font-medium text-gray-700 mb-1">Date</label> | |
| <input type="date" id="editDate" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <label for="editNotes" class="block text-sm font-medium text-gray-700 mb-1">Notes</label> | |
| <textarea id="editNotes" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="editRecurring" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"> | |
| <label for="editRecurring" class="ml-2 block text-sm text-gray-700">Recurring annually</label> | |
| </div> | |
| <div class="flex justify-between pt-4"> | |
| <button type="button" id="deleteBtn" class="bg-red-100 hover:bg-red-200 text-red-700 font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center"> | |
| <i class="fas fa-trash mr-2"></i> Delete | |
| </button> | |
| <button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center"> | |
| <i class="fas fa-save mr-2"></i> Save Changes | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const anniversaryForm = document.getElementById('anniversaryForm'); | |
| const anniversaryList = document.getElementById('anniversaryList'); | |
| const todaySection = document.getElementById('todaySection'); | |
| const todayList = document.getElementById('todayList'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const filterBtn = document.getElementById('filterBtn'); | |
| const filterDropdown = document.getElementById('filterDropdown'); | |
| const editModal = document.getElementById('editModal'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const editForm = document.getElementById('editForm'); | |
| const deleteBtn = document.getElementById('deleteBtn'); | |
| const enableNotifications = document.getElementById('enableNotifications'); | |
| const daysBefore = document.getElementById('daysBefore'); | |
| // State | |
| let anniversaries = JSON.parse(localStorage.getItem('anniversaries')) || []; | |
| let filterDays = 30; // Default filter: next 30 days | |
| // Initialize | |
| renderAnniversaries(); | |
| checkTodayAnniversaries(); | |
| setupNotificationPermission(); | |
| // Event Listeners | |
| anniversaryForm.addEventListener('submit', handleAddAnniversary); | |
| filterBtn.addEventListener('click', toggleFilterDropdown); | |
| document.addEventListener('click', closeFilterDropdown); | |
| closeModal.addEventListener('click', () => editModal.classList.add('hidden')); | |
| // Functions | |
| function handleAddAnniversary(e) { | |
| e.preventDefault(); | |
| const name = document.getElementById('name').value; | |
| const date = document.getElementById('date').value; | |
| const notes = document.getElementById('notes').value; | |
| const recurring = document.getElementById('recurring').checked; | |
| const newAnniversary = { | |
| id: Date.now().toString(), | |
| name, | |
| date, | |
| notes, | |
| recurring, | |
| createdAt: new Date().toISOString() | |
| }; | |
| anniversaries.push(newAnniversary); | |
| saveAnniversaries(); | |
| renderAnniversaries(); | |
| checkTodayAnniversaries(); | |
| // Reset form | |
| anniversaryForm.reset(); | |
| // Show success message | |
| showToast('Anniversary added successfully!', 'success'); | |
| } | |
| function renderAnniversaries() { | |
| if (anniversaries.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| anniversaryList.innerHTML = ''; | |
| anniversaryList.appendChild(emptyState); | |
| return; | |
| } | |
| emptyState.classList.add('hidden'); | |
| anniversaryList.innerHTML = ''; | |
| // Sort anniversaries by upcoming date | |
| const sortedAnniversaries = [...anniversaries].sort((a, b) => { | |
| return daysUntilDate(a.date) - daysUntilDate(b.date); | |
| }); | |
| // Filter based on selected days | |
| const filteredAnniversaries = sortedAnniversaries.filter(anniversary => { | |
| if (filterDays === 'all') return true; | |
| return daysUntilDate(anniversary.date) <= filterDays; | |
| }); | |
| if (filteredAnniversaries.length === 0) { | |
| const noResults = document.createElement('div'); | |
| noResults.className = 'col-span-full text-center py-10 text-gray-500'; | |
| noResults.innerHTML = ` | |
| <i class="fas fa-calendar-times text-4xl mb-3"></i> | |
| <p>No anniversaries found for this filter.</p> | |
| `; | |
| anniversaryList.appendChild(noResults); | |
| return; | |
| } | |
| filteredAnniversaries.forEach(anniversary => { | |
| const card = createAnniversaryCard(anniversary); | |
| anniversaryList.appendChild(card); | |
| }); | |
| } | |
| function createAnniversaryCard(anniversary) { | |
| const daysUntil = daysUntilDate(anniversary.date); | |
| const isToday = daysUntil === 0; | |
| const isUpcoming = daysUntil > 0 && daysUntil <= 7; | |
| const card = document.createElement('div'); | |
| card.className = 'anniversary-card bg-white rounded-xl shadow-md p-5 relative'; | |
| card.dataset.id = anniversary.id; | |
| if (isToday) { | |
| card.classList.add('border-2', 'border-indigo-400'); | |
| } | |
| let badge = ''; | |
| if (isToday) { | |
| badge = ` | |
| <div class="absolute top-3 right-3 bg-indigo-600 text-white text-xs font-bold px-2 py-1 rounded-full"> | |
| TODAY | |
| </div> | |
| `; | |
| } else if (isUpcoming) { | |
| badge = ` | |
| <div class="absolute top-3 right-3 bg-yellow-500 text-white text-xs font-bold px-2 py-1 rounded-full"> | |
| SOON | |
| </div> | |
| `; | |
| } | |
| const dateObj = new Date(anniversary.date); | |
| const formattedDate = dateObj.toLocaleDateString('en-US', { | |
| month: 'long', | |
| day: 'numeric', | |
| year: 'numeric' | |
| }); | |
| const daysText = isToday ? 'Today!' : | |
| daysUntil === 1 ? 'Tomorrow' : | |
| `${daysUntil} days from now`; | |
| card.innerHTML = ` | |
| ${badge} | |
| <div class="flex items-start mb-3"> | |
| <div class="bg-indigo-100 text-indigo-600 rounded-lg p-3 mr-4"> | |
| <i class="fas fa-calendar-day text-xl"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <h3 class="text-lg font-semibold text-gray-800">${anniversary.name}</h3> | |
| <p class="text-sm text-gray-600">${formattedDate}</p> | |
| ${anniversary.notes ? `<p class="text-sm text-gray-500 mt-2">${anniversary.notes}</p>` : ''} | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center pt-3 border-t border-gray-100"> | |
| <span class="text-sm font-medium ${isToday ? 'text-indigo-600' : 'text-gray-500'}"> | |
| <i class="far fa-clock mr-1"></i> ${daysText} | |
| </span> | |
| <div class="flex space-x-2"> | |
| <button class="edit-btn text-indigo-600 hover:text-indigo-800 p-1 rounded-full"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| ${anniversary.recurring ? '<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">Recurring</span>' : ''} | |
| </div> | |
| </div> | |
| `; | |
| // Add edit event | |
| const editBtn = card.querySelector('.edit-btn'); | |
| editBtn.addEventListener('click', () => openEditModal(anniversary)); | |
| return card; | |
| } | |
| function openEditModal(anniversary) { | |
| document.getElementById('editId').value = anniversary.id; | |
| document.getElementById('editName').value = anniversary.name; | |
| document.getElementById('editDate').value = anniversary.date; | |
| document.getElementById('editNotes').value = anniversary.notes || ''; | |
| document.getElementById('editRecurring').checked = anniversary.recurring; | |
| editModal.classList.remove('hidden'); | |
| // Set up form submission | |
| editForm.onsubmit = function(e) { | |
| e.preventDefault(); | |
| const id = document.getElementById('editId').value; | |
| const name = document.getElementById('editName').value; | |
| const date = document.getElementById('editDate').value; | |
| const notes = document.getElementById('editNotes').value; | |
| const recurring = document.getElementById('editRecurring').checked; | |
| const index = anniversaries.findIndex(a => a.id === id); | |
| if (index !== -1) { | |
| anniversaries[index] = { | |
| ...anniversaries[index], | |
| name, | |
| date, | |
| notes, | |
| recurring | |
| }; | |
| saveAnniversaries(); | |
| renderAnniversaries(); | |
| checkTodayAnniversaries(); | |
| editModal.classList.add('hidden'); | |
| showToast('Anniversary updated successfully!', 'success'); | |
| } | |
| }; | |
| // Set up delete button | |
| deleteBtn.onclick = function() { | |
| if (confirm('Are you sure you want to delete this anniversary?')) { | |
| anniversaries = anniversaries.filter(a => a.id !== anniversary.id); | |
| saveAnniversaries(); | |
| renderAnniversaries(); | |
| checkTodayAnniversaries(); | |
| editModal.classList.add('hidden'); | |
| showToast('Anniversary deleted successfully!', 'success'); | |
| } | |
| }; | |
| } | |
| function checkTodayAnniversaries() { | |
| const todayAnniversaries = anniversaries.filter(anniversary => { | |
| return daysUntilDate(anniversary.date) === 0; | |
| }); | |
| if (todayAnniversaries.length > 0) { | |
| todaySection.classList.remove('hidden'); | |
| todayList.innerHTML = ''; | |
| todayAnniversaries.forEach(anniversary => { | |
| const card = createAnniversaryCard(anniversary); | |
| card.classList.add('pulse-animation'); | |
| todayList.appendChild(card); | |
| }); | |
| // Check if notifications are enabled | |
| if (enableNotifications.checked && Notification.permission === 'granted') { | |
| const notificationDays = parseInt(daysBefore.value) || 1; | |
| todayAnniversaries.forEach(anniversary => { | |
| // Check if we should notify for this anniversary | |
| if (daysUntilDate(anniversary.date) <= notificationDays) { | |
| const notificationTitle = `Anniversary Reminder: ${anniversary.name}`; | |
| const notificationBody = anniversary.notes | |
| ? `Today is ${anniversary.name}! ${anniversary.notes}` | |
| : `Today is ${anniversary.name}!`; | |
| new Notification(notificationTitle, { | |
| body: notificationBody, | |
| icon: 'https://cdn-icons-png.flaticon.com/512/3652/3652191.png' | |
| }); | |
| } | |
| }); | |
| } | |
| } else { | |
| todaySection.classList.add('hidden'); | |
| } | |
| } | |
| function daysUntilDate(dateString) { | |
| const today = new Date(); | |
| today.setHours(0, 0, 0, 0); | |
| const targetDate = new Date(dateString); | |
| targetDate.setHours(0, 0, 0, 0); | |
| // Adjust year for recurring anniversaries | |
| if (targetDate < today) { | |
| targetDate.setFullYear(today.getFullYear()); | |
| if (targetDate < today) { | |
| targetDate.setFullYear(today.getFullYear() + 1); | |
| } | |
| } | |
| const diffTime = targetDate - today; | |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |
| return diffDays; | |
| } | |
| function saveAnniversaries() { | |
| localStorage.setItem('anniversaries', JSON.stringify(anniversaries)); | |
| } | |
| function toggleFilterDropdown(e) { | |
| e.stopPropagation(); | |
| filterDropdown.classList.toggle('hidden'); | |
| } | |
| function closeFilterDropdown(e) { | |
| if (!filterDropdown.contains(e.target) && e.target !== filterBtn) { | |
| filterDropdown.classList.add('hidden'); | |
| } | |
| } | |
| function setupNotificationPermission() { | |
| // Check if notifications are supported | |
| if (!('Notification' in window)) { | |
| enableNotifications.disabled = true; | |
| enableNotifications.parentNode.querySelector('label').textContent += ' (not supported)'; | |
| return; | |
| } | |
| // Check current permission | |
| if (Notification.permission === 'granted') { | |
| enableNotifications.checked = true; | |
| } else if (Notification.permission === 'denied') { | |
| enableNotifications.disabled = true; | |
| enableNotifications.parentNode.querySelector('label').textContent += ' (blocked)'; | |
| } | |
| // Set up change listener | |
| enableNotifications.addEventListener('change', function() { | |
| if (this.checked && Notification.permission !== 'granted') { | |
| Notification.requestPermission().then(permission => { | |
| if (permission === 'granted') { | |
| showToast('Notifications enabled!', 'success'); | |
| } else { | |
| this.checked = false; | |
| showToast('Notifications blocked', 'warning'); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| function showToast(message, type) { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white font-medium flex items-center ${ | |
| type === 'success' ? 'bg-green-500' : 'bg-yellow-500' | |
| }`; | |
| toast.innerHTML = ` | |
| <i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle'} mr-2"></i> | |
| ${message} | |
| `; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // Set up filter options | |
| document.querySelectorAll('.filter-option').forEach(option => { | |
| option.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| filterDays = this.dataset.days === 'all' ? 'all' : parseInt(this.dataset.days); | |
| renderAnniversaries(); | |
| filterDropdown.classList.add('hidden'); | |
| // Update filter button text | |
| let filterText = ''; | |
| if (filterDays === 'all') filterText = 'All'; | |
| else filterText = `Next ${filterDays} days`; | |
| filterBtn.innerHTML = `<i class="fas fa-filter mr-1"></i> ${filterText}`; | |
| }); | |
| }); | |
| // Check for anniversaries every day | |
| setInterval(checkTodayAnniversaries, 24 * 60 * 60 * 1000); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=mahen23/anniversay" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |