Invitation Manager - Day 8
Advanced LevelThis project uses a minimal, dependency-free approach. Everything runs client-side with no build step required.
For small, self-contained apps, vanilla JS is often faster to write, has zero bundle size overhead, and makes the code more educational. Every line is visible and understandable.
The app follows a simple single-file architecture with clear separation of concerns through functions.
// Event object structure
const event = {
id: Date.now(), // Unique timestamp ID
type: 'pedra', // pedra | restaurant | sortie | other
name: 'Party at Momo',
date: '2025-01-15',
time: '21:00',
location: '123 Main St',
notes: 'Bring snacks',
autoReminder: true,
guests: [
{ name: 'Alice', status: 'confirmed' },
{ name: 'Bob', status: 'pending' }
],
createdAt: '2025-01-10T...'
};
Without Redux or React state, we use a simple pattern: global arrays synced with localStorage.
// Initialize state from localStorage (or empty array)
let events = JSON.parse(
localStorage.getItem('invitationEvents') || '[]'
);
// Save function - call after every mutation
function saveEvents() {
localStorage.setItem(
'invitationEvents',
JSON.stringify(events)
);
}
// Usage pattern
events.unshift(newEvent); // Mutate
saveEvents(); // Persist
renderEvents(); // Update UI
This pattern doesn't scale well for complex apps. Consider using a state management library or the Observer pattern for larger projects.
Each render function completely rebuilds its section of the DOM using template literals:
function renderEvents() {
const list = document.getElementById('event-list');
// Handle empty state
if (events.length === 0) {
list.innerHTML = '';
empty.style.display = 'block';
return;
}
// Map data to HTML
list.innerHTML = events.map(event => `
<div class="event-card">
<div class="event-title">${event.name}</div>
...
</div>
`).join('');
}
Using data-* attributes creates a clean, declarative connection between tabs and content.
<!-- Tab buttons with data-tab attribute -->
<div class="tabs">
<button class="tab active" data-tab="events">My Events</button>
<button class="tab" data-tab="create">Create</button>
<button class="tab" data-tab="invites">Invitations</button>
</div>
<!-- Content panels with matching IDs -->
<div id="events-tab" class="tab-content active">...</div>
<div id="create-tab" class="tab-content">...</div>
<div id="invites-tab" class="tab-content">...</div>
function initTabs() {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
// Remove active from all tabs and contents
document.querySelectorAll('.tab')
.forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content')
.forEach(c => c.classList.remove('active'));
// Activate clicked tab and matching content
tab.classList.add('active');
const contentId = tab.dataset.tab + '-tab';
document.getElementById(contentId).classList.add('active');
});
});
}
The naming convention data-tab="create" → id="create-tab" makes the connection predictable and maintainable.
Guest status follows a cyclic state machine: clicking cycles through states in order.
function cycleGuestStatus(eventId, guestIndex) {
const event = events.find(e => e.id === eventId);
if (!event) return;
// Define state order
const statuses = ['pending', 'confirmed', 'declined'];
// Get current index and move to next
const current = event.guests[guestIndex].status;
const currentIndex = statuses.indexOf(current);
const nextIndex = (currentIndex + 1) % statuses.length;
// Update and persist
event.guests[guestIndex].status = statuses[nextIndex];
saveEvents();
renderEvents();
}
The modulo operator % statuses.length creates the cyclic behavior - when we reach the end, we wrap back to index 0.
A reusable modal pattern with multiple close triggers: button, overlay click, and Escape key.
.modal {
position: fixed;
inset: 0; /* Shorthand for top/right/bottom/left */
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
}
.modal.active {
display: flex; /* Show with flexbox centering */
}
// 1. Close button
function closeModal() {
document.getElementById('event-modal')
.classList.remove('active');
}
// 2. Click outside (on overlay)
modal.addEventListener('click', (e) => {
// Only close if clicking the overlay itself
if (e.target.id === 'event-modal') closeModal();
});
// 3. Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
Always provide multiple ways to close a modal. Some users expect clicking outside to close it, others reach for Escape. Support both.
A lightweight toast system using CSS transforms for smooth enter/exit animations.
.toast {
position: fixed;
bottom: 20px;
left: 50%;
/* Start hidden below viewport */
transform: translateX(-50%) translateY(100px);
opacity: 0;
transition: all 0.3s;
}
.toast.show {
/* Slide up into view */
transform: translateX(-50%) translateY(0);
opacity: 1;
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
// Set content and type
toast.textContent = message;
toast.className = 'toast ' + type; // 'toast success' or 'toast error'
// Trigger animation
toast.classList.add('show');
// Auto-hide after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
The frosted glass effect uses backdrop-filter combined with semi-transparent backgrounds.
.main-card {
/* Semi-transparent background */
background: rgba(255, 255, 255, 0.05);
/* Subtle border for depth */
border: 1px solid rgba(255, 255, 255, 0.1);
/* The blur effect - THIS is glassmorphism */
backdrop-filter: blur(10px);
/* Rounded corners enhance the effect */
border-radius: 16px;
}
backdrop-filter is supported in all modern browsers, but may have performance implications on older devices. The app remains functional without it - it just loses the blur effect.
.btn-primary {
background: linear-gradient(135deg, #00d4ff, #0099cc);
border: none;
color: #fff;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 212, 255, 0.4);
}
The hover lift effect (translateY(-2px)) combined with an expanding shadow creates a satisfying "press" feeling.
New users see sample data to understand the app. A localStorage flag prevents regeneration.
function generateDemoData() {
// Check if already generated
if (localStorage.getItem('invitationDemoGenerated')) {
return; // Exit early - not first visit
}
// Seed demo invites
receivedInvites = [{
id: 1,
type: 'restaurant',
name: 'Dinner at Luigi\'s',
date: getFutureDate(3), // 3 days from now
time: '20:00',
host: 'Marco'
}];
// Mark as generated
localStorage.setItem('invitationDemoGenerated', 'true');
}
// Helper: get date N days in future
function getFutureDate(daysAhead) {
const date = new Date();
date.setDate(date.getDate() + daysAhead);
return date.toISOString().split('T')[0]; // 'YYYY-MM-DD'
}
Demo data helps users immediately understand what the app does. Without it, they'd see empty states and might not explore further.
When you have clickable elements inside other clickable elements, you need to manage event bubbling.
<!-- Card is clickable (opens modal) -->
<div class="event-card" onclick="openModal(id)">
<h3>Event Title</h3>
<!-- Buttons inside - also clickable! -->
<div class="actions">
<button onclick="sendReminder(id)">Remind</button>
<button onclick="deleteEvent(id)">Delete</button>
</div>
</div>
Without handling, clicking "Delete" would also trigger the card's click handler, opening the modal.
<!-- Stop propagation on the actions container -->
<div class="event-actions" onclick="event.stopPropagation()">
<button onclick="sendReminder(${event.id})">Remind</button>
<button onclick="deleteEvent(${event.id})">Delete</button>
</div>
event.stopPropagation() prevents the click from bubbling up to the parent card.
You could also check e.target in the card's click handler and ignore clicks on buttons. Both approaches work - choose based on your HTML structure.