diff --git a/06_react/unterricht/tag38/01_react-with-forms.zip b/06_react/unterricht/tag38/01_react-with-forms.zip deleted file mode 100644 index 624e96f..0000000 Binary files a/06_react/unterricht/tag38/01_react-with-forms.zip and /dev/null differ diff --git a/06_react/unterricht/tag38/01_react-with-forms/package.json b/06_react/unterricht/tag38/01_react-with-forms/package.json index 74e3126..4c79159 100644 --- a/06_react/unterricht/tag38/01_react-with-forms/package.json +++ b/06_react/unterricht/tag38/01_react-with-forms/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "bootstrap": "^5.3.8", + "joi": "^18.3.0", "react": "^19.3.1", "react-dom": "^19.3.1", "react-hook-form": "^7.91.0", diff --git a/06_react/unterricht/tag38/01_react-with-forms/src/components/forms/joi/FormContact.jsx b/06_react/unterricht/tag38/01_react-with-forms/src/components/forms/joi/FormContact.jsx new file mode 100644 index 0000000..2e17e5c --- /dev/null +++ b/06_react/unterricht/tag38/01_react-with-forms/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/tag38/01_react-with-forms/src/components/forms/joi/validate.js b/06_react/unterricht/tag38/01_react-with-forms/src/components/forms/joi/validate.js new file mode 100644 index 0000000..e1d9df0 --- /dev/null +++ b/06_react/unterricht/tag38/01_react-with-forms/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/tag38/01_react-with-forms/src/pages/ContactPage.jsx b/06_react/unterricht/tag38/01_react-with-forms/src/pages/ContactPage.jsx index d00ae27..ca4e431 100644 --- a/06_react/unterricht/tag38/01_react-with-forms/src/pages/ContactPage.jsx +++ b/06_react/unterricht/tag38/01_react-with-forms/src/pages/ContactPage.jsx @@ -1,4 +1,4 @@ -import FormContact from '../components/forms/react-hook-form/FormContact'; +import FormContact from '../components/forms/joi/FormContact'; function ContactPage() { return ( diff --git a/06_react/unterricht/tag38/01_react-with-forms/src/utils/validators/validate.js b/06_react/unterricht/tag38/01_react-with-forms/src/utils/validators/validate.js index d8395a4..9d778df 100644 --- a/06_react/unterricht/tag38/01_react-with-forms/src/utils/validators/validate.js +++ b/06_react/unterricht/tag38/01_react-with-forms/src/utils/validators/validate.js @@ -1,4 +1,6 @@ -import validator from 'validator'; +import isEmail from 'validator/lib/isEmail'; +import isLength from 'validator/lib/isLength'; +import isEmpty from 'validator/lib/isEmpty'; const validate = (formData) => { const errors = {}; @@ -7,27 +9,27 @@ const validate = (formData) => { const email = formData.email.trim(); const message = formData.message.trim(); - if (validator.isEmpty(firstname)) { + if (isEmpty(firstname)) { errors.firstname = 'Firstname is required'; - } else if (!validator.isLength(firstname, { min: 2, max: 50 })) { + } else if (!isLength(firstname, { min: 2, max: 50 })) { errors.firstname = 'Firstname must be between 2 and 50 characters'; } - if (validator.isEmpty(lastname)) { + if (isEmpty(lastname)) { errors.lastname = 'Lastname is required'; - } else if (!validator.isLength(lastname, { min: 2, max: 60 })) { + } else if (!isLength(lastname, { min: 2, max: 60 })) { errors.lastname = 'Lastname must be between 2 and 60 characters'; } - if (validator.isEmpty(email)) { + if (isEmpty(email)) { errors.email = 'Email is required'; - } else if (!validator.isEmail(email)) { + } else if (!isEmail(email)) { errors.email = 'Email is invalid'; } - if (validator.isEmpty(message)) { + if (isEmpty(message)) { errors.message = 'Message is required'; - } else if (!validator.isLength(message, { min: 10, max: 1000 })) { + } else if (!isLength(message, { min: 10, max: 1000 })) { errors.message = 'Message must be between 10 and 1000 characters'; } diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/.gitignore b/06_react/unterricht/tag38/02_react-api-with-hooks/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/.oxlintrc.json b/06_react/unterricht/tag38/02_react-api-with-hooks/.oxlintrc.json new file mode 100644 index 0000000..1255078 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/README.md b/06_react/unterricht/tag38/02_react-api-with-hooks/README.md new file mode 100644 index 0000000..d937833 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/index.html b/06_react/unterricht/tag38/02_react-api-with-hooks/index.html new file mode 100644 index 0000000..2ea4e09 --- /dev/null +++ b/06_react/unterricht/tag38/02_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.json b/06_react/unterricht/tag38/02_react-api-with-hooks/package.json new file mode 100644 index 0000000..ab968d8 --- /dev/null +++ b/06_react/unterricht/tag38/02_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.0", + "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.106.1", + "vite": "^8.3.5" + } +} diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/public/favicon.svg b/06_react/unterricht/tag38/02_react-api-with-hooks/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/public/icons.svg b/06_react/unterricht/tag38/02_react-api-with-hooks/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/App.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/App.jsx new file mode 100644 index 0000000..38600c8 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/App.jsx @@ -0,0 +1,28 @@ +import Navbar from './components/navs/Navbar'; +import Router from './routes/Router'; +import HeaderMain from './components/headers/HeaderMain'; +import FooterMain from './components/footers/FooterMain'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; + +function App() { + // const navigate = useNavigate(); + + // useEffect(() => { + // navigate('/contact'); + // }, []); + + return ( + <> + + + +
+ +
+ + + ); +} + +export default App; diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/react.svg b/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/vite.svg b/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/common/Badge.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/common/Badge.jsx new file mode 100644 index 0000000..df1b229 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/footers/FooterMain.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/footers/FooterMain.jsx new file mode 100644 index 0000000..5006bda --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/forms/FormContact.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/forms/FormContact.jsx new file mode 100644 index 0000000..9f2d181 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/forms/FormNewsletter.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/forms/FormNewsletter.jsx new file mode 100644 index 0000000..d8c7ec7 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/forms/joi/FormContact.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/forms/joi/FormContact.jsx new file mode 100644 index 0000000..2e17e5c --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/forms/joi/validate.js b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/forms/joi/validate.js new file mode 100644 index 0000000..e1d9df0 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/forms/react-hook-form/FormContact.jsx b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/components/headers/HeaderMain.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/headers/HeaderMain.jsx new file mode 100644 index 0000000..acbddb2 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/icons/Logo.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/icons/Logo.jsx new file mode 100644 index 0000000..dd3b6f0 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/inputs/InputAmount.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/inputs/InputAmount.jsx new file mode 100644 index 0000000..d6ac3f5 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/logos/LogoMain.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/logos/LogoMain.jsx new file mode 100644 index 0000000..2649b9e --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/components/navs/Navbar.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/navs/Navbar.jsx new file mode 100644 index 0000000..983afdb --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/components/navs/Navbar.jsx @@ -0,0 +1,99 @@ +import { NavLink } from 'react-router-dom'; + +function Navbar() { + return ( + + ); +} + +export default Navbar; diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/layouts/Layout.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/layouts/Layout.jsx new file mode 100644 index 0000000..8b55dbf --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/main.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/main.jsx new file mode 100644 index 0000000..672352b --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/pages/AboutPage.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/AboutPage.jsx new file mode 100644 index 0000000..8112904 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/pages/ContactPage.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/ContactPage.jsx new file mode 100644 index 0000000..ca4e431 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/pages/HomePage.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/HomePage.jsx new file mode 100644 index 0000000..d4a4d32 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/HomePage.jsx @@ -0,0 +1,10 @@ +function HomePage() { + return ( +
+

Home

+

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ad, voluptatibus.

+
+ ); +} + +export default HomePage; diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/UserPage.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/UserPage.jsx new file mode 100644 index 0000000..3d35b61 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/pages/UserProfilePage.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/pages/UserProfilePage.jsx new file mode 100644 index 0000000..053f8ce --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/routes/Router.jsx b/06_react/unterricht/tag38/02_react-api-with-hooks/src/routes/Router.jsx new file mode 100644 index 0000000..38f5bd2 --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/routes/Router.jsx @@ -0,0 +1,77 @@ +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 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/tag38/02_react-api-with-hooks/src/styles/01_frameworks/_index.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/01_frameworks/_index.scss new file mode 100644 index 0000000..42edceb --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/02_fonts/_gloria-hallelujah-v24-latin.scss b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/02_fonts/_index.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/02_fonts/_index.scss new file mode 100644 index 0000000..d377888 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/02_fonts/_lato-v25-latin.scss b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/02_fonts/_source-sans-3-v19-latin.scss b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/03_base/_colors.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/03_base/_colors.scss new file mode 100644 index 0000000..a550f3d --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/03_base/_index.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/03_base/_index.scss new file mode 100644 index 0000000..aaf4e12 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/03_base/_theme.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/03_base/_theme.scss new file mode 100644 index 0000000..26b9863 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/03_base/_typography.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/03_base/_typography.scss new file mode 100644 index 0000000..7c9b095 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/04_plugins/_index.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/04_plugins/_index.scss new file mode 100644 index 0000000..bd5ba6a --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/04_plugins/toastify-1.12/_toastify.scss b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_button.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_button.scss new file mode 100644 index 0000000..1660f6b --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_chuck-norris-joke.scss b/06_react/unterricht/tag38/02_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/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_footerMain.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_footerMain.scss new file mode 100644 index 0000000..fd73f7c --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_header-main.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_header-main.scss new file mode 100644 index 0000000..2884477 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_index.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_index.scss new file mode 100644 index 0000000..484b0de --- /dev/null +++ b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_index.scss @@ -0,0 +1,8 @@ +@forward 'button'; +// @forward 'chuck-norris-joke'; +@forward 'footerMain'; +@forward 'header-main'; +@forward 'input-amount'; +@forward 'logo-main'; +// @forward 'menu-main'; +// @forward 'product-manager'; diff --git a/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_input-amount.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_input-amount.scss new file mode 100644 index 0000000..8701a7c --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_logo-main.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_logo-main.scss new file mode 100644 index 0000000..399bd12 --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_menu-main.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_menu-main.scss new file mode 100644 index 0000000..df4627e --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/05_modules/_product-manager.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/05_modules/_product-manager.scss new file mode 100644 index 0000000..4f3ed4d --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/styles/main.scss b/06_react/unterricht/tag38/02_react-api-with-hooks/src/styles/main.scss new file mode 100644 index 0000000..297e74f --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/src/utils/validators/validate.js b/06_react/unterricht/tag38/02_react-api-with-hooks/src/utils/validators/validate.js new file mode 100644 index 0000000..9d778df --- /dev/null +++ b/06_react/unterricht/tag38/02_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/tag38/02_react-api-with-hooks/vite.config.js b/06_react/unterricht/tag38/02_react-api-with-hooks/vite.config.js new file mode 100644 index 0000000..24ad103 --- /dev/null +++ b/06_react/unterricht/tag38/02_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'], + }, + }, + }, +});