Compare commits

..

4 Commits

Author SHA1 Message Date
e560248
cd4f632c85 add Page Router & improve styling 2025-04-07 17:01:44 +02:00
e560248
dbfc23068c custom hook useLocalStorage 2025-04-07 16:28:29 +02:00
e560248
42d87e5275 custom Hook useMediaQuery 2025-04-07 15:57:46 +02:00
e560248
3a4be8d359 add performance calc 2025-04-07 15:38:36 +02:00
22 changed files with 311 additions and 26 deletions

View File

@@ -9,7 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-router-dom": "^7.5.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",
@@ -1393,6 +1394,12 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@types/estree/-/estree-1.0.7.tgz",
@@ -2035,6 +2042,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3077,6 +3093,46 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.5.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/react-router/-/react-router-7.5.0.tgz",
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.5.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/react-router-dom/-/react-router-dom-7.5.0.tgz",
"integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
"license": "MIT",
"dependencies": {
"react-router": "7.5.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3178,6 +3234,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3328,6 +3390,12 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/type-check/-/type-check-0.4.0.tgz", "resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/type-check/-/type-check-0.4.0.tgz",

View File

@@ -11,7 +11,8 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-router-dom": "^7.5.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",

View File

@@ -1,9 +1,4 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo { .logo {
height: 6em; height: 6em;

View File

@@ -1,17 +1,23 @@
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 './routes/HomePage'
import UsersPage from './routes/UsersPage'
import EffectExercisesPage from './routes/EffectExercisesPage'
import FormsPage from './routes/FormsPage'
import MemoCallbackPage from './routes/MemoCallbackPage'
function App() { function App() {
return <MainLayout> return <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 />} />
</Routes>
</MainLayout> </MainLayout>
} }

View File

@@ -2,8 +2,11 @@ export default function Navigation () {
return ( return (
<nav> <nav>
<ul> <ul>
<li>home</li> <li><a href="/">Home</a></li>
<li>test</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>
</ul> </ul>
</nav>); </nav>);
} }

View File

@@ -4,7 +4,7 @@ const Header = () => {
console.log("Header rendered"); console.log("Header rendered");
return ( return (
<header> <header>
<h2>My Application</h2> <h1>My Application</h1>
<Navigation /> <Navigation />
</header> </header>
); );

View File

@@ -23,7 +23,6 @@ export default function MemoCallback() {
}); });
return ( return (
<div> <div>
<h1>Memo Callback Übung</h1>
<button onClick={handleShuffle}>Shuffle</button> <button onClick={handleShuffle}>Shuffle</button>
<Search onChange={handleSearch} /> <Search onChange={handleSearch} />
<div> <div>

View File

@@ -1,16 +1,30 @@
import { FormEvent, useRef, useState } from "react"; import { FormEvent, useRef, useState } from "react";
import useLocalStorage from "../../../hooks/useLocalStorage";
interface FormValues {
email: 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", {
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({
email: emailRef.current?.value || "",
password: passwordRef.current?.value || ""
});
passwordRef.current?.focus(); passwordRef.current?.focus();
} }
@@ -47,8 +61,8 @@ export default function RefExercise(): React.ReactElement {
<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}/> <input type="text" placeholder="email" ref={emailRef} defaultValue={formDataValues?.email}/>
<input type="text" placeholder="password" ref={passwordRef}/> <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" }}>

View File

@@ -19,6 +19,8 @@ export default function UserEffect() {
return; return;
} }
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
@@ -33,6 +35,9 @@ export default function UserEffect() {
return response.json(); return response.json();
}).then(data => { }).then(data => {
setUser(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); setLoading(false);
}).catch(error => { }).catch(error => {
console.error("There was a problem with the fetch operation:", error); console.error("There was a problem with the fetch operation:", error);

View File

@@ -1,8 +1,13 @@
import { useFetch } from "../../hooks/useFetch"; import { useFetch } from "../../hooks/useFetch";
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)');
console.log("isMobile", isMobile);
if (error) { if (error) {
return <p>Error: {error}</p>; return <p>Error: {error}</p>;
} }
@@ -18,7 +23,7 @@ export function UserOverview() {
<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> ({user.email}) - {user.website} <strong>{user.name}</strong> <div style={{display: isMobile ? 'none' : 'block' }}>({user.email}) - {user.website}</div>
</li> </li>
))} ))}
</ul> </ul>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
#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;
}

View File

@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
import Header from "../components/Header"; import Header from "../components/Header";
import './MainLayout.css'
export default function MainLayout({children}: {children: React.ReactNode | React.ReactElement | React.ReactElement[]}): React.ReactElement { export default function MainLayout({children}: {children: React.ReactNode | React.ReactElement | React.ReactElement[]}): React.ReactElement {
@@ -17,9 +18,13 @@ export default function MainLayout({children}: {children: React.ReactNode | Reac
return ( return (
<> <>
<Header /> <Header />
<div className="content">
{children} {children}
<button onClick={handleClick}>Click</button> <button onClick={handleClick}>Click</button>
</div>
<Footer /> <Footer />
</> </>
); );

View File

@@ -2,9 +2,12 @@ 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'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<BrowserRouter>
<App /> <App />
</BrowserRouter>
</StrictMode>, </StrictMode>,
) )

View File

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

View File

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

View File

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

View File

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

View File

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