Compare commits

..

6 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
e560248
048120adae different form examples 2025-04-07 15:07:00 +02:00
e560248
8a3165a26d MemoCallback 2025-04-07 14:29:13 +02:00
25 changed files with 871 additions and 25 deletions

View File

@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -22,7 +23,8 @@
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
"vite": "^6.2.0",
"vitest": "^3.1.1"
}
},
"node_modules/@ampproject/remapping": {
@@ -1392,6 +1394,12 @@
"@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": {
"version": "1.0.7",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@types/estree/-/estree-1.0.7.tgz",
@@ -1665,6 +1673,119 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/@vitest/expect": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/expect/-/expect-3.1.1.tgz",
"integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.1",
"@vitest/utils": "3.1.1",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/mocker/-/mocker-3.1.1.tgz",
"integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.1",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/pretty-format/-/pretty-format-3.1.1.tgz",
"integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/runner/-/runner-3.1.1.tgz",
"integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.1.1",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/snapshot/-/snapshot-3.1.1.tgz",
"integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/spy/-/spy-3.1.1.tgz",
"integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^3.0.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/@vitest/utils/-/utils-3.1.1.tgz",
"integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.1",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/acorn/-/acorn-8.14.1.tgz",
@@ -1728,6 +1849,16 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1792,6 +1923,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/callsites/-/callsites-3.1.0.tgz",
@@ -1823,6 +1964,23 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.2.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/chai/-/chai-5.2.0.tgz",
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/chalk/-/chalk-4.1.2.tgz",
@@ -1840,6 +1998,16 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/color-convert/-/color-convert-2.0.1.tgz",
@@ -1874,6 +2042,15 @@
"dev": true,
"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": {
"version": "7.0.6",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1914,6 +2091,16 @@
}
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/deep-is/-/deep-is-0.1.4.tgz",
@@ -1928,6 +2115,13 @@
"dev": true,
"license": "ISC"
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/esbuild/-/esbuild-0.25.2.tgz",
@@ -2160,6 +2354,16 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/esutils/-/esutils-2.0.3.tgz",
@@ -2170,6 +2374,16 @@
"node": ">=0.10.0"
}
},
"node_modules/expect-type": {
"version": "1.2.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/expect-type/-/expect-type-1.2.1.tgz",
"integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2554,6 +2768,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/loupe": {
"version": "3.1.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/loupe/-/loupe-3.1.3.tgz",
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2564,6 +2785,16 @@
"yallist": "^3.0.2"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/merge2/-/merge2-1.4.1.tgz",
@@ -2724,6 +2955,23 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/picocolors/-/picocolors-1.1.1.tgz",
@@ -2845,6 +3093,46 @@
"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": {
"version": "4.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2946,6 +3234,12 @@
"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": {
"version": "2.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2969,6 +3263,13 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2979,6 +3280,20 @@
"node": ">=0.10.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "3.9.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/std-env/-/std-env-3.9.0.tgz",
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
"dev": true,
"license": "MIT"
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3005,6 +3320,50 @@
"node": ">=8"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true,
"license": "MIT"
},
"node_modules/tinypool": {
"version": "1.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/tinypool/-/tinypool-1.0.2.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "2.0.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "3.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3031,6 +3390,12 @@
"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": {
"version": "0.4.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/type-check/-/type-check-0.4.0.tgz",
@@ -3194,6 +3559,99 @@
}
}
},
"node_modules/vite-node": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/vite-node/-/vite-node-3.1.1.tgz",
"integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.0",
"es-module-lexer": "^1.6.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vitest": {
"version": "3.1.1",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/vitest/-/vitest-3.1.1.tgz",
"integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.1.1",
"@vitest/mocker": "3.1.1",
"@vitest/pretty-format": "^3.1.1",
"@vitest/runner": "3.1.1",
"@vitest/snapshot": "3.1.1",
"@vitest/spy": "3.1.1",
"@vitest/utils": "3.1.1",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.2.0",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"std-env": "^3.8.1",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.1.1",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.1.1",
"@vitest/ui": "3.1.1",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/which/-/which-2.0.2.tgz",
@@ -3210,6 +3668,23 @@
"node": ">= 8"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://bin.sbb.ch/artifactory/api/npm/npm/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -24,6 +25,7 @@
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
"vite": "^6.2.0",
"vitest": "^3.1.1"
}
}

View File

@@ -1,9 +1,4 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
@@ -40,3 +35,8 @@
.read-the-docs {
color: #888;
}
input[type="text"] {
border-radius: 6px;
padding: 1rem 0.5rem;
}

View File

@@ -1,17 +1,23 @@
import { Route, Routes } from 'react-router-dom'
import './App.css'
// 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 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() {
return <MainLayout>
<Heading />
{/* <EffectExercises /> */}
<UserEffect />
<UserOverview />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/user" element={<UsersPage />} />
<Route path="/effect" element={<EffectExercisesPage />} />
<Route path="/forms" element={<FormsPage />} />
<Route path="/memocallback" element={<MemoCallbackPage />} />
</Routes>
</MainLayout>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,8 @@ export default function UserEffect() {
return;
}
const start = performance.now(); // Start measuring performance
setLoading(true);
// Using AbortController to cancel the fetch request if the component unmounts
@@ -33,6 +35,9 @@ export default function UserEffect() {
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);

View File

@@ -1,8 +1,13 @@
import { useFetch } from "../../hooks/useFetch";
import { useMediaQuery } from "../../hooks/useMediaQuery";
export function UserOverview() {
const {data, loading, error} = useFetch("https://jsonplaceholder.typicode.com/users");
const isMobile = useMediaQuery('(max-width: 600px)');
console.log("isMobile", isMobile);
if (error) {
return <p>Error: {error}</p>;
}
@@ -18,7 +23,7 @@ export function UserOverview() {
<ul>
{data.map((user: { id: number; name: string; email: string; website: string }) => (
<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>
))}
</ul>

View File

@@ -1,8 +1,8 @@
export default function Heading({title = "Hallo Teilnehmer"}: {title?: string}): React.ReactElement {
console.log("Heading rendered");
return (
<h1 >
<h2 >
{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 {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
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 Footer from "../components/Footer";
import Header from "../components/Header";
import './MainLayout.css'
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 (
<>
<Header />
<div className="content">
{children}
<button onClick={handleClick}>Click</button>
</div>
<Footer />
</>
);

View File

@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { BrowserRouter } from 'react-router-dom'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</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>
);
}

View File

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

View File

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