feat(web): set up React app shell with routing and layout
Adds react-router-dom with routes for ship list, dashboard, and rules pages. Includes TopBar with navigation and mobile-first dark theme CSS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,19 @@
|
|||||||
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
|
import ShipListPage from './pages/ShipListPage';
|
||||||
|
import ShipDashboardPage from './pages/ShipDashboardPage';
|
||||||
|
import RulesPage from './pages/RulesPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<BrowserRouter>
|
||||||
<h1>Spelljammer Ship Tracker</h1>
|
<div className="app">
|
||||||
<p>App is running. Routes coming soon.</p>
|
<Routes>
|
||||||
</div>
|
<Route path="/" element={<ShipListPage />} />
|
||||||
|
<Route path="/ship/:id" element={<ShipDashboardPage />} />
|
||||||
|
<Route path="/rules" element={<RulesPage />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
web/src/components/layout/PageContainer.tsx
Normal file
5
web/src/components/layout/PageContainer.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export default function PageContainer({ children }: { children: ReactNode }) {
|
||||||
|
return <main className="page-container">{children}</main>;
|
||||||
|
}
|
||||||
32
web/src/components/layout/TopBar.tsx
Normal file
32
web/src/components/layout/TopBar.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function TopBar({ title }: { title?: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const isHome = location.pathname === '/';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="top-bar">
|
||||||
|
<div className="top-bar-left">
|
||||||
|
{!isHome && (
|
||||||
|
<button className="top-bar-back" onClick={() => navigate(-1)}>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<h1 className="top-bar-title">{title || 'Spelljammer'}</h1>
|
||||||
|
</div>
|
||||||
|
<nav className="top-bar-right">
|
||||||
|
{location.pathname !== '/rules' && (
|
||||||
|
<button className="top-bar-link" onClick={() => navigate('/rules')}>
|
||||||
|
Rules
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!isHome && location.pathname !== '/rules' && (
|
||||||
|
<button className="top-bar-link" onClick={() => navigate('/')}>
|
||||||
|
Ships
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -88,3 +88,92 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top Bar */
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--spacing-sm) 0;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-back {
|
||||||
|
background: none;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-link {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-link:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Container */
|
||||||
|
.page-container {
|
||||||
|
padding-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--color-danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
background: none;
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|||||||
13
web/src/pages/RulesPage.tsx
Normal file
13
web/src/pages/RulesPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import TopBar from '../components/layout/TopBar';
|
||||||
|
import PageContainer from '../components/layout/PageContainer';
|
||||||
|
|
||||||
|
export default function RulesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopBar title="Battle Reference" />
|
||||||
|
<PageContainer>
|
||||||
|
<p style={{ color: 'var(--color-text-muted)' }}>Rules reference coming soon...</p>
|
||||||
|
</PageContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/src/pages/ShipDashboardPage.tsx
Normal file
16
web/src/pages/ShipDashboardPage.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import TopBar from '../components/layout/TopBar';
|
||||||
|
import PageContainer from '../components/layout/PageContainer';
|
||||||
|
|
||||||
|
export default function ShipDashboardPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopBar title="Ship Dashboard" />
|
||||||
|
<PageContainer>
|
||||||
|
<p style={{ color: 'var(--color-text-muted)' }}>Dashboard for ship {id} coming soon...</p>
|
||||||
|
</PageContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
web/src/pages/ShipListPage.tsx
Normal file
13
web/src/pages/ShipListPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import TopBar from '../components/layout/TopBar';
|
||||||
|
import PageContainer from '../components/layout/PageContainer';
|
||||||
|
|
||||||
|
export default function ShipListPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopBar title="Ships" />
|
||||||
|
<PageContainer>
|
||||||
|
<p style={{ color: 'var(--color-text-muted)' }}>Ship list coming soon...</p>
|
||||||
|
</PageContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user