initial commit

This commit is contained in:
Simeon Radivoev 2026-01-23 04:56:39 +02:00
commit 3e90445fab
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
20 changed files with 961 additions and 0 deletions

38
src/bun/index.ts Normal file
View file

@ -0,0 +1,38 @@
import { BrowserWindow, Updater } from "electrobun/bun";
const DEV_SERVER_PORT = 5173;
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}/Dashboard`;
// Check if Vite dev server is running for HMR
async function getMainViewUrl(): Promise<string> {
const channel = await Updater.localInfo.channel();
if (channel === "dev") {
try {
await fetch(DEV_SERVER_URL, { method: "HEAD" });
console.log(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`);
return DEV_SERVER_URL;
} catch {
console.log("Vite dev server not running. Run 'bun run dev:hmr' for HMR support.");
}
}
return "views://mainview/index.html";
}
// Create the main application window
const url = await getMainViewUrl();
const mainWindow = new BrowserWindow({
title: "GameFlow",
url,
styleMask: {
Borderless: true,
},
frame: {
width: 1280,
height: 800,
x: 200,
y: 200,
},
});
console.log("React Tailwind Vite app started!");

View 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>
);
}

View 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
View 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
View 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
View 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>,
);
}

View 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>()

View 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>
);
}

View 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 chiefs 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>
);
}

View 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>
);
}