mirror of
https://github.com/Xevion/Pac-Man.git
synced 2026-01-31 02:25:04 -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": {
|
"dependencies": {
|
||||||
"@fontsource/nunito": "^5.2.7",
|
"@fontsource/nunito": "^5.2.7",
|
||||||
"@fontsource/pixelify-sans": "^5.2.7",
|
"@fontsource/pixelify-sans": "^5.2.7",
|
||||||
|
"@fontsource/russo-one": "^5.2.7",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"react": "^19.1.1",
|
"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/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/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=="],
|
"@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 "./tailwind.css";
|
||||||
import "@fontsource/pixelify-sans";
|
import "@fontsource/pixelify-sans";
|
||||||
import "@fontsource/nunito/800.css";
|
import "@fontsource/outfit/400.css";
|
||||||
import "@fontsource/nunito";
|
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 { usePageContext } from "vike-react/usePageContext";
|
||||||
import { IconBrandGithub, IconDownload, IconDeviceGamepad3, IconTrophy } from "@tabler/icons-react";
|
import { IconBrandGithub, IconDownload, IconDeviceGamepad3, IconTrophy } from "@tabler/icons-react";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
|
|
||||||
const links = [
|
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 pageContext = usePageContext();
|
||||||
const { urlPathname } = pageContext;
|
const { urlPathname } = pageContext;
|
||||||
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
|
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
|
||||||
return (
|
return (
|
||||||
<a href={href} className={isActive ? "text-yellow-400" : "text-gray-400"}>
|
<a
|
||||||
{label}
|
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>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,9 +61,11 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
|||||||
const toggle = () => setOpened((v) => !v);
|
const toggle = () => setOpened((v) => !v);
|
||||||
const close = () => setOpened(false);
|
const close = () => setOpened(false);
|
||||||
|
|
||||||
const mainLinks = links
|
const pageContext = usePageContext();
|
||||||
.filter((link) => link.href.startsWith("/"))
|
const { urlPathname } = pageContext;
|
||||||
.map((link) => <Link href={link.href} key={link.label} label={link.label} />);
|
const isIndexPage = urlPathname === "/";
|
||||||
|
const isLeaderboardPage = urlPathname.startsWith("/leaderboard");
|
||||||
|
const isDownloadPage = urlPathname.startsWith("/download");
|
||||||
|
|
||||||
const sourceLinks = links
|
const sourceLinks = links
|
||||||
.filter((link) => !link.href.startsWith("/"))
|
.filter((link) => !link.href.startsWith("/"))
|
||||||
@@ -58,31 +75,68 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
|||||||
title={link.label}
|
title={link.label}
|
||||||
key={link.label}
|
key={link.label}
|
||||||
target="_blank"
|
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}
|
{link.icon}
|
||||||
</a>
|
</a>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black text-yellow-400 min-h-screen flex flex-col">
|
<div className="bg-black text-yellow-400 h-screen flex flex-col overflow-hidden">
|
||||||
<header className="sticky top-0 z-20 h-[60px] border-b border-yellow-400/25 bg-black">
|
<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-between">
|
<div className="h-full px-4 flex items-center justify-center">
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
<button
|
||||||
aria-label="Open navigation"
|
aria-label="Open navigation"
|
||||||
onClick={toggle}
|
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>
|
<span className="sr-only">Toggle menu</span>
|
||||||
<div className="w-5 h-0.5 bg-yellow-400" />
|
<div className="w-5 h-0.5 bg-yellow-400" />
|
||||||
</button>
|
</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>
|
||||||
<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>
|
</div>
|
||||||
</header>
|
</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 && (
|
{opened && (
|
||||||
<div className="fixed inset-0 z-30">
|
<div className="fixed inset-0 z-30">
|
||||||
@@ -100,7 +154,7 @@ export default function LayoutDefault({ children }: { children: React.ReactNode
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{links.map((link) => (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@layer base {
|
@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 {
|
:root {
|
||||||
font-family:
|
font-family:
|
||||||
"Nunito",
|
"Outfit",
|
||||||
ui-sans-serif,
|
ui-sans-serif,
|
||||||
system-ui,
|
system-ui,
|
||||||
-apple-system,
|
-apple-system,
|
||||||
@@ -24,6 +41,57 @@
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
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.",
|
"description": "A web frontend for the Pac-Man game, including leaderboards and OAuth.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/nunito": "^5.2.7",
|
"@fontsource/nunito": "^5.2.7",
|
||||||
|
"@fontsource/outfit": "^5.2.8",
|
||||||
"@fontsource/pixelify-sans": "^5.2.7",
|
"@fontsource/pixelify-sans": "^5.2.7",
|
||||||
|
"@fontsource/russo-one": "^5.2.7",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
|
"overlayscrollbars": "^2.13.0",
|
||||||
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"vike": "^0.4.240",
|
"vike": "^0.4.240",
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
|||||||
|
|
||||||
// Restart the game loop when returning to the game page
|
// Restart the game loop when returning to the game page
|
||||||
if (pageContext.urlPathname === "/") {
|
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 win = getPacmanWindow();
|
||||||
const module = win.Module;
|
const module = win.Module;
|
||||||
|
|
||||||
@@ -31,5 +42,4 @@ export const onPageTransitionEnd: OnPageTransitionEndAsync = async (
|
|||||||
console.error("Failed to restart game:", error);
|
console.error("Failed to restart game:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { OnPageTransitionStartAsync } from "vike/types";
|
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||||
import { getPacmanWindow } from "@/lib/pacman";
|
import { getPacmanWindow } from "@/lib/pacman";
|
||||||
|
|
||||||
|
const TRANSITION_DURATION = 200;
|
||||||
|
|
||||||
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||||
console.log("Page transition start");
|
console.log("Page transition start");
|
||||||
document.querySelector("body")?.classList.add("page-is-transitioning");
|
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");
|
console.log("Stopping game loop for page transition");
|
||||||
win.Module._stop_game();
|
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 }) {
|
export default function GameLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-black text-yellow-400 min-h-screen flex flex-col">
|
<div className="bg-black text-yellow-400 h-full flex flex-col overflow-hidden">
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1 overflow-hidden">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,11 @@ export default function Page() {
|
|||||||
}, [gameReady, gameStarted, handleInteraction]);
|
}, [gameReady, gameStarted, handleInteraction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex justify-center h-[calc(100vh-120px)]">
|
<div className="flex justify-center items-center h-full pt-4">
|
||||||
<div
|
<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={{
|
style={{
|
||||||
boxShadow:
|
height: "min(calc(100vh - 96px), calc((100vw - 32px) * 6 / 5))",
|
||||||
"0 0 12px rgba(250, 204, 21, 0.35), 0 0 2px rgba(255, 255, 255, 0.25)",
|
|
||||||
}}
|
}}
|
||||||
onClick={handleInteraction}
|
onClick={handleInteraction}
|
||||||
>
|
>
|
||||||
|
|||||||
Generated
+3779
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user