feat: scaffold frontend app structure

This commit is contained in:
Anton
2026-04-06 15:15:53 +03:00
parent 698ae37553
commit d7fb5b71ef
29 changed files with 2753 additions and 56 deletions

12
frontend/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>Calendar Run</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1796
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
frontend/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "calendar-run-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1"
},
"devDependencies": {
"@types/node": "^22.7.5",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.2",
"typescript": "^5.6.2",
"vite": "^5.4.8"
}
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,33 @@
import { NavLink, Outlet } from "react-router-dom";
export function AppLayout(): JSX.Element {
return (
<div className="app-shell">
<header className="app-shell__header">
<div className="app-shell__brand">Calendar Run</div>
<nav className="app-shell__nav" aria-label="Primary navigation">
<NavLink
to="/"
end
className={({ isActive }) =>
isActive ? "app-shell__link app-shell__link--active" : "app-shell__link"
}
>
Dashboard
</NavLink>
<NavLink
to="/races"
className={({ isActive }) =>
isActive ? "app-shell__link app-shell__link--active" : "app-shell__link"
}
>
Races
</NavLink>
</nav>
</header>
<main className="app-shell__main">
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { createBrowserRouter } from "react-router-dom";
import { AppLayout } from "./layouts/AppLayout";
import { DashboardPage } from "../pages/DashboardPage";
import { RacesPage } from "../pages/RacesPage";
export const appRouter = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
{ index: true, element: <DashboardPage /> },
{ path: "races", element: <RacesPage /> }
]
}
]);

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

12
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { appRouter } from "./app/router";
import "./styles/tokens.css";
import "./styles/global.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={appRouter} />
</React.StrictMode>
);

View File

@@ -0,0 +1,8 @@
export function DashboardPage(): JSX.Element {
return (
<section className="page page--dashboard">
<h1 className="page__title">Dashboard</h1>
<p className="page__subtitle">Overview cards and quick actions will be added in the next task.</p>
</section>
);
}

View File

@@ -0,0 +1,8 @@
export function RacesPage(): JSX.Element {
return (
<section className="page page--races">
<h1 className="page__title">Races</h1>
<p className="page__subtitle">Upcoming and completed race lists will be added in the next task.</p>
</section>
);
}

View File

@@ -0,0 +1,93 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#root {
min-height: 100%;
}
body {
margin: 0;
font-family: var(--font-family-base);
font-size: var(--font-size-body);
line-height: var(--line-height-base);
background: var(--color-bg);
color: var(--color-text);
}
a {
color: inherit;
text-decoration: none;
}
.app-shell {
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
}
.app-shell__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-4) var(--space-6);
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.app-shell__brand {
font-size: var(--font-size-h2);
font-weight: 700;
}
.app-shell__nav {
display: flex;
align-items: center;
gap: var(--space-2);
}
.app-shell__link {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
}
.app-shell__link:hover,
.app-shell__link:focus-visible {
color: var(--color-text);
background: #eef2f6;
outline: none;
}
.app-shell__link--active {
color: var(--color-surface);
background: var(--color-accent);
}
.app-shell__main {
width: min(1080px, 100%);
margin: 0 auto;
padding: var(--space-6);
}
.page {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6);
}
.page__title {
margin: 0 0 var(--space-2);
font-size: var(--font-size-h1);
}
.page__subtitle {
margin: 0;
color: var(--color-text-muted);
}

View File

@@ -0,0 +1,29 @@
:root {
--color-bg: #f3f5f7;
--color-surface: #ffffff;
--color-text: #13202b;
--color-text-muted: #5d6b77;
--color-accent: #1f7ae0;
--color-border: #dce2e8;
--color-success: #2f9e63;
--color-warning: #c0821f;
--color-error: #cc3a3a;
--font-family-base: "Inter", "Segoe UI", Arial, sans-serif;
--font-size-h1: 2rem;
--font-size-h2: 1.5rem;
--font-size-body: 1rem;
--font-size-caption: 0.875rem;
--line-height-base: 1.5;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--radius-sm: 0.375rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": false,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}

View File

@@ -0,0 +1 @@
{"root":["./src/main.tsx","./src/api/index.ts","./src/app/router.tsx","./src/app/layouts/applayout.tsx","./src/components/index.ts","./src/features/index.ts","./src/lib/index.ts","./src/pages/dashboardpage.tsx","./src/pages/racespage.tsx"],"version":"5.9.3"}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"types": ["node"],
"strict": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because one or more lines are too long

2
frontend/vite.config.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;

5
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,5 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()]
});

6
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()]
});