TECH DETAILS

Invitation Manager - Day 8

Advanced Level

Table of Contents

01 / Tech Stack

This project uses a minimal, dependency-free approach. Everything runs client-side with no build step required.

📜
Vanilla JS
No framework
💾
LocalStorage
Client persistence
🎨
CSS3
Modern features
🔤
Google Fonts
JetBrains Mono
Why No Framework?

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.

02 / Architecture Overview

The app follows a simple single-file architecture with clear separation of concerns through functions.

User Action → Event Handler → Update State → Save to Storage → Re-render UI

Key Data Structures

// 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...' };

03 / State Management Pattern

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
Caveat

This pattern doesn't scale well for complex apps. Consider using a state management library or the Observer pattern for larger projects.

The Render Loop

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(''); }

04 / Tab System with Data Attributes

Using data-* attributes creates a clean, declarative connection between tabs and content.

HTML Structure

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

JavaScript Logic

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'); }); }); }
Pro Tip

The naming convention data-tab="create" → id="create-tab" makes the connection predictable and maintainable.

05 / Finite State Machine for Status

Guest status follows a cyclic state machine: clicking cycles through states in order.

pending → confirmed → declined → (back to pending)
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.

07 / Toast Notification System

A lightweight toast system using CSS transforms for smooth enter/exit animations.

CSS Animation

.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; }

JavaScript Controller

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); }

08 / CSS Glassmorphism

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; }
Browser Support

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.

Gradient Buttons

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

09 / First-Visit Demo Data

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' }
Onboarding UX

Demo data helps users immediately understand what the app does. Without it, they'd see empty states and might not explore further.

10 / Event Delegation & stopPropagation

When you have clickable elements inside other clickable elements, you need to manage event bubbling.

The Problem

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

The Solution

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

Alternative Approach

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.