add prettier setup

This commit is contained in:
e560248
2025-04-08 12:54:07 +02:00
parent 419113541b
commit 6577698459
43 changed files with 781 additions and 654 deletions

View File

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

View File

@@ -0,0 +1,12 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
trailingComma: "es5",
tabWidth: 2,
semi: false,
singleQuote: true,
};
export default config;

View File

@@ -27,6 +27,7 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"prettier": "3.5.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",
@@ -4257,6 +4258,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/pretty-format/-/pretty-format-27.5.1.tgz",

View File

@@ -13,7 +13,8 @@
"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"
"test:coverage:report:open": "vitest --coverage --reporter=html && open coverage/index.html",
"prettier": "prettier ./src --write"
},
"dependencies": {
"react": "^19.0.0",
@@ -35,6 +36,7 @@
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"jsdom": "^26.0.0",
"prettier": "3.5.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",

View File

@@ -1,5 +1,3 @@
.logo {
height: 6em;
padding: 1.5em;
@@ -36,7 +34,7 @@
color: #888;
}
input[type="text"] {
input[type='text'] {
border-radius: 6px;
padding: 1rem 0.5rem;
}

View File

@@ -13,8 +13,8 @@ import ModalPage from './pages/ModalPage'
import { HocPage } from './pages/HocPage'
function App() {
return <MainLayout>
return (
<MainLayout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/user" element={<UsersPage />} />
@@ -27,6 +27,7 @@ function App() {
<Route path="/hoc" element={<HocPage />} />
</Routes>
</MainLayout>
)
}
export default App

View File

@@ -1,17 +1,18 @@
import './ComponentWrapper.css'
interface ComponentWrapperProps {
title?: string;
children: React.ReactNode;
title?: string
children: React.ReactNode
}
export default function ComponentWrapper({ children, title }: ComponentWrapperProps): React.ReactElement {
console.log("ComponentWrapper rendered:", title);
export default function ComponentWrapper({
children,
title,
}: ComponentWrapperProps): React.ReactElement {
console.log('ComponentWrapper rendered:', title)
return (
<div className='component-wrapper'>
<div className="component-wrapper">
{title && <h2>{title}</h2>}
<div className='component-content'>
{children}
<div className="component-content">{children}</div>
</div>
</div>
);
)
}

View File

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

View File

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

View File

@@ -2,15 +2,34 @@ export default function Navigation () {
return (
<nav>
<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">ZustandCounterPage</a></li>
<li><a href="/hoc">HOC</a></li>
<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">ZustandCounterPage</a>
</li>
<li>
<a href="/hoc">HOC</a>
</li>
</ul>
</nav>);
</nav>
)
}

View File

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

View File

@@ -1,18 +1,25 @@
import'./Modal.css';
import './Modal.css'
interface ModalProps {
open?: boolean;
onClose?: () => void;
open?: boolean
onClose?: () => void
}
export default function Modal({ open= true, onClose }: ModalProps): React.ReactElement {
console.log("Modal rendered");
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'>
<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>
);
)
}

View File

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

View File

@@ -1,18 +1,17 @@
import withLoader, { LoaderData } from "./WithLoader";
import './DogImages.css';
import withLoader, { LoaderData } from './WithLoader'
import './DogImages.css'
interface DogImagesProps {
data: LoaderData;
data: LoaderData
}
function DogImages({ data }: DogImagesProps) {
return data.message.map((img, index) => (
<img
key={index}
src={img}
className="dog-image"
/>
));
<img key={index} src={img} className="dog-image" />
))
}
const DogImagesWithLoader = withLoader(DogImages, "https://dog.ceo/api/breed/labrador/images/random/6");
export default DogImagesWithLoader;
const DogImagesWithLoader = withLoader(
DogImages,
'https://dog.ceo/api/breed/labrador/images/random/6'
)
export default DogImagesWithLoader

View File

@@ -1,50 +1,44 @@
import { ComponentType, useEffect, useState } from "react";
import { ComponentType, useEffect, useState } from 'react'
export type LoaderData = {
message: string[];
status: string;
message: string[]
status: string
}
interface WithLoaderProps {
data: LoaderData;
data: LoaderData
}
// HOC definition
export default function withLoader<P>(
WrappedComponent: ComponentType<P & WithLoaderProps>,
url: string,
url: string
) {
return function WithLoaderComponent(props: P) {
const [data, setData] = useState<LoaderData | null>(null);
const [data, setData] = useState<LoaderData | null>(null)
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
const response = await fetch(url)
const json = await response.json()
// setData(json);
setTimeout(() => {
setData(json);
}
, 5000);
}
catch (error) {
console.error("Error fetching data:", error);
setData(json)
}, 5000)
} catch (error) {
console.error('Error fetching data:', error)
}
}
fetchData();
}, []);
fetchData()
}, [])
if (!data) {
return <p>Loading...</p>;
return <p>Loading...</p>
}
return (
<WrappedComponent {...(props as P)} data={data} />
);
};
return <WrappedComponent {...(props as P)} data={data} />
}
}

View File

@@ -1,12 +1,18 @@
import React, { memo } from "react";
import React, { memo } from 'react'
interface SearchProps {
onChange: (text: string) => void;
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..." />;
console.log('Search rendered')
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 MemoCallback from "./index";
import { describe, it, expect } from "vitest";
import { users } from "../../../utils/shuffle";
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 />);
describe('MemoCallback Component', () => {
it('should render the component with initial users', () => {
render(<MemoCallback />)
users.forEach((user) => {
expect(screen.getByText(user)).toBeDefined();
});
});
expect(screen.getByText(user)).toBeDefined()
})
})
it("should filter users based on search input", () => {
render(<MemoCallback />);
const searchInput = screen.getByPlaceholderText(/search/i);
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"));
fireEvent.change(searchInput, { target: { value: 'a' } })
const filteredUsers = users.filter((user) =>
user.toLowerCase().includes('a')
)
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) => {
expect(screen.queryByText(user)).not.toBeDefined();
});
});
expect(screen.queryByText(user)).not.toBeDefined()
})
})
it("should shuffle users when the shuffle button is clicked", () => {
render(<MemoCallback />);
const shuffleButton = screen.getByText(/shuffle/i);
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);
const initialOrder = screen.getAllByText(/./).map((el) => el.textContent)
fireEvent.click(shuffleButton)
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 Search from "./Search";
import { users, shuffleArray } from "../../../utils/shuffle";
import { useCallback, useState } from 'react'
import Search from './Search'
import { users, shuffleArray } from '../../../utils/shuffle'
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 filteredUsers = users.filter((user) => {
return user.toLowerCase().includes(text.toLowerCase());
});
setAllUsers(filteredUsers);
}, []);
return user.toLowerCase().includes(text.toLowerCase())
})
setAllUsers(filteredUsers)
}, [])
const handleShuffle = () => {
setAllUsers(shuffleArray(users));
};
setAllUsers(shuffleArray(users))
}
const ListOfUsers = allUsers.map((user) => {
return <p key={user}>{user}</p>;
});
return <p key={user}>{user}</p>
})
return (
<div>
<button onClick={handleShuffle}>Shuffle</button>
<Search onChange={handleSearch} />
<div>
{ListOfUsers}
<div>{ListOfUsers}</div>
</div>
</div>
);
)
}

View File

@@ -1,83 +1,112 @@
import { FormEvent, useRef, useState } from "react";
import useLocalStorage from "../../../hooks/useLocalStorage";
import { FormEvent, useRef, useState } from 'react'
import useLocalStorage from '../../../hooks/useLocalStorage'
interface FormValues {
email: string;
password: string;
email: string
password: string
}
export default function RefExercise(): React.ReactElement {
console.log("RefExercise rendered");
console.log('RefExercise rendered')
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const {setStoredValue, value: formDataValues} = useLocalStorage<FormValues>("formdata", {
email: "",
password: ""
} );
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const { setStoredValue, value: formDataValues } = useLocalStorage<FormValues>(
'formdata',
{
email: '',
password: '',
}
)
const handleSubmit = () => {
console.log("submit", {
console.log('submit', {
email: emailRef.current?.value,
password: passwordRef.current?.value
});
password: passwordRef.current?.value,
})
setStoredValue({
email: emailRef.current?.value || "",
password: passwordRef.current?.value || ""
});
passwordRef.current?.focus();
email: emailRef.current?.value || '',
password: passwordRef.current?.value || '',
})
passwordRef.current?.focus()
}
const [formData, setFormData] = useState({
strasse: "",
city: ""
});
strasse: '',
city: '',
})
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[event.target.name]: event.target.value
});
[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", {
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
});
city: city.value,
})
}
function handleFormSubmit(e: FormEvent) {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
e.preventDefault()
const data = new FormData(e.target as HTMLFormElement)
console.log('handleFormSubmit', {
email: data.get('email'),
password: data.get('password')
});
password: data.get('password'),
})
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: "3rem" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<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}/>
<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" }}>
<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}/>
<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" }}>
<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>
);
)
}

View File

@@ -1,66 +1,97 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react'
interface User {
id: number;
name: string;
email: string;
website: string;
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);
const [user, setUser] = useState<User>()
const [userId, setUserId] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(false)
useEffect(() => {
console.log("UserEffect mounted");
console.log('UserEffect mounted')
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
// or if the userId changes before the fetch completes
const controller = new AbortController();
const signal = controller.signal;
const controller = new AbortController()
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) {
throw new Error("Network response was not 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 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");
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');
controller.abort('Fetch aborted')
}
}, [userId]);
}, [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>}
{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>
<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>
)
}

View File

@@ -1,33 +1,45 @@
import { useFetch } from "../../hooks/useFetch";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useFetch } from '../../hooks/useFetch'
import { useMediaQuery } from '../../hooks/useMediaQuery'
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) {
return <p>Error: {error}</p>;
return <p>Error: {error}</p>
}
if (loading) {
return <p>Loading...</p>;
return <p>Loading...</p>
}
if (!data) {
return <p>No data available</p>;
return <p>No data available</p>
}
return (
<div>
<h2>User Overview</h2>
<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}>
<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>
))}
)
)}
</ul>
<p>Total Users: {data.length}</p>
</div>
);
)
}

View File

@@ -1,8 +1,8 @@
export default function Heading({title = "Hallo Teilnehmer"}: {title?: string}): React.ReactElement {
console.log("Heading rendered");
return (
<h2 >
{title}
</h2>
);
export default function Heading({
title = 'Hallo Teilnehmer',
}: {
title?: string
}): React.ReactElement {
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 {
id: number;
name: string;
email: string;
website: string;
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);
const [data, setData] = useState<User[] | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// alternativ
// const [fetchValues, setFetchValues] = useState({
@@ -22,28 +22,23 @@ export function useFetch(url: string) {
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
const response = await fetch(url)
if (!response.ok) {
setError("Network response was not ok");
throw new Error("Network response was not ok");
setError('Network response was not ok')
throw new Error('Network response was not ok')
}
const data = await response.json();
setData(data);
setError(null);
const data = await response.json()
setData(data)
setError(null)
} catch (error) {
console.error("Error fetching data:", error);
setError("Error fetching data");
}
finally {
setLoading(false);
console.error('Error fetching data:', error)
setError('Error fetching data')
} finally {
setLoading(false)
}
}
fetchData();
}, [url]);
return { data, loading, error };
fetchData()
}, [url])
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>(() => {
try {
if (typeof window === "undefined") {
return initialValue;
if (typeof window === 'undefined') {
return initialValue
}
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
const storedValue = localStorage.getItem(key)
return storedValue ? JSON.parse(storedValue) : initialValue
} catch (error) {
console.error("Error reading from localStorage", error);
return initialValue;
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));
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);
console.error('Error writing to localStorage', error)
}
}
const removeStoredValue = () => {
try {
if (typeof window !== "undefined") {
localStorage.removeItem(key);
if (typeof window !== 'undefined') {
localStorage.removeItem(key)
}
setValue(null);
setValue(null)
} 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.
@@ -7,25 +6,25 @@ import { useEffect, useState } from "react";
* @returns A boolean indicating whether the media query matches.
*/
export function useMediaQuery(query: string) {
const [matches, setMatches] = useState<boolean>(false);
const [matches, setMatches] = useState<boolean>(false)
useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const mediaQueryList = window.matchMedia(query)
const handleChange = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
setMatches(event.matches)
}
// Set the initial value
setMatches(mediaQueryList.matches);
setMatches(mediaQueryList.matches)
// Add event listener
mediaQueryList.addEventListener('change', handleChange);
mediaQueryList.addEventListener('change', handleChange)
// Cleanup function to remove the event listener
return () => {
mediaQueryList.removeEventListener('change', handleChange);
};
}, [query]);
return matches;
mediaQueryList.removeEventListener('change', handleChange)
}
}, [query])
return matches
}

View File

@@ -1,15 +1,15 @@
import { create } from "zustand";
import { create } from 'zustand'
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
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;
export default useCounterStore

View File

@@ -1,22 +1,22 @@
import { describe, it, expect } from 'vitest';
import { shuffleArray, users } from './shuffle';
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]);
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
});
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([]);
});
const result = shuffleArray([])
expect(result).toEqual([])
})
it('should handle arrays with one element', () => {
const result = shuffleArray(['onlyElement']);
expect(result).toEqual(['onlyElement']);
});
});
const result = shuffleArray(['onlyElement'])
expect(result).toEqual(['onlyElement'])
})
})

View File

@@ -1,8 +1,7 @@
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]];
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return [...array]

View File

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

View File

@@ -1,20 +1,22 @@
import { useState } from "react";
import { useState } from 'react'
import './MainLayout.css'
import Footer from "../common/components/Footer";
import Header from "../common/components/Header";
import Footer from '../common/components/Footer'
import Header from '../common/components/Header'
export default function MainLayout({children}: {children: React.ReactNode | React.ReactElement | React.ReactElement[]}): React.ReactElement {
const [clicked, setClicked] = useState(false);
export default function MainLayout({
children,
}: {
children: React.ReactNode | React.ReactElement | React.ReactElement[]
}): React.ReactElement {
const [clicked, setClicked] = useState(false)
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 (
<>
<Header />
@@ -27,5 +29,5 @@ export default function MainLayout({children}: {children: React.ReactNode | Reac
<Footer />
</>
);
)
}

View File

@@ -9,5 +9,5 @@ createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
</StrictMode>
)

View File

@@ -1,11 +1,11 @@
import ComponentWrapper from "../common/components/ComponentWrapper/ComponentWrapper";
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'>
<ComponentWrapper title="Komponenten Wrapper Test">
<h1>Hallo</h1>
</ComponentWrapper>
</div>
);
)
}

View File

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

View File

@@ -1,5 +1,5 @@
import RefExercise from "../common/components/exercises/RefExercise";
import Heading from "../common/components/globals/Heading";
import RefExercise from '../common/components/exercises/RefExercise'
import Heading from '../common/components/globals/Heading'
export default function FormsPage() {
return (
@@ -7,5 +7,5 @@ export default function FormsPage() {
<Heading title="Formulare" />
<RefExercise />
</div>
);
)
}

View File

@@ -1,6 +1,5 @@
import ComponentWrapper from "../common/components/ComponentWrapper/ComponentWrapper";
import DogImagesWithLoader from "../common/components/exercises/HocExercise/DogImages";
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
import DogImagesWithLoader from '../common/components/exercises/HocExercise/DogImages'
export function HocPage() {
return (

View File

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

View File

@@ -1,5 +1,5 @@
import MemoCallback from "../common/components/exercises/MemoCallback";
import Heading from "../common/components/globals/Heading";
import MemoCallback from '../common/components/exercises/MemoCallback'
import Heading from '../common/components/globals/Heading'
export default function MemoCallbackPage() {
return (

View File

@@ -1,14 +1,18 @@
import { useState } from "react";
import { createPortal } from "react-dom";
import ComponentWrapper from "../common/components/ComponentWrapper/ComponentWrapper";
import Modal from "../common/components/Modal";
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);
const [modalOpen, setModalOpen] = useState(false)
return (
<ComponentWrapper title='Modal Test'>
<ComponentWrapper title="Modal Test">
<button onClick={() => setModalOpen(!modalOpen)}>Toggle Modal</button>
{modalOpen && createPortal(<Modal onClose={() => setModalOpen(false)}/>,document.getElementById("modal") as HTMLElement)}
{modalOpen &&
createPortal(
<Modal onClose={() => setModalOpen(false)} />,
document.getElementById('modal') as HTMLElement
)}
</ComponentWrapper>
);
)
}

View File

@@ -1,6 +1,6 @@
import UserEffect from "../common/components/exercises/UserEffect";
import { UserOverview } from "../common/components/exercises/UserOverview";
import Heading from "../common/components/globals/Heading";
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 (
@@ -9,5 +9,5 @@ export default function UsersPage() {
<UserEffect />
<UserOverview />
</div>
);
)
}

View File

@@ -1,18 +1,18 @@
import ComponentWrapper from "../common/components/ComponentWrapper/ComponentWrapper";
import useCounterStore from "../common/stores/counter-store";
import ComponentWrapper from '../common/components/ComponentWrapper/ComponentWrapper'
import useCounterStore from '../common/stores/counter-store'
export default function ZustandCounterPage() {
const {decrement, increment} = useCounterStore();
const { decrement, increment } = useCounterStore()
return (
<ComponentWrapper title='Zustand Counter'>
<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>;
const count = useCounterStore((state) => state.count)
return <p>Count: {count}</p>
}