Compare commits

..

12 Commits

Author SHA1 Message Date
e560248
49b67f7654 add comments in storybook 2025-04-08 15:54:16 +02:00
e560248
e6f57d3cbc add storybook 2025-04-08 15:47:25 +02:00
e560248
456def177a fix light mode 2025-04-08 15:01:56 +02:00
e560248
241ea51d34 use ComponentWrapper 2025-04-08 14:38:45 +02:00
e560248
554f14c8d9 use ComponentWrapper in UsersPage 2025-04-08 14:35:30 +02:00
e560248
d5c52968a6 add MobX 2025-04-08 14:05:32 +02:00
e560248
88de79086e add immerJs example 2025-04-08 13:28:46 +02:00
e560248
6577698459 add prettier setup 2025-04-08 12:54:07 +02:00
e560248
419113541b add Higher-Order Components 2025-04-08 11:52:10 +02:00
e560248
22031864fb rename folder 2025-04-08 10:55:52 +02:00
e560248
832bc2b209 move Modal into Router 2025-04-08 10:54:33 +02:00
e560248
8b13335c6c add Zustand Counter 2025-04-08 10:23:00 +02:00
65 changed files with 3242 additions and 609 deletions

View File

@@ -29,5 +29,6 @@
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"prettier.configPath": "react-advanced-tag1/.prettierrc.mjs"
} }

View File

@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*storybook.log

View 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

View 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;

View 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

View 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);

File diff suppressed because it is too large Load Diff

View File

@@ -13,30 +13,55 @@
"test:watch": "vitest --watch", "test:watch": "vitest --watch",
"test:coverage": "vitest --coverage", "test:coverage": "vitest --coverage",
"test:coverage:report": "vitest --coverage --reporter=html", "test:coverage:report": "vitest --coverage --reporter=html",
"test:coverage:report:open": "vitest --coverage --reporter=html && open coverage/index.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-router-dom": "^7.5.0",
"react-scan": "^0.3.3" "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", "@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/coverage-v8": "^3.1.1",
"@vitest/ui": "^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", "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"
]
} }
} }

View File

@@ -1,5 +1,3 @@
.logo { .logo {
height: 6em; height: 6em;
padding: 1.5em; padding: 1.5em;
@@ -36,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;
} }

View File

@@ -2,16 +2,21 @@ 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 MainLayout from './layouts/MainLayout' import MainLayout from './layouts/MainLayout'
import HomePage from './routes/HomePage' import HomePage from './pages/HomePage'
import UsersPage from './routes/UsersPage' import UsersPage from './pages/UsersPage'
import EffectExercisesPage from './routes/EffectExercisesPage' import EffectExercisesPage from './pages/EffectExercisesPage'
import FormsPage from './routes/FormsPage' import FormsPage from './pages/FormsPage'
import MemoCallbackPage from './routes/MemoCallbackPage' import MemoCallbackPage from './pages/MemoCallbackPage'
import ComponentWrapperPage from './routes/ComponentWrapperPage' 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>
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/user" element={<UsersPage />} /> <Route path="/user" element={<UsersPage />} />
@@ -19,9 +24,14 @@ function App() {
<Route path="/forms" element={<FormsPage />} /> <Route path="/forms" element={<FormsPage />} />
<Route path="/memocallback" element={<MemoCallbackPage />} /> <Route path="/memocallback" element={<MemoCallbackPage />} />
<Route path="/componentwrapper" element={<ComponentWrapperPage />} /> <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> </Routes>
</MainLayout> </MainLayout>
)
} }
export default App export default App

View File

@@ -17,3 +17,12 @@
padding: 20px; padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 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;
}
}

View File

@@ -1,17 +1,28 @@
import './ComponentWrapper.css' import './ComponentWrapper.css'
interface ComponentWrapperProps { interface ComponentWrapperProps {
title?: string; /** Title to show */
children: React.ReactNode; title?: string
/** Description to show */
description?: string
/** Content to show */
children: React.ReactNode
} }
export default function ComponentWrapper({ children, title }: ComponentWrapperProps): React.ReactElement {
console.log("ComponentWrapper rendered"); /**
* 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 ( return (
<div className='component-wrapper'> <section className="component-wrapper">
{title && <h2>{title}</h2>} {title && <h2>{title}</h2>}
<div className='component-content'> {description && <p>{description}</p>}
{children} <div className="component-content">{children}</div>
</div> </section>
</div> )
);
} }

View File

@@ -1,10 +1,10 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from '@testing-library/react'
import Footer from "./Footer"; import Footer from './Footer'
import { describe, it, expect } from "vitest"; import { describe, it, expect } from 'vitest'
describe("Footer Component", () => { describe('Footer Component', () => {
it("should render the footer with correct content", () => { it('should render the footer with correct content', () => {
render(<Footer />); render(<Footer />)
expect(screen.getByText(/© 2025 Your Company/i)).toBeDefined(); expect(screen.getByText(/© 2025 Your Company/i)).toBeDefined()
}); })
}); })

View File

@@ -1,10 +1,10 @@
function Footer() { function Footer() {
console.log("Footer rendered"); console.log('Footer rendered')
return ( return (
<footer> <footer>
<p>&copy; 2025 Your Company</p> <p>&copy; 2025 Your Company</p>
</footer> </footer>
); )
} }
export default Footer; export default Footer

View File

@@ -1,13 +1,43 @@
export default function Navigation () { import './Navigation.css'
export default function Navigation() {
return ( return (
<nav> <nav className="navigation">
<ul> <ul>
<li><a href="/">Home</a></li> <li>
<li><a href="/user">Users</a></li> <a href="/">Home</a>
<li><a href="/effect">Effects</a></li> </li>
<li><a href="/forms">Forms</a></li> <li>
<li><a href="/memocallback">Memo und Callback</a></li> <a href="/user">Users</a>
<li><a href="/componentwrapper">Component Wrapper</a></li> </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> </ul>
</nav>); </nav>
)
} }

View File

@@ -1,13 +1,13 @@
import Navigation from "./Navigation"; import Navigation from './Navigation'
const Header = () => { const Header = () => {
console.log("Header rendered"); console.log('Header rendered')
return ( return (
<header> <header>
<h1>My Application</h1> <h1>My Application</h1>
<Navigation /> <Navigation />
</header> </header>
); )
} }
export default Header; export default Header

View File

@@ -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;
}
}

View File

@@ -1,18 +1,25 @@
import'./Modal.css'; import './Modal.css'
interface ModalProps { interface ModalProps {
open?: boolean; open?: boolean
onClose?: () => void; onClose?: () => void
} }
export default function Modal({ open= true, onClose }: ModalProps): React.ReactElement { export default function Modal({
console.log("Modal rendered"); open = true,
onClose,
}: ModalProps): React.ReactElement {
console.log('Modal rendered')
return ( return (
<div style={{ display: open ? "block" : "none" }} className='modal' onClick={() => onClose && onClose()}> <div
<div className='modal-content'> style={{ display: open ? 'block' : 'none' }}
className="modal"
onClick={() => onClose && onClose()}
>
<div className="modal-content">
<h2>Modal</h2> <h2>Modal</h2>
<p>This is a modal component.</p> <p>This is a modal component.</p>
</div> </div>
</div> </div>
); )
} }

View File

@@ -1,20 +1,18 @@
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
export default function EffectExercises() { export default function EffectExercises() {
const [counter, setCounter] = useState(0)
const [counter, setCounter] = useState(0);
useEffect(() => { useEffect(() => {
console.log("EffectExercises mounted"); console.log('EffectExercises mounted')
const interval = setInterval(() => { const interval = setInterval(() => {
setCounter(prevCounter => prevCounter + 1); setCounter((prevCounter) => prevCounter + 1)
} }, 1000)
, 1000);
return () => { return () => {
console.log("EffectExercises unmounted"); console.log('EffectExercises unmounted')
clearInterval(interval); clearInterval(interval)
} }
}, [counter]); }, [counter])
return ( return (
<section> <section>
<h2>Effect Exercises</h2> <h2>Effect Exercises</h2>

View File

@@ -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

View File

@@ -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} />
}
}

View File

@@ -0,0 +1,6 @@
.dog-image {
width: 100px;
height: 100px;
border-radius: 50%;
margin: 10px;
}

View File

@@ -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>
</>
)
}

View File

@@ -1,12 +1,18 @@
import React, { memo } from "react"; import React, { memo } from 'react'
interface SearchProps { interface SearchProps {
onChange: (text: string) => void; onChange: (text: string) => void
} }
function Search({ onChange }: SearchProps): React.ReactElement { function Search({ onChange }: SearchProps): React.ReactElement {
console.log("Search rendered"); console.log('Search rendered')
return <input type="text" onChange={(e) => onChange(e.target.value)} placeholder="Search user..." />; return (
<input
type="text"
onChange={(e) => onChange(e.target.value)}
placeholder="Search user..."
/>
)
} }
export default memo(Search); export default memo(Search)

View File

@@ -1,42 +1,44 @@
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from '@testing-library/react'
import MemoCallback from "./index"; import MemoCallback from './index'
import { describe, it, expect } from "vitest"; import { describe, it, expect } from 'vitest'
import { users } from "../../../utils/shuffle"; import { users } from '../../../utils/shuffle'
describe('MemoCallback Component', () => {
it('should render the component with initial users', () => {
describe("MemoCallback Component", () => { render(<MemoCallback />)
it("should render the component with initial users", () => {
render(<MemoCallback />);
users.forEach((user) => { users.forEach((user) => {
expect(screen.getByText(user)).toBeDefined(); expect(screen.getByText(user)).toBeDefined()
}); })
}); })
it("should filter users based on search input", () => { it('should filter users based on search input', () => {
render(<MemoCallback />); render(<MemoCallback />)
const searchInput = screen.getByPlaceholderText(/search/i); const searchInput = screen.getByPlaceholderText(/search/i)
fireEvent.change(searchInput, { target: { value: "a" } }); fireEvent.change(searchInput, { target: { value: 'a' } })
const filteredUsers = users.filter((user) => user.toLowerCase().includes("a")); const filteredUsers = users.filter((user) =>
user.toLowerCase().includes('a')
)
filteredUsers.forEach((user) => { filteredUsers.forEach((user) => {
expect(screen.getByText(user)).toBeDefined(); expect(screen.getByText(user)).toBeDefined()
}); })
const nonMatchingUsers = users.filter((user) => !user.toLowerCase().includes("a")); const nonMatchingUsers = users.filter(
(user) => !user.toLowerCase().includes('a')
)
nonMatchingUsers.forEach((user) => { nonMatchingUsers.forEach((user) => {
expect(screen.queryByText(user)).not.toBeDefined(); expect(screen.queryByText(user)).not.toBeDefined()
}); })
}); })
it("should shuffle users when the shuffle button is clicked", () => { it('should shuffle users when the shuffle button is clicked', () => {
render(<MemoCallback />); render(<MemoCallback />)
const shuffleButton = screen.getByText(/shuffle/i); const shuffleButton = screen.getByText(/shuffle/i)
const initialOrder = screen.getAllByText(/./).map((el) => el.textContent); const initialOrder = screen.getAllByText(/./).map((el) => el.textContent)
fireEvent.click(shuffleButton); fireEvent.click(shuffleButton)
const shuffledOrder = screen.getAllByText(/./).map((el) => el.textContent); const shuffledOrder = screen.getAllByText(/./).map((el) => el.textContent)
expect(initialOrder).not.toEqual(shuffledOrder); expect(initialOrder).not.toEqual(shuffledOrder)
}); })
}); })

View File

@@ -1,33 +1,31 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from 'react'
import Search from "./Search"; import Search from './Search'
import { users, shuffleArray } from "../../../utils/shuffle"; import { users, shuffleArray } from '../../../utils/shuffle'
export default function MemoCallback() { export default function MemoCallback() {
console.log("MemoCallback rendered"); console.log('MemoCallback rendered')
const [allUsers, setAllUsers] = useState(users); const [allUsers, setAllUsers] = useState(users)
const handleSearch = useCallback((text: string) => { const handleSearch = useCallback((text: string) => {
const filteredUsers = users.filter((user) => { const filteredUsers = users.filter((user) => {
return user.toLowerCase().includes(text.toLowerCase()); return user.toLowerCase().includes(text.toLowerCase())
}); })
setAllUsers(filteredUsers); setAllUsers(filteredUsers)
}, []); }, [])
const handleShuffle = () => { const handleShuffle = () => {
setAllUsers(shuffleArray(users)); setAllUsers(shuffleArray(users))
}; }
const ListOfUsers = allUsers.map((user) => { const ListOfUsers = allUsers.map((user) => {
return <p key={user}>{user}</p>; return <p key={user}>{user}</p>
}); })
return ( return (
<div> <div>
<button onClick={handleShuffle}>Shuffle</button> <button onClick={handleShuffle}>Shuffle</button>
<Search onChange={handleSearch} /> <Search onChange={handleSearch} />
<div> <div>{ListOfUsers}</div>
{ListOfUsers}
</div> </div>
</div> )
);
} }

View File

@@ -1,83 +1,114 @@
import { FormEvent, useRef, useState } from "react"; import { FormEvent, useRef, useState } from 'react'
import useLocalStorage from "../../../hooks/useLocalStorage"; import useLocalStorage from '../../../hooks/useLocalStorage'
interface FormValues { interface FormValues {
email: string; email: string
password: string; password: string
} }
export default function RefExercise(): React.ReactElement { export default function RefExercise(): React.ReactElement {
console.log("RefExercise rendered"); console.log('RefExercise rendered')
const emailRef = useRef<HTMLInputElement>(null); const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null); const passwordRef = useRef<HTMLInputElement>(null)
const {setStoredValue, value: formDataValues} = useLocalStorage<FormValues>("formdata", { const { setStoredValue, value: formDataValues } = useLocalStorage<FormValues>(
email: "", 'formdata',
password: "" {
} ); email: '',
password: '',
}
)
const handleSubmit = () => { const handleSubmit = () => {
console.log("submit", { console.log('submit', {
email: emailRef.current?.value, email: emailRef.current?.value,
password: passwordRef.current?.value password: passwordRef.current?.value,
}); })
setStoredValue({ setStoredValue({
email: emailRef.current?.value || "", email: emailRef.current?.value || '',
password: passwordRef.current?.value || "" password: passwordRef.current?.value || '',
}); })
passwordRef.current?.focus(); passwordRef.current?.focus()
} }
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
strasse: "", strasse: '',
city: "" city: '',
}); })
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ setFormData({
...formData, ...formData,
[event.target.name]: event.target.value [event.target.name]: event.target.value,
}); })
} }
const handleChangeSubmit = () => { const handleChangeSubmit = () => {
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; const strasse = document.querySelector(
console.log("submit", { '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, strasse: strasse.value,
city: city.value city: city.value,
}); })
} }
function handleFormSubmit(e: FormEvent) { function handleFormSubmit(e: FormEvent) {
e.preventDefault(); e.preventDefault()
const data = new FormData(e.target as HTMLFormElement); const data = new FormData(e.target as HTMLFormElement)
console.log('handleFormSubmit', { console.log('handleFormSubmit', {
email: data.get('email'), email: data.get('email'),
password: data.get('password') password: data.get('password'),
}); })
} }
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: "3rem" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<h3>values by ref</h3> <h3>values by ref</h3>
<input type="text" placeholder="email" ref={emailRef} defaultValue={formDataValues?.email}/> <input
<input type="text" placeholder="password" ref={passwordRef} defaultValue={formDataValues?.password}/> type="text"
placeholder="email"
ref={emailRef}
defaultValue={formDataValues?.email}
/>
<input
type="text"
placeholder="password"
ref={passwordRef}
defaultValue={formDataValues?.password}
/>
<button onClick={handleSubmit}>Submit</button> <button onClick={handleSubmit}>Submit</button>
</div> </div>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<h3>change event (bad way, but validation is possible)</h3> <h3>change event (bad way, but validation is possible)</h3>
<input type="text" name="strasse" placeholder="Strasse" onChange={handleChange} /> <input
<input type="text" name="city" placeholder="Stadt" onChange={handleChange}/> type="text"
name="strasse"
placeholder="Strasse"
onChange={handleChange}
/>
<input
type="text"
name="city"
placeholder="Stadt"
onChange={handleChange}
/>
<button onClick={handleChangeSubmit}>submit</button> <button onClick={handleChangeSubmit}>submit</button>
</div> </div>
<form onSubmit={handleFormSubmit} style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> <form
onSubmit={handleFormSubmit}
style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
>
<h3>By Form Event</h3> <h3>By Form Event</h3>
<input type="text" name="email" placeholder="Email" /> <input type="text" name="email" placeholder="Email" />
<input type="text" name="password" placeholder="password" /> <input type="text" name="password" placeholder="password" />
<button type="submit">Send</button> <button type="submit">Send</button>
</form> </form>
</div> </div>
); )
} }

View File

@@ -1,66 +1,95 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
interface User { interface User {
id: number; id: number
name: string; name: string
email: string; email: string
website: string; website: string
} }
export default function UserEffect() { export default function UserEffect() {
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>()
const [userId, setUserId] = useState<number>(0); const [userId, setUserId] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
console.log("UserEffect mounted"); console.log('UserEffect mounted')
if (userId === 0) { if (userId === 0) {
return; return
} }
const start = performance.now(); // Start measuring performance const start = performance.now() // Start measuring performance
setLoading(true); setLoading(true)
// Using AbortController to cancel the fetch request if the component unmounts // Using AbortController to cancel the fetch request if the component unmounts
// or if the userId changes before the fetch completes // or if the userId changes before the fetch completes
const controller = new AbortController(); const controller = new AbortController()
const signal = controller.signal; const signal = controller.signal
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {signal: signal}).then(response => { fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, {
signal: signal,
})
.then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error("Network response was not ok"); throw new Error('Network response was not ok')
} }
return response.json(); return response.json()
}).then(data => { })
setUser(data); .then((data) => {
const end = performance.now(); // End measuring performance setUser(data)
const timeTaken = end - start; // Calculate the time taken const end = performance.now() // End measuring performance
console.log(`Fetch completed in ${Math.round(timeTaken)} milliseconds`); const timeTaken = end - start // Calculate the time taken
setLoading(false); console.log(`Fetch completed in ${Math.round(timeTaken)} milliseconds`)
}).catch(error => { setLoading(false)
console.error("There was a problem with the fetch operation:", error); })
}); .catch((error) => {
console.error('There was a problem with the fetch operation:', error)
})
return () => { return () => {
console.log("UserEffect unmounted"); console.log('UserEffect unmounted')
// Cleanup function to abort the fetch request if the component unmounts // Cleanup function to abort the fetch request if the component unmounts
// or if the userId changes before the fetch completes // or if the userId changes before the fetch completes
controller.abort('Fetch aborted'); controller.abort('Fetch aborted')
} }
}, [userId]); }, [userId])
return ( return (
<section> <section>
<h2>users Effect Exercise</h2>
{userId === 0 && <p>Please select a user</p>} {userId === 0 && <p>Please select a user</p>}
{loading || userId === 0 ? <p>Loading...</p> : <p>[{user?.id}] {user?.name}: {user?.website}</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
<button onClick={() => setUserId(2)} style={{padding: 10, border: '1px solid gray'}}>User 2</button> onClick={() => setUserId(1)}
<button onClick={() => setUserId(3)} style={{padding: 10, border: '1px solid gray'}}>User 3</button> style={{ padding: 10, border: '1px solid gray' }}
<button onClick={() => setUserId(4)} style={{padding: 10, border: '1px solid gray'}}>User 4</button> >
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> </section>
) )
} }

View File

@@ -1,33 +1,44 @@
import { useFetch } from "../../hooks/useFetch"; import { useFetch } from '../../hooks/useFetch'
import { useMediaQuery } from "../../hooks/useMediaQuery"; import { useMediaQuery } from '../../hooks/useMediaQuery'
export function UserOverview() { export function UserOverview() {
const {data, loading, error} = useFetch("https://jsonplaceholder.typicode.com/users"); const { data, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/users'
)
const isMobile = useMediaQuery('(max-width: 600px)'); const isMobile = useMediaQuery('(max-width: 600px)')
console.log("isMobile", isMobile); console.log('isMobile', isMobile)
if (error) { if (error) {
return <p>Error: {error}</p>; return <p>Error: {error}</p>
} }
if (loading) { if (loading) {
return <p>Loading...</p>; return <p>Loading...</p>
} }
if (!data) { if (!data) {
return <p>No data available</p>; return <p>No data available</p>
} }
return ( return (
<div> <div>
<h2>User Overview</h2>
<ul> <ul>
{data.map((user: { id: number; name: string; email: string; website: string }) => ( {data.map(
(user: {
id: number
name: string
email: string
website: string
}) => (
<li key={user.id}> <li key={user.id}>
<strong>{user.name}</strong> <div style={{display: isMobile ? 'none' : 'block' }}>({user.email}) - {user.website}</div> <strong>{user.name}</strong>{' '}
<div style={{ display: isMobile ? 'none' : 'block' }}>
({user.email}) - {user.website}
</div>
</li> </li>
))} )
)}
</ul> </ul>
<p>Total Users: {data.length}</p> <p>Total Users: {data.length}</p>
</div> </div>
); )
} }

View 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>
)
}

View File

@@ -1,8 +1,8 @@
export default function Heading({title = "Hallo Teilnehmer"}: {title?: string}): React.ReactElement { export default function Heading({
console.log("Heading rendered"); title = 'Hallo Teilnehmer',
return ( }: {
<h2 > title?: string
{title} }): React.ReactElement {
</h2> console.log('Heading rendered')
); return <h2>{title}</h2>
} }

View File

@@ -1,16 +1,16 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
interface User { interface User {
id: number; id: number
name: string; name: string
email: string; email: string
website: string; website: string
} }
export function useFetch(url: string) { export function useFetch(url: string) {
const [data, setData] = useState<User[] | null>(null); const [data, setData] = useState<User[] | null>(null)
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null)
// alternativ // alternativ
// const [fetchValues, setFetchValues] = useState({ // const [fetchValues, setFetchValues] = useState({
@@ -22,28 +22,23 @@ export function useFetch(url: string) {
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
try { try {
const response = await fetch(url); const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
setError("Network response was not ok"); setError('Network response was not ok')
throw new Error("Network response was not ok"); throw new Error('Network response was not ok')
} }
const data = await response.json(); const data = await response.json()
setData(data); setData(data)
setError(null); setError(null)
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error('Error fetching data:', error)
setError("Error fetching data"); setError('Error fetching data')
} } finally {
finally { setLoading(false)
setLoading(false);
} }
} }
fetchData(); fetchData()
}, [url])
}, [url]);
return { data, loading, error };
return { data, loading, error }
} }

View File

@@ -1,39 +1,43 @@
import { useState } from "react"; import { useState } from 'react'
export default function useLocalStorage<T>(key: string, initialValue: T | null) { export default function useLocalStorage<T>(
key: string,
initialValue: T | null
) {
const [value, setValue] = useState<T | null>(() => { const [value, setValue] = useState<T | null>(() => {
try { try {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return initialValue; return initialValue
} }
const storedValue = localStorage.getItem(key); const storedValue = localStorage.getItem(key)
return storedValue ? JSON.parse(storedValue) : initialValue; return storedValue ? JSON.parse(storedValue) : initialValue
} catch (error) { } catch (error) {
console.error("Error reading from localStorage", error); console.error('Error reading from localStorage', error)
return initialValue; return initialValue
} }
}); })
const setStoredValue = (newValue: T) => { const setStoredValue = (newValue: T) => {
try { try {
const valueToStore = newValue instanceof Function ? newValue(value) : newValue; const valueToStore =
setValue(valueToStore); newValue instanceof Function ? newValue(value) : newValue
if (typeof window !== "undefined") { setValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore)); if (typeof window !== 'undefined') {
localStorage.setItem(key, JSON.stringify(valueToStore))
} }
} catch (error) { } catch (error) {
console.error("Error writing to localStorage", error); console.error('Error writing to localStorage', error)
} }
} }
const removeStoredValue = () => { const removeStoredValue = () => {
try { try {
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
localStorage.removeItem(key); localStorage.removeItem(key)
} }
setValue(null); setValue(null)
} catch (error) { } catch (error) {
console.error("Error removing from localStorage", error); console.error('Error removing from localStorage', error)
} }
} }
return {value, setStoredValue, removeStoredValue}; return { value, setStoredValue, removeStoredValue }
} }

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
/** /**
* Custom hook to check if a media query matches the current viewport. * Custom hook to check if a media query matches the current viewport.
@@ -7,25 +6,25 @@ import { useEffect, useState } from "react";
* @returns A boolean indicating whether the media query matches. * @returns A boolean indicating whether the media query matches.
*/ */
export function useMediaQuery(query: string) { export function useMediaQuery(query: string) {
const [matches, setMatches] = useState<boolean>(false); const [matches, setMatches] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
const mediaQueryList = window.matchMedia(query); const mediaQueryList = window.matchMedia(query)
const handleChange = (event: MediaQueryListEvent) => { const handleChange = (event: MediaQueryListEvent) => {
setMatches(event.matches); setMatches(event.matches)
}; }
// Set the initial value // Set the initial value
setMatches(mediaQueryList.matches); setMatches(mediaQueryList.matches)
// Add event listener // Add event listener
mediaQueryList.addEventListener('change', handleChange); mediaQueryList.addEventListener('change', handleChange)
// Cleanup function to remove the event listener // Cleanup function to remove the event listener
return () => { return () => {
mediaQueryList.removeEventListener('change', handleChange); mediaQueryList.removeEventListener('change', handleChange)
}; }
}, [query]); }, [query])
return matches; return matches
} }

View 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

View 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

View File

@@ -1,22 +1,22 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest'
import { shuffleArray, users } from './shuffle'; import { shuffleArray, users } from './shuffle'
describe('shuffleArray', () => { describe('shuffleArray', () => {
it('should shuffle the array and return a new order', () => { it('should shuffle the array and return a new order', () => {
const originalArray = [...users]; const originalArray = [...users]
const shuffledArray = shuffleArray([...users]); const shuffledArray = shuffleArray([...users])
expect(shuffledArray).not.toEqual(originalArray); // Ensure the order is different expect(shuffledArray).not.toEqual(originalArray) // Ensure the order is different
expect(shuffledArray.sort()).toEqual(originalArray.sort()); // Ensure all elements are still present expect(shuffledArray.sort()).toEqual(originalArray.sort()) // Ensure all elements are still present
}); })
it('should return an empty array when input is empty', () => { it('should return an empty array when input is empty', () => {
const result = shuffleArray([]); const result = shuffleArray([])
expect(result).toEqual([]); expect(result).toEqual([])
}); })
it('should handle arrays with one element', () => { it('should handle arrays with one element', () => {
const result = shuffleArray(['onlyElement']); const result = shuffleArray(['onlyElement'])
expect(result).toEqual(['onlyElement']); expect(result).toEqual(['onlyElement'])
}); })
}); })

View File

@@ -1,11 +1,10 @@
export function shuffleArray(array: string[]){ export function shuffleArray(array: string[]) {
for(let i = array.length -1; i>0; i--){ for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const j = Math.floor(Math.random()* (i +1)); ;[array[i], array[j]] = [array[j], array[i]]
[array[i], array[j]] = [array[j], array[i]];
} }
return [...array] return [...array]
} }
export const users =['John', 'Peter', 'Stefanie', 'Marta', 'Klaus', 'Anna'] export const users = ['John', 'Peter', 'Stefanie', 'Marta', 'Klaus', 'Anna']

View File

@@ -28,7 +28,9 @@ header nav a {
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
transition: background-color 0.3s, color 0.3s; transition:
background-color 0.3s,
color 0.3s;
} }
header nav a:hover { header nav a:hover {
@@ -38,8 +40,6 @@ header nav a.active {
text-decoration: underline; text-decoration: underline;
} }
footer { footer {
background-color: #1a1a1a; background-color: #1a1a1a;
color: #ffffff; color: #ffffff;
@@ -47,10 +47,27 @@ footer {
text-align: center; text-align: center;
} }
.content { .content {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
text-align: center; 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;
}
}

View File

@@ -1,20 +1,23 @@
import { useState } from "react"; import { useState } from 'react'
import './MainLayout.css' import './MainLayout.css'
import Footer from "../common/components/Footer"; import Footer from '../common/components/Footer'
import Header from "../common/components/Header"; 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 />
@@ -22,10 +25,12 @@ export default function MainLayout({children}: {children: React.ReactNode | Reac
<div className="content"> <div className="content">
{children} {children}
<button onClick={handleClick}>Click</button> <Button onClick={handleClick} variant="secondary" size="sm">
Click
</Button>
</div> </div>
<Footer /> <Footer />
</> </>
); )
} }

View File

@@ -3,11 +3,14 @@ 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 { BrowserRouter } from 'react-router-dom'
import { enableMapSet } from 'immer'
enableMapSet()
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</StrictMode>, </StrictMode>
) )

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
}

View File

@@ -1,19 +0,0 @@
import { createPortal } from "react-dom";
import ComponentWrapper from "../common/components/ComponentWrapper/ComponentWrapper";
import Modal from "../common/components/Modal";
import { useState } from "react";
export default function ComponentWrapperPage() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
<ComponentWrapper title='Komponenten Wrapper Test'>
<h1>Hallo</h1>
</ComponentWrapper>
<ComponentWrapper title='Modal Test'>
<button onClick={() => setModalOpen(!modalOpen)}>Toggle Modal</button>
{modalOpen && createPortal(<Modal onClose={() => setModalOpen(false)}/>,document.getElementById("modal") as HTMLElement)}
</ComponentWrapper>
</div>
);
}

View File

@@ -1,12 +0,0 @@
import EffectExercises from "../common/components/exercises/EffectExercises";
import Heading from "../common/components/globals/Heading";
export default function EffectExercisesPage() {
return (
<div>
<Heading title="Effekte" />
<EffectExercises />
</div>
);
}

View File

@@ -1,11 +0,0 @@
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>
);
}

View File

@@ -1,10 +0,0 @@
import Heading from "../common/components/globals/Heading";
export default function HomePage() {
return (
<>
<Heading title="Willkommen" />
<p>Willkommen auf der Startseite!</p>
</>
);
}

View File

@@ -1,11 +0,0 @@
import MemoCallback from "../common/components/exercises/MemoCallback";
import Heading from "../common/components/globals/Heading";
export default function MemoCallbackPage() {
return (
<>
<Heading title="Memo und Callback" />
<MemoCallback />
</>
)
}

View File

@@ -1,13 +0,0 @@
import UserEffect from "../common/components/exercises/UserEffect";
import { UserOverview } from "../common/components/exercises/UserOverview";
import Heading from "../common/components/globals/Heading";
export default function UsersPage() {
return (
<div>
<Heading title="Benutzer" />
<UserEffect />
<UserOverview />
</div>
);
}

View 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',
},
}

View 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>,
},
}

View 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 />,
}

View 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
react-advanced-tag1/vitest.shims.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@vitest/browser/providers/playwright" />

View 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'],
},
},
]);