initial site

This commit is contained in:
merrow committed 2026-10-11 22:24:54 +05:00
commit da5b259d06
167 files changed
+34157

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
import { useEffect, useRef, useState } from "react";
import Preloader from "./components/Preloader";
import AgeGate from "./components/AgeGate";
import Header from "./components/Header";
import Hero from "./components/Hero";
import AtmosphereCarousel from "./components/AtmosphereCarousel";
import MenuCarousel from "./components/MenuCarousel";
import VipCalculator from "./components/VipCalculator";
import Features from "./components/Features";
import Contacts from "./components/Contacts";
import Footer from "./components/Footer";
import FlavorsPage from "./pages/FlavorsPage";
import GamesPage from "./pages/GamesPage";
import MixesPage from "./pages/MixesPage";
import AiMixPage from "./pages/AiMixPage";
import BoardGamesPage from "./pages/BoardGamesPage";
import MenuPage from "./pages/MenuPage";
import TrackOrderPage from "./pages/TrackOrderPage";
import { useHashLocation } from "./hooks/useHashLocation";
import { isPrerender } from "./lib/prerender";
type CatalogPage = "flavors" | "games" | "mixes" | "ai-mix" | "board-games" | "menu" | "track" | null;
function getCatalogPage(hash: string): CatalogPage {
if (hash === "#/flavors") return "flavors";
if (hash === "#/games") return "games";
if (hash === "#/mixes") return "mixes";
if (hash === "#/ai-mix" || hash === "#/mix-builder") return "ai-mix";
if (hash.startsWith("#mix=")) return "ai-mix"; // share-ссылка на генерацию
if (hash === "#/board-games") return "board-games";
if (hash === "#/menu" || hash === "#/tea") return "menu";
if (hash.startsWith("#/track")) return "track"; // #/track и #/track?code=XXX
return null;
}
export default function App() {
const [loaded, setLoaded] = useState(false);
const hash = useHashLocation();
const catalogPage = getCatalogPage(hash);
const isCatalog = catalogPage !== null;
const wasCatalog = useRef(isCatalog);
const homeTitle = useRef(document.title);
// в пререндер-снапшоте рендерим финальное видимое состояние страницы
const prerender = isPrerender();
const started = loaded || prerender;
useEffect(() => {
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const t = setTimeout(() => {
document.body.style.overflow = previousOverflow;
setLoaded(true);
}, 2450);
return () => {
clearTimeout(t);
document.body.style.overflow = previousOverflow;
};
}, []);
useEffect(() => {
if (catalogPage === "flavors") {
document.title = "Вкусы табака | НЕОН, Нягань";
} else if (catalogPage === "games") {
document.title = "Игры PS5 | НЕОН, Нягань";
} else if (catalogPage === "mixes") {
document.title = "Миксы кальянов | НЕОН, Нягань";
} else if (catalogPage === "ai-mix") {
document.title = "Автоподбор микса | НЕОН, Нягань";
} else if (catalogPage === "board-games") {
document.title = "Настольные игры | НЕОН, Нягань";
} else if (catalogPage === "menu") {
document.title = "Меню бара: Авторский чай и Пивная карта | НЕОН, Нягань";
} else if (catalogPage === "track") {
document.title = "Статус заказа | НЕОН, Нягань";
} else {
document.title = homeTitle.current;
}
}, [catalogPage]);
useEffect(() => {
if (!loaded) return;
const returningToHome = wasCatalog.current && !isCatalog;
wasCatalog.current = isCatalog;
if (catalogPage === "flavors") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("flavors-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "games") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("games-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "mixes") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("mixes-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "ai-mix") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("ai-mix-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "board-games") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("board-games-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "menu") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("menu-page-title")?.focus({ preventScroll: true });
return;
}
if (catalogPage === "track") {
window.scrollTo({ top: 0, behavior: "instant" });
document.getElementById("track-title")?.focus({ preventScroll: true });
return;
}
// The landing section mounts after the hash changes when leaving the catalog.
const section = document.getElementById(hash.slice(1) || "home");
if (!section) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
section.scrollIntoView({
block: "start",
behavior: returningToHome || reducedMotion ? "instant" : "smooth",
});
if (returningToHome) {
section.tabIndex = -1;
section.focus({ preventScroll: true });
}
}, [hash, catalogPage, isCatalog, loaded]);
return (
<div className="relative min-h-screen w-full max-w-full overflow-x-hidden bg-void font-sans text-mist antialiased">
<div className="noise" aria-hidden />
{/* в пререндер-снапшоте прелоадер и age-gate не попадают в HTML:
бот должен видеть финальную страницу, а не оверлеи */}
{!prerender && <Preloader done={loaded} />}
{!prerender && <AgeGate ready={loaded} />}
<Header started={started} />
<main className="w-full max-w-full overflow-x-hidden">
{catalogPage === "flavors" ? (
<FlavorsPage />
) : catalogPage === "games" ? (
<GamesPage />
) : catalogPage === "mixes" ? (
<MixesPage />
) : catalogPage === "ai-mix" ? (
<AiMixPage />
) : catalogPage === "board-games" ? (
<BoardGamesPage />
) : catalogPage === "menu" ? (
<MenuPage />
) : catalogPage === "track" ? (
<TrackOrderPage />
) : (
<>
<Hero started={started} />
<AtmosphereCarousel />
<MenuCarousel />
<Features />
<VipCalculator />
<Contacts />
</>
)}
</main>
<Footer />
</div>
);
}