| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| document.addEventListener('DOMContentLoaded', async () => { |
| triggerHeroEntrance(); |
| await loadCafeData(); |
| setupNavigation(); |
| setupNewsletter(); |
| initScrollReveal(); |
| initMenuCardReveal(); |
| setupSmoothScroll(); |
| }); |
|
|
| |
| |
| |
| function triggerHeroEntrance() { |
| const hero = document.querySelector('.hero'); |
| if (!hero) return; |
|
|
| |
| requestAnimationFrame(() => { |
| requestAnimationFrame(() => { |
| hero.classList.add('hero-entered'); |
| }); |
| }); |
| } |
|
|
| |
| let menuItems = []; |
| let menuCategories = []; |
|
|
| async function loadCafeData() { |
| try { |
| const res = await fetch('/api/menu'); |
| const data = await res.json(); |
| menuItems = data.items; |
| menuCategories = data.categories; |
| populateHero(data.info); |
| buildCategories(data.categories); |
| buildMenu(data.items, data.categories); |
| buildVisit(data.info); |
| |
| initMenuCardReveal(); |
| } catch (err) { |
| console.error('Error loading cafe data:', err); |
| } |
| } |
|
|
| function populateHero(info) { |
| if (info.name) { |
| document.getElementById('cafeName').textContent = info.name; |
| document.title = `${info.name} — Specialty Coffee Roasters`; |
| } |
| if (info.address) { |
| const el = document.getElementById('cafeAddressSmall'); |
| if (el) el.textContent = info.address; |
| } |
| if (info.phone) { |
| const el = document.getElementById('cafePhoneSmall'); |
| if (el) el.textContent = info.phone; |
| } |
| } |
|
|
| |
| |
| let activeCategory = 'all'; |
|
|
| function buildCategories(categories) { |
| const container = document.getElementById('categoryFilters'); |
| if (!container) return; |
|
|
| container.innerHTML = categories |
| .map(cat => ` |
| <button class="cat-btn ${cat.id === 'all' ? 'active' : ''}" |
| data-category="${cat.id}"> |
| ${cat.name} |
| </button> |
| `).join(''); |
|
|
| container.addEventListener('click', (e) => { |
| const btn = e.target.closest('.cat-btn'); |
| if (!btn) return; |
| document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| activeCategory = btn.dataset.category; |
| rebuildGrid(); |
| }); |
| } |
|
|
| function rebuildGrid() { |
| const grid = document.getElementById('menuGrid'); |
| if (!grid) return; |
|
|
| const filtered = activeCategory === 'all' |
| ? menuItems |
| : menuItems.filter(item => item.category === activeCategory); |
|
|
| if (filtered.length === 0) { |
| grid.innerHTML = '<div class="menu-empty">No items in this category yet.</div>'; |
| return; |
| } |
|
|
| buildMenu(filtered, menuCategories); |
| initMenuCardReveal(); |
| } |
|
|
| function filterCards() { |
| |
| rebuildGrid(); |
| } |
|
|
| |
| function buildMenu(items, categories) { |
| const grid = document.getElementById('menuGrid'); |
| if (!grid) return; |
|
|
| const catMeta = {}; |
| categories.forEach(c => { catMeta[c.id] = { name: c.name, icon: c.icon }; }); |
|
|
| grid.innerHTML = items.map((item, i) => { |
| const cat = catMeta[item.category] || { name: item.category, icon: '/img/icons/default.svg' }; |
| const tagClass = !item.available ? ' soldout' : ''; |
| const tagText = !item.available ? 'Sold Out' : cat.name; |
|
|
| |
| const thumbSvg = svgPlaceholder(item.category); |
| const thumbImg = (item.image && item.image.startsWith('http')) |
| ? `<img src="${escapeHtml(item.image)}" alt="${escapeHtml(item.name)}" loading="lazy" onerror="this.style.display='none'">` |
| : ''; |
|
|
| return ` |
| <div class="menu-card" |
| data-category="${item.category}" |
| data-card-index="${i}" |
| style="--card-index: ${i}"> |
| <div class="menu-card-thumb"> |
| <div class="thumb-icon">${thumbImg}${thumbSvg}</div> |
| </div> |
| <div class="menu-card-header"> |
| <h3 class="menu-card-name">${escapeHtml(item.name)}</h3> |
| <span class="menu-card-price">₹${item.price.toFixed(0)}</span> |
| </div> |
| <span class="menu-card-label">Tasting</span> |
| <p class="menu-card-description">${escapeHtml(item.description)}</p> |
| <div class="menu-card-footer"> |
| <span class="menu-card-cat-icon">${svgInline(cat.icon)}</span> |
| <span class="menu-card-tag${tagClass}">${tagText}</span> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
|
|
| |
| function svgInline(path) { |
| const icons = { |
| '/img/icons/coffee.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h14v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"/><path d="M18 9h2a2 2 0 012 2v1a2 2 0 01-2 2h-2"/><ellipse cx="8" cy="14" rx="2" ry="1.5"/><path d="M8 1v2" opacity="0.4"/><path d="M6 0v2" opacity="0.3"/><path d="M10 0v2" opacity="0.3"/></svg>', |
| '/img/icons/tea.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h10a3 3 0 013 3v3a3 3 0 01-3 3H5a3 3 0 01-3-3v-3a3 3 0 013-3z"/><path d="M2 11l-2 3h2l1-3"/><path d="M7 8V6a1 1 0 011-1h2a1 1 0 011 1v2"/><circle cx="10" cy="3.5" r="1.2"/><path d="M8 1v2" opacity="0.4"/><path d="M10 0v2" opacity="0.3"/><path d="M12 1v2" opacity="0.3"/></svg>', |
| '/img/icons/pastry.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 16C4 16 6 10 12 8s10 0 10 0c-4 2-6 8-6 8"/><path d="M5 14c1-3 4-6 7-7"/><path d="M19 14c-1-3-4-6-7-7"/><path d="M8 10h8" opacity="0.3"/></svg>', |
| '/img/icons/food.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="18" rx="10" ry="3"/><rect x="8" y="8" width="8" height="7" rx="1"/><line x1="10" y1="9.5" x2="14" y2="9.5" opacity="0.4"/><line x1="10" y1="11.5" x2="14" y2="11.5" opacity="0.3"/><ellipse cx="12" cy="16" rx="3" ry="2" fill="currentColor" opacity="0.15"/></svg>', |
| '/img/icons/default.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="12" rx="8" ry="4.5"/><path d="M12 7.5v9"/><path d="M7 10c0 3 2 5 5 5s5-2 5-5" opacity="0.4"/></svg>' |
| }; |
| return icons[path] || icons['/img/icons/default.svg']; |
| } |
|
|
| function svgPlaceholder(category) { |
| const icons = { |
| 'coffee': '/img/icons/coffee.svg', |
| 'tea': '/img/icons/tea.svg', |
| 'pastries': '/img/icons/pastry.svg', |
| 'food': '/img/icons/food.svg' |
| }; |
| return svgInline(icons[category] || '/img/icons/default.svg'); |
| } |
|
|
| |
| let menuCardObserver = null; |
|
|
| function initMenuCardReveal() { |
| |
| if (menuCardObserver) menuCardObserver.disconnect(); |
|
|
| const cards = document.querySelectorAll('.menu-card:not(.revealed)'); |
| if (cards.length === 0) return; |
|
|
| menuCardObserver = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting) { |
| entry.target.classList.add('revealed'); |
| menuCardObserver.unobserve(entry.target); |
| } |
| }); |
| }, { |
| threshold: 0.05, |
| rootMargin: '0px 0px -30px 0px' |
| }); |
|
|
| cards.forEach(card => menuCardObserver.observe(card)); |
| } |
|
|
| |
| function initScrollReveal() { |
| const observer = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting) { |
| entry.target.classList.add('visible'); |
| observer.unobserve(entry.target); |
| } |
| }); |
| }, { |
| threshold: 0.08, |
| rootMargin: '0px 0px -40px 0px' |
| }); |
|
|
| document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); |
| } |
|
|
| |
| function buildVisit(info) { |
| const hoursList = document.getElementById('hoursList'); |
| if (hoursList && info.hours) { |
| hoursList.innerHTML = info.hours |
| .map(h => `<div class="hours-item"><span class="days">${h.days}</span><span>${h.time}</span></div>`) |
| .join(''); |
| } |
|
|
| const addr = document.getElementById('cafeAddress'); |
| if (addr && info.address) addr.textContent = info.address; |
|
|
| const phone = document.getElementById('cafePhone'); |
| if (phone && info.phone) phone.textContent = info.phone; |
|
|
| const email = document.getElementById('cafeEmailText'); |
| if (email && info.email) email.textContent = info.email; |
| } |
|
|
| |
| function setupNavigation() { |
| const nav = document.getElementById('nav'); |
| const toggle = document.getElementById('navToggle'); |
| if (!nav) return; |
|
|
| let ticking = false; |
| window.addEventListener('scroll', () => { |
| if (!ticking) { |
| requestAnimationFrame(() => { |
| nav.classList.toggle('scrolled', window.scrollY > 40); |
| ticking = false; |
| }); |
| ticking = true; |
| } |
| }); |
|
|
| if (toggle) { |
| toggle.addEventListener('click', () => nav.classList.toggle('open')); |
| } |
|
|
| document.querySelectorAll('.nav-links a, .nav-logo').forEach(link => { |
| link.addEventListener('click', () => nav.classList.remove('open')); |
| }); |
| } |
|
|
| |
| function setupNewsletter() { |
| const form = document.getElementById('newsletterForm'); |
| if (!form) return; |
| form.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| const btn = form.querySelector('button'); |
| const orig = btn.textContent; |
| btn.textContent = 'Subscribed ✓'; |
| btn.style.background = '#3C6E55'; |
| form.querySelector('input').value = ''; |
| setTimeout(() => { |
| btn.textContent = orig; |
| btn.style.background = ''; |
| }, 2800); |
| }); |
| } |
|
|
| |
| function setupSmoothScroll() { |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { |
| anchor.addEventListener('click', function(e) { |
| const target = document.querySelector(this.getAttribute('href')); |
| if (target) { |
| e.preventDefault(); |
| window.scrollTo({ |
| top: target.offsetTop - 72, |
| behavior: 'smooth' |
| }); |
| } |
| }); |
| }); |
| } |
|
|
| |
| function escapeHtml(str) { |
| const div = document.createElement('div'); |
| div.textContent = str; |
| return div.innerHTML; |
| } |
|
|