From 16e037ef4b948943f4387d06e5c30c191431cb63 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Fri, 5 Sep 2025 23:07:15 +0300 Subject: [PATCH 01/22] feat: add vite, eslint, prettier, husky, vitest --- .gitignore | 30 +++++++++++++++++++ .husky/pre-commit | 1 + .husky/pre-push | 1 + .prettierrc | 6 ++++ README.md | 70 +++++++++++++++++++++++++++++++++++++++++++- __tests__/setup.ts | 1 + eslint.config.js | 48 ++++++++++++++++++++++++++++++ index.html | 13 ++++++++ package.json | 48 ++++++++++++++++++++++++++++++ public/vite.svg | 1 + src/App.sass | 34 +++++++++++++++++++++ src/App.test.tsx | 9 ++++++ src/App.tsx | 35 ++++++++++++++++++++++ src/assets/react.svg | 1 + src/index.sass | 61 ++++++++++++++++++++++++++++++++++++++ src/main.tsx | 10 +++++++ src/vite-env.d.ts | 1 + tsconfig.app.json | 27 +++++++++++++++++ tsconfig.json | 7 +++++ tsconfig.node.json | 25 ++++++++++++++++ vite.config.ts | 7 +++++ vitest.config.ts | 28 ++++++++++++++++++ 22 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .husky/pre-commit create mode 100644 .husky/pre-push create mode 100644 .prettierrc create mode 100644 __tests__/setup.ts create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/App.sass create mode 100644 src/App.test.tsx create mode 100644 src/App.tsx create mode 100644 src/assets/react.svg create mode 100644 src/index.sass create mode 100644 src/main.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5f6016 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +build +package-lock.json +*.lnk +coverage +.editorconfig + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..3867a0f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run lint diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..e37998f --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run test diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0a72520 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/README.md b/README.md index 615ca05..88a8952 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ -# rest-client-app +# 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 + +## 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 tseslint.config([ + 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 tseslint.config([ + 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... + }, + }, +]); +``` diff --git a/__tests__/setup.ts b/__tests__/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/__tests__/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..7246ca3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,48 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import react from 'eslint-plugin-react'; +import tseslint from 'typescript-eslint'; +import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'; +import reactCompiler from 'eslint-plugin-react-compiler'; + +export default tseslint.config( + { + ignores: ['dist', 'coverage', '**/owid-co2-data.json'], + }, + { + extends: [ + js.configs.recommended, + ...tseslint.configs.strict, + eslintPluginPrettier, + ], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'react-compiler': reactCompiler, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'react-compiler/react-compiler': 'error', + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + '@typescript-eslint/no-non-null-assertion': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, + } +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..084b7e1 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "rest-client-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "format:fix": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "^4.0.0", + "@vitest/coverage-v8": "^3.2.4", + "classnames": "^2.5.1", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "husky": "^9.1.7", + "jsdom": "^26.1.0", + "prettier": "3.6.2", + "sass": "^1.92.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2", + "vitest": "^3.2.4" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.sass b/src/App.sass new file mode 100644 index 0000000..446cd13 --- /dev/null +++ b/src/App.sass @@ -0,0 +1,34 @@ +#root + max-width: 1280px + margin: 0 auto + padding: 2rem + text-align: center + +.logo + height: 6em + padding: 1.5em + will-change: filter + transition: filter 300ms + + &:hover + filter: drop-shadow(0 0 2em #646cffaa) + + &.react:hover + filter: drop-shadow(0 0 2em #61dafbaa) + +@keyframes logo-spin + from + transform: rotate(0deg) + + to + transform: rotate(360deg) + +@media (prefers-reduced-motion: no-preference) + a:nth-of-type(2) .logo + animation: logo-spin infinite 20s linear + +.card + padding: 2em + +.read-the-docs + color: #888 diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..b53da93 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/react'; +import { expect, test } from 'vitest'; +import App from './App'; + +test('Show modal Uncontrolled components Form', async () => { + render(); + + expect(screen.getByTestId('app')).toBeInTheDocument(); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1f0d9cc --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,35 @@ +import './App.sass'; +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.sass b/src/index.sass new file mode 100644 index 0000000..d43992a --- /dev/null +++ b/src/index.sass @@ -0,0 +1,61 @@ +: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 diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..728f3b6 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import './index.sass'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..11bd5a8 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "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", "__tests__"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "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"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6da11b5 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..53e175d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + include: ['**/*.tsx'], + exclude: [ + '**/node_modules/**', + '**/*.test.tsx', + '**/*.spec.tsx', + 'src/__tests__/setup.ts', + 'src/index.{js,jsx,ts,tsx}', + 'src/main.{js,jsx,ts,tsx}', + 'src/**/*.d.ts', + ], + thresholds: { + statements: 80, + branches: 50, + functions: 50, + lines: 50, + }, + }, + globals: true, + environment: 'jsdom', + setupFiles: './__tests__/setup.ts', + testTimeout: 60000, + }, +}); From 26b7f4644b9e3b8b6c0748e9db2f4466c329a79c Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 6 Sep 2025 04:28:33 +0300 Subject: [PATCH 02/22] feat: add pages and routing --- package.json | 16 ++++- src/App.sass | 34 ----------- src/App.tsx | 35 ----------- src/components/Footer/Footer.tsx | 9 +++ src/components/Header/Header.module.sass | 10 +++ src/components/Header/Header.tsx | 37 +++++++++++ src/entry.browser.tsx | 52 ++++++++++++++++ src/entry.rsc.tsx | 46 ++++++++++++++ src/entry.ssr.tsx | 38 ++++++++++++ src/index.sass | 61 ------------------- src/main.tsx | 10 --- src/routes/config.ts | 38 ++++++++++++ src/routes/history/route.tsx | 12 ++++ .../home/home.test.tsx} | 6 +- src/routes/home/route.tsx | 15 +++++ src/routes/login/route.tsx | 12 ++++ src/routes/rest/route.tsx | 12 ++++ src/routes/root/app.sass | 18 ++++++ src/routes/root/client.tsx | 42 +++++++++++++ src/routes/root/route.tsx | 24 ++++++++ src/routes/root/server-function.tsx | 8 +++ src/routes/root/styles.sass | 54 ++++++++++++++++ src/routes/variables/route.tsx | 12 ++++ vite.config.ts | 14 ++++- 24 files changed, 468 insertions(+), 147 deletions(-) delete mode 100644 src/App.sass delete mode 100644 src/App.tsx create mode 100644 src/components/Footer/Footer.tsx create mode 100644 src/components/Header/Header.module.sass create mode 100644 src/components/Header/Header.tsx create mode 100644 src/entry.browser.tsx create mode 100644 src/entry.rsc.tsx create mode 100644 src/entry.ssr.tsx delete mode 100644 src/index.sass delete mode 100644 src/main.tsx create mode 100644 src/routes/config.ts create mode 100644 src/routes/history/route.tsx rename src/{App.test.tsx => routes/home/home.test.tsx} (60%) create mode 100644 src/routes/home/route.tsx create mode 100644 src/routes/login/route.tsx create mode 100644 src/routes/rest/route.tsx create mode 100644 src/routes/root/app.sass create mode 100644 src/routes/root/client.tsx create mode 100644 src/routes/root/route.tsx create mode 100644 src/routes/root/server-function.tsx create mode 100644 src/routes/root/styles.sass create mode 100644 src/routes/variables/route.tsx diff --git a/package.json b/package.json index 084b7e1..56c97df 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "cross-env NODE_ENV=development vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", @@ -14,8 +14,13 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@remix-run/node-fetch-server": "^0.8.0", + "compression": "^1.8.1", + "cross-env": "^10.0.0", + "express": "^5.1.0", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router": "7.7.0" }, "devDependencies": { "@eslint/js": "^9.33.0", @@ -23,16 +28,20 @@ "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/compression": "^1.8.1", + "@types/express": "^5.0.3", + "@types/node": "^24.3.1", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react-swc": "^4.0.0", + "@vitejs/plugin-rsc": "^0.4.27", "@vitest/coverage-v8": "^3.2.4", "classnames": "^2.5.1", "eslint": "^9.33.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", + "eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", @@ -43,6 +52,7 @@ "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", "vite": "^7.1.2", + "vite-plugin-devtools-json": "^1.0.0", "vitest": "^3.2.4" } } diff --git a/src/App.sass b/src/App.sass deleted file mode 100644 index 446cd13..0000000 --- a/src/App.sass +++ /dev/null @@ -1,34 +0,0 @@ -#root - max-width: 1280px - margin: 0 auto - padding: 2rem - text-align: center - -.logo - height: 6em - padding: 1.5em - will-change: filter - transition: filter 300ms - - &:hover - filter: drop-shadow(0 0 2em #646cffaa) - - &.react:hover - filter: drop-shadow(0 0 2em #61dafbaa) - -@keyframes logo-spin - from - transform: rotate(0deg) - - to - transform: rotate(360deg) - -@media (prefers-reduced-motion: no-preference) - a:nth-of-type(2) .logo - animation: logo-spin infinite 20s linear - -.card - padding: 2em - -.read-the-docs - color: #888 diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 1f0d9cc..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import './App.sass'; -import { useState } from 'react'; -import reactLogo from './assets/react.svg'; -import viteLogo from '/vite.svg'; - -function App() { - const [count, setCount] = useState(0); - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ); -} - -export default App; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..766e4ca --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,9 @@ +import type { JSX } from 'react'; + +export function Footer(): JSX.Element { + return ( +
+
Footer
+
+ ); +} diff --git a/src/components/Header/Header.module.sass b/src/components/Header/Header.module.sass new file mode 100644 index 0000000..4d73225 --- /dev/null +++ b/src/components/Header/Header.module.sass @@ -0,0 +1,10 @@ + +.headerContainer + display: flex + justify-content: start + align-items: center + column-gap: 30px + +.links + display: flex + column-gap: 20px diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..7955467 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,37 @@ +import s from './Header.module.sass'; +import { type JSX } from 'react'; +import classNames from 'classnames'; +import { NavLink } from 'react-router'; + +export function Header(): JSX.Element { + const headerStyles = classNames('header ', s.header); + const headerContainerStyles = classNames('container ', s.headerContainer); + + return ( +
+
+ + + +
+
+ ); +} diff --git a/src/entry.browser.tsx b/src/entry.browser.tsx new file mode 100644 index 0000000..6fc925b --- /dev/null +++ b/src/entry.browser.tsx @@ -0,0 +1,52 @@ +import { + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + setServerCallback, +} from '@vitejs/plugin-rsc/browser'; +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { + unstable_createCallServer as createCallServer, + unstable_getRSCStream as getRSCStream, + unstable_RSCHydratedRouter as RSCHydratedRouter, + type unstable_RSCPayload as RSCServerPayload, + type DataRouter, +} from 'react-router'; + +// Create and set the callServer function to support post-hydration server actions. +setServerCallback( + createCallServer({ + createFromReadableStream, + createTemporaryReferenceSet, + encodeReply, + }) +); + +// Get and decode the initial server payload +createFromReadableStream(getRSCStream()).then((payload) => { + startTransition(async () => { + const formState = + payload.type === 'render' ? await payload.formState : undefined; + + hydrateRoot( + document, + + + , + { + // @ts-expect-error - no types for this yet + formState, + } + ); + }); +}); + +if (import.meta.hot) { + import.meta.hot.on('rsc:update', () => { + (window as unknown as { __router: DataRouter }).__router.revalidate(); + }); +} diff --git a/src/entry.rsc.tsx b/src/entry.rsc.tsx new file mode 100644 index 0000000..c065dc8 --- /dev/null +++ b/src/entry.rsc.tsx @@ -0,0 +1,46 @@ +import { + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + renderToReadableStream, +} from '@vitejs/plugin-rsc/rsc'; +import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-router'; + +import { routes } from './routes/config'; + +function fetchServer(request: Request) { + return matchRSCServerRequest({ + // Provide the React Server touchpoints. + createTemporaryReferenceSet, + decodeAction, + decodeFormState, + decodeReply, + loadServerAction, + // The incoming request. + request, + // The app routes. + routes: routes(), + // Encode the match with the React Server implementation. + generateResponse(match, options) { + return new Response(renderToReadableStream(match.payload, options), { + status: match.statusCode, + headers: match.headers, + }); + }, + }); +} + +export default async function handler(request: Request) { + // Import the generateHTML function from the client environment + const ssr = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr') + >('ssr', 'index'); + + return ssr.generateHTML(request, fetchServer); +} + +if (import.meta.hot) { + import.meta.hot.accept(); +} diff --git a/src/entry.ssr.tsx b/src/entry.ssr.tsx new file mode 100644 index 0000000..ee73f44 --- /dev/null +++ b/src/entry.ssr.tsx @@ -0,0 +1,38 @@ +import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'; +import { renderToReadableStream as renderHTMLToReadableStream } from 'react-dom/server.edge'; +import { + unstable_routeRSCServerRequest as routeRSCServerRequest, + unstable_RSCStaticRouter as RSCStaticRouter, +} from 'react-router'; + +export async function generateHTML( + request: Request, + fetchServer: (request: Request) => Promise +): Promise { + return await routeRSCServerRequest({ + // The incoming request. + request, + // How to call the React Server. + fetchServer, + // Provide the React Server touchpoints. + createFromReadableStream, + // Render the router to HTML. + async renderHTML(getPayload) { + const payload = await getPayload(); + const formState = + payload.type === 'render' ? await payload.formState : undefined; + + const bootstrapScriptContent = + await import.meta.viteRsc.loadBootstrapScriptContent('index'); + + return await renderHTMLToReadableStream( + , + { + bootstrapScriptContent, + // @ts-expect-error - no types for this yet + formState, + } + ); + }, + }); +} diff --git a/src/index.sass b/src/index.sass deleted file mode 100644 index d43992a..0000000 --- a/src/index.sass +++ /dev/null @@ -1,61 +0,0 @@ -: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 diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 728f3b6..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import './index.sass'; -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App.tsx'; - -createRoot(document.getElementById('root')!).render( - - - -); diff --git a/src/routes/config.ts b/src/routes/config.ts new file mode 100644 index 0000000..a024987 --- /dev/null +++ b/src/routes/config.ts @@ -0,0 +1,38 @@ +import type { unstable_RSCRouteConfig as RSCRouteConfig } from 'react-router'; + +export function routes() { + return [ + { + id: 'root', + path: '', + lazy: () => import('./root/route'), + children: [ + { + id: 'home', + index: true, + lazy: () => import('./home/route'), + }, + { + id: 'login', + path: 'login', + lazy: () => import('./login/route'), + }, + { + id: 'variables', + path: 'variables', + lazy: () => import('./variables/route'), + }, + { + id: 'history', + path: 'history', + lazy: () => import('./history/route'), + }, + { + id: 'rest', + path: 'rest', + lazy: () => import('./rest/route'), + }, + ], + }, + ] satisfies RSCRouteConfig; +} diff --git a/src/routes/history/route.tsx b/src/routes/history/route.tsx new file mode 100644 index 0000000..58a36f2 --- /dev/null +++ b/src/routes/history/route.tsx @@ -0,0 +1,12 @@ +export default function History() { + return ( +
+
+
+

History Page

+

This is the History Page of our application.

+
+
+
+ ); +} diff --git a/src/App.test.tsx b/src/routes/home/home.test.tsx similarity index 60% rename from src/App.test.tsx rename to src/routes/home/home.test.tsx index b53da93..75047a6 100644 --- a/src/App.test.tsx +++ b/src/routes/home/home.test.tsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react'; import { expect, test } from 'vitest'; -import App from './App'; +import Home from './route'; test('Show modal Uncontrolled components Form', async () => { - render(); + render(); - expect(screen.getByTestId('app')).toBeInTheDocument(); + expect(screen.getByTestId('home')).toBeInTheDocument(); }); diff --git a/src/routes/home/route.tsx b/src/routes/home/route.tsx new file mode 100644 index 0000000..3a1c24f --- /dev/null +++ b/src/routes/home/route.tsx @@ -0,0 +1,15 @@ +export default function Home() { + return ( +
+
+
+

Home Page

+

general information about the developers, project, and course

+ + + +
+
+
+ ); +} diff --git a/src/routes/login/route.tsx b/src/routes/login/route.tsx new file mode 100644 index 0000000..a488fb6 --- /dev/null +++ b/src/routes/login/route.tsx @@ -0,0 +1,12 @@ +export default function Login() { + return ( +
+
+
+

Login Page

+

This is the Login Page of our application.

+
+
+
+ ); +} diff --git a/src/routes/rest/route.tsx b/src/routes/rest/route.tsx new file mode 100644 index 0000000..a88f908 --- /dev/null +++ b/src/routes/rest/route.tsx @@ -0,0 +1,12 @@ +export default function Rest() { + return ( +
+
+
+

Rest Page

+

This is the Rest Page of our application.

+
+
+
+ ); +} diff --git a/src/routes/root/app.sass b/src/routes/root/app.sass new file mode 100644 index 0000000..02eb0d9 --- /dev/null +++ b/src/routes/root/app.sass @@ -0,0 +1,18 @@ +body + display: flex + flex-direction: column + min-height: 100% + +main + flex: 1 0 auto + flex: 1 1 auto + padding: 20px 0 + background-color: #92D8F9 + +.header, +.footer + min-height: 50px + background-color: #c6f7b0 + +.main + background-color: #92D8F9 diff --git a/src/routes/root/client.tsx b/src/routes/root/client.tsx new file mode 100644 index 0000000..ccb7a56 --- /dev/null +++ b/src/routes/root/client.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { isRouteErrorResponse, useRouteError } from 'react-router'; +import { Footer } from '../../components/Footer/Footer'; +import { Header } from '../../components/Header/Header'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + +
+ {children} +