PageSourceSearch

https://cafe333kc.com/js/main.js

js cafe333kc.com collected 2026-10-07 22:07:08 UTC 11,306 bytes, 293 lines download raw bytes

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.