chore(frontend): scaffold Vite React TypeScript application
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Existing s2s-client FastAPI server. Used only by Vite's server-side proxy.
|
||||
S2S_BACKEND_URL=http://127.0.0.1:8420
|
||||
# Comma-separated hostnames, only needed when accessing Vite through a remote host.
|
||||
S2S_DEV_ALLOWED_HOSTS=
|
||||
@@ -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?
|
||||
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
coverage/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
@@ -0,0 +1,14 @@
|
||||
# Vocero React frontend
|
||||
|
||||
React, TypeScript, Tailwind CSS, and Vite client for the existing `s2s-client` FastAPI backend.
|
||||
|
||||
Requires Node.js 22.18+ or 24+ and npm. Run `npm ci`, then `npm run dev`.
|
||||
The frontend listens on `http://127.0.0.1:5173` and proxies backend requests to
|
||||
`http://127.0.0.1:8420`. Copy `.env.example` to `.env.local` to override that target.
|
||||
|
||||
`npm run check` runs formatting, strict ESLint, TypeScript, and Vitest checks.
|
||||
`npm run build` typechecks and produces `dist/`. `npm run format` and `npm run lint:fix`
|
||||
apply mechanical formatting and lint fixes.
|
||||
|
||||
The repository's `start_ui.sh` still serves the original UI. This project is started
|
||||
separately during the frontend migration.
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProxyOptions } from "vite";
|
||||
|
||||
/** Preserve the public origin used by FastAPI to construct the speech WebSocket URL. */
|
||||
export function createBackendProxy(target: string): Record<string, ProxyOptions> {
|
||||
const http: ProxyOptions = { target, changeOrigin: false };
|
||||
return {
|
||||
"/api": http,
|
||||
"/v1/realtime": { ...http, ws: true },
|
||||
"/docs": http,
|
||||
"/redoc": http,
|
||||
"/openapi.json": http,
|
||||
"/oauth": http,
|
||||
"/project": http,
|
||||
"/public/docs.html": http,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import js from "@eslint/js";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import prettier from "eslint-config-prettier/flat";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["dist/**", "coverage/**", "node_modules/**"]),
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [js.configs.recommended, tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked],
|
||||
languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname } },
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
|
||||
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignorePrimitives: { string: true } }],
|
||||
"no-console": ["error", { allow: ["warn", "error"] }],
|
||||
"no-debugger": "error",
|
||||
curly: ["error", "all"],
|
||||
eqeqeq: ["error", "always", { null: "ignore" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
plugins: { "react-hooks": { rules: reactHooks.rules }, "react-refresh": reactRefresh },
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-refresh/only-export-components": ["error", { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
]);
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#07080d" />
|
||||
<title>Vocero · Conversación de voz</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4609
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "frontend-s2s",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --cache --cache-strategy content --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc -b",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"check": "npm run format:check && npm run lint && npm run typecheck && npm test"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.4.16",
|
||||
"lucide-react": "^1.52.0",
|
||||
"marked": "^18.1.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@testing-library/user-event": "^14.6.7",
|
||||
"@types/node": "^24.13.3",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"eslint": "^10.12.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.7",
|
||||
"jiti": "^2.7.0",
|
||||
"jsdom": "^30.1.2",
|
||||
"prettier": "^3.9.9",
|
||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.71.1",
|
||||
"vite": "^8.3.0",
|
||||
"vitest": "^5.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Config } from "prettier";
|
||||
|
||||
export default {
|
||||
arrowParens: "always",
|
||||
endOfLine: "lf",
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
printWidth: 120,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
tabWidth: 2,
|
||||
tailwindStylesheet: "./src/styles.css",
|
||||
trailingComma: "all",
|
||||
} satisfies Config & { tailwindStylesheet: string };
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function App() {
|
||||
return <main>Vocero</main>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./styles.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
if (!root) {
|
||||
throw new Error("The application root was not found.");
|
||||
}
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
@layer theme, base, components, utilities;
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createBackendProxy } from "../../config/backend-proxy";
|
||||
|
||||
describe("development backend proxy", () => {
|
||||
it("keeps the public host and original paths for the FastAPI runtime URL", () => {
|
||||
const proxy = createBackendProxy("http://127.0.0.1:8420");
|
||||
expect(proxy["/api"]).toEqual({ target: "http://127.0.0.1:8420", changeOrigin: false });
|
||||
expect(proxy["/v1/realtime"]).toEqual({ target: "http://127.0.0.1:8420", changeOrigin: false, ws: true });
|
||||
expect(proxy["/openapi.json"]).toEqual(proxy["/api"]);
|
||||
expect(proxy["/oauth"]).toEqual(proxy["/api"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["*.config.ts", "config/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
|
||||
import { createBackendProxy } from "./config/backend-proxy.ts";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Read this project's configuration; the repository .env contains server secrets.
|
||||
const env = loadEnv(mode, process.cwd(), "S2S_");
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
allowedHosts: (env.S2S_DEV_ALLOWED_HOSTS ?? "")
|
||||
.split(",")
|
||||
.map((host) => host.trim())
|
||||
.filter(Boolean),
|
||||
proxy: createBackendProxy(env.S2S_BACKEND_URL ?? "http://127.0.0.1:8420"),
|
||||
},
|
||||
build: { target: "es2022" },
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
execArgv: process.allowedNodeEnvironmentFlags.has("--no-experimental-webstorage")
|
||||
? ["--no-experimental-webstorage"]
|
||||
: [],
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
restoreMocks: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user