1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 CAFÃ 333 â Main JavaScript 3 Smooth, jazz-inspired interactions. 4 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 5 6document.addEventListener('DOMContentLoaded', () => { 7 8 // âââ NAVBAR SCROLL EFFECT ââââââââââââââââââââââââââââââââââââ 9 const navbar = document.getElementById('navbar'); 10 let lastScroll = 0; 11 12 window.addEventListener('scroll', () => { 13 const currentScroll = window.scrollY; 14 navbar.classList.toggle('scrolled', currentScroll > 80); 15 lastScroll = currentScroll; 16 }, { passive: true }); 17 18 // âââ MOBILE NAV TOGGLE ââââââââââââââââââââââââââââââââââââââ 19 const navToggle = document.querySelector('.nav-toggle'); 20 const navLinks = document.querySelector('.nav-links'); 21 22 navToggle.addEventListener('click', () => { 23 const isOpen = navToggle.getAttribute('aria-expanded') === 'true'; 24 navToggle.setAttribute('aria-expanded', !isOpen); 25 navLinks.classList.toggle('open'); 26 document.body.style.overflow = isOpen ? '' : 'hidden'; 27 }); 28 29 // Close mobile nav on link click 30 navLinks.querySelectorAll('a').forEach(link => { 31 link.addEventListener('click', () => { 32 navToggle.setAttribute('aria-expanded', 'false'); 33 navLinks.classList.remove('open'); 34 document.body.style.overflow = ''; 35 }); 36 }); 37 38 // âââ SMOOTH SCROLL ââââââââââââââââââââââââââââââââââââââââââ 39 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 40 anchor.addEventListener('click', (e) => { 41 e.preventDefault(); 42 const target = document.querySelector(anchor.getAttribute('href')); 43 if (target) { 44 const offset = navbar.offsetHeight + 20; 45 const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; 46 window.scrollTo({ top: targetPosition, behavior: 'smooth' }); 47 } 48 }); 49 }); 50 51 // âââ SCROLL REVEAL ANIMATIONS âââââââââââââââââââââââââââââââ 52 const revealElements = () => { 53 const reveals = document.querySelectorAll( 54 '.section-header, .about-image, .about-text, .exp-card, .menu-category, ' + 55 '.perf-card, .gallery-item, .signup-content, .contact-info, .contact-map' 56 ); 57 58 reveals.forEach(el => { 59 if (!el.classList.contains('reveal')) { 60 el.classList.add('reveal'); 61 } 62 }); 63 64 const observer = new IntersectionObserver((entries) => { 65 entries.forEach(entry => { 66 if (entry.isIntersecting) { 67 entry.target.classList.add('visible'); 68 observer.unobserve(entry.target); 69 } 70 }); 71 }, { 72 threshold: 0.1, 73 rootMargin: '0px 0px -50px 0px' 74 }); 75 76 document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); 77 }; 78 79 revealElements(); 80 81 // âââ STAGGERED REVEAL FOR GRIDS âââââââââââââââââââââââââââââ 82 const staggerObserver = new IntersectionObserver((entries) => { 83 entries.forEach(entry => { 84 if (entry.isIntersecting) { 85 const cards = entry.target.querySelectorAll('.exp-card, .perf-card, .menu-category, .gallery-item'); 86 cards.forEach((card, i) => { 87 card.style.transitionDelay = `${i * 0.1}s`; 88 }); 89 staggerObserver.unobserve(entry.target); 90 } 91 }); 92 }, { threshold: 0.1 }); 93 94 document.querySelectorAll('.experience-cards, .perf-grid, .menu-grid, .gallery-grid').forEach(grid => { 95 staggerObserver.observe(grid); 96 }); 97 98 // âââ ACTIVE NAV LINK HIGHLIGHTING ââââââââââââââââââââââââââ 99 const sections = document.querySelectorAll('.section[id]'); 100 const navItems = document.querySelectorAll('.nav-links a:not(.nav-cta)'); 101 102 const highlightNav = () => { 103 const scrollPos = window.scrollY + 200; 104 105 sections.forEach(section => { 106 const top = section.offsetTop; 107 const height = section.offsetHeight; 108 const id = section.getAttribute('id'); 109 110 if (scrollPos >= top && scrollPos < top + height) {
111 navItems.forEach(link => { 112 link.classList.remove('active'); 113 if (link.getAttribute('href') === `#${id}`) { 114 link.classList.add('active'); 115 } 116 }); 117 } 118 }); 119 }; 120 121 window.addEventListener('scroll', highlightNav, { passive: true }); 122 123 // âââ FLOATING MUSIC NOTES ââââââââââââââââââââââââââââââââââ 124 const notesContainer = document.getElementById('floatingNotes'); 125 const noteSymbols = ['âª', 'â«', 'â¬', 'â©', 'â', 'â¯']; 126 127 const createNote = () => { 128 const note = document.createElement('span'); 129 note.className = 'note'; 130 note.textContent = noteSymbols[Math.floor(Math.random() * noteSymbols.length)]; 131 note.style.left = `${Math.random() * 100}%`; 132 note.style.animationDuration = `${12 + Math.random() * 10}s`; 133 note.style.animationDelay = `${Math.random() * 5}s`; 134 note.style.fontSize = `${0.8 + Math.random() * 1}rem`; 135 notesContainer.appendChild(note); 136 137 setTimeout(() => note.remove(), 25000); 138 }; 139 140 // Create initial batch of notes 141 for (let i = 0; i < 5; i++) { 142 setTimeout(createNote, i * 2000); 143 } 144 145 // Continue creating notes periodically 146 setInterval(createNote, 4000); 147 148 // âââ HERO PARTICLE EFFECT ââââââââââââââââââââââââââââââââââ 149 const heroParticles = document.getElementById('heroParticles'); 150 if (heroParticles) { 151 const createParticle = () => { 152 const particle = document.createElement('div'); 153 particle.style.cssText = ` 154 position: absolute; 155 width: ${1 + Math.random() * 2}px; 156 height: ${1 + Math.random() * 2}px; 157 background: rgba(74, 158, 255, ${0.08 + Math.random() * 0.15}); 158 border-radius: 50%; 159 left: ${Math.random() * 100}%; 160 top: ${Math.random() * 100}%; 161 animation: particleDrift ${8 + Math.random() * 12}s linear infinite; 162 `; 163 heroParticles.appendChild(particle); 164 165 setTimeout(() => particle.remove(), 20000); 166 }; 167 168 // Add particle animation 169 const particleStyle = document.createElement('style'); 170 particleStyle.textContent = ` 171 @keyframes particleDrift { 172 0% { opacity: 0; transform: translate(0, 0); } 173 20% { opacity: 1; } 174 80% { opacity: 1; } 175 100% { opacity: 0; transform: translate(${Math.random() > 0.5 ? '' : '-'}${20 + Math.random() * 40}px, -${50 + Math.random() * 100}px); } 176 } 177 `; 178 document.head.appendChild(particleStyle); 179 180 for (let i = 0; i < 20; i++) { 181 setTimeout(createParticle, i * 500); 182 } 183 setInterval(createParticle, 2000); 184 } // end heroParticles guard 185 186 // âââ EMAIL SIGNUP FORM âââââââââââââââââââââââââââââââââââââ 187 const signupForm = document.getElementById('signupForm'); 188 const APPS_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycbzEuvesE-mjkHJYZ8PYXuqKqCxmyfCoJYD55NuGAp7NATg2W4eJnxGbY5S86Azl6LOhiQ/exec'; 189 190 if (signupForm) signupForm.addEventListener('submit', (e) => { 191 e.preventDefault(); 192 193 const formData = new FormData(signupForm); 194 const data = Object.fromEntries(formData.entries()); 195 const submitBtn = signupForm.querySelector('.btn-submit'); 196 submitBtn.classList.add('submitted'); 197 198 fetch(APPS_SCRIPT_URL, { 199 method: 'POST', 200 mode: 'no-cors', 201 body: new URLSearchParams(data) 202 }).catch(() => { 203 // Fallback: store locally if network fails 204 const signups = JSON.parse(localStorage.getItem('cafe333_signups') || '[]'); 205 signups.push({ ...data, timestamp: new Date().toISOString() }); 206 localStorage.setItem('cafe333_signups', JSON.stringify(signups)); 207 }); 208 209 setTimeout(() => { 210 signupForm.reset(); 211 submitBtn.classList.remove('submitted'); 212 }, 3000); 213 }); 214 215 // âââ IMAGE PLACEHOLDER GENERATION ââââââââââââââââââââââââââ 216 // Generate beautiful placeholder images using canvas for sections 217 // that don't have real photos yet 218 const generatePlaceholder = (width, height, label, theme = 'dark') => { 219 const canvas = document.createElement('canvas'); 220 canvas.width = width; 221 canvas.height = height; 222 const ctx = canvas.getContext('2d'); 223
224 // Background gradient 225 const gradient = ctx.createLinearGradient(0, 0, width, height); 226 if (theme === 'warm') { 227 gradient.addColorStop(0, '#1A1518'); 228 gradient.addColorStop(1, '#2A1F22'); 229 } else if (theme === 'blue') { 230 gradient.addColorStop(0, '#111118'); 231 gradient.addColorStop(1, '#1E2030'); 232 } else { 233 gradient.addColorStop(0, '#1A1A1A'); 234 gradient.addColorStop(1, '#2A2A35'); 235 } 236 ctx.fillStyle = gradient; 237 ctx.fillRect(0, 0, width, height); 238 239 // Subtle gold accent circles 240 for (let i = 0; i < 3; i++) { 241 ctx.beginPath(); 242 ctx.arc( 243 width * (0.3 + Math.random() * 0.4), 244 height * (0.3 + Math.random() * 0.4), 245 Math.min(width, height) * (0.1 + Math.random() * 0.2), 246 0, Math.PI * 2 247 ); 248 ctx.fillStyle = `rgba(74, 158, 255, ${0.03 + Math.random() * 0.04})`; 249 ctx.fill(); 250 } 251 252 // Label text 253 ctx.fillStyle = 'rgba(74, 158, 255, 0.3)'; 254 ctx.font = `${Math.min(width, height) * 0.06}px serif`; 255 ctx.textAlign = 'center'; 256 ctx.textBaseline = 'middle'; 257 ctx.fillText(label, width / 2, height / 2); 258 259 return canvas.toDataURL('image/jpeg', 0.8); 260 }; 261 262 // Apply placeholders to images that fail to load 263 document.querySelectorAll('img').forEach(img => { 264 img.addEventListener('error', () => { 265 const alt = img.alt || 'Café 333'; 266 const rect = img.parentElement.getBoundingClientRect(); 267 const w = Math.max(rect.width || 800, 400); 268 const h = Math.max(rect.height || 500, 300); 269 270 const themes = ['dark', 'warm', 'blue']; 271 const theme = themes[Math.floor(Math.random() * themes.length)]; 272 img.src = generatePlaceholder(w, h, alt, theme); 273 }); 274 275 // Trigger error check for images that may have already failed 276 if (img.complete && img.naturalWidth === 0) { 277 img.dispatchEvent(new Event('error')); 278 } 279 }); 280 281 // âââ PARALLAX SUBTLE EFFECT ââââââââââââââââââââââââââââââââ 282 const hero = document.querySelector('.hero-content'); 283 if (hero) { 284 window.addEventListener('scroll', () => { 285 const scrolled = window.scrollY; 286 if (scrolled < window.innerHeight) { 287 hero.style.transform = `translateY(${scrolled * 0.15}px)`; 288 hero.style.opacity = 1 - (scrolled / window.innerHeight) * 0.5; 289 } 290 }, { passive: true }); 291 } 292 293});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.