sushi_atelier / script.js
pd3rvr
Initial commit (history reset)
360707c
(() => {
const THEME_KEY = "sushi-atelier-theme";
const applyChrome = (theme) => {
const meta = document.getElementById("meta-theme-color");
if (meta) {
meta.setAttribute("content", theme === "light" ? "#f6f3ee" : "#070707");
}
};
const syncToggle = () => {
const th = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
const toggle = document.querySelector("[data-theme-toggle]");
if (!toggle) return;
const goLight = th === "dark";
toggle.setAttribute("aria-label", goLight ? "Switch to light theme" : "Switch to dark theme");
toggle.setAttribute("title", goLight ? "Light theme" : "Dark theme");
};
const setTheme = (theme) => {
const th = theme === "light" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", th);
try {
localStorage.setItem(THEME_KEY, th);
} catch (_) {}
applyChrome(th);
syncToggle();
};
let stored = null;
try {
stored = localStorage.getItem(THEME_KEY);
} catch (_) {}
if (stored === "light" || stored === "dark") {
setTheme(stored);
} else {
applyChrome(document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark");
syncToggle();
}
document.querySelector("[data-theme-toggle]")?.addEventListener("click", () => {
const cur = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
setTheme(cur === "dark" ? "light" : "dark");
});
const navToggle = document.querySelector("[data-nav-toggle]");
const navPanel = document.querySelector("[data-nav-panel]");
if (navToggle && navPanel) {
navToggle.addEventListener("click", () => {
const open = navPanel.classList.toggle("is-open");
navToggle.setAttribute("aria-expanded", open ? "true" : "false");
});
navPanel.querySelectorAll("a").forEach((el) => {
el.addEventListener("click", () => {
navPanel.classList.remove("is-open");
navToggle.setAttribute("aria-expanded", "false");
});
});
}
document.querySelectorAll("[data-tab]").forEach((btn) => {
btn.addEventListener("click", () => {
const tab = btn.dataset.tab;
const root = btn.closest("[data-tabs]");
if (!root || !tab) return;
root.querySelectorAll("[data-tab]").forEach((b) =>
b.classList.toggle("is-active", b.dataset.tab === tab)
);
root.querySelectorAll("[data-panel]").forEach((panel) => {
panel.hidden = panel.dataset.panel !== tab;
});
});
});
const y = document.querySelector("#year");
if (y) y.textContent = String(new Date().getFullYear());
const openNowEls = Array.from(document.querySelectorAll("[data-open-now]"));
if (openNowEls.length) {
const toMinutes = (hh, mm) => hh * 60 + mm;
const DAY_ABBR = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const fmtTime = (mins) => {
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
};
const daySlots = (day) => {
// 0 Sun, 1 Mon, ... 6 Sat
if (day === 0 || day === 1) return [];
if (day >= 2 && day <= 4) return [toMinutes(12, 0), toMinutes(17, 30)];
if (day === 5) return [toMinutes(12, 0), toMinutes(17, 50)];
return [toMinutes(12, 0)]; // Saturday
};
const isOpenNow = (d) => {
const day = d.getDay(); // 0 Sun ... 6 Sat
const mins = toMinutes(d.getHours(), d.getMinutes());
// Hours from section:
// Sun/Mon closed
// Tue-Thu: 12:00-15:00 and 17:30-22:00
// Fri: 12:00-15:00 and 17:50-22:00
// Sat: 12:00-22:00
if (day === 0 || day === 1) return false;
if (day >= 2 && day <= 4) {
return (
(mins >= toMinutes(12, 0) && mins < toMinutes(15, 0)) ||
(mins >= toMinutes(17, 30) && mins < toMinutes(22, 0))
);
}
if (day === 5) {
return (
(mins >= toMinutes(12, 0) && mins < toMinutes(15, 0)) ||
(mins >= toMinutes(17, 50) && mins < toMinutes(22, 0))
);
}
// Saturday
return mins >= toMinutes(12, 0) && mins < toMinutes(22, 0);
};
const nextOpenText = (d) => {
const today = d.getDay();
const nowMins = toMinutes(d.getHours(), d.getMinutes());
for (let offset = 0; offset < 7; offset += 1) {
const day = (today + offset) % 7;
const starts = daySlots(day);
if (!starts.length) continue;
const candidate = offset === 0 ? starts.find((m) => m > nowMins) : starts[0];
if (candidate == null) continue;
if (offset === 0) return fmtTime(candidate);
return `${DAY_ABBR[day]} ${fmtTime(candidate)}`;
}
return "";
};
const syncOpenNow = () => {
const open = isOpenNow(new Date());
const opensAt = nextOpenText(new Date());
openNowEls.forEach((el) => {
el.textContent = open ? "We are open" : `Opens at ${opensAt}`;
el.classList.toggle("is-open", open);
el.classList.toggle("is-closed", !open);
});
};
syncOpenNow();
const openNowTimer = window.setInterval(syncOpenNow, 30000);
window.addEventListener(
"pagehide",
() => {
window.clearInterval(openNowTimer);
},
{ once: true }
);
}
const hoursQuote = document.querySelector("[data-hours-quote]");
if (hoursQuote) {
const quotes = [
"“Simple and to the point, this no-frills favourite in Fitzrovia has a modern take on sushi.”",
"“Small but lovely … Sushi fans congregate at the brightly lit counter.”",
"“Great value sushi where you least expect it.”",
];
let quoteIndex = 0;
let cycleTimer = 0;
const QUOTE_MS = 5200;
if (quotes.length > 1) {
const playQuote = (idx) => {
hoursQuote.classList.remove("is-typing");
hoursQuote.textContent = quotes[idx];
void hoursQuote.offsetWidth;
hoursQuote.classList.add("is-typing");
};
playQuote(quoteIndex);
cycleTimer = window.setInterval(() => {
if (document.hidden) return;
quoteIndex = (quoteIndex + 1) % quotes.length;
playQuote(quoteIndex);
}, QUOTE_MS);
window.addEventListener(
"pagehide",
() => {
window.clearInterval(cycleTimer);
},
{ once: true }
);
}
}
const heroRoot = document.querySelector("[data-hero-slideshow]");
const heroSlides = heroRoot ? Array.from(heroRoot.querySelectorAll(".hero-slide")) : [];
if (heroRoot && heroSlides.length > 0) {
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let cur = heroSlides.findIndex((el) => el.classList.contains("is-active"));
if (cur < 0) cur = 0;
const syncHeroMedia = () => {
heroSlides.forEach((el, i) => {
const active = i === cur;
el.classList.toggle("is-active", active);
if (el.tagName !== "VIDEO") return;
const vid = /** @type {HTMLVideoElement} */ (el);
vid.muted = true;
vid.playsInline = true;
vid.defaultPlaybackRate = 1;
vid.playbackRate = 1;
if (active) {
const playActive = () => {
const p = vid.play();
if (p && typeof p.catch === "function") p.catch(() => {});
};
if (vid.readyState >= 1) {
try {
vid.currentTime = 0;
} catch (_) {}
playActive();
} else {
vid.addEventListener(
"loadedmetadata",
() => {
try {
vid.currentTime = 0;
} catch (_) {}
playActive();
},
{ once: true }
);
vid.load();
}
} else {
vid.pause();
}
});
};
syncHeroMedia();
if (!reduceMotion && heroSlides.length > 1) {
const ms = 8000;
const tick = () => {
cur = (cur + 1) % heroSlides.length;
syncHeroMedia();
};
const timer = window.setInterval(tick, ms);
window.addEventListener(
"pagehide",
() => {
window.clearInterval(timer);
},
{ once: true }
);
}
}
const spotlightRoot = document.querySelector("[data-spotlight-carousel]");
if (spotlightRoot) {
const track = spotlightRoot.querySelector("[data-spotlight-viewport]");
const slides = Array.from(spotlightRoot.querySelectorAll("[data-spotlight-slide]"));
const prevBtn = spotlightRoot.querySelector("[data-spotlight-prev]");
const nextBtn = spotlightRoot.querySelector("[data-spotlight-next]");
const n = slides.length;
/** Spotlight slideshow cadence. */
const AUTOPLAY_MS = 5200;
if (track && n > 0) {
let current = 0;
let autoplayTimer = 0;
let exitCleanupTimer = 0;
const sync = () => {
slides.forEach((sl, j) => {
const isActive = j === current;
sl.classList.toggle("is-active", isActive);
if (isActive) sl.classList.remove("is-exiting");
const vid = sl.querySelector("video");
if (!vid) return;
vid.muted = true;
vid.playsInline = true;
if (isActive) {
const play = () => {
const p = vid.play();
if (p && typeof p.catch === "function") p.catch(() => {});
};
if (vid.readyState >= 1) {
play();
} else {
vid.addEventListener("loadedmetadata", play, { once: true });
vid.load();
}
} else {
vid.pause();
try {
vid.currentTime = 0;
} catch (_) {}
}
});
};
const goTo = (index) => {
const prev = current;
current = ((index % n) + n) % n;
if (prev !== current) {
const prevSlide = slides[prev];
if (prevSlide) {
prevSlide.classList.add("is-exiting");
window.clearTimeout(exitCleanupTimer);
exitCleanupTimer = window.setTimeout(() => {
prevSlide.classList.remove("is-exiting");
}, 1900);
}
}
sync();
};
const goDelta = (delta) => {
goTo(current + delta);
};
const scheduleAutoplay = () => {
window.clearTimeout(autoplayTimer);
if (n <= 1) return;
const tick = () => {
if (document.hidden) {
autoplayTimer = window.setTimeout(tick, AUTOPLAY_MS);
return;
}
goDelta(1);
autoplayTimer = window.setTimeout(tick, AUTOPLAY_MS);
};
autoplayTimer = window.setTimeout(tick, AUTOPLAY_MS);
};
prevBtn?.addEventListener("click", () => {
goDelta(-1);
});
nextBtn?.addEventListener("click", () => {
goDelta(1);
});
track.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
e.preventDefault();
goDelta(-1);
} else if (e.key === "ArrowRight") {
e.preventDefault();
goDelta(1);
} else if (e.key === "Home") {
e.preventDefault();
goTo(0);
} else if (e.key === "End") {
e.preventDefault();
goTo(n - 1);
}
});
document.addEventListener("visibilitychange", () => {
if (!document.hidden) scheduleAutoplay();
});
let resizeTimer = 0;
window.addEventListener("resize", () => {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(() => {
sync();
}, 120);
});
window.addEventListener(
"pagehide",
() => {
window.clearTimeout(autoplayTimer);
window.clearTimeout(exitCleanupTimer);
},
{ once: true }
);
sync();
scheduleAutoplay();
}
}
const aboutGlide = document.querySelector("[data-about-glide]");
if (aboutGlide) {
const aboutSlidesEl = aboutGlide.querySelector(".glide__slides");
const aboutSlides = Array.from(aboutGlide.querySelectorAll(".glide__slide"));
if (aboutSlidesEl && aboutSlides.length > 0) {
let current = 0;
let startX = null;
let startY = null;
let wasSwipe = false;
const ABOUT_MS = 3000;
const captionWords = [
"Atelier Moment",
"Chef's Detail",
"Counter Light",
"Omakase Mood",
"Evening Plate",
"Sushi Study",
];
const makeMeta = (imgSrc, idx) => {
const title = `${captionWords[idx % captionWords.length]}`;
const desc = "Curated sushi frame from the Atelier gallery.";
return { title, desc };
};
const lightbox = document.createElement("div");
lightbox.className = "about-lightbox";
lightbox.setAttribute("aria-hidden", "true");
lightbox.innerHTML = `
<button type="button" class="about-lightbox__close" aria-label="Close image">×</button>
<figure class="about-lightbox__figure">
<img class="about-lightbox__img" alt="">
<figcaption class="about-lightbox__meta">
<strong class="about-lightbox__title"></strong>
<span class="about-lightbox__desc"></span>
</figcaption>
</figure>
`;
document.body.appendChild(lightbox);
const lightboxImg = lightbox.querySelector(".about-lightbox__img");
const lightboxTitle = lightbox.querySelector(".about-lightbox__title");
const lightboxDesc = lightbox.querySelector(".about-lightbox__desc");
const openLightbox = (slide) => {
const img = slide.querySelector("img");
if (!img || !lightboxImg || !lightboxTitle || !lightboxDesc) return;
lightboxImg.src = img.currentSrc || img.src;
lightboxImg.alt = img.alt || "";
lightboxTitle.textContent = slide.dataset.aboutTitle || "";
lightboxDesc.textContent = slide.dataset.aboutDesc || "";
lightbox.classList.add("is-open");
lightbox.setAttribute("aria-hidden", "false");
};
const closeLightbox = () => {
lightbox.classList.remove("is-open");
lightbox.setAttribute("aria-hidden", "true");
};
lightbox.addEventListener("click", (e) => {
if (e.target === lightbox || e.target.closest(".about-lightbox__close")) {
closeLightbox();
}
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && lightbox.classList.contains("is-open")) closeLightbox();
});
aboutSlides.forEach((slide, idx) => {
const img = slide.querySelector("img");
if (!img) return;
const meta = makeMeta(img.getAttribute("src") || "", idx);
slide.dataset.aboutTitle = meta.title;
slide.dataset.aboutDesc = meta.desc;
slide.setAttribute("role", "button");
slide.setAttribute("tabindex", "0");
slide.setAttribute("aria-label", `${meta.title}. Open expanded image.`);
const cap = document.createElement("span");
cap.className = "about-glide__caption";
cap.textContent = meta.title;
slide.appendChild(cap);
slide.addEventListener("click", () => {
if (wasSwipe) {
wasSwipe = false;
return;
}
openLightbox(slide);
});
slide.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openLightbox(slide);
}
});
});
const gap = () => {
const st = window.getComputedStyle(aboutSlidesEl);
return parseFloat(st.columnGap || st.gap || "0") || 0;
};
const visible = () => {
if (window.matchMedia("(max-width: 640px)").matches) return 1;
if (window.matchMedia("(max-width: 900px)").matches) return 2;
return 3;
};
const maxIndex = () => Math.max(0, aboutSlides.length - visible());
const step = () => {
const v = visible();
const g = gap();
return v <= 1 ? aboutGlide.clientWidth : (aboutGlide.clientWidth - g * (v - 1)) / v + g;
};
const syncAbout = (animate = true) => {
const clamped = Math.max(0, Math.min(maxIndex(), current));
if (clamped !== current) current = clamped;
aboutSlidesEl.style.transition = animate
? "transform 0.55s cubic-bezier(0.22, 1, 0.36, 1)"
: "none";
aboutSlidesEl.style.transform = `translate3d(-${current * step()}px, 0, 0)`;
};
const goAbout = (index, animate = true) => {
const max = maxIndex();
if (max <= 0) {
current = 0;
} else if (index > max) {
current = 0;
} else if (index < 0) {
current = max;
} else {
current = index;
}
syncAbout(animate);
};
let aboutTimer = 0;
const startAboutLoop = () => {
window.clearInterval(aboutTimer);
if (aboutSlides.length <= visible()) return;
aboutTimer = window.setInterval(() => {
if (document.hidden) return;
goAbout(current + 1, true);
}, ABOUT_MS);
};
const onTouchStart = (e) => {
const touch = e.touches && e.touches[0];
if (!touch) return;
startX = touch.clientX;
startY = touch.clientY;
wasSwipe = false;
};
const onTouchEnd = (e) => {
if (startX === null || startY === null) return;
const touch = e.changedTouches && e.changedTouches[0];
if (!touch) return;
const dx = touch.clientX - startX;
const dy = touch.clientY - startY;
startX = null;
startY = null;
if (Math.abs(dx) < 44 || Math.abs(dx) <= Math.abs(dy)) return;
wasSwipe = true;
goAbout(current + (dx < 0 ? 1 : -1), true);
};
aboutGlide.addEventListener("touchstart", onTouchStart, { passive: true });
aboutGlide.addEventListener("touchend", onTouchEnd, { passive: true });
aboutGlide.addEventListener("pointerdown", (e) => {
startX = e.clientX;
startY = e.clientY;
wasSwipe = false;
});
aboutGlide.addEventListener("pointerup", (e) => {
if (startX === null || startY === null) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
startX = null;
startY = null;
if (Math.abs(dx) < 44 || Math.abs(dx) <= Math.abs(dy)) return;
wasSwipe = true;
goAbout(current + (dx < 0 ? 1 : -1), true);
});
aboutGlide.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
e.preventDefault();
goAbout(current - 1, true);
} else if (e.key === "ArrowRight") {
e.preventDefault();
goAbout(current + 1, true);
}
});
syncAbout(false);
window.addEventListener("resize", () => syncAbout(false));
startAboutLoop();
window.addEventListener(
"pagehide",
() => {
window.clearInterval(aboutTimer);
},
{ once: true }
);
}
}
const pressSection = document.querySelector("section#press.press-section");
const pressCarousel = document.querySelector(".press-grid--single");
if (pressCarousel) {
const cards = Array.from(pressCarousel.querySelectorAll(".press-card"));
if (cards.length > 0) {
let current = 0;
const sync = () => {
cards.forEach((card, idx) => {
card.classList.toggle("is-active", idx === current);
card.setAttribute("aria-hidden", idx === current ? "false" : "true");
});
};
sync();
if (cards.length > 1) {
const PRESS_MS = 4800;
const timer = window.setInterval(() => {
if (document.hidden) return;
current = (current + 1) % cards.length;
sync();
}, PRESS_MS);
window.addEventListener(
"pagehide",
() => {
window.clearInterval(timer);
},
{ once: true }
);
}
}
}
if (pressSection && "IntersectionObserver" in window) {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
pressSection.classList.add("press-section--visible");
} else {
const pressIo = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add("press-section--visible");
observer.unobserve(entry.target);
});
},
{ threshold: 0.12, rootMargin: "0px 0px -5% 0px" }
);
pressIo.observe(pressSection);
}
} else if (pressSection) {
pressSection.classList.add("press-section--visible");
}
})();