mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 03:15:48 -06:00
feat: setup 'web' frontend
This commit is contained in:
145
web/.gitignore
vendored
Normal file
145
web/.gitignore
vendored
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# Cloudflare
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel/
|
||||||
|
|
||||||
|
# Sentry Vite Plugin
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
||||||
|
# aws-cdk
|
||||||
|
.cdk.staging
|
||||||
|
cdk.out
|
||||||
145
web/.prettierignore
Normal file
145
web/.prettierignore
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# Cloudflare
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel/
|
||||||
|
|
||||||
|
# Sentry Vite Plugin
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
||||||
|
# aws-cdk
|
||||||
|
.cdk.staging
|
||||||
|
cdk.out
|
||||||
64
web/eslint.config.ts
Normal file
64
web/eslint.config.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import eslint from "@eslint/js";
|
||||||
|
import prettier from "eslint-plugin-prettier/recommended";
|
||||||
|
import react from "eslint-plugin-react";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint, { type ConfigArray } from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"dist/*",
|
||||||
|
// Temporary compiled files
|
||||||
|
"**/*.ts.build-*.mjs",
|
||||||
|
|
||||||
|
// JS files at the root of the project
|
||||||
|
"*.js",
|
||||||
|
"*.cjs",
|
||||||
|
"*.mjs",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
warnOnUnsupportedTypeScriptVersion: false,
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-namespace": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
|
||||||
|
...react.configs.flat.recommended,
|
||||||
|
languageOptions: {
|
||||||
|
...react.configs.flat.recommended.languageOptions,
|
||||||
|
globals: {
|
||||||
|
...globals.serviceworker,
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ConfigArray[number],
|
||||||
|
|
||||||
|
react.configs.flat["jsx-runtime"] as ConfigArray[number],
|
||||||
|
|
||||||
|
prettier as ConfigArray[number],
|
||||||
|
);
|
||||||
91
web/layouts/LayoutDefault.tsx
Normal file
91
web/layouts/LayoutDefault.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import "./tailwind.css";
|
||||||
|
import "@fontsource/pixelify-sans";
|
||||||
|
import "@fontsource/nunito/800.css";
|
||||||
|
|
||||||
|
import { AppShell, Burger, Group, MantineProvider, Flex, Stack, Drawer } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import theme from "./theme";
|
||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
import { IconBrandGithub, IconDownload, IconDeviceGamepad3, IconTrophy } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: "Play",
|
||||||
|
href: "/",
|
||||||
|
icon: <IconDeviceGamepad3 size={28} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Leaderboard",
|
||||||
|
href: "/leaderboard",
|
||||||
|
icon: <IconTrophy size={28} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download",
|
||||||
|
href: "/download",
|
||||||
|
icon: <IconDownload size={28} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "GitHub",
|
||||||
|
href: "https://github.com/Xevion/Pac-Man",
|
||||||
|
icon: <IconBrandGithub size={28} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Link({ href, label }: { href: string; label: string }) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LayoutDefault({ children }: { children: React.ReactNode }) {
|
||||||
|
const [opened, { toggle, close }] = useDisclosure();
|
||||||
|
|
||||||
|
const mainLinks = links
|
||||||
|
.filter((link) => link.href.startsWith("/"))
|
||||||
|
.map((link) => <Link href={link.href} label={link.label} />);
|
||||||
|
|
||||||
|
const sourceLinks = links
|
||||||
|
.filter((link) => !link.href.startsWith("/"))
|
||||||
|
.map((link) => (
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
title={link.label}
|
||||||
|
target="_blank"
|
||||||
|
className="transition-all duration-300 hover:drop-shadow-sm hover:drop-shadow-yellow-400"
|
||||||
|
>
|
||||||
|
{link.icon}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider forceColorScheme="dark" theme={theme}>
|
||||||
|
<div className="bg-black text-yellow-400 min-h-screen flex flex-col">
|
||||||
|
<AppShell header={{ height: 60 }} padding="md">
|
||||||
|
<AppShell.Header>
|
||||||
|
<Flex h="100%" px="md" align="center" justify="space-between">
|
||||||
|
<Flex h="100%" align="center" gap="md">
|
||||||
|
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||||
|
<Group visibleFrom="sm">{mainLinks}</Group>
|
||||||
|
</Flex>
|
||||||
|
<Group visibleFrom="sm">{sourceLinks}</Group>
|
||||||
|
</Flex>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Main>{children}</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
<Drawer opened={opened} onClose={close} title="Navigation">
|
||||||
|
<Stack>
|
||||||
|
{links.map((link) => (
|
||||||
|
<Link href={link.href} label={link.label} />
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
web/layouts/tailwind.css
Normal file
1
web/layouts/tailwind.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
29
web/layouts/theme.ts
Normal file
29
web/layouts/theme.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { MantineThemeOverride } from "@mantine/core";
|
||||||
|
import { createTheme } from "@mantine/core";
|
||||||
|
|
||||||
|
const theme: MantineThemeOverride = createTheme({
|
||||||
|
/** Put your mantine theme override here */
|
||||||
|
primaryColor: "yellow",
|
||||||
|
|
||||||
|
fontFamily: "'Nunito', sans-serif",
|
||||||
|
headings: {
|
||||||
|
fontFamily: "'Nunito', sans-serif",
|
||||||
|
fontWeight: "800",
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
AppShell: {
|
||||||
|
styles: {
|
||||||
|
header: {
|
||||||
|
backgroundColor: "#000",
|
||||||
|
borderBottom: "1px solid rgba(250, 204, 21, 0.25)",
|
||||||
|
},
|
||||||
|
navbar: {
|
||||||
|
backgroundColor: "#000",
|
||||||
|
borderRight: "1px solid rgba(250, 204, 21, 0.25)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
42
web/package.json
Normal file
42
web/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vike dev",
|
||||||
|
"build": "vike build",
|
||||||
|
"preview": "vike preview",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"name": "pacman-web",
|
||||||
|
"description": "A web frontend for the Pac-Man game, including leaderboards and OAuth.",
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/nunito": "^5.2.7",
|
||||||
|
"@fontsource/pixelify-sans": "^5.2.7",
|
||||||
|
"@mantine/core": "^8.2.8",
|
||||||
|
"@mantine/hooks": "^8.2.8",
|
||||||
|
"@tabler/icons-react": "^3.35.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"vike": "^0.4.240",
|
||||||
|
"vike-react": "^0.6.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.35.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@types/react": "^19.1.12",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"eslint": "^9.35.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"typescript-eslint": "^8.42.0",
|
||||||
|
"vite": "^7.1.4"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
13
web/pages/+Head.tsx
Normal file
13
web/pages/+Head.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// https://vike.dev/Head
|
||||||
|
|
||||||
|
//# BATI.has("mantine")
|
||||||
|
import { ColorSchemeScript } from "@mantine/core";
|
||||||
|
|
||||||
|
export default function HeadDefault() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<ColorSchemeScript />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
web/pages/+config.ts
Normal file
17
web/pages/+config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Config } from "vike/types";
|
||||||
|
import vikeReact from "vike-react/config";
|
||||||
|
import Layout from "../layouts/LayoutDefault.js";
|
||||||
|
|
||||||
|
// Default config (can be overridden by pages)
|
||||||
|
// https://vike.dev/config
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// https://vike.dev/Layout
|
||||||
|
Layout,
|
||||||
|
|
||||||
|
// https://vike.dev/head-tags
|
||||||
|
title: "Pac-Man",
|
||||||
|
description: "A Pac-Man game built with Rust and React.",
|
||||||
|
|
||||||
|
extends: vikeReact,
|
||||||
|
} satisfies Config;
|
||||||
6
web/pages/+onPageTransitionEnd.ts
Normal file
6
web/pages/+onPageTransitionEnd.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { OnPageTransitionEndAsync } from "vike/types";
|
||||||
|
|
||||||
|
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => {
|
||||||
|
console.log("Page transition end");
|
||||||
|
document.querySelector("body")?.classList.remove("page-is-transitioning");
|
||||||
|
};
|
||||||
6
web/pages/+onPageTransitionStart.ts
Normal file
6
web/pages/+onPageTransitionStart.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { OnPageTransitionStartAsync } from "vike/types";
|
||||||
|
|
||||||
|
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
|
||||||
|
console.log("Page transition start");
|
||||||
|
document.querySelector("body")?.classList.add("page-is-transitioning");
|
||||||
|
};
|
||||||
19
web/pages/_error/+Page.tsx
Normal file
19
web/pages/_error/+Page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { usePageContext } from "vike-react/usePageContext";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { is404 } = usePageContext();
|
||||||
|
if (is404) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Page Not Found</h1>
|
||||||
|
<p>This page could not be found.</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Internal Error</h1>
|
||||||
|
<p>Something went wrong.</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/pages/index/+Page.tsx
Normal file
16
web/pages/index/+Page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { AspectRatio } from "@mantine/core";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex justify-center h-[calc(100vh-120px)]">
|
||||||
|
<AspectRatio ratio={1.0 / 1.2} w="min(100vh * 1.2, 100vw)" maw="95vw">
|
||||||
|
<canvas
|
||||||
|
className="block border-1 border-yellow-400/50 w-full h-full"
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 12px rgba(250, 204, 21, 0.35), 0 0 2px rgba(255, 255, 255, 0.25)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AspectRatio>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
294
web/pages/leaderboard/+Page.tsx
Normal file
294
web/pages/leaderboard/+Page.tsx
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Container, Title, Table, Tabs, Avatar, Text, Group, Badge, Stack, Paper } from "@mantine/core";
|
||||||
|
import { IconTrophy, IconCalendar } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
interface LeaderboardEntry {
|
||||||
|
id: number;
|
||||||
|
rank: number;
|
||||||
|
name: string;
|
||||||
|
score: number;
|
||||||
|
duration: string;
|
||||||
|
levelCount: number;
|
||||||
|
submittedAt: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockGlobalData: LeaderboardEntry[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
rank: 1,
|
||||||
|
name: "PacMaster2024",
|
||||||
|
score: 125000,
|
||||||
|
duration: "45:32",
|
||||||
|
levelCount: 12,
|
||||||
|
submittedAt: "2 hours ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PacMaster2024",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
rank: 2,
|
||||||
|
name: "GhostHunter",
|
||||||
|
score: 118750,
|
||||||
|
duration: "42:18",
|
||||||
|
levelCount: 11,
|
||||||
|
submittedAt: "5 hours ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=GhostHunter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
rank: 3,
|
||||||
|
name: "DotCollector",
|
||||||
|
score: 112500,
|
||||||
|
duration: "38:45",
|
||||||
|
levelCount: 10,
|
||||||
|
submittedAt: "1 day ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=DotCollector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
rank: 4,
|
||||||
|
name: "MazeRunner",
|
||||||
|
score: 108900,
|
||||||
|
duration: "41:12",
|
||||||
|
levelCount: 10,
|
||||||
|
submittedAt: "2 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=MazeRunner",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
rank: 5,
|
||||||
|
name: "PowerPellet",
|
||||||
|
score: 102300,
|
||||||
|
duration: "36:28",
|
||||||
|
levelCount: 9,
|
||||||
|
submittedAt: "3 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PowerPellet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
rank: 6,
|
||||||
|
name: "CherryPicker",
|
||||||
|
score: 98750,
|
||||||
|
duration: "39:15",
|
||||||
|
levelCount: 9,
|
||||||
|
submittedAt: "4 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=CherryPicker",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
rank: 7,
|
||||||
|
name: "BlinkyBeater",
|
||||||
|
score: 94500,
|
||||||
|
duration: "35:42",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "5 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlinkyBeater",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
rank: 8,
|
||||||
|
name: "PinkyPac",
|
||||||
|
score: 91200,
|
||||||
|
duration: "37:55",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "1 week ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=PinkyPac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
rank: 9,
|
||||||
|
name: "InkyDestroyer",
|
||||||
|
score: 88800,
|
||||||
|
duration: "34:18",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "1 week ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=InkyDestroyer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
rank: 10,
|
||||||
|
name: "ClydeChaser",
|
||||||
|
score: 85600,
|
||||||
|
duration: "33:45",
|
||||||
|
levelCount: 7,
|
||||||
|
submittedAt: "1 week ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ClydeChaser",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockMonthlyData: LeaderboardEntry[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
rank: 1,
|
||||||
|
name: "JanuaryChamp",
|
||||||
|
score: 115000,
|
||||||
|
duration: "43:22",
|
||||||
|
levelCount: 11,
|
||||||
|
submittedAt: "1 day ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=JanuaryChamp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
rank: 2,
|
||||||
|
name: "NewYearPac",
|
||||||
|
score: 108500,
|
||||||
|
duration: "40:15",
|
||||||
|
levelCount: 10,
|
||||||
|
submittedAt: "3 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=NewYearPac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
rank: 3,
|
||||||
|
name: "WinterWarrior",
|
||||||
|
score: 102000,
|
||||||
|
duration: "38:30",
|
||||||
|
levelCount: 10,
|
||||||
|
submittedAt: "5 days ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=WinterWarrior",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
rank: 4,
|
||||||
|
name: "FrostyPac",
|
||||||
|
score: 98500,
|
||||||
|
duration: "37:45",
|
||||||
|
levelCount: 9,
|
||||||
|
submittedAt: "1 week ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrostyPac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
rank: 5,
|
||||||
|
name: "IceBreaker",
|
||||||
|
score: 95200,
|
||||||
|
duration: "36:12",
|
||||||
|
levelCount: 9,
|
||||||
|
submittedAt: "1 week ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=IceBreaker",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
rank: 6,
|
||||||
|
name: "SnowPac",
|
||||||
|
score: 91800,
|
||||||
|
duration: "35:28",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "2 weeks ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=SnowPac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
rank: 7,
|
||||||
|
name: "BlizzardBeast",
|
||||||
|
score: 88500,
|
||||||
|
duration: "34:15",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "2 weeks ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BlizzardBeast",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
rank: 8,
|
||||||
|
name: "ColdSnap",
|
||||||
|
score: 85200,
|
||||||
|
duration: "33:42",
|
||||||
|
levelCount: 8,
|
||||||
|
submittedAt: "3 weeks ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ColdSnap",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
rank: 9,
|
||||||
|
name: "FrozenFury",
|
||||||
|
score: 81900,
|
||||||
|
duration: "32:55",
|
||||||
|
levelCount: 7,
|
||||||
|
submittedAt: "3 weeks ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=FrozenFury",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
rank: 10,
|
||||||
|
name: "ArcticAce",
|
||||||
|
score: 78600,
|
||||||
|
duration: "31:18",
|
||||||
|
levelCount: 7,
|
||||||
|
submittedAt: "4 weeks ago",
|
||||||
|
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ArcticAce",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function LeaderboardTable({ data }: { data: LeaderboardEntry[] }) {
|
||||||
|
const rows = data.map((entry) => (
|
||||||
|
<Table.Tr
|
||||||
|
key={entry.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#000",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap="sm">
|
||||||
|
<Avatar src={entry.avatar} size="md" radius="sm" alt={entry.name} />
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600} size="lg" c="yellow.4">
|
||||||
|
{entry.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{entry.submittedAt}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={500} size="lg" c="yellow.3">
|
||||||
|
{entry.score.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="md" c="gray.3">
|
||||||
|
{entry.duration}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>Level {entry.levelCount}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>("global");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md" py="xl">
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Paper shadow="lg" p="xl" radius="md" bg="none">
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="global" leftSection={<IconTrophy size={16} />}>
|
||||||
|
Global
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="monthly" leftSection={<IconCalendar size={16} />}>
|
||||||
|
Monthly
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="global" pt="md">
|
||||||
|
<LeaderboardTable data={mockGlobalData} />
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="monthly" pt="md">
|
||||||
|
<LeaderboardTable data={mockMonthlyData} />
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
web/postcss.config.cjs
Normal file
14
web/postcss.config.cjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
9
web/prettier.config.js
Normal file
9
web/prettier.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @see https://prettier.io/docs/configuration
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
printWidth: 120,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
28
web/tsconfig.json
Normal file
28
web/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"noEmit": true,
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"vike-react"
|
||||||
|
],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
web/vite.config.ts
Normal file
11
web/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import vike from "vike/plugin";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vike(), react(), tailwindcss()],
|
||||||
|
build: {
|
||||||
|
target: "es2022",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user