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> |