initial commit
This commit is contained in:
commit
3e90445fab
20 changed files with 961 additions and 0 deletions
21
src/mainview/components/Clock.tsx
Normal file
21
src/mainview/components/Clock.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export default function Clock() {
|
||||
const locale = "en";
|
||||
const [today, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 60 * 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="flex font-semibold gap-2 items-center">
|
||||
{today.toLocaleTimeString(locale, { hour: "numeric", minute: "numeric" })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/mainview/components/GamepadIcon.tsx
Normal file
23
src/mainview/components/GamepadIcon.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
|
||||
export default function GamepadIcon({
|
||||
platform,
|
||||
variant,
|
||||
button,
|
||||
text,
|
||||
}: {
|
||||
platform: "xbox" | "playstation" | "nintendo";
|
||||
variant: string;
|
||||
button: string;
|
||||
text?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="gamepad-button-wrapper">
|
||||
<i
|
||||
className={`gamepad-button gamepad-button-${platform} gamepad-button-${platform}--${button} gamepad-button-${platform}--variant-${variant}`}
|
||||
>
|
||||
{text}
|
||||
</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/mainview/index.css
Normal file
23
src/mainview/index.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-dark: #333333;
|
||||
--color-light: #464646;
|
||||
--color-light: #828282;
|
||||
--color-light2: #bcbcbc;
|
||||
--color-primary: #E5FF00;
|
||||
--color-alt: #4656E6;
|
||||
--color-alert: #E60012;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
}
|
||||
|
||||
#root {
|
||||
}
|
||||
12
src/mainview/index.html
Normal file
12
src/mainview/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GameFlow</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
src/mainview/index.tsx
Normal file
30
src/mainview/index.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
// Set up a Router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
scrollRestoration: true,
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
77
src/mainview/routeTree.gen.ts
Normal file
77
src/mainview/routeTree.gen.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as GameDetailsRouteImport } from './routes/GameDetails'
|
||||
import { Route as DashboardRouteImport } from './routes/Dashboard'
|
||||
|
||||
const GameDetailsRoute = GameDetailsRouteImport.update({
|
||||
id: '/GameDetails',
|
||||
path: '/GameDetails',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DashboardRoute = DashboardRouteImport.update({
|
||||
id: '/Dashboard',
|
||||
path: '/Dashboard',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/Dashboard': typeof DashboardRoute
|
||||
'/GameDetails': typeof GameDetailsRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/Dashboard': typeof DashboardRoute
|
||||
'/GameDetails': typeof GameDetailsRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/Dashboard': typeof DashboardRoute
|
||||
'/GameDetails': typeof GameDetailsRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/Dashboard' | '/GameDetails'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/Dashboard' | '/GameDetails'
|
||||
id: '__root__' | '/Dashboard' | '/GameDetails'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
DashboardRoute: typeof DashboardRoute
|
||||
GameDetailsRoute: typeof GameDetailsRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/GameDetails': {
|
||||
id: '/GameDetails'
|
||||
path: '/GameDetails'
|
||||
fullPath: '/GameDetails'
|
||||
preLoaderRoute: typeof GameDetailsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/Dashboard': {
|
||||
id: '/Dashboard'
|
||||
path: '/Dashboard'
|
||||
fullPath: '/Dashboard'
|
||||
preLoaderRoute: typeof DashboardRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
DashboardRoute: DashboardRoute,
|
||||
GameDetailsRoute: GameDetailsRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
225
src/mainview/routes/Dashboard.tsx
Normal file
225
src/mainview/routes/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
Power,
|
||||
Sun,
|
||||
Wifi,
|
||||
BatteryFull,
|
||||
Gamepad2,
|
||||
Bluetooth,
|
||||
Settings2,
|
||||
Bell,
|
||||
HardDrive,
|
||||
} from "lucide-react";
|
||||
import { createFileRoute, Link, linkOptions } from "@tanstack/react-router";
|
||||
import "gamepad.css/styles.min.css";
|
||||
import GamepadIcon from "../components/GamepadIcon";
|
||||
import Clock from "../components/Clock";
|
||||
import classNames from "classnames";
|
||||
|
||||
export const Route = createFileRoute("/Dashboard")({
|
||||
component: ConsoleHomeUI,
|
||||
});
|
||||
|
||||
const games = [
|
||||
{
|
||||
title: "The Legend of Zelda",
|
||||
subtitle: "Link's Awakening",
|
||||
},
|
||||
{
|
||||
title: "Captain Toad",
|
||||
subtitle: "Treasure Tracker",
|
||||
focused: true,
|
||||
},
|
||||
{
|
||||
title: "Crash Bandicoot",
|
||||
subtitle: "N. Sane Trilogy",
|
||||
},
|
||||
{
|
||||
title: "Super Mario",
|
||||
subtitle: "Odyssey",
|
||||
},
|
||||
{
|
||||
title: "Animal Crossing",
|
||||
subtitle: "New Horizons",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ConsoleHomeUI() {
|
||||
const [focus, setFocus] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowRight")
|
||||
setFocus((i) => Math.min(i + 1, games.length - 1));
|
||||
if (e.key === "ArrowLeft") setFocus((i) => Math.max(i - 1, 0));
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full flex flex-col overflow-hidden justify-around"
|
||||
style={{
|
||||
background: `linear-gradient(
|
||||
color-mix(in srgb, var(--color-dark) 60%, transparent),
|
||||
color-mix(in srgb, var(--color-dark) 60%, transparent)
|
||||
), url(https://picsum.photos/id/${10 + focus}/1920/1080.webp?blur=10)`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<header className="h-14 px-6 mt-2 flex items-center justify-between text-white">
|
||||
<div className="flex items-center gap-3 drop-shadow-sm">
|
||||
<div className="w-16 h-16 rounded-full bg-alert" />
|
||||
<div className="w-16 h-16 rounded-full bg-cyan-500 ring-4 ring-primary" />
|
||||
<button className="w-16 h-16 rounded-full bg-dark flex items-center justify-center">
|
||||
<Plus className="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5 text drop-shadow-sm">
|
||||
<Clock />
|
||||
<Wifi className="w-6 h-6" />
|
||||
<Bluetooth className="w-6 h-6" />
|
||||
<Bell className="w-6 h-6" />
|
||||
<div className="flex gap-2 items-center">
|
||||
<BatteryFull className="w-6 h-6" />
|
||||
<span className="font-semibold">100%</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center text-dark bg-white">
|
||||
<Sun className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center text-dark bg-white">
|
||||
<Power className="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Filter bar */}
|
||||
<div className="flex items-center justify-center px-8 gap-2 py-3 drop-shadow-sm">
|
||||
<button className="flex w-14 h-14 items-center justify-center bg-dark rounded-full text-white">
|
||||
<Settings2 className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex bg-dark rounded-full p-1">
|
||||
<button className="px-4 h-12 rounded-full text-white/70">All</button>
|
||||
<button className="px-4 h-12 rounded-full bg-primary drop-shadow-sm text-black font-bold">
|
||||
Digital
|
||||
</button>
|
||||
<button className="px-4 h-12 rounded-full text-white/70">
|
||||
Physical
|
||||
</button>
|
||||
</div>
|
||||
<button className="flex w-14 h-14 items-center justify-center bg-dark rounded-full text-white">
|
||||
<Search className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Game carousel */}
|
||||
<main
|
||||
className="flex w-full px-8 py-4 overflow-x-scroll items-center gap-6"
|
||||
style={{ scrollbarWidth: "none" }}
|
||||
>
|
||||
{games.map((g, i) => {
|
||||
const focused = i === focus;
|
||||
return (
|
||||
<div
|
||||
key={g.title}
|
||||
className={classNames(
|
||||
`min-w-64 h-82 rounded-2xl bg-dark flex flex-col justify-end overflow-hidden transition-all duration-200 drop-shadow-md`,
|
||||
{
|
||||
"ring-7 ring-primary scale-105": focused,
|
||||
"drop-shadow-lg": focused,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="flex-1 bg-white p-4"
|
||||
style={{
|
||||
backgroundImage: `url(https://picsum.photos/id/${10 + i}/300/300.webp)`,
|
||||
}}
|
||||
></div>
|
||||
<div className="h-0 flex pr-2 justify-end items-center">
|
||||
<div className="flex rounded-full bg-white w-10 h-10 justify-center items-center text-dark drop-shadow-sm">
|
||||
<HardDrive className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col p-4 pt-6 text-light2">
|
||||
<div className="text-xl font-bold">{g.title}</div>
|
||||
<div className="text-s">{g.subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
|
||||
{/* Menu */}
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-3">
|
||||
<CircleIcon
|
||||
to={linkOptions({
|
||||
to: "/Dashboard",
|
||||
})}
|
||||
label="Home"
|
||||
active
|
||||
/>
|
||||
<CircleIcon label="News" />
|
||||
<CircleIcon label="Shop" />
|
||||
<CircleIcon label="Album" />
|
||||
<CircleIcon label="Controllers" />
|
||||
<CircleIcon label="Settings" highlight />
|
||||
<span className="flex items-center rounded-full bg-primary text-dark px-4 py-2 font-semibold">
|
||||
Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<footer className="px-8 flex flex-col items-center justify-between text-light2">
|
||||
<div className="flex gap-2 text-sm text-light2">
|
||||
<span className="flex gap-2 bg-dark pl-2 pr-3 py-1.5 rounded-full items-center text-lg font-semibold drop-shadow-sm">
|
||||
<GamepadIcon platform="xbox" variant="one" button="a" text="a" />
|
||||
Continue
|
||||
</span>
|
||||
<span className="flex gap-2 bg-dark pl-2 pr-3 py-1.5 rounded-full items-center text-lg font-semibold drop-shadow-sm">
|
||||
<GamepadIcon platform="xbox" variant="one" button="b" text="b" />
|
||||
Back
|
||||
</span>
|
||||
<span className="flex gap-2 bg-dark pl-2 pr-3 py-1.5 rounded-full items-center text-lg font-semibold drop-shadow-sm">
|
||||
<GamepadIcon platform="xbox" variant="one" button="x" text="x" />
|
||||
Close
|
||||
</span>
|
||||
<span className="flex gap-2 bg-dark pl-2 pr-3 py-1.5 rounded-full items-center text-lg font-semibold drop-shadow-sm">
|
||||
<GamepadIcon platform="xbox" variant="one" button="y" text="y" />
|
||||
Options
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CircleIcon({
|
||||
to,
|
||||
active,
|
||||
highlight,
|
||||
}: {
|
||||
to?: any;
|
||||
active?: boolean;
|
||||
highlight?: boolean;
|
||||
label?: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
{...to}
|
||||
className={`w-20 h-20 rounded-full flex items-center justify-center text-dark drop-shadow-lg
|
||||
${highlight === true ? "bg-primary" : active === true ? "bg-alert text-white" : "bg-white"}`}
|
||||
>
|
||||
<Gamepad2 className="w-10 h-10" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
110
src/mainview/routes/GameDetails.tsx
Normal file
110
src/mainview/routes/GameDetails.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Bell, Library, Store, Settings, Gamepad2 } from "lucide-react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
const games = [
|
||||
"Halo Infinite",
|
||||
"Cyberpunk",
|
||||
"Hades",
|
||||
"Stardew Valley",
|
||||
"Neon Skies",
|
||||
"Void Runner",
|
||||
"Rogue Light",
|
||||
"Drift City",
|
||||
];
|
||||
|
||||
export const Route = createFileRoute("/GameDetails")({
|
||||
loader: ({ params }) => params.postId,
|
||||
component: GameDetailsUI,
|
||||
});
|
||||
|
||||
export function GameDetailsUI() {
|
||||
// In a component!
|
||||
const { postId } = Route.useParams();
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-10 flex flex-col gap-10">
|
||||
{/* Header */}
|
||||
<header className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-slate-400">Now Playing</div>
|
||||
<h1 className="text-3xl font-semibold text-cyan-400">
|
||||
Halo Infinite
|
||||
</h1>
|
||||
<div className="mt-2 text-slate-400 text-sm">
|
||||
Action · FPS · Sci-Fi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-slate-300">
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="text-sm">3</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content split */}
|
||||
<section className="flex gap-10 flex-1">
|
||||
{/* Cover / media */}
|
||||
<div className="w-[360px] shrink-0">
|
||||
<div className="relative h-[480px] rounded-3xl bg-gradient-to-br from-slate-700/60 to-slate-900/90 ring-4 ring-cyan-400/80 shadow-[0_0_50px_rgba(34,211,238,0.6)]" />
|
||||
|
||||
{/* Primary action */}
|
||||
<button className="mt-6 w-full rounded-xl bg-cyan-400 text-black font-semibold py-3 text-lg shadow-[0_0_30px_rgba(34,211,238,0.6)]">
|
||||
▶ Play
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex-1 flex flex-col gap-6">
|
||||
{/* Description */}
|
||||
<p className="text-slate-300 leading-relaxed max-w-3xl">
|
||||
Experience the epic sci-fi saga and master chief’s greatest journey
|
||||
yet. Explore vast open worlds, engage in tactical combat, and
|
||||
uncover the mysteries of Zeta Halo.
|
||||
</p>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="grid grid-cols-2 gap-6 max-w-3xl">
|
||||
<Detail label="Developer" value="343 Industries" />
|
||||
<Detail label="Publisher" value="Xbox Game Studios" />
|
||||
<Detail label="Release" value="Dec 8, 2021" />
|
||||
<Detail label="Playtime" value="42 hours" />
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-4 mt-4">
|
||||
<SecondaryButton label="Achievements" />
|
||||
<SecondaryButton label="DLC" />
|
||||
<SecondaryButton label="Settings" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer hints */}
|
||||
<footer className="text-sm text-slate-400 flex gap-6">
|
||||
<span>A Play</span>
|
||||
<span>B Back</span>
|
||||
<span>Y Options</span>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function Detail({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wide text-slate-500">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-slate-200 text-sm mt-1">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SecondaryButton({ label }: { label: string }) {
|
||||
return (
|
||||
<button className="px-5 py-3 rounded-xl bg-white/5 hover:bg-white/10 text-slate-200">
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
16
src/mainview/routes/__root.tsx
Normal file
16
src/mainview/routes/__root.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { Gamepad2, Library, Settings, Store } from "lucide-react";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden">
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue