From ee954431cc2bdf9b3e328fa23b393275e3168d9b Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Mon, 19 Oct 2026 16:55:08 +0530 Subject: [PATCH] feat: add client template (React admin + auth shell) Generalized from the vet reference client: - @aetoskia/react-auth + @aetoskia/react-openapi from npm.aetoskia.com (.npmrc), no local aliases - generic Home (resource cards + live preview table), Header, Footer - AppTheme/shared-theme with MUI v5 type shims - VITE_API_BASE_URL / VITE_AUTH_BASE_URL env wiring Verified: tsc --noEmit + vite build pass against built @aetoskia packages. --- standards/src/templates/client/.env.example | 2 + standards/src/templates/client/.gitignore | 21 + standards/src/templates/client/.npmrc | 1 + standards/src/templates/client/CHANGELOG.md | 15 + standards/src/templates/client/index.html | 13 + standards/src/templates/client/package.json | 34 ++ standards/src/templates/client/src/Footer.tsx | 66 +++ standards/src/templates/client/src/Header.tsx | 83 +++ standards/src/templates/client/src/Home.tsx | 214 ++++++++ standards/src/templates/client/src/main.jsx | 94 ++++ .../templates/client/src/openapi-config.ts | 29 ++ .../client/src/shared-theme/AppTheme.tsx | 145 ++++++ .../customizations/dataDisplay.tsx | 243 +++++++++ .../shared-theme/customizations/feedback.tsx | 56 ++ .../shared-theme/customizations/inputs.tsx | 456 +++++++++++++++++ .../customizations/navigation.tsx | 290 +++++++++++ .../shared-theme/customizations/surfaces.ts | 123 +++++ .../client/src/shared-theme/mui.d.ts | 27 + .../client/src/shared-theme/themeConfig.ts | 112 ++++ .../src/shared-theme/themePrimitives.ts | 480 ++++++++++++++++++ .../src/templates/client/src/vite-env.d.ts | 10 + standards/src/templates/client/tsconfig.json | 21 + standards/src/templates/client/vite.config.ts | 6 + 23 files changed, 2541 insertions(+) create mode 100644 standards/src/templates/client/.env.example create mode 100644 standards/src/templates/client/.gitignore create mode 100644 standards/src/templates/client/.npmrc create mode 100644 standards/src/templates/client/CHANGELOG.md create mode 100644 standards/src/templates/client/index.html create mode 100644 standards/src/templates/client/package.json create mode 100644 standards/src/templates/client/src/Footer.tsx create mode 100644 standards/src/templates/client/src/Header.tsx create mode 100644 standards/src/templates/client/src/Home.tsx create mode 100644 standards/src/templates/client/src/main.jsx create mode 100644 standards/src/templates/client/src/openapi-config.ts create mode 100644 standards/src/templates/client/src/shared-theme/AppTheme.tsx create mode 100644 standards/src/templates/client/src/shared-theme/customizations/dataDisplay.tsx create mode 100644 standards/src/templates/client/src/shared-theme/customizations/feedback.tsx create mode 100644 standards/src/templates/client/src/shared-theme/customizations/inputs.tsx create mode 100644 standards/src/templates/client/src/shared-theme/customizations/navigation.tsx create mode 100644 standards/src/templates/client/src/shared-theme/customizations/surfaces.ts create mode 100644 standards/src/templates/client/src/shared-theme/mui.d.ts create mode 100644 standards/src/templates/client/src/shared-theme/themeConfig.ts create mode 100644 standards/src/templates/client/src/shared-theme/themePrimitives.ts create mode 100644 standards/src/templates/client/src/vite-env.d.ts create mode 100644 standards/src/templates/client/tsconfig.json create mode 100644 standards/src/templates/client/vite.config.ts diff --git a/standards/src/templates/client/.env.example b/standards/src/templates/client/.env.example new file mode 100644 index 0000000..996e6f8 --- /dev/null +++ b/standards/src/templates/client/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:8000 +VITE_AUTH_BASE_URL=http://localhost:8000 diff --git a/standards/src/templates/client/.gitignore b/standards/src/templates/client/.gitignore new file mode 100644 index 0000000..434c113 --- /dev/null +++ b/standards/src/templates/client/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +.env +# dependencies +node_modules + +# React Router +.react-router +build + +# misc +.DS_Store +dist +dist-ssr +*.local +.idea +tsconfig.tsbuildinfo + +# generated documentation +docs/mcp +site/ diff --git a/standards/src/templates/client/.npmrc b/standards/src/templates/client/.npmrc new file mode 100644 index 0000000..a412fd6 --- /dev/null +++ b/standards/src/templates/client/.npmrc @@ -0,0 +1 @@ +registry=https://npm.aetoskia.com diff --git a/standards/src/templates/client/CHANGELOG.md b/standards/src/templates/client/CHANGELOG.md new file mode 100644 index 0000000..3973a5a --- /dev/null +++ b/standards/src/templates/client/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [Unreleased] diff --git a/standards/src/templates/client/index.html b/standards/src/templates/client/index.html new file mode 100644 index 0000000..51be3f3 --- /dev/null +++ b/standards/src/templates/client/index.html @@ -0,0 +1,13 @@ + + + + + + + App Title + + +
+ + + diff --git a/standards/src/templates/client/package.json b/standards/src/templates/client/package.json new file mode 100644 index 0000000..02fee5a --- /dev/null +++ b/standards/src/templates/client/package.json @@ -0,0 +1,34 @@ +{ + "name": "client-template", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "quality-gate": "tsc --noEmit" + }, + "dependencies": { + "@aetoskia/react-auth": "^0.1.0", + "@aetoskia/react-openapi": "^0.1.0", + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@mui/icons-material": "^5.16.7", + "@mui/material": "^5.16.7", + "axios": "^1.7.7", + "js-yaml": "^4.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.9.3", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.1" + } +} diff --git a/standards/src/templates/client/src/Footer.tsx b/standards/src/templates/client/src/Footer.tsx new file mode 100644 index 0000000..564c0b5 --- /dev/null +++ b/standards/src/templates/client/src/Footer.tsx @@ -0,0 +1,66 @@ +/** + * Site footer. + * + * A fixed bottom `AppBar` (pinned above the drawer via + * `zIndex.drawer + 1`) with a blurred translucent background and a top + * divider, rendering the `Copyright` line. Rendered once by `main.jsx` below + * the routed content. + * + * @packageDocumentation + */ + +import * as React from 'react'; +import Container from '@mui/material/Container'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; +import { AppBar, Toolbar } from "@mui/material"; + +/** Copyright line linking to aetoskia.com with the current year. */ +function Copyright() { + return ( + + + {'Copyright © Aetoskia Internal Infrastructure — All rights reserved.'} + +   + {new Date().getFullYear()} + + ); +} + +/** + * Fixed bottom app bar showing the site copyright. + * + * @returns The footer `AppBar` element. + */ +export default function Footer() { + return ( + theme.zIndex.drawer + 1, + backdropFilter: 'blur(8px)', + boxShadow: 'none', + borderTop: '1px solid', + borderColor: 'divider', + }} + > + + + + + + + ); +} diff --git a/standards/src/templates/client/src/Header.tsx b/standards/src/templates/client/src/Header.tsx new file mode 100644 index 0000000..81e9e79 --- /dev/null +++ b/standards/src/templates/client/src/Header.tsx @@ -0,0 +1,83 @@ +/** + * Top navigation bar. + * + * A fixed `AppBar` with blurred translucent background and bottom divider, + * showing the app title/logo, the signed-in user (or a login button), and + * an Admin button. Rendered once by `main.jsx` above the routed content; + * the title navigates to `/` and the Admin button to `/admin`. + * + * @packageDocumentation + */ + +import * as React from "react"; +import { AppBar, Toolbar, Typography, Button, Chip, IconButton, Tooltip } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import DashboardIcon from "@mui/icons-material/Dashboard"; +import LogoutIcon from "@mui/icons-material/Logout"; +import { useAuth } from "@aetoskia/react-auth"; + +/** + * Fixed top app bar with the title/logo, current user, and an Admin button. + * + * Uses `useNavigate` and `useAuth`, so it must be rendered inside both a + * router and an `AuthProvider`. Clicking the title navigates to `/`; the + * Admin button navigates to `/admin`; the logout icon clears the session. + * + * @returns The header `AppBar` element. + */ +export default function Header() { + const navigate = useNavigate(); + const { currentUser, logout } = useAuth(); + + return ( + theme.zIndex.drawer + 1, + backdropFilter: "blur(8px)", + boxShadow: "none", + borderBottom: "1px solid", + borderColor: "divider", + }} + > + + + navigate("/")} + > + App Title + + + + + {currentUser && ( + + )} + + + + {currentUser && ( + + logout()} aria-label="Log out"> + + + + )} + + + ); +} diff --git a/standards/src/templates/client/src/Home.tsx b/standards/src/templates/client/src/Home.tsx new file mode 100644 index 0000000..09430b6 --- /dev/null +++ b/standards/src/templates/client/src/Home.tsx @@ -0,0 +1,214 @@ +/** + * Landing page: a grid of resource cards and a live preview table. + * + * Exercises the `@aetoskia/react-openapi` building blocks end-to-end: + * - `ResourceCard` - a grid of every top-level resource with a live record + * count (or SSE connection status for streaming resources). + * - `LiveTable` - preview of the first 5 records of the first non-streaming + * resource. + * + * Add custom pages as sibling routes in `main.jsx`; the auto-generated + * admin lives at `/admin/*` regardless. + * + * @packageDocumentation + */ + +import React, { useEffect, useMemo, useState } from "react"; +import { + Box, Container, Typography, Paper, Grid, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, + TableRow, CircularProgress, +} from "@mui/material"; + +import { useNavigate } from "react-router-dom"; +import { useAppContext, useResource, ListCellRenderer, SseConnectionStatus, applyDisplayFormat } from "@aetoskia/react-openapi"; +import type { ResourceConfig, FieldConfig } from "@aetoskia/react-openapi"; + +/** + * Clickable card summarizing one resource. + * + * Shows the display name, field count and either a record-count chip (loaded + * via `useResource().list({ limit: 0 })`) or the SSE connection status for + * streaming resources; clicking navigates to `/admin/`. + * + * Props: + * - `resource` - the `ResourceConfig` to summarize. + */ +function ResourceCard({ resource }: { resource: ResourceConfig }) { + const navigate = useNavigate(); + const { list } = useResource(resource.name); + const [count, setCount] = useState(null); + + useEffect(() => { + if (resource.streaming) return; + list({ limit: 0 }).then((res) => setCount(res.total ?? res.items?.length ?? 0)); + }, [resource.name]); + + return ( + navigate(`/admin/${resource.name}`)} + sx={{ + p: 3, + borderRadius: 2, + border: "1px solid", + borderColor: "divider", + cursor: "pointer", + transition: "all 0.2s", + "&:hover": { borderColor: "primary.main", boxShadow: 1 }, + }} + > + + {resource.displayName} + + + {resource.fields.length} fields + + + {resource.streaming ? ( + + ) : ( + 0 ? "primary" : "default"} + variant="outlined" + /> + )} + + + + ); +} + +/** + * Preview table of the first 5 records of a resource. + * + * Columns come from `resource.listColumns`; each cell renders through + * `ListCellRenderer` using the resource display format (or the FK target's + * format for foreign-key columns). Shows a spinner while loading and a + * "No records found." message when empty. + * + * Props: + * - `resource` - the `ResourceConfig` to preview. + */ +function LiveTable({ resource }: { resource: ResourceConfig }) { + const { list } = useResource(resource.name); + const { resources: allResources } = useAppContext(); + const [rows, setRows] = useState(null); + const columns = resource.listColumns + .map((n) => resource.fields.find((f) => f.name === n)) + .filter(Boolean) as FieldConfig[]; + + useEffect(() => { + list({ limit: 5 }).then((res) => setRows(res.items ?? [])); + }, [resource.name]); + + if (!rows) { + return ( + + + + ); + } + + if (rows.length === 0) { + return No records found.; + } + + return ( + + + + + {columns.map((col) => ( + + {col.label} + + ))} + + + + {rows.map((row, i) => ( + + {columns.map((col) => { + const displayFormat = col.fk + ? (allResources.find((r) => r.name === col.fk!.resource)?.displayFormat ?? resource.displayFormat) + : resource.displayFormat; + return ( + + + + ); + })} + + ))} + +
+
+ ); +} + +/** + * Render the landing page: resource grid and live preview. + * + * @returns The page container, or a "no resources" notice when the spec + * exposes none. + */ +export default function Home() { + const { resources } = useAppContext(); + const topLevel = useMemo(() => resources.filter((r) => !r.parent), [resources]); + const previewResource = useMemo(() => topLevel.find((r) => !r.streaming) ?? topLevel[0], [topLevel]); + + if (resources.length === 0) { + return ( + + + No resources found in the OpenAPI spec. + + + ); + } + + return ( + + + + App Title + + + Auto-generated CRUD from an OpenAPI 3.1 spec using{" "} + + @aetoskia/react-openapi + + . + + + + + Resources + + + {topLevel.map((r) => ( + + + + ))} + + + + Live Preview + {previewResource && ( + + — first 5 records from {previewResource.displayName} + + )} + + {previewResource && } + + ); +} diff --git a/standards/src/templates/client/src/main.jsx b/standards/src/templates/client/src/main.jsx new file mode 100644 index 0000000..d0a3229 --- /dev/null +++ b/standards/src/templates/client/src/main.jsx @@ -0,0 +1,94 @@ +/** + * Application entry point. + * + * Creates the React root from `#root` and mounts the app: `AuthProvider` + * (JWT session against `VITE_AUTH_BASE_URL`) wraps a `LoginGate` (renders + * `AuthPage` until authenticated) which wraps `AppProvider` (fed by + * `specConfiguration`) → `BrowserRouter` → `AppTheme` → `Header` → routed + * content (`Home` at `/`, `Admin` at `/admin/*`) → `Footer`. + * + * @packageDocumentation + */ + +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Box, CssBaseline, Toolbar } from "@mui/material"; +import { AuthProvider, AuthPage, useAuth } from "@aetoskia/react-auth"; +import Home from './Home'; +import { Admin, AppProvider } from '@aetoskia/react-openapi'; +import { specConfiguration } from './openapi-config'; +import Header from './Header'; +import Footer from './Footer'; +import AppTheme from './shared-theme/AppTheme'; + +const authServer = import.meta.env.VITE_AUTH_BASE_URL; + +const authConfig = { + serverUrl: authServer, + loginPath: "/login", + registerPath: "/register", + logoutPath: "/logout", + mePath: "/me", + introspectPath: "/introspect", +}; + +/** + * Route guard: renders the split-screen `AuthPage` until a user is + * authenticated, then renders the full application shell. + * + * @returns The login gate or the authenticated app. + */ +function LoginGate() { + const auth = useAuth(); + const [mode, setMode] = React.useState("login"); + + if (!auth.currentUser) { + return ( + + + {}} + onSwitchMode={() => setMode((m) => (m === "login" ? "register" : "login"))} + login={auth.login} + register={auth.register} + loading={auth.loading} + error={auth.error} + currentUser={auth.currentUser} + /> + + ); + } + + return ( + + + + +
+ + + + + + } /> + } /> + + + +