mirror of
https://github.com/Xevion/Pac-Man.git
synced 2026-01-31 00:24:59 -06:00
feat(web): redesign navigation with centered logo and smooth page transitions
- Replace sidebar nav with centered header featuring PAC-MAN title - Add glimmer animation effect for non-active logo state - Implement 200ms fade transitions between pages - Add custom scrollbar styling with OverlayScrollbars - Switch to Russo One font for title, Outfit for body text
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource/nunito": "^5.2.7",
|
||||
"@fontsource/pixelify-sans": "^5.2.7",
|
||||
"@fontsource/russo-one": "^5.2.7",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"react": "^19.1.1",
|
||||
@@ -157,6 +158,8 @@
|
||||
|
||||
"@fontsource/pixelify-sans": ["@fontsource/pixelify-sans@5.2.7", "", {}, "sha512-F/UuV2M9poAj/BJ5/6u95mOy6ptp8/+dpfnxh4TFzKeAB+vdanAjQ8fLJcS0q+WHYgesRRxPwNFr2Pqm18CVGg=="],
|
||||
|
||||
"@fontsource/russo-one": ["@fontsource/russo-one@5.2.7", "", {}, "sha512-/VyiuTzAFUjxwpJdtwR9ByC9BggUFWS5Hw/JT8ziEtNAd++BNfh/fDrKCC6oayFG69gfP0zhS5+0UenYcOK3UQ=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import "./tailwind.css";
|
||||
import "@fontsource/pixelify-sans";
|
||||
import "@fontsource/nunito/800.css";
|
||||
import "@fontsource/nunito";
|
||||
import "@fontsource/outfit/400.css";
|
||||
import "@fontsource/outfit/500.css";
|
||||
import "@fontsource/russo-one";
|
||||
import "overlayscrollbars/overlayscrollbars.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePageContext } from "vike-react/usePageContext";
|
||||
import { IconBrandGithub, IconDownload, IconDeviceGamepad3, IconTrophy } from "@tabler/icons-react";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
|
||||
const links = [
|
||||
{
|
||||
@@ -30,13 +33,25 @@ const links = [
|
||||
},
|
||||
];
|
||||
|
||||
export function Link({ href, label }: { href: string; label: string }) {
|
||||
export function Link({ href, label, icon }: { href: string; label: string; icon?: React.ReactNode }) {
|
||||
const pageContext = usePageContext();
|
||||
const { urlPathname } = pageContext;
|
||||
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
|
||||
return (
|
||||
<a href={href} className={isActive ? "text-yellow-400" : "text-gray-400"}>
|
||||
{label}
|
||||
<a
|
||||
href={href}
|
||||
className={`
|
||||
flex items-center gap-1.5
|
||||
tracking-wide
|
||||
transition-colors duration-200
|
||||
${isActive
|
||||
? "text-white"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -46,9 +61,11 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
||||
const toggle = () => setOpened((v) => !v);
|
||||
const close = () => setOpened(false);
|
||||
|
||||
const mainLinks = links
|
||||
.filter((link) => link.href.startsWith("/"))
|
||||
.map((link) => <Link href={link.href} key={link.label} label={link.label} />);
|
||||
const pageContext = usePageContext();
|
||||
const { urlPathname } = pageContext;
|
||||
const isIndexPage = urlPathname === "/";
|
||||
const isLeaderboardPage = urlPathname.startsWith("/leaderboard");
|
||||
const isDownloadPage = urlPathname.startsWith("/download");
|
||||
|
||||
const sourceLinks = links
|
||||
.filter((link) => !link.href.startsWith("/"))
|
||||
@@ -58,31 +75,68 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
||||
title={link.label}
|
||||
key={link.label}
|
||||
target="_blank"
|
||||
className="transition-[drop-shadow] duration-1000 hover:drop-shadow-sm drop-shadow-yellow-400"
|
||||
className="text-gray-500 hover:text-gray-300 transition-colors duration-200"
|
||||
>
|
||||
{link.icon}
|
||||
</a>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="bg-black text-yellow-400 min-h-screen flex flex-col">
|
||||
<header className="sticky top-0 z-20 h-[60px] border-b border-yellow-400/25 bg-black">
|
||||
<div className="h-full px-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-black text-yellow-400 h-screen flex flex-col overflow-hidden">
|
||||
<header className="shrink-0 h-[60px] border-b border-yellow-400/25 bg-black z-20">
|
||||
<div className="h-full px-4 flex items-center justify-center">
|
||||
<button
|
||||
aria-label="Open navigation"
|
||||
onClick={toggle}
|
||||
className="sm:hidden inline-flex items-center justify-center w-9 h-9 rounded border border-yellow-400/30 text-yellow-400"
|
||||
className="sm:hidden absolute left-4 inline-flex items-center justify-center w-9 h-9 rounded border border-yellow-400/30 text-yellow-400"
|
||||
>
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
<div className="w-5 h-0.5 bg-yellow-400" />
|
||||
</button>
|
||||
<nav className="hidden sm:flex gap-4 items-center">{mainLinks}</nav>
|
||||
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/leaderboard" label="Leaderboard" icon={<IconTrophy size={18} />} />
|
||||
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
if (isIndexPage) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
className={`text-3xl tracking-[0.3em] title-hover ${
|
||||
isIndexPage
|
||||
? 'text-yellow-400'
|
||||
: 'glimmer-text'
|
||||
}`}
|
||||
style={{ fontFamily: 'Russo One' }}
|
||||
>
|
||||
PAC-MAN
|
||||
</h1>
|
||||
</a>
|
||||
|
||||
<Link href="/download" label="Download" icon={<IconDownload size={18} />} />
|
||||
</div>
|
||||
<div className="hidden sm:flex gap-4 items-center">{sourceLinks}</div>
|
||||
|
||||
<div className="absolute right-4 hidden sm:flex gap-4 items-center">{sourceLinks}</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
options={{
|
||||
scrollbars: {
|
||||
theme: 'os-theme-light',
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 1300,
|
||||
},
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<main>{children}</main>
|
||||
</OverlayScrollbarsComponent>
|
||||
|
||||
{opened && (
|
||||
<div className="fixed inset-0 z-30">
|
||||
@@ -100,7 +154,7 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{links.map((link) => (
|
||||
<Link href={link.href} key={link.label} label={link.label} />
|
||||
<Link href={link.href} key={link.label} label={link.label} icon={link.icon} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
/* Page transitions */
|
||||
body {
|
||||
--transition-duration: 200ms;
|
||||
}
|
||||
|
||||
body main {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity var(--transition-duration) ease-out,
|
||||
transform var(--transition-duration) ease-out;
|
||||
}
|
||||
|
||||
body.page-is-transitioning main {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
:root {
|
||||
font-family:
|
||||
"Nunito",
|
||||
"Outfit",
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
@@ -24,6 +41,57 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 800;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.os-scrollbar-handle {
|
||||
background: rgb(250 204 21 / 0.25) !important;
|
||||
}
|
||||
|
||||
.os-scrollbar-handle:hover {
|
||||
background: rgb(250 204 21 / 0.4) !important;
|
||||
}
|
||||
|
||||
.os-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glimmer {
|
||||
0% {
|
||||
background-position: -200% center;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer utilities {
|
||||
.title-hover {
|
||||
transition: transform 0.2s ease-out, filter 0.2s ease-out;
|
||||
}
|
||||
|
||||
.title-hover:hover {
|
||||
transform: scale(1.03);
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.glimmer-text {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(156 163 175) 0%,
|
||||
rgb(156 163 175) 35%,
|
||||
rgb(250 204 21) 45%,
|
||||
rgb(250 204 21) 55%,
|
||||
rgb(156 163 175) 65%,
|
||||
rgb(156 163 175) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: glimmer 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
"description": "A web frontend for the Pac-Man game, including leaderboards and OAuth.",
|
||||
"dependencies": {
|
||||
"@fontsource/nunito": "^5.2.7",
|
||||
"@fontsource/outfit": "^5.2.8",
|
||||
"@fontsource/pixelify-sans": "^5.2.7",
|
||||
"@fontsource/russo-one": "^5.2.7",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"overlayscrollbars": "^2.13.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"vike": "^0.4.240",
|
||||
|
||||
@@ -9,6 +9,17 @@ export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
||||
|
||||
// Restart the game loop when returning to the game page
|
||||
if (pageContext.urlPathname === "/") {
|
||||
// Defer game restart to allow fade-in animation to complete first
|
||||
// This prevents the heavy WebGL initialization from blocking the UI
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
restartGame();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function restartGame() {
|
||||
const win = getPacmanWindow();
|
||||
const module = win.Module;
|
||||
|
||||
@@ -31,5 +42,4 @@ export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
||||
console.error("Failed to restart game:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||
import { getPacmanWindow } from "@/lib/pacman";
|
||||
|
||||
const TRANSITION_DURATION = 200;
|
||||
|
||||
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||
console.log("Page transition start");
|
||||
document.querySelector("body")?.classList.add("page-is-transitioning");
|
||||
@@ -11,4 +13,7 @@ export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||
console.log("Stopping game loop for page transition");
|
||||
win.Module._stop_game();
|
||||
}
|
||||
|
||||
// Wait for fade-out animation to complete before page content changes
|
||||
await new Promise((resolve) => setTimeout(resolve, TRANSITION_DURATION));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl py-8 px-4">
|
||||
<div className="space-y-6">
|
||||
<div className="border border-yellow-400/20 rounded-md bg-transparent p-6 shadow-[0_4px_20px_rgba(250,204,21,0.08)]">
|
||||
<h2 className="text-2xl font-bold mb-4">Download Pac-Man</h2>
|
||||
<p className="text-gray-300 mb-4">
|
||||
Download instructions and releases will be available here soon.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import "../../layouts/tailwind.css";
|
||||
|
||||
export default function GameLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-black text-yellow-400 min-h-screen flex flex-col">
|
||||
<main className="flex-1">{children}</main>
|
||||
<div className="bg-black text-yellow-400 h-full flex flex-col overflow-hidden">
|
||||
<main className="flex-1 overflow-hidden">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,12 +71,11 @@ export default function Page() {
|
||||
}, [gameReady, gameStarted, handleInteraction]);
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex justify-center h-[calc(100vh-120px)]">
|
||||
<div className="flex justify-center items-center h-full pt-4">
|
||||
<div
|
||||
className="relative block border-1 border-yellow-400/50 aspect-[5/6] h-[min(calc(100vh-120px),_calc(95vw_*_6/5))] w-auto"
|
||||
className="relative block aspect-[5/6]"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 0 12px rgba(250, 204, 21, 0.35), 0 0 2px rgba(255, 255, 255, 0.25)",
|
||||
height: "min(calc(100vh - 96px), calc((100vw - 32px) * 6 / 5))",
|
||||
}}
|
||||
onClick={handleInteraction}
|
||||
>
|
||||
|
||||
Generated
+3779
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user