feat(web): scaffold client-side rendered SPA foundation

- feat: initialize Vite project with React 19, React Router 8, TanStack
  Query, Ant Design 6, and Tailwind CSS 4
- feat: add Oxlint with React/TypeScript rules, strict TypeScript
  config, and layered CSS import order
- docs: add decision record for client-rendered Web foundation with
  React, Ant Design, and Tailwind CSS
- docs: create web roadmap defining architecture boundaries and deferred
  dependencies
- build: pin Node.js 24 in mise.toml
This commit is contained in:
2026-07-14 16:50:13 +08:00
parent a62a5bc0e2
commit b49bf648be
20 changed files with 3093 additions and 0 deletions
+18
View File
@@ -106,3 +106,21 @@
- REST remains a handler/API concern, and future protocols can reuse services without HTTP leakage. - REST remains a handler/API concern, and future protocols can reuse services without HTTP leakage.
- Error responses keep the same top-level shape, with optional `log_id` instead of embedding log references in the message. - Error responses keep the same top-level shape, with optional `log_id` instead of embedding log references in the message.
- Admin and auth middleware behavior is testable through service contracts rather than database access. - Admin and auth middleware behavior is testable through service contracts rather than database access.
## 2026-07-14: Client-rendered Web Foundation
**Context**: MyGO needs a browser client now and native clients later. The Web application must share the versioned REST API instead of introducing browser-only server logic.
**Decisions**:
| Area | Choice | Guidance |
|------|--------|----------|
| Rendering | Pure client-side rendered SPA | Vite emits static assets; do not introduce SSR, React Server Components, or a Node API server. |
| Application stack | React, strict TypeScript, React Router, and TanStack Query | Keep routing and remote-data state explicit and client-side. |
| UI system | Ant Design plus Tailwind CSS 4 | Ant Design owns reusable controls and theme tokens; Tailwind initially owns layout, spacing, and responsive utilities. |
| Dependency policy | Install capabilities when their feature starts | Keep API generation, transfer, virtualization, drag-and-drop, test, and preview libraries deferred in `docs/web-roadmap.md`. |
**Consequences**:
- The Web and future native clients consume the same client-neutral API contracts.
- MyGO or a reverse proxy may host `web/dist` with an SPA fallback without changing the rendering model.
- MyGO domain components own file-browser behavior and must not depend on Ant Design request behavior for business logic.
+49
View File
@@ -0,0 +1,49 @@
# Web Roadmap
## Product Boundary
- The Web client is a pure client-side rendered single-page application.
- Vite produces static assets only. The project does not use SSR, React Server Components, a Node API server, or a browser-specific business backend.
- The Web client consumes the same versioned REST API as future Android and other native clients.
- Production may serve `web/dist` from MyGO or a reverse proxy.
- Shared API contracts should remain client-neutral and eventually be described by OpenAPI.
## Foundation
The initial project contains only the framework and styling foundation:
- Node.js 24 and npm
- Vite
- React
- TypeScript in strict mode
- TanStack Query provider
- Tailwind CSS 4 through its Vite plugin
- Ant Design provider and components
- Oxlint from the Vite template
Ant Design owns reusable UI components and theme tokens. Tailwind CSS is initially limited to application layout, spacing, and responsive utilities.
## Planned Structure
```text
web/src/
├── app/ # Router, providers, and application composition
├── api/ # API client and generated contracts
├── components/ # Shared presentation components
├── features/ # Auth, files, account, and admin features
├── pages/ # Route entry points
├── lib/ # Framework-independent utilities
└── test/ # Shared test setup and fixtures
```
## Deferred Dependencies
Some dependencies are suggested for future implementation. Refer these only when the corresponding feature is implemented and propose better choices if any:
- `@ant-design/icons`: Add Ant Design-consistent application and action icons when real screens require them.
- `openapi-typescript`: Generate TypeScript API types from the shared OpenAPI document.
- `openapi-fetch`: Provide a small type-safe Fetch client based on generated OpenAPI types.
- `openapi-react-query`: Connect generated OpenAPI operations to TanStack Query if handwritten query adapters become repetitive.
- `zustand`: Manage a cross-route upload queue, bulk selection, or other complex client-only state if React state is insufficient.
- `vitest`: Run unit and integration tests using the Vite toolchain.
- `pdfjs-dist`: Preview PDF files in the browser when document preview is implemented.
+1
View File
@@ -1,2 +1,3 @@
[tools] [tools]
go = "1.26.2" go = "1.26.2"
node = "24"
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
+18
View File
@@ -0,0 +1,18 @@
# MyGO Web
MyGO's browser client is a pure client-side rendered application built with React, TypeScript, and Vite.
## Development
```bash
npm install
npm run dev
```
## Checks
```bash
npm run check
```
The production build is emitted to `dist/` as static assets. See `docs/web-roadmap.md` at the repository root for architecture boundaries and planned dependencies.
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="MyGO web client" />
<title>MyGO</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2761
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "mygo-web",
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=24 <25"
},
"packageManager": "npm@11.9.0",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"typecheck": "tsc -b",
"check": "npm run lint && npm run build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.101.2",
"antd": "^6.5.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router": "^8.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"oxlint": "^1.71.0",
"tailwindcss": "^4.3.2",
"typescript": "~6.0.2",
"vite": "^8.1.1"
}
}
+9
View File
@@ -0,0 +1,9 @@
import { RouterProvider } from 'react-router'
import { router } from './app/router.tsx'
function App() {
return <RouterProvider router={router} />
}
export default App
+15
View File
@@ -0,0 +1,15 @@
import type { PropsWithChildren } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { App as AntDesignApp, ConfigProvider } from 'antd'
const queryClient = new QueryClient()
export function AppProviders({ children }: PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>
<ConfigProvider>
<AntDesignApp>{children}</AntDesignApp>
</ConfigProvider>
</QueryClientProvider>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { createBrowserRouter } from 'react-router'
import { HomePage } from '../pages/HomePage.tsx'
import { NotFoundPage } from '../pages/NotFoundPage.tsx'
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '*',
element: <NotFoundPage />,
},
])
+13
View File
@@ -0,0 +1,13 @@
@layer theme, base, antd, components, utilities;
@import 'tailwindcss';
html,
body,
#root {
min-width: 320px;
min-height: 100%;
}
body {
margin: 0;
}
+13
View File
@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { AppProviders } from './app/providers.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AppProviders>
<App />
</AppProviders>
</StrictMode>,
)
+19
View File
@@ -0,0 +1,19 @@
import { Card, Space, Tag, Typography } from 'antd'
const { Paragraph, Title } = Typography
export function HomePage() {
return (
<main className="grid min-h-screen place-items-center bg-slate-50 p-6">
<Card className="w-full max-w-xl">
<Space orientation="vertical" size="middle">
<Tag color="blue">MyGO Web</Tag>
<Title level={1}>MyGO</Title>
<Paragraph>
The client-side web application foundation is ready.
</Paragraph>
</Space>
</Card>
</main>
)
}
+16
View File
@@ -0,0 +1,16 @@
import { Button, Result } from 'antd'
import { Link } from 'react-router'
export function NotFoundPage() {
return (
<Result
status="404"
title="Page not found"
extra={
<Button type="primary">
<Link to="/">Back home</Link>
</Button>
}
/>
)
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
"strict": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
"strict": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})