chore(frontend): scaffold Vite React TypeScript application

This commit is contained in:
2026-10-06 15:56:59 -03:00
parent 0965077cac
commit 36e2bee885
21 changed files with 4928 additions and 0 deletions
+4
View File
@@ -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=
+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?
+4
View File
@@ -0,0 +1,4 @@
dist/
coverage/
node_modules/
package-lock.json
+14
View File
@@ -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.
+16
View File
@@ -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,
};
}
+36
View File
@@ -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,
]);
+13
View File
@@ -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>
+4609
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -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"
}
}
+13
View File
@@ -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 };
+3
View File
@@ -0,0 +1,3 @@
export default function App() {
return <main>Vocero</main>;
}
+14
View File
@@ -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>,
);
+3
View File
@@ -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"]);
});
});
+10
View File
@@ -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();
});
+28
View File
@@ -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"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}
+25
View File
@@ -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"]
}
+27
View File
@@ -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" },
};
});
+12
View File
@@ -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,
},
});