updates to frontend-react

This commit is contained in:
2025-10-01 07:49:34 -04:00
parent 0345f3d2d0
commit 0e8b84b462
29 changed files with 1693 additions and 6 deletions

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?

View File

@@ -0,0 +1,2 @@
save-exact=true
package-lock=false

View File

@@ -0,0 +1 @@
24

View File

@@ -0,0 +1,12 @@
{
"arrowParens": "avoid",
"bracketSameLine": true,
"bracketSpacing": false,
"printWidth": 160,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname
}
// other options...
}
}
]);
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x';
import reactDom from 'eslint-plugin-react-dom';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname
}
// other options...
}
}
]);
```

View File

@@ -0,0 +1,18 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import {defineConfig, globalIgnores} from 'eslint/config';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs['recommended-latest'], reactRefresh.configs.vite],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser
}
}
]);

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend-react</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"name": "frontend-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "2.9.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-redux": "9.2.0",
"styled-components": "6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@techniker-me/websocket-shared-types": "*",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react-swc": "^4.1.0",
"babel-plugin-styled-components": "2.1.4",
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.44.0",
"vite": "^7.1.7",
"vite-plugin-babel": "1.3.2"
}
}

View File

@@ -0,0 +1,10 @@
import {LoginView} from './views';
export default function App() {
console.log('APP')
return (
<>
<LoginView />
</>
);
}

View File

@@ -0,0 +1,3 @@
export interface ILoginFormProps {
onSubmit: (username: string, secret: string) => void;
}

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import ILoginFormPops from './ILoginFormProps';
import { Input } from './Styled';
export function LoginForm({ onSubmit }: ILoginFormProps) {
const [username, setUsername] = useState<string>('');
const [secret, setSecret] = useState<string>('');
// The submit handler now correctly uses 'event'
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit(username, secret);
};
return (
<form onSubmit={handleFormSubmit}>
{/*
- The 'placeholder' prop is now lowercase.
- The 'onChange' handler correctly destructures {target}.
*/}
<Input
type="text"
placeholder="Username"
autoComplete="username"
value={username}
onChange={({ target }) => setUsername(target.value)}
/>
<Input
type="password"
placeholder="Password"
autoComplete="current-password"
value={secret}
onChange={({ target }) => setSecret(target.value)}
/>
<button type="submit">Login</button>
</form>
);
}

View File

@@ -0,0 +1,15 @@
import styled from 'styled-components';
export const Input = styled.input`
width: 100%;
heght: 1.3rem;
backgroundColor: blue
`
export const Button = styled.button`
width: fit-content;
margin: auto;
text-align: center;
background-color: #a7c42;
`

View File

@@ -0,0 +1,2 @@
export * from './ILoginFormProps';
export * from './LoginForm';

View File

@@ -0,0 +1 @@
export * from './LoginForm';

View File

@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,13 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import {Provider} from 'react-redux';
import store from './store';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<StrictMode>
<App />
</StrictMode>
</Provider>
);

View File

@@ -0,0 +1,18 @@
import {configureStore} from '@reduxjs/toolkit';
import {userSlice} from './slices';
// Configure the main store
const store = configureStore({
reducer: {
// Add the reducer from your slice to the store
user: userSlice.reducer
}
});
export default store;
// Type definition for the root state, can be used with useSelector hook
export type RootState = ReturnType<typeof store.getState>;
// Type definition for dispatch, can be used with useDispatch hook
export type AppDispatch = typeof store.dispatch;

View File

@@ -0,0 +1,40 @@
import {createSlice, configureStore, PayloadAction} from '@reduxjs/toolkit';
interface IUserState {
id: string | null;
name: string | null;
isLoading: boolean;
error: string | null;
}
const initialState: IUserState = {
id: null,
name: null,
isLoading: false,
error: null
};
export const userSlice = createSlice({
name: 'user', // A name for this slice, used in action types
initialState,
reducers: {
// The key here becomes part of the action type: 'user/setUserLoading'
setUserLoading(state, action: PayloadAction<boolean>) {
// Redux Toolkit uses Immer, so you can "mutate" the state directly.
// Immer handles the immutable update behind the scenes.
state.isLoading = action.payload;
},
userFetchSuccess(state, action: PayloadAction<{id: string; name: string}>) {
state.isLoading = false;
state.id = action.payload.id;
state.name = action.payload.name;
state.error = null;
},
userFetchError(state, action: PayloadAction<string>) {
state.isLoading = false;
state.error = action.payload;
}
}
});
export const {setUserLoading, userFetchSuccess, userFetchError} = userSlice.actions;

View File

@@ -0,0 +1 @@
export * from './User.slice';

View File

@@ -0,0 +1,14 @@
import {LoginForm} from '../../components';
export function LoginView() {
const handleLoginSubmit = (username: string, secret: string) => {
console.log('handling login submit username [%o] secret [%o]', username, secret)
};
console.log('LoginView...')
return <>
<LoginForm onSubmit={handleLoginSubmit} />
</>
}

View File

@@ -0,0 +1 @@
export * from './LoginView';

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": [],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,95 @@
import type {UserConfig} from 'vite';
import path from 'node:path';
import {defineConfig} from 'vite';
import VitePluginReact from '@vitejs/plugin-react-swc';
import ViteBabelPlugin from 'vite-plugin-babel';
// https://vite.dev/config/
export default defineConfig(({mode}) => {
const isProductionMode = mode === 'production';
// optional: const modeDependentEnvironmentVariables = loadEnv(mode, process.cwd()); // https://vite.dev/config/#using-environment-variables-in-config
return {
define: {},
root: path.resolve(process.cwd()),
publicDir: 'public',
cacheDir: 'node_modules/.vite', // default is `node_modules/.vite` <-- https://vite.dev/config/shared-options.html#cachedir
base: '/',
mode,
resolve: {
alias: {
assets: path.resolve(process.cwd(), 'src/assets'),
components: path.resolve(process.cwd(), 'src/components'),
services: path.resolve(process.cwd(), 'src/services'),
store: path.resolve(process.cwd(), 'src/store'),
styles: path.resolve(process.cwd(), 'src/styles'),
themes: path.resolve(process.cwd(), 'src/themes'),
utils: path.resolve(process.cwd(), 'src/utils'),
views: path.resolve(process.cwd(), 'src/views')
},
dedupe: [],
conditions: ['module', 'browser', 'development|production'], // default is `['module', 'browser', 'development|production']`
mainFields: ['browser', 'module'], // default is `['browser', 'module', 'jsnext:main', 'jsnext']`
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
preserveSymlinks: false // default is false
},
css: {
modules: undefined, // default is `undefined`
// postcss: {},
devSourcemap: true, // default is `false` **experimental Vite feature**
transformer: 'postcss' // default is `postcss` **experimental Vite feature**
},
json: {
stringify: true // default is 'auto' // https://vite.dev/config/shared-options.html#json-stringify
},
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
jsxInject: `import React from 'react'`
},
logLevel: 'debug', // default is `info`
clearScreen: false, // default is `true`
envDir: '.',
appType: 'spa',
server: {
host: 'localhost',
allowedHosts: ['localhost', '.phenixrts.com'],
port: 5173,
open: true,
hmr: true,
fs: {
strict: true,
deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] // default is `['.env', '.env.*', '*.{crt,pem}', '**/.git/**']`
}
},
build: {
target: ['es2020', 'chrome80', 'firefox78', 'safari14'],
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096, // default is `4096` ~ 4KiB
cssCodeSplit: true,
minify: isProductionMode,
cssMinify: isProductionMode,
sourcemap: !isProductionMode,
manifest: false, // default is `false`<-- https://vite.dev/config/build-options.html#build-manifest ,
ssrManifest: false,
ssr: false,
emitAssets: false,
terserOptions: {},
emptyOutDir: true,
copyPublicDir: true,
reportCompressedSize: true,
chunkSizeWarningLimit: 500,
watch: null
},
plugins: [
VitePluginReact(),
ViteBabelPlugin({
babelConfig: {
plugins: ['transform-amd-to-commonjs', 'babel-plugin-styled-components']
}
})
]
} satisfies UserConfig;
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "@techniker-me/websocket-chat",
"name": "@techniker-me/websocket-chat",
"version": "0.0.0",
"private": true,
"workspaces": [
@@ -11,16 +11,16 @@
"build:types": "npm run build -w @techniker-me/websocket-shared-types",
"dev": "npm run dev --workspaces --if-present",
"dev:server": "npm run dev -w @techniker-me/websocket-server",
"dev:frontend": "npm run dev -w @techniker-me/websocket-frontend",
"dev:all": "concurrently \"npm run dev:server\" \"npm run dev:frontend\"",
"dev:frontend:vanilla": "npm run dev -w @techniker-me/websocket-frontend",
"clean": "npm run clean --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present",
"start": "npm run build:types && npm run dev:all"
},
"devDependencies": {
"typescript": "5.9.2",
"concurrently": "8.2.2"
"concurrently": "8.2.2",
"prettier": "3.6.2",
"typescript": "5.9.2"
},
"author": "Alexander Zinn"
}

View File

@@ -11,7 +11,7 @@
"typecheck": "tsc"
},
"devDependencies": {
"typescript": "~5.8.3"
"typescript": "~5.9.2"
},
"dependencies": {}
}

View File

@@ -39,3 +39,5 @@ export interface ExtendedWebSocket {
isOpen(): boolean;
isClosed(): boolean;
}
export type Nullable<T> = T | null;