spb/note-spboucher-ai
Public
1"use client";2import Link from "next/link";3import { usePathname, useRouter } from "next/navigation";4import { useEffect, useState } from "react";5import { Home, Search, Sparkles, Plus, Menu as MenuIcon, X } from "lucide-react";6import { useUI } from "@/lib/client/store";7import { cn } from "@/lib/utils";8import { api } from "@/lib/client/api";9import { Sidebar } from "./Sidebar";10import { useQuery } from "@tanstack/react-query";11import type { TreeNode } from "./types";12export function MobileNav() {13 const pathname = usePathname(); const ui = useUI(); const r = useRouter(); const [drawer, setDrawer] = useState(false);14 const { data: tree = [] } = useQuery({ queryKey: ["tree"], queryFn: () => api<TreeNode[]>("/api/tree") });15 useEffect(() => {16 let sx = 0, sy = 0, edge = false;17 const ts = (e: TouchEvent) => { const t = e.touches[0]; sx = t.clientX; sy = t.clientY; edge = sx < 24; };18 const te = (e: TouchEvent) => { if (!edge) return; const t = e.changedTouches[0]; if (t.clientX - sx > 70 && Math.abs(t.clientY - sy) < 60) setDrawer(true); };19 window.addEventListener("touchstart", ts, { passive: true }); window.addEventListener("touchend", te, { passive: true });20 return () => { window.removeEventListener("touchstart", ts); window.removeEventListener("touchend", te); };21 }, []);22 if (ui.focusMode) return null;23 const I = ({ active, onClick, href, icon, label }: { active?: boolean; onClick?: () => void; href?: string; icon: React.ReactNode; label: string }) => { const cls = cn("flex flex-1 flex-col items-center justify-center gap-0.5 py-1.5 text-[10px] text-fg-muted [&_svg]:size-5", active && "text-accent"); return href ? <Link href={href} className={cls}>{icon}{label}</Link> : <button className={cls} onClick={onClick}>{icon}{label}</button>; };24 return (25 <>26 <nav className="fixed inset-x-0 bottom-0 z-30 flex border-t border-line bg-bg-elev/95 backdrop-blur md:hidden safe-bottom">27 <I href="/" active={pathname === "/"} icon={<Home />} label="Home" />28 <I onClick={() => ui.set({ paletteOpen: true })} icon={<Search />} label="Search" />29 <button onClick={async () => { const p = await api("/api/pages", { method: "POST", json: { title: "" } }); r.push(`/p/${p.id}`); }} className="-mt-4 grid size-12 place-items-center rounded-full bg-fg text-bg shadow-pop"><Plus className="size-6" /></button>30 <I onClick={() => ui.toggleAI()} active={ui.aiOpen} icon={<Sparkles />} label="AI" />31 <I onClick={() => setDrawer(true)} icon={<MenuIcon />} label="Pages" />32 </nav>33 {drawer && (34 <div className="fixed inset-0 z-40 md:hidden" onClick={() => setDrawer(false)}>35 <div className="absolute inset-0 bg-black/40" />36 <div className="absolute inset-y-0 left-0 w-[85vw] max-w-sm bg-bg-elev shadow-pop fade-up" onClick={(e) => e.stopPropagation()}>37 <button className="absolute right-2 top-3 z-10 grid size-8 place-items-center rounded-md text-fg-muted" onClick={() => setDrawer(false)}><X className="size-4" /></button>38 <Sidebar tree={tree} user={{ id: "", username: "", displayName: "" }} workspace={{ name: "Note SPBoucher AI" }} onNavigate={() => setDrawer(false)} />39 </div>40 </div>41 )}42 </>43 );44}45