From 7afe3bd45b332786be87fb4dc4f5fc6c1d31b292 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 7 Nov 2025 18:51:49 -0800 Subject: [PATCH] Stub out menu bar --- .../app/(modern)/scroll/BottomMenuBar.tsx | 340 ++++++++++++++++++ .../app/(modern)/scroll/Events.ts | 4 + .../app/(modern)/scroll/SkinPage.tsx | 10 +- .../app/(modern)/scroll/SkinScroller.tsx | 66 ++-- .../app/(modern)/scroll/layout.tsx | 46 +++ .../app/(modern)/scroll/page.tsx | 13 +- 6 files changed, 444 insertions(+), 35 deletions(-) create mode 100644 packages/skin-database/app/(modern)/scroll/BottomMenuBar.tsx create mode 100644 packages/skin-database/app/(modern)/scroll/layout.tsx diff --git a/packages/skin-database/app/(modern)/scroll/BottomMenuBar.tsx b/packages/skin-database/app/(modern)/scroll/BottomMenuBar.tsx new file mode 100644 index 00000000..db0f508c --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/BottomMenuBar.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { + Smartphone, + Search, + Info, + Grid3x3, + Menu, + MessageSquare, + Upload, + Github, +} from "lucide-react"; +import Link from "next/link"; +import { useState, useEffect, useRef } from "react"; +import { usePathname } from "next/navigation"; +import { logUserEvent } from "./Events"; + +type Props = { + sessionId?: string; +}; + +export default function BottomMenuBar({ sessionId }: Props) { + const [isHamburgerOpen, setIsHamburgerOpen] = useState(false); + const menuRef = useRef(null); + const pathname = usePathname(); + + const handleMenuClick = (menuItem: string) => { + if (sessionId) { + logUserEvent(sessionId, { + type: "menu_click", + menuItem, + }); + } + }; + + const toggleHamburger = () => { + setIsHamburgerOpen(!isHamburgerOpen); + handleMenuClick("hamburger"); + }; + + // Close hamburger menu when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + isHamburgerOpen + ) { + setIsHamburgerOpen(false); + } + } + + if (isHamburgerOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isHamburgerOpen]); + + return ( + <> + {/* Hamburger Menu Overlay */} + {isHamburgerOpen && ( +
+ } + label="About" + onClick={() => { + handleMenuClick("about"); + setIsHamburgerOpen(false); + }} + /> + } + label="Upload" + onClick={() => { + handleMenuClick("upload"); + setIsHamburgerOpen(false); + }} + />{" "} + } + label="Feedback" + onClick={() => { + handleMenuClick("feedback"); + setIsHamburgerOpen(false); + }} + external + /> + } + label="GitHub" + onClick={() => { + handleMenuClick("feedback"); + setIsHamburgerOpen(false); + }} + external + /> +
+ )} + + {/* Bottom Menu Bar */} +
+ } + label="Feed" + onClick={() => handleMenuClick("feed")} + isActive={pathname === "/scroll"} + /> + } + label="Grid" + onClick={() => handleMenuClick("grid")} + isActive={pathname === "/"} + /> + } + label="Search" + onClick={() => handleMenuClick("search")} + isActive={false} + /> + } + label="Menu" + onClick={toggleHamburger} + isButton + isActive={false} + /> +
+ + ); +} + +type MenuButtonProps = { + href?: string; + icon: React.ReactNode; + label: string; + onClick: () => void; + isButton?: boolean; + isActive?: boolean; +}; + +function MenuButton({ + href, + icon, + label, + onClick, + isButton = false, + isActive = false, +}: MenuButtonProps) { + const touchTargetSize = "3.0rem"; + + const containerStyle = { + display: "flex", + flexDirection: "column" as const, + alignItems: "center", + justifyContent: "center", + gap: "0.25rem", + color: "#ccc", + textDecoration: "none", + cursor: "pointer", + transition: "color 0.2s ease", + background: "none", + border: "none", + padding: 0, + position: "relative" as const, + width: touchTargetSize, + minWidth: touchTargetSize, + }; + + const handleMouseEnter = (e: React.MouseEvent) => { + e.currentTarget.style.color = "#fff"; + }; + + const handleMouseLeave = (e: React.MouseEvent) => { + e.currentTarget.style.color = "#ccc"; + }; + + const content = ( + <> + {/* Active indicator line */} + {isActive && ( +
+ )} +
+ {icon} +
+ + {label} + + + ); + + if (isButton) { + return ( + + ); + } + + return ( + + {content} + + ); +} + +type HamburgerMenuItemProps = { + href: string; + icon: React.ReactNode; + label: string; + onClick: () => void; + external?: boolean; +}; + +function HamburgerMenuItem({ + href, + icon, + label, + onClick, + external = false, +}: HamburgerMenuItemProps) { + const content = ( +
{ + e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.05)"; + e.currentTarget.style.color = "#fff"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = "transparent"; + e.currentTarget.style.color = "#ccc"; + }} + > + {icon} + + {label} + +
+ ); + + if (external) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +} diff --git a/packages/skin-database/app/(modern)/scroll/Events.ts b/packages/skin-database/app/(modern)/scroll/Events.ts index b43833f5..c2238578 100644 --- a/packages/skin-database/app/(modern)/scroll/Events.ts +++ b/packages/skin-database/app/(modern)/scroll/Events.ts @@ -93,4 +93,8 @@ export type UserEvent = type: "share_failure"; skinMd5: string; errorMessage: string; + } + | { + type: "menu_click"; + menuItem: string; }; diff --git a/packages/skin-database/app/(modern)/scroll/SkinPage.tsx b/packages/skin-database/app/(modern)/scroll/SkinPage.tsx index 18614bfe..f444eb24 100644 --- a/packages/skin-database/app/(modern)/scroll/SkinPage.tsx +++ b/packages/skin-database/app/(modern)/scroll/SkinPage.tsx @@ -19,20 +19,22 @@ export default function SkinPage({ skin, index, sessionId }: Props) { style={{ display: "flex", flexDirection: "column", + justifyContent: "center", width: "100%", height: "100vh", scrollSnapAlign: "start", scrollSnapStop: "always", position: "relative", + paddingTop: "2rem", // Space for top shadow + paddingBottom: "5rem", // Space for bottom menu bar + boxSizing: "border-box", }} > -
+
{skin.fileName}

- {skins.map((skin, i) => { - return ( - - ); - })} -

+ <> +
+ {skins.map((skin, i) => { + return ( + + ); + })} +
+ {/* Top shadow overlay */} +
+ ); } diff --git a/packages/skin-database/app/(modern)/scroll/layout.tsx b/packages/skin-database/app/(modern)/scroll/layout.tsx new file mode 100644 index 00000000..bb5a596a --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/layout.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { ReactNode, createContext, useContext } from "react"; +import BottomMenuBar from "./BottomMenuBar"; + +type SessionContextType = { + sessionId: string | null; +}; + +const SessionContext = createContext({ sessionId: null }); + +export function useSession() { + return useContext(SessionContext); +} + +type LayoutWrapperProps = { + children: ReactNode; + sessionId: string; +}; + +export function LayoutWrapper({ children, sessionId }: LayoutWrapperProps) { + return ( + +
+ {children} + +
+
+ ); +} + +type Props = { + children: ReactNode; +}; + +export default function ScrollLayout({ children }: Props) { + return <>{children}; +} diff --git a/packages/skin-database/app/(modern)/scroll/page.tsx b/packages/skin-database/app/(modern)/scroll/page.tsx index 25576a49..76ba1609 100644 --- a/packages/skin-database/app/(modern)/scroll/page.tsx +++ b/packages/skin-database/app/(modern)/scroll/page.tsx @@ -4,6 +4,7 @@ import "./scroll.css"; import SkinScroller, { ClientSkin } from "./SkinScroller"; import { getScrollPage } from "../../../data/skins"; import SkinModel from "../../../data/SkinModel"; +import { LayoutWrapper } from "./layout"; // Ensure each page load gets a new session export const dynamic = "force-dynamic"; @@ -51,10 +52,12 @@ export default async function ScrollPage() { const initialSkins = await getClientSkins(sessionId); return ( - + + + ); }