import { type AnyRoute, useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useTranslation } from "react-i18next"; import { AnimatePresence } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; import GlassSurface from "@components/surface/GlassSurface"; import classes from "./Navigation.module.css"; import ShopRoute from "@/routes/shop"; import ApiaryRoute from "@/routes/apiary"; import GameRoute from "@/routes/game"; import CashdeskRoute from "@/routes/cashdesk"; import RouletteRoute from "@/routes/roulette"; import TasksRoute from "@/routes/tasks"; import EarningsRoute from "@/routes/earnings"; import shopIcon from "./assets/shop.svg"; import apiaryIcon from "./assets/apiary.svg"; import gameIcon from "./assets/game.svg"; import cashdeskIcon from "./assets/cashdesk.svg"; import menuIcon from "./assets/menu.svg"; import rouletteIcon from "./assets/roulette.svg"; import tasksIcon from "./assets/tasks.svg"; import earningsIcon from "./assets/earnings.svg"; import tg, { useTelegramViewportValue } from "@/tg"; import { usePlaySound } from "@/audio"; const ANIMATION_DURATION = 0.2; const SPRING_ANIMATION = { type: "spring", stiffness: 400, damping: 20, duration: ANIMATION_DURATION, } as const; const BAR_HEIGHT = 64; const ACTIVE_BAR_HEIGHT = 74; const NAV_ITEMS = [ { key: "nav.shop", route: ShopRoute, icon: shopIcon }, { key: "nav.apiary", route: ApiaryRoute, icon: apiaryIcon }, { key: "nav.game", route: GameRoute, icon: gameIcon }, { key: "nav.cashdesk", route: CashdeskRoute, icon: cashdeskIcon }, { key: "nav.menu", isMenu: true, icon: menuIcon }, ]; const HORIZONTAL_ENTRANCE_DELAYS = [0.2, 0.1, 0, 0.1, 0.2]; const MENU_ITEMS = [ { key: "nav.roulette", route: RouletteRoute, icon: rouletteIcon, delay: 0.1 }, { key: "nav.tasks", route: TasksRoute, icon: tasksIcon, delay: 0.05 }, { key: "nav.earnings", route: EarningsRoute, icon: earningsIcon, delay: 0 }, ]; // 200px is larger than any OS chrome resize but smaller than the smallest software keyboard const KEYBOARD_THRESHOLD = 200; function useKeyboardVisible(): boolean { const [keyboardVisible, setKeyboardVisible] = useState(false); const baselineHeight = useRef(0); useEffect(() => { const vv = window.visualViewport; if (!vv) return; baselineHeight.current = vv.height; const handleResize = () => { const newHeight = vv.height; if (baselineHeight.current - newHeight > KEYBOARD_THRESHOLD) { setKeyboardVisible(true); } else { setKeyboardVisible(false); baselineHeight.current = newHeight; } }; vv.addEventListener("resize", handleResize); return () => vv.removeEventListener("resize", handleResize); }, []); return keyboardVisible; } type BarProps = { labelKey: string; icon: string; active: boolean; entranceDelay: number; onClick: () => void; }; function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) { const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const safeAreaBottom = useTelegramViewportValue("safeAreaInsetBottom"); const safeContentBottom = useTelegramViewportValue("contentSafeAreaInsetBottom"); const height = (active ? ACTIVE_BAR_HEIGHT : BAR_HEIGHT) + safeAreaBottom + safeContentBottom; const offscreenOffset = height * 0.3; return ( variant === "visible" && setIsReady(true)} initial="hidden" animate={isReady ? "ready" : "visible"} whileTap={{ scale: 0.95 }} onClick={onClick} >
{t(labelKey)}
); } type MenuBarProps = { labelKey: string; icon: string; active: boolean; delay: number; onClick: () => void; }; const MENU_BAR_WIDTH = 94; const ACTIVE_MENU_BAR_WIDTH = 104; function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) { const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const safeAreaRight = useTelegramViewportValue("safeAreaInsetRight"); const safeContentRight = useTelegramViewportValue("contentSafeAreaInsetRight"); const width = (active ? ACTIVE_MENU_BAR_WIDTH : MENU_BAR_WIDTH) + safeAreaRight + safeContentRight; const offscreenOffset = width * 0.3; return ( variant === "visible" && setIsReady(true)} initial="hidden" animate={isReady ? "ready" : "visible"} exit="exit" whileTap={{ scale: 0.95 }} onClick={onClick} >
{t(labelKey)}
); } export default function Navigation() { const play = usePlaySound(); const matchRoute = useMatchRoute(); const navigate = useNavigate(); const [menuOpen, setMenuOpen] = useState(0); const navRef = useRef(null); const keyboardVisible = useKeyboardVisible(); const handleOutsideClick = useCallback((e: MouseEvent) => { if (navRef.current && !navRef.current.contains(e.target as Node)) { setMenuOpen(0); } }, []); useEffect(() => { if (menuOpen) { document.addEventListener("click", handleOutsideClick); return () => document.removeEventListener("click", handleOutsideClick); } }, [menuOpen, handleOutsideClick]); const navigateRoute = async (route: AnyRoute, wait = false) => { play("click"); tg.hapticFeedback.click(); const redirection = navigate({ to: route.to }); if (wait) { await new Promise((resolve) => setTimeout(resolve, 1000 * ANIMATION_DURATION - 100)); await redirection; } setMenuOpen(0); }; return (
{menuOpen && MENU_ITEMS.map((item) => ( navigateRoute(item.route, true)} /> ))}
); }