Compare commits
18 Commits
dbfc23068c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49b67f7654 | ||
|
|
e6f57d3cbc | ||
|
|
456def177a | ||
|
|
241ea51d34 | ||
|
|
554f14c8d9 | ||
|
|
d5c52968a6 | ||
|
|
88de79086e | ||
|
|
6577698459 | ||
|
|
419113541b | ||
|
|
22031864fb | ||
|
|
832bc2b209 | ||
|
|
8b13335c6c | ||
|
|
0804eba920 | ||
|
|
6d7060aceb | ||
|
|
9ad33b98e7 | ||
|
|
4502c7aef0 | ||
|
|
b7ce3dbc21 | ||
|
|
cd4f632c85 |
34
.vscode/settings.json
vendored
Normal file
34
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.addMissingImports": "always",
|
||||||
|
"source.fixAll": "always"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.next": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/package-lock.json": true
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json][jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"prettier.configPath": "react-advanced-tag1/.prettierrc.mjs"
|
||||||
|
}
|
||||||
2
react-advanced-tag1/.gitignore
vendored
2
react-advanced-tag1/.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
|||||||
12
react-advanced-tag1/.prettierrc.mjs
Normal file
12
react-advanced-tag1/.prettierrc.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @see https://prettier.io/docs/configuration
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
19
react-advanced-tag1/.storybook/main.ts
Normal file
19
react-advanced-tag1/.storybook/main.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@storybook/addon-onboarding",
|
||||||
|
"@chromatic-com/storybook",
|
||||||
|
"@storybook/experimental-addon-test"
|
||||||
|
],
|
||||||
|
"framework": {
|
||||||
|
"name": "@storybook/react-vite",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
15
react-advanced-tag1/.storybook/preview.ts
Normal file
15
react-advanced-tag1/.storybook/preview.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Preview } from '@storybook/react'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default preview
|
||||||
9
react-advanced-tag1/.storybook/vitest.setup.ts
Normal file
9
react-advanced-tag1/.storybook/vitest.setup.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { beforeAll } from 'vitest';
|
||||||
|
import { setProjectAnnotations } from '@storybook/react';
|
||||||
|
import * as projectAnnotations from './preview';
|
||||||
|
|
||||||
|
// This is an important step to apply the right configuration when testing your stories.
|
||||||
|
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||||
|
const project = setProjectAnnotations([projectAnnotations]);
|
||||||
|
|
||||||
|
beforeAll(project.beforeAll);
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<title>React Advanced Cours</title>
|
<title>React Advanced Cours</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="modal"></div>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
4444
react-advanced-tag1/package-lock.json
generated
4444
react-advanced-tag1/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,26 +5,63 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"scan": "vite & npx react-scan@latest localhost:5173",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"test:coverage:report": "vitest --coverage --reporter=html",
|
||||||
|
"test:coverage:report:open": "vitest --coverage --reporter=html && open coverage/index.html",
|
||||||
|
"prettier": "prettier ./src --write",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"mobx": "^6.13.7",
|
||||||
|
"mobx-react-lite": "^4.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.5.0",
|
||||||
|
"react-scan": "^0.3.3",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@chromatic-com/storybook": "^3.2.6",
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@storybook/addon-essentials": "^8.6.12",
|
||||||
|
"@storybook/addon-onboarding": "^8.6.12",
|
||||||
|
"@storybook/blocks": "^8.6.12",
|
||||||
|
"@storybook/experimental-addon-test": "^8.6.12",
|
||||||
|
"@storybook/react": "^8.6.12",
|
||||||
|
"@storybook/react-vite": "^8.6.12",
|
||||||
|
"@storybook/test": "^8.6.12",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/browser": "^3.1.1",
|
||||||
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
|
"@vitest/ui": "^3.1.1",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"eslint-plugin-storybook": "^0.12.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"playwright": "^1.51.1",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"storybook": "^8.6.12",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.24.1",
|
"typescript-eslint": "^8.24.1",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.0",
|
||||||
"vitest": "^3.1.1"
|
"vitest": "^3.1.1"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"plugin:storybook/recommended"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 6em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
@@ -41,7 +34,7 @@
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type='text'] {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
// import EffectExercises from './components/exercises/EffectExercises'
|
// import EffectExercises from './components/exercises/EffectExercises'
|
||||||
import UserEffect from './components/exercises/UserEffect'
|
|
||||||
import { UserOverview } from './components/exercises/UserOverview'
|
|
||||||
import Heading from './components/globals/Heading'
|
|
||||||
import MainLayout from './layouts/MainLayout'
|
import MainLayout from './layouts/MainLayout'
|
||||||
|
import HomePage from './pages/HomePage'
|
||||||
|
import UsersPage from './pages/UsersPage'
|
||||||
|
import EffectExercisesPage from './pages/EffectExercisesPage'
|
||||||
|
import FormsPage from './pages/FormsPage'
|
||||||
|
import MemoCallbackPage from './pages/MemoCallbackPage'
|
||||||
|
import ComponentWrapperPage from './pages/ComponentWrapperPage'
|
||||||
|
import ZustandCounterPage from './pages/ZustandCounterPage'
|
||||||
|
import ModalPage from './pages/ModalPage'
|
||||||
|
import { HocPage } from './pages/HocPage'
|
||||||
|
import ImmerPage from './pages/ImmerPage'
|
||||||
|
import MobXPage from './pages/MobXPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
return (
|
||||||
return <MainLayout>
|
<MainLayout>
|
||||||
<Heading />
|
<Routes>
|
||||||
{/* <EffectExercises /> */}
|
<Route path="/" element={<HomePage />} />
|
||||||
<UserEffect />
|
<Route path="/user" element={<UsersPage />} />
|
||||||
<UserOverview />
|
<Route path="/effect" element={<EffectExercisesPage />} />
|
||||||
|
<Route path="/forms" element={<FormsPage />} />
|
||||||
|
<Route path="/memocallback" element={<MemoCallbackPage />} />
|
||||||
|
<Route path="/componentwrapper" element={<ComponentWrapperPage />} />
|
||||||
|
<Route path="/modalpage" element={<ModalPage />} />
|
||||||
|
<Route path="/zustandcounterpage" element={<ZustandCounterPage />} />
|
||||||
|
<Route path="/hoc" element={<HocPage />} />
|
||||||
|
<Route path="/immer" element={<ImmerPage />} />
|
||||||
|
<Route path="/mobx" element={<MobXPage />} />
|
||||||
|
</Routes>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.component-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
background-color: #888;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.component-wrapper {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.component-content {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import './ComponentWrapper.css'
|
||||||
|
|
||||||
|
interface ComponentWrapperProps {
|
||||||
|
/** Title to show */
|
||||||
|
title?: string
|
||||||
|
/** Description to show */
|
||||||
|
description?: string
|
||||||
|
/** Content to show */
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper component that displays a title, description, and content.
|
||||||
|
*/
|
||||||
|
export default function ComponentWrapper({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: ComponentWrapperProps): React.ReactElement {
|
||||||
|
console.log('ComponentWrapper rendered:', title)
|
||||||
|
return (
|
||||||
|
<section className="component-wrapper">
|
||||||
|
{title && <h2>{title}</h2>}
|
||||||
|
{description && <p>{description}</p>}
|
||||||
|
<div className="component-content">{children}</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
react-advanced-tag1/src/common/components/Footer.test.tsx
Normal file
10
react-advanced-tag1/src/common/components/Footer.test.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import Footer from './Footer'
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
describe('Footer Component', () => {
|
||||||
|
it('should render the footer with correct content', () => {
|
||||||
|
render(<Footer />)
|
||||||
|
expect(screen.getByText(/© 2025 Your Company/i)).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
10
react-advanced-tag1/src/common/components/Footer.tsx
Normal file
10
react-advanced-tag1/src/common/components/Footer.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function Footer() {
|
||||||
|
console.log('Footer rendered')
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Your Company</p>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import './Navigation.css'
|
||||||
|
|
||||||
|
export default function Navigation() {
|
||||||
|
return (
|
||||||
|
<nav className="navigation">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/user">Users</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/effect">Effects</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/forms">Forms</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/memocallback">Memo und Callback</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/componentwrapper">Component Wrapper</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/modalpage">Modal</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/zustandcounterpage">Store: Zustand</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/mobx">Store: MobX</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/hoc">HOC</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/immer">Immer</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
react-advanced-tag1/src/common/components/Header/index.tsx
Normal file
13
react-advanced-tag1/src/common/components/Header/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Navigation from './Navigation'
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
console.log('Header rendered')
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<h1>My Application</h1>
|
||||||
|
<Navigation />
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #282c34;
|
||||||
|
}
|
||||||
|
.navigation a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.navigation a:hover {
|
||||||
|
background-color: #057e9f;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.navigation ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.navigation ul li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.navigation {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.navigation a {
|
||||||
|
color: #282c34;
|
||||||
|
}
|
||||||
|
.navigation a:hover {
|
||||||
|
background-color: #98e1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation ul li {
|
||||||
|
color: #282c34;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
react-advanced-tag1/src/common/components/Modal.css
Normal file
18
react-advanced-tag1/src/common/components/Modal.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: red;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
25
react-advanced-tag1/src/common/components/Modal.tsx
Normal file
25
react-advanced-tag1/src/common/components/Modal.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import './Modal.css'
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
open?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
open = true,
|
||||||
|
onClose,
|
||||||
|
}: ModalProps): React.ReactElement {
|
||||||
|
console.log('Modal rendered')
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ display: open ? 'block' : 'none' }}
|
||||||
|
className="modal"
|
||||||
|
onClick={() => onClose && onClose()}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<h2>Modal</h2>
|
||||||
|
<p>This is a modal component.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export default function EffectExercises() {
|
||||||
|
const [counter, setCounter] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('EffectExercises mounted')
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCounter((prevCounter) => prevCounter + 1)
|
||||||
|
}, 1000)
|
||||||
|
return () => {
|
||||||
|
console.log('EffectExercises unmounted')
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [counter])
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>Effect Exercises</h2>
|
||||||
|
<p>Counter: {counter}</p>
|
||||||
|
|
||||||
|
<button onClick={() => setCounter(0)}>Reset</button>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import withLoader, { LoaderData } from './WithLoader'
|
||||||
|
import './DogImages.css'
|
||||||
|
interface DogImagesProps {
|
||||||
|
data: LoaderData
|
||||||
|
}
|
||||||
|
|
||||||
|
function DogImages({ data }: DogImagesProps) {
|
||||||
|
return data.message.map((img, index) => (
|
||||||
|
<img key={index} src={img} className="dog-image" />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const DogImagesWithLoader = withLoader(
|
||||||
|
DogImages,
|
||||||
|
'https://dog.ceo/api/breed/labrador/images/random/6'
|
||||||
|
)
|
||||||
|
export default DogImagesWithLoader
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { ComponentType, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export type LoaderData = {
|
||||||
|
message: string[]
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WithLoaderProps {
|
||||||
|
data: LoaderData
|
||||||
|
}
|
||||||
|
|
||||||
|
// HOC definition
|
||||||
|
export default function withLoader<P>(
|
||||||
|
WrappedComponent: ComponentType<P & WithLoaderProps>,
|
||||||
|
url: string
|
||||||
|
) {
|
||||||
|
return function WithLoaderComponent(props: P) {
|
||||||
|
const [data, setData] = useState<LoaderData | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const json = await response.json()
|
||||||
|
// setData(json);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setData(json)
|
||||||
|
}, 5000)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <p>Loading...</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <WrappedComponent {...(props as P)} data={data} />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.dog-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { produce } from 'immer'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function ImmerExercise() {
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
name: 'Alice',
|
||||||
|
|
||||||
|
address: {
|
||||||
|
city: 'Wonderland',
|
||||||
|
|
||||||
|
zip: '12345',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateUser = () => {
|
||||||
|
// this is "normal" way to update state in objects
|
||||||
|
setUser((prevUser) => ({
|
||||||
|
...prevUser,
|
||||||
|
address: {
|
||||||
|
...prevUser.address,
|
||||||
|
city: 'New Wonderland',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserWithImmer = () => {
|
||||||
|
// immer way
|
||||||
|
setUser((prevUser) =>
|
||||||
|
produce(prevUser, (draft) => {
|
||||||
|
draft.address.city = 'New Wonderland'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{user.address.city}: {user.name}
|
||||||
|
</p>
|
||||||
|
<button onClick={updateUser}>Update User</button>
|
||||||
|
<button onClick={updateUserWithImmer}>Update User with immerJs</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
interface SearchProps {
|
||||||
|
onChange: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function Search({ onChange }: SearchProps): React.ReactElement {
|
||||||
|
console.log('Search rendered')
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder="Search user..."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Search)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import MemoCallback from './index'
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { users } from '../../../utils/shuffle'
|
||||||
|
|
||||||
|
describe('MemoCallback Component', () => {
|
||||||
|
it('should render the component with initial users', () => {
|
||||||
|
render(<MemoCallback />)
|
||||||
|
users.forEach((user) => {
|
||||||
|
expect(screen.getByText(user)).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter users based on search input', () => {
|
||||||
|
render(<MemoCallback />)
|
||||||
|
const searchInput = screen.getByPlaceholderText(/search/i)
|
||||||
|
|
||||||
|
fireEvent.change(searchInput, { target: { value: 'a' } })
|
||||||
|
const filteredUsers = users.filter((user) =>
|
||||||
|
user.toLowerCase().includes('a')
|
||||||
|
)
|
||||||
|
filteredUsers.forEach((user) => {
|
||||||
|
expect(screen.getByText(user)).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
const nonMatchingUsers = users.filter(
|
||||||
|
(user) => !user.toLowerCase().includes('a')
|
||||||
|
)
|
||||||
|
nonMatchingUsers.forEach((user) => {
|
||||||
|
expect(screen.queryByText(user)).not.toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should shuffle users when the shuffle button is clicked', () => {
|
||||||
|
render(<MemoCallback />)
|
||||||
|
const shuffleButton = screen.getByText(/shuffle/i)
|
||||||
|
|
||||||
|
const initialOrder = screen.getAllByText(/./).map((el) => el.textContent)
|
||||||
|
fireEvent.click(shuffleButton)
|
||||||
|
const shuffledOrder = screen.getAllByText(/./).map((el) => el.textContent)
|
||||||
|
|
||||||
|
expect(initialOrder).not.toEqual(shuffledOrder)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import Search from './Search'
|
||||||
|
import { users, shuffleArray } from '../../../utils/shuffle'
|
||||||
|
|
||||||
|
export default function MemoCallback() {
|
||||||
|
console.log('MemoCallback rendered')
|
||||||
|
|
||||||
|
const [allUsers, setAllUsers] = useState(users)
|
||||||
|
|
||||||
|
const handleSearch = useCallback((text: string) => {
|
||||||
|
const filteredUsers = users.filter((user) => {
|
||||||
|
return user.toLowerCase().includes(text.toLowerCase())
|
||||||
|
})
|
||||||
|
setAllUsers(filteredUsers)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleShuffle = () => {
|
||||||
|
setAllUsers(shuffleArray(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListOfUsers = allUsers.map((user) => {
|
||||||
|
return <p key={user}>{user}</p>
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={handleShuffle}>Shuffle</button>
|
||||||
|
<Search onChange={handleSearch} />
|
||||||
|
<div>{ListOfUsers}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { FormEvent, useRef, useState } from 'react'
|
||||||
|
import useLocalStorage from '../../../hooks/useLocalStorage'
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RefExercise(): React.ReactElement {
|
||||||
|
console.log('RefExercise rendered')
|
||||||
|
|
||||||
|
const emailRef = useRef<HTMLInputElement>(null)
|
||||||
|
const passwordRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { setStoredValue, value: formDataValues } = useLocalStorage<FormValues>(
|
||||||
|
'formdata',
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
console.log('submit', {
|
||||||
|
email: emailRef.current?.value,
|
||||||
|
password: passwordRef.current?.value,
|
||||||
|
})
|
||||||
|
setStoredValue({
|
||||||
|
email: emailRef.current?.value || '',
|
||||||
|
password: passwordRef.current?.value || '',
|
||||||
|
})
|
||||||
|
passwordRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
strasse: '',
|
||||||
|
city: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[event.target.name]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeSubmit = () => {
|
||||||
|
// This is a bad way to handle form submission
|
||||||
|
const strasse = document.querySelector(
|
||||||
|
'input[name="strasse"]'
|
||||||
|
) as HTMLInputElement
|
||||||
|
// This is a bad way to handle form submission
|
||||||
|
const city = document.querySelector(
|
||||||
|
'input[name="city"]'
|
||||||
|
) as HTMLInputElement
|
||||||
|
console.log('submit', {
|
||||||
|
strasse: strasse.value,
|
||||||
|
city: city.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
const data = new FormData(e.target as HTMLFormElement)
|
||||||
|
console.log('handleFormSubmit', {
|
||||||
|
email: data.get('email'),
|
||||||
|
password: data.get('password'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<h3>values by ref</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="email"
|
||||||
|
ref={emailRef}
|
||||||
|
defaultValue={formDataValues?.email}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="password"
|
||||||
|
ref={passwordRef}
|
||||||
|
defaultValue={formDataValues?.password}
|
||||||
|
/>
|
||||||
|
<button onClick={handleSubmit}>Submit</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||||
|
<h3>change event (bad way, but validation is possible)</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="strasse"
|
||||||
|
placeholder="Strasse"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="city"
|
||||||
|
placeholder="Stadt"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<button onClick={handleChangeSubmit}>submit</button>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
|
||||||
|
>
|
||||||
|
<h3>By Form Event</h3>
|
||||||
|
<input type="text" name="email" placeholder="Email" />
|
||||||
|
<input type="text" name="password" placeholder="password" />
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
website: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserEffect() {
|
||||||
|
const [user, setUser] = useState<User>()
|
||||||
|
const [userId, setUserId] = useState<number>(0)
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('UserEffect mounted')
|
||||||
|
|
||||||
|
if (userId === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now() // Start measuring performance
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// Using AbortController to cancel the fetch request if the component unmounts
|
||||||
|
// or if the userId changes before the fetch completes
|
||||||
|
const controller = new AbortController()
|
||||||
|
const signal = controller.signal
|
||||||
|
|
||||||
|
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
|
||||||
|
signal: signal,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
setUser(data)
|
||||||
|
const end = performance.now() // End measuring performance
|
||||||
|
const timeTaken = end - start // Calculate the time taken
|
||||||
|
console.log(`Fetch completed in ${Math.round(timeTaken)} milliseconds`)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('There was a problem with the fetch operation:', error)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
console.log('UserEffect unmounted')
|
||||||
|
// Cleanup function to abort the fetch request if the component unmounts
|
||||||
|
// or if the userId changes before the fetch completes
|
||||||
|
controller.abort('Fetch aborted')
|
||||||
|
}
|
||||||
|
}, [userId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
{userId === 0 && <p>Please select a user</p>}
|
||||||
|
{loading || userId === 0 ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
[{user?.id}] {user?.name}: {user?.website}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setUserId(1)}
|
||||||
|
style={{ padding: 10, border: '1px solid gray' }}
|
||||||
|
>
|
||||||
|
User 1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setUserId(2)}
|
||||||
|
style={{ padding: 10, border: '1px solid gray' }}
|
||||||
|
>
|
||||||
|
User 2
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setUserId(3)}
|
||||||
|
style={{ padding: 10, border: '1px solid gray' }}
|
||||||
|
>
|
||||||
|
User 3
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setUserId(4)}
|
||||||
|
style={{ padding: 10, border: '1px solid gray' }}
|
||||||
|
>
|
||||||
|
User 4
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useFetch } from '../../hooks/useFetch'
|
||||||
|
import { useMediaQuery } from '../../hooks/useMediaQuery'
|
||||||
|
|
||||||
|
export function UserOverview() {
|
||||||
|
const { data, loading, error } = useFetch(
|
||||||
|
'https://jsonplaceholder.typicode.com/users'
|
||||||
|
)
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery('(max-width: 600px)')
|
||||||
|
|
||||||
|
console.log('isMobile', isMobile)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p>Error: {error}</p>
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return <p>Loading...</p>
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <p>No data available</p>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{data.map(
|
||||||
|
(user: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
website: string
|
||||||
|
}) => (
|
||||||
|
<li key={user.id}>
|
||||||
|
<strong>{user.name}</strong>{' '}
|
||||||
|
<div style={{ display: isMobile ? 'none' : 'block' }}>
|
||||||
|
({user.email}) - {user.website}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<p>Total Users: {data.length}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
react-advanced-tag1/src/common/components/globals/Button.tsx
Normal file
43
react-advanced-tag1/src/common/components/globals/Button.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
||||||
|
|
||||||
|
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
|
||||||
|
variant: 'primary' | 'secondary'
|
||||||
|
size: 'sm' | 'md' | 'lg'
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Button component
|
||||||
|
*/
|
||||||
|
export const Button = ({
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps) => {
|
||||||
|
const sizeStyles: Record<ButtonProps['size'], CSSProperties> = {
|
||||||
|
sm: {
|
||||||
|
padding: '0.5rem',
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
padding: '0.75rem',
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const variantStyles: Record<ButtonProps['variant'], CSSProperties> = {
|
||||||
|
primary: {
|
||||||
|
backgroundColor: 'blue',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
backgroundColor: 'gray',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...restProps}
|
||||||
|
style={{ ...variantStyles[variant], ...sizeStyles[size] }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function Heading({
|
||||||
|
title = 'Hallo Teilnehmer',
|
||||||
|
}: {
|
||||||
|
title?: string
|
||||||
|
}): React.ReactElement {
|
||||||
|
console.log('Heading rendered')
|
||||||
|
return <h2>{title}</h2>
|
||||||
|
}
|
||||||
44
react-advanced-tag1/src/common/hooks/useFetch.ts
Normal file
44
react-advanced-tag1/src/common/hooks/useFetch.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
website: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetch(url: string) {
|
||||||
|
const [data, setData] = useState<User[] | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// alternativ
|
||||||
|
// const [fetchValues, setFetchValues] = useState({
|
||||||
|
// data: null,
|
||||||
|
// loading: true,
|
||||||
|
// error: null,
|
||||||
|
// });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
setError('Network response was not ok')
|
||||||
|
throw new Error('Network response was not ok')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setData(data)
|
||||||
|
setError(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error)
|
||||||
|
setError('Error fetching data')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData()
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
return { data, loading, error }
|
||||||
|
}
|
||||||
43
react-advanced-tag1/src/common/hooks/useLocalStorage.ts
Normal file
43
react-advanced-tag1/src/common/hooks/useLocalStorage.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T | null
|
||||||
|
) {
|
||||||
|
const [value, setValue] = useState<T | null>(() => {
|
||||||
|
try {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
const storedValue = localStorage.getItem(key)
|
||||||
|
return storedValue ? JSON.parse(storedValue) : initialValue
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from localStorage', error)
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const setStoredValue = (newValue: T) => {
|
||||||
|
try {
|
||||||
|
const valueToStore =
|
||||||
|
newValue instanceof Function ? newValue(value) : newValue
|
||||||
|
setValue(valueToStore)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing to localStorage', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeStoredValue = () => {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
setValue(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing from localStorage', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { value, setStoredValue, removeStoredValue }
|
||||||
|
}
|
||||||
30
react-advanced-tag1/src/common/hooks/useMediaQuery.ts
Normal file
30
react-advanced-tag1/src/common/hooks/useMediaQuery.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to check if a media query matches the current viewport.
|
||||||
|
* @param query - The media query string (e.g., '(max-width: 600px)')
|
||||||
|
* @returns A boolean indicating whether the media query matches.
|
||||||
|
*/
|
||||||
|
export function useMediaQuery(query: string) {
|
||||||
|
const [matches, setMatches] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQueryList = window.matchMedia(query)
|
||||||
|
const handleChange = (event: MediaQueryListEvent) => {
|
||||||
|
setMatches(event.matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the initial value
|
||||||
|
setMatches(mediaQueryList.matches)
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
mediaQueryList.addEventListener('change', handleChange)
|
||||||
|
|
||||||
|
// Cleanup function to remove the event listener
|
||||||
|
return () => {
|
||||||
|
mediaQueryList.removeEventListener('change', handleChange)
|
||||||
|
}
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
15
react-advanced-tag1/src/common/stores/counter-store.ts
Normal file
15
react-advanced-tag1/src/common/stores/counter-store.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface CounterState {
|
||||||
|
count: number
|
||||||
|
increment: () => void
|
||||||
|
decrement: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCounterStore = create<CounterState>((set) => ({
|
||||||
|
count: 0,
|
||||||
|
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||||
|
decrement: () => set((state) => ({ count: state.count - 1 })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default useCounterStore
|
||||||
29
react-advanced-tag1/src/common/stores/mobx-store.ts
Normal file
29
react-advanced-tag1/src/common/stores/mobx-store.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { makeAutoObservable } from 'mobx'
|
||||||
|
|
||||||
|
class CounterStore {
|
||||||
|
count = 0
|
||||||
|
message = 'Hello, MobX!'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.count--
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessage(newMessage: string) {
|
||||||
|
this.message = newMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const counterStore = new CounterStore()
|
||||||
|
export default counterStore
|
||||||
22
react-advanced-tag1/src/common/utils/shuffle.test.ts
Normal file
22
react-advanced-tag1/src/common/utils/shuffle.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { shuffleArray, users } from './shuffle'
|
||||||
|
|
||||||
|
describe('shuffleArray', () => {
|
||||||
|
it('should shuffle the array and return a new order', () => {
|
||||||
|
const originalArray = [...users]
|
||||||
|
const shuffledArray = shuffleArray([...users])
|
||||||
|
|
||||||
|
expect(shuffledArray).not.toEqual(originalArray) // Ensure the order is different
|
||||||
|
expect(shuffledArray.sort()).toEqual(originalArray.sort()) // Ensure all elements are still present
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when input is empty', () => {
|
||||||
|
const result = shuffleArray([])
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle arrays with one element', () => {
|
||||||
|
const result = shuffleArray(['onlyElement'])
|
||||||
|
expect(result).toEqual(['onlyElement'])
|
||||||
|
})
|
||||||
|
})
|
||||||
10
react-advanced-tag1/src/common/utils/shuffle.ts
Normal file
10
react-advanced-tag1/src/common/utils/shuffle.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function shuffleArray(array: string[]) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1))
|
||||||
|
;[array[i], array[j]] = [array[j], array[i]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...array]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const users = ['John', 'Peter', 'Stefanie', 'Marta', 'Klaus', 'Anna']
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
function Footer() {
|
|
||||||
console.log("Footer rendered");
|
|
||||||
return (
|
|
||||||
<footer>
|
|
||||||
<p>© 2025 Your Company</p>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default function Navigation () {
|
|
||||||
return (
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li>home</li>
|
|
||||||
<li>test</li>
|
|
||||||
</ul>
|
|
||||||
</nav>);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import Navigation from "./Navigation";
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
console.log("Header rendered");
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<h2>My Application</h2>
|
|
||||||
<Navigation />
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
export default function EffectExercises() {
|
|
||||||
|
|
||||||
const [counter, setCounter] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("EffectExercises mounted");
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setCounter(prevCounter => prevCounter + 1);
|
|
||||||
}
|
|
||||||
, 1000);
|
|
||||||
return () => {
|
|
||||||
console.log("EffectExercises unmounted");
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [counter]);
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<h2>Effect Exercises</h2>
|
|
||||||
<p>Counter: {counter}</p>
|
|
||||||
|
|
||||||
<button onClick={() => setCounter(0)}>Reset</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import React, { memo } from "react";
|
|
||||||
|
|
||||||
interface SearchProps {
|
|
||||||
onChange: (text: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Search({ onChange }: SearchProps): React.ReactElement {
|
|
||||||
console.log("Search rendered");
|
|
||||||
return <input type="text" onChange={(e) => onChange(e.target.value)} placeholder="Search user..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Search);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
import { shuffleArray, users } from "../../../utils/shuffle";
|
|
||||||
import Search from "./Search";
|
|
||||||
|
|
||||||
export default function MemoCallback() {
|
|
||||||
console.log("MemoCallback rendered");
|
|
||||||
|
|
||||||
const [allUsers, setAllUsers] = useState(users);
|
|
||||||
|
|
||||||
const handleSearch = useCallback((text: string) => {
|
|
||||||
const filteredUsers = users.filter((user) => {
|
|
||||||
return user.toLowerCase().includes(text.toLowerCase());
|
|
||||||
});
|
|
||||||
setAllUsers(filteredUsers);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShuffle = () => {
|
|
||||||
setAllUsers(shuffleArray(users));
|
|
||||||
};
|
|
||||||
|
|
||||||
const ListOfUsers = allUsers.map((user) => {
|
|
||||||
return <p key={user}>{user}</p>;
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Memo Callback Übung</h1>
|
|
||||||
<button onClick={handleShuffle}>Shuffle</button>
|
|
||||||
<Search onChange={handleSearch} />
|
|
||||||
<div>
|
|
||||||
{ListOfUsers}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { FormEvent, useRef, useState } from "react";
|
|
||||||
import useLocalStorage from "../../../hooks/useLocalStorage";
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RefExercise(): React.ReactElement {
|
|
||||||
console.log("RefExercise rendered");
|
|
||||||
|
|
||||||
const emailRef = useRef<HTMLInputElement>(null);
|
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
|
||||||
const {setStoredValue, value: formDataValues} = useLocalStorage<FormValues>("formdata", {
|
|
||||||
email: "",
|
|
||||||
password: ""
|
|
||||||
} );
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
console.log("submit", {
|
|
||||||
email: emailRef.current?.value,
|
|
||||||
password: passwordRef.current?.value
|
|
||||||
});
|
|
||||||
setStoredValue({
|
|
||||||
email: emailRef.current?.value || "",
|
|
||||||
password: passwordRef.current?.value || ""
|
|
||||||
});
|
|
||||||
passwordRef.current?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
strasse: "",
|
|
||||||
city: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[event.target.name]: event.target.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeSubmit = () => {
|
|
||||||
const strasse = document.querySelector('input[name="strasse"]') as HTMLInputElement;
|
|
||||||
const city = document.querySelector('input[name="city"]') as HTMLInputElement;
|
|
||||||
console.log("submit", {
|
|
||||||
strasse: strasse.value,
|
|
||||||
city: city.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFormSubmit(e: FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
const data = new FormData(e.target as HTMLFormElement);
|
|
||||||
console.log('handleFormSubmit', {
|
|
||||||
email: data.get('email'),
|
|
||||||
password: data.get('password')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "3rem" }}>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<h3>values by ref</h3>
|
|
||||||
<input type="text" placeholder="email" ref={emailRef} defaultValue={formDataValues?.email}/>
|
|
||||||
<input type="text" placeholder="password" ref={passwordRef} defaultValue={formDataValues?.password}/>
|
|
||||||
<button onClick={handleSubmit}>Submit</button>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<h3>change event (bad way, but validation is possible)</h3>
|
|
||||||
<input type="text" name="strasse" placeholder="Strasse" onChange={handleChange} />
|
|
||||||
<input type="text" name="city" placeholder="Stadt" onChange={handleChange}/>
|
|
||||||
<button onClick={handleChangeSubmit}>submit</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleFormSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<h3>By Form Event</h3>
|
|
||||||
<input type="text" name="email" placeholder="Email" />
|
|
||||||
<input type="text" name="password" placeholder="password" />
|
|
||||||
<button type="submit">Send</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
website: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserEffect() {
|
|
||||||
const [user, setUser] = useState<User>();
|
|
||||||
const [userId, setUserId] = useState<number>(0);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("UserEffect mounted");
|
|
||||||
|
|
||||||
if (userId === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = performance.now(); // Start measuring performance
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// Using AbortController to cancel the fetch request if the component unmounts
|
|
||||||
// or if the userId changes before the fetch completes
|
|
||||||
const controller = new AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
|
|
||||||
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {signal: signal}).then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Network response was not ok");
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}).then(data => {
|
|
||||||
setUser(data);
|
|
||||||
const end = performance.now(); // End measuring performance
|
|
||||||
const timeTaken = end - start; // Calculate the time taken
|
|
||||||
console.log(`Fetch completed in ${Math.round(timeTaken)} milliseconds`);
|
|
||||||
setLoading(false);
|
|
||||||
}).catch(error => {
|
|
||||||
console.error("There was a problem with the fetch operation:", error);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
console.log("UserEffect unmounted");
|
|
||||||
// Cleanup function to abort the fetch request if the component unmounts
|
|
||||||
// or if the userId changes before the fetch completes
|
|
||||||
controller.abort('Fetch aborted');
|
|
||||||
}
|
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<h2>users Effect Exercise</h2>
|
|
||||||
|
|
||||||
{userId === 0 && <p>Please select a user</p>}
|
|
||||||
{loading || userId === 0 ? <p>Loading...</p> : <p>[{user?.id}] {user?.name}: {user?.website}</p>}
|
|
||||||
|
|
||||||
<button onClick={() => setUserId(1)} style={{padding: 10, border: '1px solid gray'}}>User 1</button>
|
|
||||||
<button onClick={() => setUserId(2)} style={{padding: 10, border: '1px solid gray'}}>User 2</button>
|
|
||||||
<button onClick={() => setUserId(3)} style={{padding: 10, border: '1px solid gray'}}>User 3</button>
|
|
||||||
<button onClick={() => setUserId(4)} style={{padding: 10, border: '1px solid gray'}}>User 4</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useFetch } from "../../hooks/useFetch";
|
|
||||||
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
|
||||||
|
|
||||||
export function UserOverview() {
|
|
||||||
const {data, loading, error} = useFetch("https://jsonplaceholder.typicode.com/users");
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(max-width: 600px)');
|
|
||||||
|
|
||||||
console.log("isMobile", isMobile);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>Error: {error}</p>;
|
|
||||||
}
|
|
||||||
if (loading) {
|
|
||||||
return <p>Loading...</p>;
|
|
||||||
}
|
|
||||||
if (!data) {
|
|
||||||
return <p>No data available</p>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>User Overview</h2>
|
|
||||||
<ul>
|
|
||||||
{data.map((user: { id: number; name: string; email: string; website: string }) => (
|
|
||||||
<li key={user.id}>
|
|
||||||
<strong>{user.name}</strong> <div style={{display: isMobile ? 'none' : 'block' }}>({user.email}) - {user.website}</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<p>Total Users: {data.length}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export default function Heading({title = "Hallo Teilnehmer"}: {title?: string}): React.ReactElement {
|
|
||||||
console.log("Heading rendered");
|
|
||||||
return (
|
|
||||||
<h1 >
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
website: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFetch(url: string) {
|
|
||||||
const [data, setData] = useState<User[] | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// alternativ
|
|
||||||
// const [fetchValues, setFetchValues] = useState({
|
|
||||||
// data: null,
|
|
||||||
// loading: true,
|
|
||||||
// error: null,
|
|
||||||
// });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
setError("Network response was not ok");
|
|
||||||
throw new Error("Network response was not ok");
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
setData(data);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
setError("Error fetching data");
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
|
|
||||||
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return { data, loading, error };
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export default function useLocalStorage<T>(key: string, initialValue: T | null) {
|
|
||||||
const [value, setValue] = useState<T | null>(() => {
|
|
||||||
try {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
const storedValue = localStorage.getItem(key);
|
|
||||||
return storedValue ? JSON.parse(storedValue) : initialValue;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading from localStorage", error);
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const setStoredValue = (newValue: T) => {
|
|
||||||
try {
|
|
||||||
const valueToStore = newValue instanceof Function ? newValue(value) : newValue;
|
|
||||||
setValue(valueToStore);
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error writing to localStorage", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeStoredValue = () => {
|
|
||||||
try {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
setValue(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error removing from localStorage", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {value, setStoredValue, removeStoredValue};
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook to check if a media query matches the current viewport.
|
|
||||||
* @param query - The media query string (e.g., '(max-width: 600px)')
|
|
||||||
* @returns A boolean indicating whether the media query matches.
|
|
||||||
*/
|
|
||||||
export function useMediaQuery(query: string) {
|
|
||||||
const [matches, setMatches] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mediaQueryList = window.matchMedia(query);
|
|
||||||
const handleChange = (event: MediaQueryListEvent) => {
|
|
||||||
setMatches(event.matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set the initial value
|
|
||||||
setMatches(mediaQueryList.matches);
|
|
||||||
|
|
||||||
// Add event listener
|
|
||||||
mediaQueryList.addEventListener('change', handleChange);
|
|
||||||
|
|
||||||
// Cleanup function to remove the event listener
|
|
||||||
return () => {
|
|
||||||
mediaQueryList.removeEventListener('change', handleChange);
|
|
||||||
};
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,6 @@ a:hover {
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
73
react-advanced-tag1/src/layouts/MainLayout.css
Normal file
73
react-advanced-tag1/src/layouts/MainLayout.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
header nav li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav a {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition:
|
||||||
|
background-color 0.3s,
|
||||||
|
color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
header nav a.active {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
header {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
header nav a {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react'
|
||||||
import Footer from "../components/Footer";
|
import './MainLayout.css'
|
||||||
import Header from "../components/Header";
|
import Footer from '../common/components/Footer'
|
||||||
|
import Header from '../common/components/Header'
|
||||||
|
import { Button } from '../common/components/globals/Button'
|
||||||
|
|
||||||
export default function MainLayout({children}: {children: React.ReactNode | React.ReactElement | React.ReactElement[]}): React.ReactElement {
|
export default function MainLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
const [clicked, setClicked] = useState(false);
|
children: React.ReactNode | React.ReactElement | React.ReactElement[]
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [clicked, setClicked] = useState(false)
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
setClicked(!clicked);
|
setClicked(!clicked)
|
||||||
|
|
||||||
console.log("clicked", clicked);
|
console.log('clicked', clicked)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("MainLayout rendered", clicked);
|
console.log('MainLayout rendered', clicked)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
|
||||||
|
|
||||||
<button onClick={handleClick}>Click</button>
|
<div className="content">
|
||||||
<Footer />
|
{children}
|
||||||
|
|
||||||
|
<Button onClick={handleClick} variant="secondary" size="sm">
|
||||||
|
Click
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,15 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { enableMapSet } from 'immer'
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</StrictMode>,
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
11
react-advanced-tag1/src/pages/ComponentWrapperPage.tsx
Normal file
11
react-advanced-tag1/src/pages/ComponentWrapperPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
|
||||||
|
export default function ComponentWrapperPage() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||||
|
<ComponentWrapper title="Komponenten Wrapper Test">
|
||||||
|
<h1>Hallo</h1>
|
||||||
|
</ComponentWrapper>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
react-advanced-tag1/src/pages/EffectExercisesPage.tsx
Normal file
10
react-advanced-tag1/src/pages/EffectExercisesPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import EffectExercises from '../common/components/exercises/EffectExercises'
|
||||||
|
|
||||||
|
export default function EffectExercisesPage() {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Effekte">
|
||||||
|
<EffectExercises />
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
react-advanced-tag1/src/pages/FormsPage.tsx
Normal file
11
react-advanced-tag1/src/pages/FormsPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import RefExercise from '../common/components/exercises/RefExercise'
|
||||||
|
import Heading from '../common/components/globals/Heading'
|
||||||
|
|
||||||
|
export default function FormsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading title="Formulare" />
|
||||||
|
<RefExercise />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
react-advanced-tag1/src/pages/HocPage.tsx
Normal file
10
react-advanced-tag1/src/pages/HocPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import DogImagesWithLoader from '../common/components/exercises/HocExercise/DogImages'
|
||||||
|
|
||||||
|
export function HocPage() {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="HOC Example">
|
||||||
|
<DogImagesWithLoader />
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
react-advanced-tag1/src/pages/HomePage.tsx
Normal file
9
react-advanced-tag1/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Willkommen">
|
||||||
|
<p>Willkommen auf der Startseite!</p>
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
react-advanced-tag1/src/pages/ImmerPage.tsx
Normal file
10
react-advanced-tag1/src/pages/ImmerPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import ImmerExercise from '../common/components/exercises/ImmerExercise'
|
||||||
|
|
||||||
|
export default function ImmerPage() {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Immer">
|
||||||
|
<ImmerExercise />
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
react-advanced-tag1/src/pages/MemoCallbackPage.tsx
Normal file
10
react-advanced-tag1/src/pages/MemoCallbackPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import MemoCallback from '../common/components/exercises/MemoCallback'
|
||||||
|
|
||||||
|
export default function MemoCallbackPage() {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Memo und Callback">
|
||||||
|
<MemoCallback />
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
react-advanced-tag1/src/pages/MobXPage.tsx
Normal file
35
react-advanced-tag1/src/pages/MobXPage.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import counterStore from '../common/stores/mobx-store'
|
||||||
|
|
||||||
|
const MobXPage = observer(() => {
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="MobX">
|
||||||
|
<div>
|
||||||
|
<h3>Counter: {counterStore.count}</h3>
|
||||||
|
<button onClick={() => counterStore.increment()}>Increment</button>
|
||||||
|
<button onClick={() => counterStore.decrement()}>Decrement</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Message: {counterStore.message}</h3>
|
||||||
|
<MessageUpdater />
|
||||||
|
<button onClick={() => counterStore.reset()}>Reset</button>
|
||||||
|
</div>
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MobXPage
|
||||||
|
|
||||||
|
function MessageUpdater() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Update Message</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={counterStore.message}
|
||||||
|
onChange={(e) => counterStore.updateMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
react-advanced-tag1/src/pages/ModalPage.tsx
Normal file
18
react-advanced-tag1/src/pages/ModalPage.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import Modal from '../common/components/Modal'
|
||||||
|
|
||||||
|
export default function ModalPage() {
|
||||||
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Modal Test">
|
||||||
|
<button onClick={() => setModalOpen(!modalOpen)}>Toggle Modal</button>
|
||||||
|
{modalOpen &&
|
||||||
|
createPortal(
|
||||||
|
<Modal onClose={() => setModalOpen(false)} />,
|
||||||
|
document.getElementById('modal') as HTMLElement
|
||||||
|
)}
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
react-advanced-tag1/src/pages/UsersPage.tsx
Normal file
22
react-advanced-tag1/src/pages/UsersPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import UserEffect from '../common/components/exercises/UserEffect'
|
||||||
|
import { UserOverview } from '../common/components/exercises/UserOverview'
|
||||||
|
|
||||||
|
export default function UsersPage() {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||||
|
<ComponentWrapper
|
||||||
|
title="Benutzer"
|
||||||
|
description="Load User with useEffect Hook"
|
||||||
|
>
|
||||||
|
<UserEffect />
|
||||||
|
</ComponentWrapper>
|
||||||
|
<ComponentWrapper
|
||||||
|
title="User Overview"
|
||||||
|
description="Load User List with useFetch Hook & use MeadiaQuery Hook"
|
||||||
|
>
|
||||||
|
<UserOverview />
|
||||||
|
</ComponentWrapper>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
react-advanced-tag1/src/pages/ZustandCounterPage.tsx
Normal file
18
react-advanced-tag1/src/pages/ZustandCounterPage.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import useCounterStore from '../common/stores/counter-store'
|
||||||
|
|
||||||
|
export default function ZustandCounterPage() {
|
||||||
|
const { decrement, increment } = useCounterStore()
|
||||||
|
return (
|
||||||
|
<ComponentWrapper title="Zustand Counter">
|
||||||
|
<button onClick={increment}>Increment</button>
|
||||||
|
<button onClick={decrement}>Decrement</button>
|
||||||
|
<CounterValue />
|
||||||
|
</ComponentWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CounterValue() {
|
||||||
|
const count = useCounterStore((state) => state.count)
|
||||||
|
return <p>Count: {count}</p>
|
||||||
|
}
|
||||||
52
react-advanced-tag1/src/stories/Button.stories.tsx
Normal file
52
react-advanced-tag1/src/stories/Button.stories.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Button } from '../common/components/globals/Button'
|
||||||
|
import { fn } from '@storybook/test'
|
||||||
|
import { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Button> = {
|
||||||
|
title: 'Elements/Button',
|
||||||
|
component: Button,
|
||||||
|
argTypes: {
|
||||||
|
variant: { control: 'radio', options: ['primary', 'secondary'] },
|
||||||
|
size: { control: 'radio', options: ['sm', 'md', 'lg'] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
onClick: fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Button>
|
||||||
|
|
||||||
|
/** This is the primary Button */
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Primary Button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Secondary Button',
|
||||||
|
variant: 'secondary',
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Small Button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Large Button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'lg',
|
||||||
|
},
|
||||||
|
}
|
||||||
43
react-advanced-tag1/src/stories/ComponentWrapper.stories.tsx
Normal file
43
react-advanced-tag1/src/stories/ComponentWrapper.stories.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
|
||||||
|
import { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ComponentWrapper> = {
|
||||||
|
title: 'Wrapper/ComponentWrapper',
|
||||||
|
component: ComponentWrapper,
|
||||||
|
argTypes: {
|
||||||
|
title: { control: 'text' },
|
||||||
|
description: { control: 'text' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof ComponentWrapper>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Default Title',
|
||||||
|
description: 'This is a default description.',
|
||||||
|
children: <p>Default content goes here.</p>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutTitle: Story = {
|
||||||
|
args: {
|
||||||
|
description: 'This wrapper has no title.',
|
||||||
|
children: <p>Content without a title.</p>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutDescription: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Title Only',
|
||||||
|
children: <p>Content without a description.</p>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
args: {
|
||||||
|
children: <p>Empty wrapper with only content.</p>,
|
||||||
|
},
|
||||||
|
}
|
||||||
15
react-advanced-tag1/src/stories/Footer.stories.tsx
Normal file
15
react-advanced-tag1/src/stories/Footer.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import Footer from '../common/components/Footer'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Footer> = {
|
||||||
|
title: 'Common/Footer',
|
||||||
|
component: Footer,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Footer>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <Footer />,
|
||||||
|
}
|
||||||
15
react-advanced-tag1/src/stories/Header.stories.tsx
Normal file
15
react-advanced-tag1/src/stories/Header.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Header from '../common/components/Header/index'
|
||||||
|
import { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
|
const meta: Meta<typeof Header> = {
|
||||||
|
title: 'Common/Header',
|
||||||
|
component: Header,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Header>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <Header />,
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { shuffleArray, users } from './shuffle';
|
|
||||||
|
|
||||||
describe('shuffleArray', () => {
|
|
||||||
it('should shuffle the array and return a new order', () => {
|
|
||||||
const originalArray = [...users];
|
|
||||||
const shuffledArray = shuffleArray([...users]);
|
|
||||||
|
|
||||||
expect(shuffledArray).not.toEqual(originalArray); // Ensure the order is different
|
|
||||||
expect(shuffledArray.sort()).toEqual(originalArray.sort()); // Ensure all elements are still present
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an empty array when input is empty', () => {
|
|
||||||
const result = shuffleArray([]);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle arrays with one element', () => {
|
|
||||||
const result = shuffleArray(['onlyElement']);
|
|
||||||
expect(result).toEqual(['onlyElement']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export function shuffleArray(array: string[]){
|
|
||||||
for(let i = array.length -1; i>0; i--){
|
|
||||||
|
|
||||||
const j = Math.floor(Math.random()* (i +1));
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...array]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const users =['John', 'Peter', 'Stefanie', 'Marta', 'Klaus', 'Anna']
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, UserConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
test: {
|
||||||
|
// 👋 add the line below to add jsdom to vite
|
||||||
|
environment: 'jsdom',
|
||||||
|
}
|
||||||
|
} as UserConfig)
|
||||||
|
|||||||
1
react-advanced-tag1/vitest.shims.d.ts
vendored
Normal file
1
react-advanced-tag1/vitest.shims.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@vitest/browser/providers/playwright" />
|
||||||
34
react-advanced-tag1/vitest.workspace.ts
Normal file
34
react-advanced-tag1/vitest.workspace.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { defineWorkspace } from 'vitest/config';
|
||||||
|
|
||||||
|
import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';
|
||||||
|
|
||||||
|
const dirname =
|
||||||
|
typeof __dirname !== 'undefined'
|
||||||
|
? __dirname
|
||||||
|
: path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
|
||||||
|
export default defineWorkspace([
|
||||||
|
'vite.config.ts',
|
||||||
|
{
|
||||||
|
extends: 'vite.config.ts',
|
||||||
|
plugins: [
|
||||||
|
// The plugin will run tests for the stories defined in your Storybook config
|
||||||
|
// See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest
|
||||||
|
storybookTest({ configDir: path.join(dirname, '.storybook') }),
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
name: 'storybook',
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
headless: true,
|
||||||
|
provider: 'playwright',
|
||||||
|
instances: [{ browser: 'chromium' }],
|
||||||
|
},
|
||||||
|
setupFiles: ['.storybook/vitest.setup.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
Reference in New Issue
Block a user