feat(ui): scaffold vite + react 19 frontend
Replaces the CRA + React 16 + class-component frontend with the
shape from architecture/generic.md §4: vite + react + swc + ts,
served as static from nginx in prod, vite dev server in dev with
/api proxied to localhost:8080.
Layout:
ui/
package.json, vite.config.ts, tsconfig.{json,app,node}.json
index.html
src/
main.tsx — react root + react-query provider
App.tsx — header, filters, vertical timeline
App.css — dark backdrop, hot-pink links
api/client.ts — TS types mirroring moments-entities;
fetchEvents, fetchSources via /api/v1
components/
Filters.tsx — source toggles, count slider, date range
TimelineEntry.tsx — renders one TimelineItem with body
support for markdown, commits, links
lib/icon.tsx — TimelineIcon → react-bootstrap-icons map
+ colour per icon
Stack: react 19, @tanstack/react-query 5, react-bootstrap 2 (on
bootstrap 5), react-vertical-timeline-component 3, rc-slider 11
(<Slider range /> replaces the removed v8 Range), react-markdown 9.
Dev proxy: /api/* → http://localhost:8080/* (rewrite strips /api).
Backend stays location-agnostic at /v1; ingress prefix is added
by nginx (and the dev proxy) so the same fetch shape works in
both environments.
Verified: tsc -b clean, vite build clean (417 KB js / 245 KB css
gzip 128 / 33), vite dev server serves the index. NOT verified
visually in a browser — that's a `pnpm run dev` away on roosta
once the api is up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
ui/src/api/client.ts
Normal file
84
ui/src/api/client.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Wire types mirror the moments-entities types serialised by the API.
|
||||
// Hand-maintained for now; if drift becomes a problem, generate them
|
||||
// from the Rust crate via ts-rs or specta.
|
||||
|
||||
export type Source = 'github' | 'gitea' | 'hg' | 'bugzilla';
|
||||
|
||||
export type TitleSegment =
|
||||
| { kind: 'text'; text: string }
|
||||
| { kind: 'link'; text: string; url: string };
|
||||
|
||||
export interface CommitSummary {
|
||||
sha: string;
|
||||
short_sha: string;
|
||||
message: string;
|
||||
url: string;
|
||||
author: string | null;
|
||||
}
|
||||
|
||||
export type TimelineBody =
|
||||
| { kind: 'markdown'; text: string }
|
||||
| { kind: 'commits'; commits: CommitSummary[] }
|
||||
| { kind: 'links'; items: TitleSegment[] };
|
||||
|
||||
export type TimelineIcon =
|
||||
| 'git-push'
|
||||
| 'git-commit'
|
||||
| 'git-merge'
|
||||
| 'git-fork'
|
||||
| 'git-branch-create'
|
||||
| 'git-branch-delete'
|
||||
| 'pull-request'
|
||||
| 'issue'
|
||||
| 'comment'
|
||||
| 'star'
|
||||
| 'release'
|
||||
| 'bug'
|
||||
| 'generic';
|
||||
|
||||
export interface TimelineItem {
|
||||
id: string;
|
||||
source: Source;
|
||||
action: string;
|
||||
occurred_at: string;
|
||||
icon: TimelineIcon;
|
||||
title: TitleSegment[];
|
||||
subtitle: TitleSegment[] | null;
|
||||
body: TimelineBody | null;
|
||||
}
|
||||
|
||||
export interface SourceSummary {
|
||||
source: Source;
|
||||
count: number;
|
||||
earliest: string | null;
|
||||
latest: string | null;
|
||||
}
|
||||
|
||||
export interface EventQuery {
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
sources?: Source[];
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
export async function fetchEvents(q: EventQuery): Promise<TimelineItem[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (q.from) params.set('from', q.from.toISOString());
|
||||
if (q.to) params.set('to', q.to.toISOString());
|
||||
if (q.sources && q.sources.length > 0) {
|
||||
params.set('source', q.sources.join(','));
|
||||
}
|
||||
if (q.limit) params.set('limit', String(q.limit));
|
||||
|
||||
const resp = await fetch(`${API_BASE}/events?${params}`);
|
||||
if (!resp.ok) throw new Error(`events: HTTP ${resp.status}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
export async function fetchSources(): Promise<SourceSummary[]> {
|
||||
const resp = await fetch(`${API_BASE}/sources`);
|
||||
if (!resp.ok) throw new Error(`sources: HTTP ${resp.status}`);
|
||||
return resp.json();
|
||||
}
|
||||
Reference in New Issue
Block a user