feat: scaffold frontend app structure
This commit is contained in:
1
frontend/src/api/index.ts
Normal file
1
frontend/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
33
frontend/src/app/layouts/AppLayout.tsx
Normal file
33
frontend/src/app/layouts/AppLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
frontend/src/app/router.tsx
Normal file
15
frontend/src/app/router.tsx
Normal 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 /> }
|
||||
]
|
||||
}
|
||||
]);
|
||||
1
frontend/src/components/index.ts
Normal file
1
frontend/src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
frontend/src/features/index.ts
Normal file
1
frontend/src/features/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
frontend/src/lib/index.ts
Normal file
1
frontend/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
12
frontend/src/main.tsx
Normal file
12
frontend/src/main.tsx
Normal 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>
|
||||
);
|
||||
8
frontend/src/pages/DashboardPage.tsx
Normal file
8
frontend/src/pages/DashboardPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
frontend/src/pages/RacesPage.tsx
Normal file
8
frontend/src/pages/RacesPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
93
frontend/src/styles/global.css
Normal file
93
frontend/src/styles/global.css
Normal 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);
|
||||
}
|
||||
29
frontend/src/styles/tokens.css
Normal file
29
frontend/src/styles/tokens.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user