From ce35a827c37ad92c96c0dd7355343c21def1f6ab Mon Sep 17 00:00:00 2001 From: Philippe Torrel Date: Thu, 29 Oct 2026 11:09:13 +0100 Subject: [PATCH] --- 06_react/linksammlung.md | 9 +- .../tag39/01_react-api-with-hooks/.gitignore | 24 +++ .../01_react-api-with-hooks/.oxlintrc.json | 8 + .../tag39/01_react-api-with-hooks/README.md | 16 ++ .../tag39/01_react-api-with-hooks/index.html | 17 ++ .../package-lock.json | 0 .../01_react-api-with-hooks/package.json | 31 ++++ .../public/favicon.svg | 1 + .../01_react-api-with-hooks/public/icons.svg | 24 +++ .../tag39/01_react-api-with-hooks/src/App.jsx | 19 +++ .../src/assets/react.svg | 1 + .../src/assets/vite.svg | 1 + .../src/components/common/Badge.jsx | 22 +++ .../components/examples/ExampleUseContext.jsx | 18 ++ .../examples/ExampleUseEffectDidUpdate.jsx | 30 ++++ .../examples/ExampleUseEffectMount.jsx | 27 +++ .../examples/ExampleUseEffectUnMount.jsx | 36 ++++ .../src/components/examples/ExampleUseId.jsx | 21 +++ .../components/examples/ExampleUseReducer.jsx | 77 +++++++++ .../components/examples/ExampleUseState.jsx | 36 ++++ .../examples/theme-switcher/ThemeContext.jsx | 8 + .../examples/theme-switcher/ThemeProvider.jsx | 25 +++ .../src/components/footers/FooterMain.jsx | 78 +++++++++ .../src/components/forms/FormContact.jsx | 159 ++++++++++++++++++ .../src/components/forms/FormNewsletter.jsx | 35 ++++ .../src/components/forms/joi/FormContact.jsx | 159 ++++++++++++++++++ .../src/components/forms/joi/validate.js | 67 ++++++++ .../forms/react-hook-form/FormContact.jsx | 132 +++++++++++++++ .../src/components/headers/HeaderMain.jsx | 12 ++ .../src/components/icons/Logo.jsx | 48 ++++++ .../src/components/inputs/InputAmount.jsx | 61 +++++++ .../src/components/lists/ListTodo.jsx | 84 +++++++++ .../src/components/lists/ListUser.jsx | 53 ++++++ .../src/components/logos/LogoMain.jsx | 15 ++ .../src/components/navs/Navbar.jsx | 110 ++++++++++++ .../src/layouts/Layout.jsx | 20 +++ .../01_react-api-with-hooks/src/main.jsx | 18 ++ .../src/pages/AboutPage.jsx | 14 ++ .../src/pages/ApiPage.jsx | 20 +++ .../src/pages/ContactPage.jsx | 20 +++ .../src/pages/HomePage.jsx | 34 ++++ .../src/pages/UserPage.jsx | 24 +++ .../src/pages/UserProfilePage.jsx | 19 +++ .../src/routes/Router.jsx | 79 +++++++++ .../src/styles/01_frameworks/_index.scss | 3 + .../_gloria-hallelujah-v24-latin.scss | 8 + .../src/styles/02_fonts/_index.scss | 3 + .../src/styles/02_fonts/_lato-v25-latin.scss | 89 ++++++++++ .../02_fonts/_source-sans-3-v19-latin.scss | 143 ++++++++++++++++ .../src/styles/03_base/_colors.scss | 13 ++ .../src/styles/03_base/_index.scss | 2 + .../src/styles/03_base/_theme.scss | 52 ++++++ .../src/styles/03_base/_typography.scss | 9 + .../src/styles/04_plugins/_index.scss | 7 + .../04_plugins/toastify-1.12/_toastify.scss | 88 ++++++++++ .../src/styles/05_modules/_button.scss | 17 ++ .../styles/05_modules/_chuck-norris-joke.scss | 38 +++++ .../src/styles/05_modules/_footerMain.scss | 45 +++++ .../src/styles/05_modules/_header-main.scss | 15 ++ .../src/styles/05_modules/_index.scss | 9 + .../src/styles/05_modules/_input-amount.scss | 27 +++ .../src/styles/05_modules/_loader.scss | 22 +++ .../src/styles/05_modules/_logo-main.scss | 21 +++ .../src/styles/05_modules/_menu-main.scss | 20 +++ .../styles/05_modules/_product-manager.scss | 47 ++++++ .../src/styles/main.scss | 13 ++ .../src/utils/validators/validate.js | 39 +++++ .../01_react-api-with-hooks/vite.config.js | 15 ++ 68 files changed, 2456 insertions(+), 1 deletion(-) create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/.gitignore create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/.oxlintrc.json create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/README.md create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/index.html rename 06_react/unterricht/{tag38/02_react-api-with-hooks => tag39/01_react-api-with-hooks}/package-lock.json (100%) create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/package.json create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/public/favicon.svg create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/public/icons.svg create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/App.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/react.svg create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/vite.svg create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/common/Badge.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseContext.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectDidUpdate.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectMount.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectUnMount.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseId.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseReducer.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseState.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeContext.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeProvider.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/footers/FooterMain.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormContact.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormNewsletter.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/FormContact.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/validate.js create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/react-hook-form/FormContact.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/headers/HeaderMain.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/icons/Logo.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/inputs/InputAmount.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListTodo.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListUser.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/logos/LogoMain.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/components/navs/Navbar.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/layouts/Layout.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/main.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/AboutPage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ApiPage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ContactPage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/HomePage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserPage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserProfilePage.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/routes/Router.jsx create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/01_frameworks/_index.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_gloria-hallelujah-v24-latin.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_index.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_lato-v25-latin.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_source-sans-3-v19-latin.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_colors.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_index.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_theme.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_typography.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/_index.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/toastify-1.12/_toastify.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_button.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_chuck-norris-joke.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_footerMain.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_header-main.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_index.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_input-amount.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_loader.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_logo-main.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_menu-main.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_product-manager.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/main.scss create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/src/utils/validators/validate.js create mode 100644 06_react/unterricht/tag39/01_react-api-with-hooks/vite.config.js diff --git a/06_react/linksammlung.md b/06_react/linksammlung.md index 0647c9c..24182d4 100644 --- a/06_react/linksammlung.md +++ b/06_react/linksammlung.md @@ -89,4 +89,11 @@ - Tipp10 -https://stackshare.io/stacks \ No newline at end of file +https://stackshare.io/stacks + + +## Loader + +- https://css-loaders.com/3d/ +- https://cssloaders.github.io/#text +- https://loading.io/ \ No newline at end of file diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/.gitignore b/06_react/unterricht/tag39/01_react-api-with-hooks/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/.oxlintrc.json b/06_react/unterricht/tag39/01_react-api-with-hooks/.oxlintrc.json new file mode 100644 index 0000000..1255078 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/README.md b/06_react/unterricht/tag39/01_react-api-with-hooks/README.md new file mode 100644 index 0000000..d937833 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project. diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/index.html b/06_react/unterricht/tag39/01_react-api-with-hooks/index.html new file mode 100644 index 0000000..2ea4e09 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/index.html @@ -0,0 +1,17 @@ + + + + + + + + React API with hooks + + + +
+ + + diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/package-lock.json b/06_react/unterricht/tag39/01_react-api-with-hooks/package-lock.json similarity index 100% rename from 06_react/unterricht/tag38/02_react-api-with-hooks/package-lock.json rename to 06_react/unterricht/tag39/01_react-api-with-hooks/package-lock.json diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/package.json b/06_react/unterricht/tag39/01_react-api-with-hooks/package.json new file mode 100644 index 0000000..afa00c4 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/package.json @@ -0,0 +1,31 @@ +{ + "name": "react-api-with-hooks", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.21.0", + "bootstrap": "^5.3.8", + "joi": "^18.3.1", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-hook-form": "^7.91.0", + "react-icons": "^5.7.0", + "react-router-dom": "^7.18.5", + "validator": "^13.15.35" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "oxlint": "^1.90.0", + "sass-embedded": "^1.107.0", + "vite": "^8.3.5" + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/public/favicon.svg b/06_react/unterricht/tag39/01_react-api-with-hooks/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/public/icons.svg b/06_react/unterricht/tag39/01_react-api-with-hooks/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/App.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/App.jsx new file mode 100644 index 0000000..fe1cec5 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/App.jsx @@ -0,0 +1,19 @@ +import Navbar from './components/navs/Navbar'; +import Router from './routes/Router'; +import HeaderMain from './components/headers/HeaderMain'; +import FooterMain from './components/footers/FooterMain'; + +function App() { + return ( + <> + + +
+ +
+ + + ); +} + +export default App; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/react.svg b/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/vite.svg b/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/common/Badge.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/common/Badge.jsx new file mode 100644 index 0000000..df1b229 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/common/Badge.jsx @@ -0,0 +1,22 @@ +const Badge = (props) => { + const { text = '', variant = 'secondary', color } = props; + // const inline = typeof color !== 'undefined' ? { backgroundColor: color } : ''; + + // if (color) { + // return ( + // + // {text} + // + // ); + // } else { + // return {text}; + // } + + return ( + + {text} + + ); +}; + +export default Badge; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseContext.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseContext.jsx new file mode 100644 index 0000000..66c0a02 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseContext.jsx @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import ThemeContext from './theme-switcher/ThemeContext'; + +const ExampleUseContext = (props) => { + // const { } = props; + + const theme = useContext(ThemeContext); + + return ( +
+

useContext

+

+ The theme of this component is: {theme} +

+
+ ); +}; +export default ExampleUseContext; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectDidUpdate.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectDidUpdate.jsx new file mode 100644 index 0000000..1250041 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectDidUpdate.jsx @@ -0,0 +1,30 @@ +import { useState, useEffect } from 'react'; +import { FaPlus } from 'react-icons/fa6'; +const ExampleUseEffectDidUpdate = (props) => { + // const { } = props; + + const [value, setValue] = useState(0); + + useEffect(() => { + // mount + console.log('first render and rerender: ', value); + + return () => { + // unmount + }; + }, [value]); // <-- wenn state sich ändert, wird useEffect erneut ausgeführt (did update) + + return ( +
+

The value is: {value}

+ +
+ ); +}; +export default ExampleUseEffectDidUpdate; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectMount.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectMount.jsx new file mode 100644 index 0000000..80bd65c --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectMount.jsx @@ -0,0 +1,27 @@ +import { useEffect, useId } from 'react'; + +const ExampleUseEffectMount = (props) => { + // const { } = props; + + const id = useId(); + + console.log(document.getElementById(`box${id}`)); // null + useEffect(() => { + //mount + console.log(document.getElementById(`box${id}`)); //
// JSX Element liegt als gerendertes HTML vor. + const box = document.getElementById(`box${id}`); + + box.style.color = 'white'; + box.style.backgroundColor = '#2a2a2a'; + box.style.padding = '1rem'; + }, []); + + return ( +
+
+

Box was rendered

+
+
+ ); +}; +export default ExampleUseEffectMount; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectUnMount.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectUnMount.jsx new file mode 100644 index 0000000..112a639 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseEffectUnMount.jsx @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; + +const ExampleUseEffectUnMount = (props) => { + // const { } = props; + + const [timer, setTimer] = useState(0); + + useEffect(() => { + // mount + const interval = setInterval(() => { + setTimer((time) => time + 1); + }, 1000); + + window.addEventListener('resize', handleResize); + + return () => { + // cleanup - unmount + clearInterval(interval); + window.removeEventListener('resize', handleResize); + }; + }, []); // didupdate + + // EventHandler + const handleResize = (e) => { + // console.log('Event: ', e); + const { x, y, width: w, height: h } = document.body.getBoundingClientRect(); // window.outerWidth, + console.log({ x, y, w: parseInt(w), h: parseInt(h) }); + }; + + return ( +
+

Timer: {timer}

+
+ ); +}; +export default ExampleUseEffectUnMount; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseId.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseId.jsx new file mode 100644 index 0000000..90ec7f8 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseId.jsx @@ -0,0 +1,21 @@ +import { useId } from 'react'; +// import { v4 as uid} from 'uuid'; +const ExampleUseId = (props) => { + // const { } = props; + + const id = useId(); // _r_1 + // const id = uid(); // von uuid ⇨ 'b18794e8-5d0d-417c-b361-ba38e78411b4' + + return ( +
+

+ ID: {id} +

+
+ + +
+
+ ); +}; +export default ExampleUseId; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseReducer.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseReducer.jsx new file mode 100644 index 0000000..d44cb1a --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseReducer.jsx @@ -0,0 +1,77 @@ +import { useReducer } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa6'; + +const initialState = { + counter: 0, + // more props.... +}; + +// Reducer Funktion verwaltet das state Objekt. Über diese Funktion lässt sich das "komplexere" state-Objekt gezielt verändern. +function reducer(state, action) { + // action Objekt hat immer folgende Eigenschaften: {type: 'ACTION_NAME', payload: changeValue } + const { type, payload = 1 } = action; + + switch (type) { + case 'INCREASE': + return { ...state, counter: state.counter + 1 }; + + case 'DECREASE': + return { ...state, counter: state.counter - 1 }; + case 'DOUBLE': + return { ...state, counter: state.counter * 2 }; + case 'INC_VALUE': + return { ...state, counter: state.counter + payload }; + default: + console.warn('Action Type not found'); + return state; + } +} + +const ExampleUseReducer = (props) => { + // const { } = props; + + // Hooks - Top Level + // const [counter, setCounter] = useState(0); + const [state, dispatch] = useReducer(reducer, initialState); + + const handleClick = (e) => { + const btn = e.currentTarget; + const label = btn.ariaLabel; // btn.getAttribute('aria-label') + switch (label.toLowerCase()) { + case 'increase': + dispatch({ type: 'INCREASE' }); + break; + case 'decrease': + dispatch({ type: 'DECREASE' }); + break; + case 'double': + dispatch({ type: 'DOUBLE' }); + break; + default: //nothing + } + }; + + const handleClickIncValue = (e) => { + dispatch({ type: 'INC_VALUE', payload: 5 }); + }; + + return ( +
+ Counter: {state.counter} + + + + +
+ ); +}; +export default ExampleUseReducer; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseState.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseState.jsx new file mode 100644 index 0000000..1a93578 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/ExampleUseState.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa6'; + +const ExampleUseState = (props) => { + // const { } = props; + const [counter, setCounter] = useState(0); + + const handleClick = (e) => { + const btn = e.currentTarget; + const label = btn.ariaLabel; // btn.getAttribute('aria-label') + switch (label.toLowerCase()) { + case 'increase': + setCounter(counter + 1); + break; + case 'decrease': + setCounter((prevState) => { + return prevState - 1; + }); + break; + default: //nothing + } + }; + + return ( +
+ Counter: {counter} + + +
+ ); +}; +export default ExampleUseState; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeContext.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeContext.jsx new file mode 100644 index 0000000..dda5e98 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeContext.jsx @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export const THEME_DEFAULT_VALUE = 'light'; + +const ThemeContext = createContext(THEME_DEFAULT_VALUE); +// createContext -> JSX-Element mit value Attribut +// ... +export default ThemeContext; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeProvider.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeProvider.jsx new file mode 100644 index 0000000..fa141c5 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/examples/theme-switcher/ThemeProvider.jsx @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import ThemeContext, { THEME_DEFAULT_VALUE } from './ThemeContext'; + +const ThemeProvider = (props) => { + const { children } = props; + + console.log(ThemeContext._currentValue); // 'light' <- _currentValue beinhaltet initialWert. Soll aber in production Umgebung nicht verwendet werden, da kein offizieller Standard. + + const [theme, setTheme] = useState(THEME_DEFAULT_VALUE); + + const handleToggleTheme = (e) => { + console.log('toggle theme'); + setTheme((prevState) => (prevState === 'light' ? 'dark' : 'light')); + }; + + return ( + + + {children} + + ); +}; +export default ThemeProvider; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/footers/FooterMain.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/footers/FooterMain.jsx new file mode 100644 index 0000000..5006bda --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/footers/FooterMain.jsx @@ -0,0 +1,78 @@ +const FooterMain = (props) => { + // const { } = props; + + return ( + + ); +}; +export default FooterMain; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormContact.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormContact.jsx new file mode 100644 index 0000000..9f2d181 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormContact.jsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import validate from '../../utils/validators/validate'; + +const INITIAL_FORM_DATA = { + firstname: '', // + lastname: '', + email: '', + message: '', +}; + +const FormContact = (props) => { + // const { } = props; + + const [formData, setFormData] = useState(INITIAL_FORM_DATA); + + const [errors, setErrors] = useState({}); + + // EventHandlers ======= + const handleSubmit = (e) => { + e.preventDefault(); // Standardverhalten (Versenden vom Formular) unterbinden. + + const form = e.currentTarget; + + const errors = validate(formData); + + if (Object.keys(errors).length === 0) { + console.log('Form submitted ....'); + // TODO: send over AJAX/Fetch + form.reset(); + setFormData(INITIAL_FORM_DATA); + setErrors({}); + } else { + console.warn('Form submit error'); + setErrors(errors); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + + setFormData((state) => ({ ...state, [name]: value })); + + // console.log({ [e.target.name]: e.target.value }); + // console.log('Formdata', formData); + }; + + const handleBlur = (e) => { + const { name, value } = e.target; + const validationErrors = validate({ ...formData, [name]: value }); + + setErrors((state) => { + const nextErrors = { ...state }; + + if (validationErrors[name]) { + nextErrors[name] = validationErrors[name]; + } else { + delete nextErrors[name]; + } + + return nextErrors; + }); + }; + + return ( +
+
+
+
+ + +
OK
+
{errors?.firstname}
+
+
+
+
+ + +
OK
+
{errors?.lastname}
+
+
+
+ +
+
+
+ + +
OK
+
{errors?.email}
+
+
+
+ +
+
+
+ + +
OK
+
{errors?.message}
+
+
+
+ +
+
+
+ +
+
+
+
+ ); +}; +export default FormContact; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormNewsletter.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormNewsletter.jsx new file mode 100644 index 0000000..d8c7ec7 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/FormNewsletter.jsx @@ -0,0 +1,35 @@ +const FormNewsletter = (props) => { + // const { } = props; + + // EventHandler + const handleSubmit = (e) => { + e.preventDefault(); // Standardverhalten unterbinden + console.log('Event: ', e); + }; + + return ( +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ ); +}; +export default FormNewsletter; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/FormContact.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/FormContact.jsx new file mode 100644 index 0000000..2e17e5c --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/FormContact.jsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import validate from './validate'; // joi validate schema object + +const INITIAL_FORM_DATA = { + firstname: '', // + lastname: '', + email: '', + message: '', +}; + +const FormContact = (props) => { + // const { } = props; + + const [formData, setFormData] = useState(INITIAL_FORM_DATA); + + const [errors, setErrors] = useState({}); + + // EventHandlers ======= + const handleSubmit = (e) => { + e.preventDefault(); // Standardverhalten (Versenden vom Formular) unterbinden. + + const form = e.currentTarget; + + const errors = validate(formData); + + if (Object.keys(errors).length === 0) { + console.log('Form submitted ....'); + // TODO: send over AJAX/Fetch + form.reset(); + setFormData(INITIAL_FORM_DATA); + setErrors({}); + } else { + console.warn('Form submit error'); + setErrors(errors); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + + setFormData((state) => ({ ...state, [name]: value })); + + // console.log({ [e.target.name]: e.target.value }); + // console.log('Formdata', formData); + }; + + const handleBlur = (e) => { + const { name, value } = e.target; + const validationErrors = validate({ ...formData, [name]: value }); + + setErrors((state) => { + const nextErrors = { ...state }; + + if (validationErrors[name]) { + nextErrors[name] = validationErrors[name]; + } else { + delete nextErrors[name]; + } + + return nextErrors; + }); + }; + + return ( +
+
+
+
+ + +
OK
+
{errors?.firstname}
+
+
+
+
+ + +
OK
+
{errors?.lastname}
+
+
+
+ +
+
+
+ + +
OK
+
{errors?.email}
+
+
+
+ +
+
+
+ + +
OK
+
{errors?.message}
+
+
+
+ +
+
+
+ +
+
+
+
+ ); +}; +export default FormContact; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/validate.js b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/validate.js new file mode 100644 index 0000000..e1d9df0 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/joi/validate.js @@ -0,0 +1,67 @@ +import Joi from 'joi'; + +const validate = (formData) => { + // const { } = props; + const schema = Joi.object({ + firstname: Joi.string() // + .min(2) + .max(50) + .required() + .messages({ + 'string.min': 'Firstname must have a min length of 2 chars', + 'string.empty': 'Firstname is required', + }) + .label('Firstname'), + lastname: Joi.string() + .required() + .min(2) + .max(60) + .messages({ + 'string.min': 'Lastname must have a min length of 2 chars', + 'string.max': 'Lastname must have a max length of 60 chars', + 'string.empty': 'Lastname is required', + }) + .label('Lastname'), + email: Joi.string() + .email({ tlds: { allow: false } }) + .message('Email is invalid') + .required() + .label('Email'), // tld = top level domain is deactivated, + message: Joi.string() + .required() + .min(10) + .max(1000) + .messages({ + 'string.min': 'Message must have a min length of 10 chars', + 'string.max': 'Message must have a max length of 1000 chars', + 'string.empty': 'Message is required', + }) + .label('Message'), + }); + + const { error } = schema.validate(formData, { abortEarly: false }); + + // Guard + if (!error) { + return {}; + } + + console.log(error); + console.log(error.details); + + // [{ + // context : {label: 'Firstname', value: '', key: 'firstname'} + // message : "\"Firstname\" is not allowed to be empty" + // path: ['firstname'] + // type : "string.empty"} + // },...]; + + const errors = error.details.reduce((obj, current) => { + obj[current.path[0]] = current.message; + return obj; + }, {}); // InitialValue {} + + // {'firstname': "\"Firstname\" is not allowed to be empty", 'lastname': "\"Lastname\" is not allowed to be empty" } + return errors; +}; +export default validate; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/react-hook-form/FormContact.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/react-hook-form/FormContact.jsx new file mode 100644 index 0000000..5151368 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/forms/react-hook-form/FormContact.jsx @@ -0,0 +1,132 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +// import validate from '../../../utils/validators/validate'; + +const FormContact = (props) => { + // const { } = props; + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + // EventHandlers ======= + const onSubmit = (data) => { + console.log('submitted'); + console.log('Form data:', data); + + // fetch('/send', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify(data), + // }) + // .then((res) => res.json()) + // .then((data) => console.log(data)) + // .catch((err) => console.error(err)); + }; + + if (errors) { + console.log(errors); + } + + return ( +
+
+
+
+ + +
OK
+
{errors?.firstname?.message || 'Error'}
+
+
+
+
+ + +
OK
+
{errors?.lastname?.message}
+
+
+
+ +
+
+
+ + +
OK
+
Email is required
+
+
+
+ +
+
+
+ + +
OK
+
{errors?.message?.message}
+
+
+
+ +
+
+
+ +
+
+
+
+ ); +}; +export default FormContact; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/headers/HeaderMain.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/headers/HeaderMain.jsx new file mode 100644 index 0000000..acbddb2 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/headers/HeaderMain.jsx @@ -0,0 +1,12 @@ +import LogoMain from '../logos/LogoMain'; + +const HeaderMain = () => { + return ( +
+
+ +
+
+ ); +}; +export default HeaderMain; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/icons/Logo.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/icons/Logo.jsx new file mode 100644 index 0000000..dd3b6f0 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/icons/Logo.jsx @@ -0,0 +1,48 @@ +const Logo = () => { + return ( + + + + + + + + + ); +}; +export default Logo; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/inputs/InputAmount.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/inputs/InputAmount.jsx new file mode 100644 index 0000000..d6ac3f5 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/inputs/InputAmount.jsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { FaMinus, FaPlus } from 'react-icons/fa6'; + +const InputAmount = (props) => { + const [amount, setAmount] = useState(0); + + // const { } = props; + + // Eventhandler + const handleChange = (e) => { + const value = Number(e.target.value); // e.target.valueAsNumber + + // Guard + if (isNaN(value)) return; + + setAmount(value); + }; + const handleClick = (e) => { + console.log('Event: ', e); + console.log(e.currentTarget.name); + switch (e.currentTarget.name) { + case 'increase': + setAmount((prevState) => prevState + 1); + console.log('inc'); + break; + case 'decrease': + setAmount(amount - 1); + console.log('dec'); + break; + default: //nothing + } + }; + + return ( +
+
+ + + + + + + + +
+
+ ); +}; +export default InputAmount; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListTodo.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListTodo.jsx new file mode 100644 index 0000000..9663ed6 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListTodo.jsx @@ -0,0 +1,84 @@ +import { useEffect, useState } from 'react'; + +const ListTodo = (props) => { + // const { } = props; + + const [todos, setTodos] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + // AbortController instanziieren (seit mid 2018) + const controller = new AbortController(); + + setIsLoading(true); + setError(null); + + fetch('https://dummyjson.com/todos/random/6', { + signal: controller.signal, // signal mitsenden + }) + .then((res) => { + if (!res.ok) { + throw new Error('HTTP-Request went wrong'); + } + return res.json(); // -> Promise HTTP-Response->Body + }) + .then((data) => { + setTodos(data); + }) + .catch((err) => { + console.log(err); + if (err?.name === 'AbortError') { + console.error('Fetch wurde abgebrochen.'); + } else if (err instanceof Error) { + console.error(err.message); + setError(err); + } + }) + .finally(() => { + if (!controller.signal.aborted) { + setIsLoading(false); + } + }); + + return () => { + // cleanup + controller.abort(); // The abort() method of the AbortController interface aborts an asynchronous operation before it has completed. + }; + }, []); + + return ( + <> + {isLoading ? ( + + ) : ( +
    + {todos.length > 0 && + todos.map(({ completed, id, todo }, idx) => ( +
  • +
    + + +
    +
  • + ))} +
+ )} + {error && ( +
+

{error?.message}

+
+ )} + + ); +}; +export default ListTodo; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListUser.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListUser.jsx new file mode 100644 index 0000000..ceb3878 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/lists/ListUser.jsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from 'react'; +import axios from 'axios'; + +const ListUser = (props) => { + // const { } = props; + + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + try { + const { data } = await axios.get('https://dummyjson.com/users?limit=5&select=firstName,lastName,age,email'); + + if (!data) throw Error('Something went wrong'); + setUsers(data.users); + } catch (error) { + console.error(error); + throw Error(`Error: ${error}`); + } + }; + + fetchUsers(); + + // axios + // .get('https://dummyjson.com/users?limit=10&select=firstName,lastName,age,email') + // .then(({ data }) => { + // setUsers(data.users); + // }) + // .catch((err) => { + // console.error(err); + // }); + + return () => {}; + }, []); + + return ( +
    + {users.length > 0 && + users.map(({ firstName, lastName, age, email }, idx) => ( +
  • +

    + Name: {firstName} {lastName} +
    + Age: {age} +
    + Email: {email} +

    +
  • + ))} +
+ ); +}; +export default ListUser; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/logos/LogoMain.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/logos/LogoMain.jsx new file mode 100644 index 0000000..2649b9e --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/logos/LogoMain.jsx @@ -0,0 +1,15 @@ +import Logo from '../icons/Logo'; + +// import Logo from '/logo-js.min.svg'; +const LogoMain = (props) => { + // const { } = props; + + return ( +
+ {/* JS Course */} + +
Firmen Slogan
+
+ ); +}; +export default LogoMain; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/navs/Navbar.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/navs/Navbar.jsx new file mode 100644 index 0000000..1dc0796 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/components/navs/Navbar.jsx @@ -0,0 +1,110 @@ +import { NavLink } from 'react-router-dom'; + +function Navbar() { + return ( + + ); +} + +export default Navbar; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/layouts/Layout.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/layouts/Layout.jsx new file mode 100644 index 0000000..8b55dbf --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/layouts/Layout.jsx @@ -0,0 +1,20 @@ +import { Outlet } from 'react-router-dom'; +import Navbar from '../components/navs/Navbar'; + +const Layout = (props) => { + // const { } = props; + + return ( + <> +
Header
+ +
+ +
+
+

© copyright 2026

+
+ + ); +}; +export default Layout; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/main.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/main.jsx new file mode 100644 index 0000000..672352b --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/main.jsx @@ -0,0 +1,18 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import App from './App.jsx'; + +import 'bootstrap'; // js +import './styles/main.scss'; + +const root = createRoot(document.getElementById('root')); + +root.render( + + + + + , +); diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/AboutPage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/AboutPage.jsx new file mode 100644 index 0000000..8112904 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/AboutPage.jsx @@ -0,0 +1,14 @@ +function AboutPage() { + return ( +
+

About

+

+ sit amet consectetur adipisicing elit. Minima, voluptatibus nobis molestiae, dignissimos id dolores minus iste + quisquam harum vitae rem dolorem debitis natus asperiores amet rerum ratione laboriosam quam. Sit corporis + voluptate laboriosam doloribus incidunt voluptatibus repellat. Excepturi vitae perferendis odit +

+
+ ); +} + +export default AboutPage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ApiPage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ApiPage.jsx new file mode 100644 index 0000000..2715797 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ApiPage.jsx @@ -0,0 +1,20 @@ +import ListTodo from '../components/lists/ListTodo'; +import ListUser from '../components/lists/ListUser'; + +const ApiPage = (props) => { + // const { } = props; + + return ( +
+
+

APIs auslesen mit React

+

Users

+ +
+

Todos

+ +
+
+ ); +}; +export default ApiPage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ContactPage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ContactPage.jsx new file mode 100644 index 0000000..ca4e431 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/ContactPage.jsx @@ -0,0 +1,20 @@ +import FormContact from '../components/forms/joi/FormContact'; + +function ContactPage() { + return ( +
+

Kontakt

+

+ Hast du Fragen? Schreib uns eine E-Mail: +
+ kontakt@example.com +
+

+ +
+ +
+ ); +} + +export default ContactPage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/HomePage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/HomePage.jsx new file mode 100644 index 0000000..b9d1853 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/HomePage.jsx @@ -0,0 +1,34 @@ +import ExampleUseEffectDidUpdate from '../components/examples/ExampleUseEffectDidUpdate'; +import ExampleUseEffectMount from '../components/examples/ExampleUseEffectMount'; +import ExampleUseEffectUnMount from '../components/examples/ExampleUseEffectUnMount'; + +function HomePage() { + return ( +
+

Example Hooks

+
+

Hooks-Regeln

+

Es gibt zwei wichtige Regeln für die Verwendung von Hooks:

+
    +
  • Hooks sollten nur in Funktionskomponenten oder anderen Hooks verwendet werden.
  • +
  • + Hooks sollten immer oben in der Komponente verwendet werden, nicht in Schleifen, Bedingungen oder + verschachtelten Funktionen. +
  • +
+
+
+

Example useEffect (mount)

+ + +
+

Example useEffect (did update)

+ +
+

Example useEffect (unmount)

+ +
+ ); +} + +export default HomePage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserPage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserPage.jsx new file mode 100644 index 0000000..3d35b61 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserPage.jsx @@ -0,0 +1,24 @@ +import { Outlet } from 'react-router-dom'; + +const UserPage = (props) => { + // const { } = props; + + return ( +
+
+
+

User

+

The nested user site:

+
+
+
+ +
+
+

© copyright 2026

+
+
+
+ ); +}; +export default UserPage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserProfilePage.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserProfilePage.jsx new file mode 100644 index 0000000..053f8ce --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/pages/UserProfilePage.jsx @@ -0,0 +1,19 @@ +import { useParams } from 'react-router-dom'; +import Badge from '../components/common/Badge'; + +function UserProfilePage() { + const { userId } = useParams(); + + return ( +
+

+ User Profile +

+

+ sit amet consectetur adipisicing elit. Minima, voluptatibus nobis molestiae, dignissimos id dolores minus iste +

+
+ ); +} + +export default UserProfilePage; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/routes/Router.jsx b/06_react/unterricht/tag39/01_react-api-with-hooks/src/routes/Router.jsx new file mode 100644 index 0000000..2e86b9b --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/routes/Router.jsx @@ -0,0 +1,79 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +//import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + +// Pages Komponenten +import HomePage from '../pages/HomePage'; +import AboutPage from '../pages/AboutPage'; +import ContactPage from '../pages/ContactPage'; +import UserProfilePage from '../pages/UserProfilePage'; +import UserPage from '../pages/UserPage'; +import ApiPage from '../pages/ApiPage'; + +// import Layout from '../layouts/Layout'; + +// Alternative Definition von Routes über "createBrowserRouter" mit Objekten +// const router = createBrowserRouter([ +// { path: '/', element: }, +// { path: '/about', element: }, +// { path: '/contact', element: }, +// ]); +// // App.jsx +// {/**/}; + +const checkAuth = () => { + const token = localStorage.getItem('token'); + return token && token === 'secret-token' ? true : false; +}; + +const ProtectedRoute = (props) => { + const { redirectTo = '/', children } = props; + + const isAuthenticated = checkAuth(); // => true | false // Authentication - Prüfung + + return isAuthenticated ? children : ; +}; + +const Router = () => { + // const { } = props; + + return ( + + {/* Static Routes */} + } /> + } /> + } /> + } /> + + {/* Dynamic Routes */} + Flights} /> + } /> + + {/* Nested Routes */} + {/* }> + } /> + } /> + } /> + */} + + }> + Index} /> + Profile} /> + Dashboard} /> + Settings} /> + + + {/* Protected Routes | Geschützte Routen */} + +
+

Admin Dashboard Page

+

Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero atque eveniet hic temporibus

+
+ + }>
+
+ ); +}; +export default Router; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/01_frameworks/_index.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/01_frameworks/_index.scss new file mode 100644 index 0000000..42edceb --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/01_frameworks/_index.scss @@ -0,0 +1,3 @@ +// Bootstrap 5.3.8 or 6 (6.0.0-alpha.1) straight from node_modules. +// @forward 'bootstrap/scss/bootstrap-grid' +@forward 'bootstrap/scss/bootstrap'; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_gloria-hallelujah-v24-latin.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_gloria-hallelujah-v24-latin.scss new file mode 100644 index 0000000..f46eba2 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_gloria-hallelujah-v24-latin.scss @@ -0,0 +1,8 @@ +// gloria-hallelujah-regular - latin +@font-face { + font-display: swap; + font-family: 'Gloria Hallelujah'; + font-style: normal; + font-weight: 400; + src: url('../../assets/fonts/gloria-hallelujah-v24-latin/gloria-hallelujah-v24-latin-regular.woff2') format('woff2'); +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_index.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_index.scss new file mode 100644 index 0000000..d377888 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_index.scss @@ -0,0 +1,3 @@ +// @forward 'gloria-hallelujah-v24-latin'; +// @forward 'lato-v25-latin'; +// @forward 'source-sans-3-v19-latin'; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_lato-v25-latin.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_lato-v25-latin.scss new file mode 100644 index 0000000..522cfda --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_lato-v25-latin.scss @@ -0,0 +1,89 @@ +// lato-100 - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: normal; + font-weight: 100; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-100.woff2') format('woff2'); +} + +// lato-100italic - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: italic; + font-weight: 100; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-100italic.woff2') format('woff2'); +} + +// lato-300 - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: normal; + font-weight: 300; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-300.woff2') format('woff2'); +} + +// lato-300italic - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: italic; + font-weight: 300; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-300italic.woff2') format('woff2'); +} + +// lato-regular - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-regular.woff2') format('woff2'); +} + +// lato-italic - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-italic.woff2') format('woff2'); +} + +// lato-700 - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-700.woff2') format('woff2'); +} + +// lato-700italic - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: italic; + font-weight: 700; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-700italic.woff2') format('woff2'); +} + +// lato-900 - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: normal; + font-weight: 900; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-900.woff2') format('woff2'); +} + +// lato-900italic - latin +@font-face { + font-display: swap; + font-family: 'Lato'; + font-style: italic; + font-weight: 900; + src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-900italic.woff2') format('woff2'); +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_source-sans-3-v19-latin.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_source-sans-3-v19-latin.scss new file mode 100644 index 0000000..b732c16 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/02_fonts/_source-sans-3-v19-latin.scss @@ -0,0 +1,143 @@ +// source-sans-3-200 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 200; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-200.woff2') format('woff2'); +} + +// source-sans-3-200italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 200; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-200italic.woff2') format('woff2'); +} + +// source-sans-3-300 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 300; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-300.woff2') format('woff2'); +} + +// source-sans-3-300italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 300; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-300italic.woff2') format('woff2'); +} + +// source-sans-3-regular - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 400; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-regular.woff2') format('woff2'); +} + +// source-sans-3-italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 400; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-italic.woff2') format('woff2'); +} + +// source-sans-3-500 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 500; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-500.woff2') format('woff2'); +} + +// source-sans-3-500italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 500; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-500italic.woff2') format('woff2'); +} + +// source-sans-3-600 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 600; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-600.woff2') format('woff2'); +} + +// source-sans-3-600italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 600; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-600italic.woff2') format('woff2'); +} + +// source-sans-3-700 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 700; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-700.woff2') format('woff2'); +} + +// source-sans-3-700italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 700; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-700italic.woff2') format('woff2'); +} + +// source-sans-3-800 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 800; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-800.woff2') format('woff2'); +} + +// source-sans-3-800italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 800; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-800italic.woff2') format('woff2'); +} + +// source-sans-3-900 - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 900; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-900.woff2') format('woff2'); +} + +// source-sans-3-900italic - latin +@font-face { + font-display: swap; + font-family: 'Source Sans 3'; + font-style: italic; + font-weight: 900; + src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-900italic.woff2') format('woff2'); +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_colors.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_colors.scss new file mode 100644 index 0000000..a550f3d --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_colors.scss @@ -0,0 +1,13 @@ +/* Color Theme Swatches in Hex */ +$color-1: #5c4b51; +$color-2: #8cbeb2; +$color-3: #f2ebbf; +$color-4: #f3b562; +$color-5: #f06060; + +$color-red: $color-5; +$color-white: white; // falls weiß geändert wird z.B.: #f8f8f8 +$color-black: #101010; + +$color-dark-blue: #283e4a; +$linear-gradient-dark: linear-gradient(180deg, rgba(40, 62, 74, 1) 0%, rgba(24, 32, 38, 1) 100%); diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_index.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_index.scss new file mode 100644 index 0000000..aaf4e12 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_index.scss @@ -0,0 +1,2 @@ +// Variables (colors, typography) are pulled in by the partials that need them via @use. +@forward 'theme'; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_theme.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_theme.scss new file mode 100644 index 0000000..26b9863 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_theme.scss @@ -0,0 +1,52 @@ +@use 'typography' as *; + +// Allgemeine Formatierung +*, +html { + box-sizing: border-box; +} + +html, +body, +#root, +#app { + height: 100%; +} +// Sticky Footer +body, +#root, +#app { + display: flex; + flex-direction: column; +} + +main, +#main { + flex: 1 0 auto; +} + +body > footer, +.footer-main { + flex-shrink: 0; +} +// sticky end + +body { + margin: 0; + padding: 0; + font-family: $font-copy; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: $font-headline; + font-weight: 600; +} + +blockquote { + font-family: $font-quote; +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_typography.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_typography.scss new file mode 100644 index 0000000..7c9b095 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/03_base/_typography.scss @@ -0,0 +1,9 @@ +$fallback-serif: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif; +$fallback-sans-serif: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + +$font-headline: 'Lato', $fallback-serif; +$font-subclaim: $fallback-sans-serif; + +$font-menu: 'Source Sans 3', $fallback-sans-serif; +$font-copy: 'Source Sans 3', $fallback-sans-serif; +$font-quote: 'Gloria Hallelujah', $fallback-serif; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/_index.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/_index.scss new file mode 100644 index 0000000..bd5ba6a --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/_index.scss @@ -0,0 +1,7 @@ +// @use '@fortawesome/fontawesome-free/scss/variables' with ( +// $font-path: '@fa-webfonts' +// ); +// @use '@fortawesome/fontawesome-free/scss/fontawesome'; +// @use '@fortawesome/fontawesome-free/scss/solid'; + +// @use 'toastify-1.12/toastify'; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/toastify-1.12/_toastify.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/toastify-1.12/_toastify.scss new file mode 100644 index 0000000..e359a14 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/04_plugins/toastify-1.12/_toastify.scss @@ -0,0 +1,88 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ + +.toastify { + padding: 12px 20px; + color: #ffffff; + display: inline-block; + box-shadow: + 0 3px 6px -1px rgba(0, 0, 0, 0.12), + 0 10px 36px -4px rgba(77, 96, 232, 0.3); + background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5); + background: linear-gradient(135deg, #73a5ff, #5477f5); + position: fixed; + opacity: 0; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + border-radius: 2px; + cursor: pointer; + text-decoration: none; + max-width: calc(50% - 20px); + z-index: 2147483647; +} + +.toastify.on { + opacity: 1; +} + +.toast-close { + background: transparent; + border: 0; + color: white; + cursor: pointer; + font-family: inherit; + font-size: 1em; + opacity: 0.4; + padding: 0 5px; +} + +.toastify-right { + right: 15px; +} + +.toastify-left { + left: 15px; +} + +.toastify-top { + top: -150px; +} + +.toastify-bottom { + bottom: -150px; +} + +.toastify-rounded { + border-radius: 25px; +} + +.toastify-avatar { + width: 1.5em; + height: 1.5em; + margin: -7px 5px; + border-radius: 2px; +} + +.toastify-center { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + max-width: -moz-fit-content; +} + +@media only screen and (max-width: 360px) { + .toastify-right, + .toastify-left { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_button.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_button.scss new file mode 100644 index 0000000..1660f6b --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_button.scss @@ -0,0 +1,17 @@ +button, +input { + &.btn-custom { + background-color: #f0f0f0; + color: #333; + border: 1px solid #ccc; + padding: 8px 16px; + border-radius: 0px; + margin: 3px; + &:hover, + &:focus { + background-color: #e0e0e0; + color: #333; + border: 1px solid #ccc; + } + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_chuck-norris-joke.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_chuck-norris-joke.scss new file mode 100644 index 0000000..232b479 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_chuck-norris-joke.scss @@ -0,0 +1,38 @@ +@use '../03_base/colors' as *; + +.chuck-norris-joke { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + min-height: calc(100vh - 3.5rem); // full screen below the navbar + background: #555 url('../../assets/img/concrete-bg.jpg') center no-repeat fixed; + background-size: cover; + + &::after { + content: ''; + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.8) 100%); + } + + > * { + position: relative; + z-index: 2; + } + + .joke-text { + color: $color-white; + text-align: center; + font-size: 3rem; + max-width: 80%; + padding: 2rem; + text-shadow: 2px 2px 5px rgba(black, 0.5); + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_footerMain.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_footerMain.scss new file mode 100644 index 0000000..fd73f7c --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_footerMain.scss @@ -0,0 +1,45 @@ +@use '../03_base/colors' as *; + +.footer-main { + background-color: $color-black; + background: $linear-gradient-dark; + padding: 3rem 0; + [class^='col'] { + color: $color-white; + a { + color: $color-white; + } + h2, + h3, + h4, + h5, + h6 { + font-size: 1.5rem; + color: $color-2; + padding: 0.5rem 0; + margin-bottom: 1rem; + letter-spacing: 2px; + } + } + // Mobile First + .row { + flex-direction: column; + > div { + &:nth-child(1) { + order: 3; + } + &:nth-child(2) { + order: 2; + } + &:nth-child(3) { + order: 1; + } + } + } + @media (min-width: 992px) { + // @include media-breakpoint-up(md) { + .row { + flex-direction: row; + } + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_header-main.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_header-main.scss new file mode 100644 index 0000000..2884477 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_header-main.scss @@ -0,0 +1,15 @@ +.m-header-main { + width: 100%; + height: 400px; + + // https://cssreference.io/property/background/ + background: #efefef url('../../assets/img/header-bg.webp') center no-repeat border-box; + background-size: cover; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + border: 20px solid rgba(white, 0.25); +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_index.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_index.scss new file mode 100644 index 0000000..2c015ec --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_index.scss @@ -0,0 +1,9 @@ +@forward 'button'; +// @forward 'chuck-norris-joke'; +@forward 'footerMain'; +@forward 'header-main'; +@forward 'input-amount'; +@forward 'logo-main'; +@forward 'loader'; +// @forward 'menu-main'; +// @forward 'product-manager'; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_input-amount.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_input-amount.scss new file mode 100644 index 0000000..8701a7c --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_input-amount.scss @@ -0,0 +1,27 @@ +.m-input-amount { + .input-group { + .input-group-text { + padding: 0; + display: inline-block; + overflow: hidden; + button { + border-radius: 0; + } + } + input { + max-width: 85px; + text-align: center; + } + /* Für Chrome, Safari, Edge und Opera */ + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Für Firefox */ + input[type='number'] { + appearance: textfield; + } + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_loader.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_loader.scss new file mode 100644 index 0000000..ad01c25 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_loader.scss @@ -0,0 +1,22 @@ +.loader { + --color-1: #478e9e; + --size: 1px; + width: calc(32 * var(--size)); + height: calc(32 * var(--size)); + border: calc(5 * var(--size)) solid var(--color-1); + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + margin: 0.5rem; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_logo-main.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_logo-main.scss new file mode 100644 index 0000000..399bd12 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_logo-main.scss @@ -0,0 +1,21 @@ +.logo-main { + margin: 0; + + img, + svg { + width: 320px; + height: auto; + max-width: 320px; + } + + figcaption { + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_menu-main.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_menu-main.scss new file mode 100644 index 0000000..df4627e --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_menu-main.scss @@ -0,0 +1,20 @@ +@use '../03_base/colors' as *; +@use '../03_base/typography' as *; + +// Main navigation = Bootstrap 6 navbar (see includes/menu-main.html). +// Only the look from frontend-gulp is added here: black bar, uppercase links. +.menu-main { + background-color: $color-black; + font-family: $font-menu; + + .nav-link { + text-transform: uppercase; + } +} + +// Mobile drawer: Bootstrap 6 alpha keeps it white although the navbar is dark +// (data-bs-theme="dark"), which leaves the light link text invisible. Give it the bar's colour. +.menu-main .drawer { + background-color: $color-black; + color: $color-white; +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_product-manager.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_product-manager.scss new file mode 100644 index 0000000..4f3ed4d --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/05_modules/_product-manager.scss @@ -0,0 +1,47 @@ +.product-manager { + .nav-products-actions { + ul { + padding: 0; + margin: 0; + list-style: none; + display: flex; + justify-content: flex-end; + + li { + button { + margin: 0.5rem 0 0.5rem 0.5rem; + } + } + } + } + + table { + .th-actions { + width: 200px; + } + .th-dragger { + width: 50px; + } + + td { + vertical-align: middle; + button { + margin: 0 2px; + } + } + + tbody:empty::after { + content: 'loading products...'; + width: max-content; + display: inline-block; + color: #999; + padding: 1rem; + } + tfoot .row > * { + margin-bottom: 0.5rem; + @media (min-width: 992px) { + margin-bottom: 0; + } + } + } +} diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/main.scss b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/main.scss new file mode 100644 index 0000000..297e74f --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/styles/main.scss @@ -0,0 +1,13 @@ +// Single stylesheet entry, linked in src/html/includes/head.html. +// Same folders as in frontend-gulp; @use instead of the deprecated @import. + +// 01 Frameworks +@use '01_frameworks' as *; +// 02 Fonts +@use '02_fonts' as *; +// 03 Base +@use '03_base' as *; +// 04 Plugins +@use '04_plugins' as *; +// 05 Modules +@use '05_modules' as *; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/src/utils/validators/validate.js b/06_react/unterricht/tag39/01_react-api-with-hooks/src/utils/validators/validate.js new file mode 100644 index 0000000..9d778df --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/src/utils/validators/validate.js @@ -0,0 +1,39 @@ +import isEmail from 'validator/lib/isEmail'; +import isLength from 'validator/lib/isLength'; +import isEmpty from 'validator/lib/isEmpty'; + +const validate = (formData) => { + const errors = {}; + const firstname = formData.firstname.trim(); + const lastname = formData.lastname.trim(); + const email = formData.email.trim(); + const message = formData.message.trim(); + + if (isEmpty(firstname)) { + errors.firstname = 'Firstname is required'; + } else if (!isLength(firstname, { min: 2, max: 50 })) { + errors.firstname = 'Firstname must be between 2 and 50 characters'; + } + + if (isEmpty(lastname)) { + errors.lastname = 'Lastname is required'; + } else if (!isLength(lastname, { min: 2, max: 60 })) { + errors.lastname = 'Lastname must be between 2 and 60 characters'; + } + + if (isEmpty(email)) { + errors.email = 'Email is required'; + } else if (!isEmail(email)) { + errors.email = 'Email is invalid'; + } + + if (isEmpty(message)) { + errors.message = 'Message is required'; + } else if (!isLength(message, { min: 10, max: 1000 })) { + errors.message = 'Message must be between 10 and 1000 characters'; + } + + return errors; +}; + +export default validate; diff --git a/06_react/unterricht/tag39/01_react-api-with-hooks/vite.config.js b/06_react/unterricht/tag39/01_react-api-with-hooks/vite.config.js new file mode 100644 index 0000000..24ad103 --- /dev/null +++ b/06_react/unterricht/tag39/01_react-api-with-hooks/vite.config.js @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + css: { + preprocessorOptions: { + scss: { + quietDeps: true, // Unix + silenceDeprecations: ['color-functions', 'global-builtin', 'import', 'legacy-js-api', 'if-function'], + }, + }, + }, +});