This commit is contained in:
Philippe Torrel
2026-10-28 09:53:04 +01:00
parent 3367fea887
commit 34172beb12
14 changed files with 2407 additions and 80 deletions
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,8 @@
"react": "^19.3.1", "react": "^19.3.1",
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4" "react-router-dom": "^7.18.5",
"validator": "^13.15.35"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
@@ -1,15 +1,17 @@
import { useState } from 'react'; import { useState } from 'react';
import validate from '../../utils/validators/validate'; import validate from '../../utils/validators/validate';
const INITIAL_FORM_DATA = {
firstname: '', //
lastname: '',
email: '',
message: '',
};
const FormContact = (props) => { const FormContact = (props) => {
// const { } = props; // const { } = props;
const [formData, setFormData] = useState({ const [formData, setFormData] = useState(INITIAL_FORM_DATA);
firstname: '', //
lastname: '',
email: '',
message: '',
});
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
@@ -25,12 +27,7 @@ const FormContact = (props) => {
console.log('Form submitted ....'); console.log('Form submitted ....');
// TODO: send over AJAX/Fetch // TODO: send over AJAX/Fetch
form.reset(); form.reset();
setFormData({ setFormData(INITIAL_FORM_DATA);
firstname: '', //
lastname: '',
email: '',
message: '',
});
setErrors({}); setErrors({});
} else { } else {
console.warn('Form submit error'); console.warn('Form submit error');
@@ -47,6 +44,23 @@ const FormContact = (props) => {
// console.log('Formdata', formData); // 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 ( return (
<form className="m-form-contact form-contact" noValidate onSubmit={handleSubmit}> <form className="m-form-contact form-contact" noValidate onSubmit={handleSubmit}>
<div className="row"> <div className="row">
@@ -62,6 +76,7 @@ const FormContact = (props) => {
className={`form-control input-firstname ${errors.firstname ? 'is-invalid' : ''}`} className={`form-control input-firstname ${errors.firstname ? 'is-invalid' : ''}`}
value={formData.firstname} value={formData.firstname}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur}
/> />
<div className="valid-feedback">OK</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.firstname}</div> <div className="invalid-feedback">{errors?.firstname}</div>
@@ -79,6 +94,7 @@ const FormContact = (props) => {
className={`form-control input-lastname ${errors.lastname ? 'is-invalid' : ''}`} className={`form-control input-lastname ${errors.lastname ? 'is-invalid' : ''}`}
value={formData.lastname} value={formData.lastname}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur}
/> />
<div className="valid-feedback">OK</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.lastname}</div> <div className="invalid-feedback">{errors?.lastname}</div>
@@ -99,6 +115,7 @@ const FormContact = (props) => {
className={`form-control input-email ${errors.email ? 'is-invalid' : ''}`} className={`form-control input-email ${errors.email ? 'is-invalid' : ''}`}
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur}
/> />
<div className="valid-feedback">OK</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.email}</div> <div className="invalid-feedback">{errors?.email}</div>
@@ -119,7 +136,8 @@ const FormContact = (props) => {
id="textarea-message" id="textarea-message"
className={`form-control textarea-message ${errors.message ? 'is-invalid' : ''}`} className={`form-control textarea-message ${errors.message ? 'is-invalid' : ''}`}
value={formData.message} value={formData.message}
onChange={handleChange}></textarea> onChange={handleChange}
onBlur={handleBlur}></textarea>
<div className="valid-feedback">OK</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.message}</div> <div className="invalid-feedback">{errors?.message}</div>
</div> </div>
@@ -1,25 +1,34 @@
import validator from 'validator';
const validate = (formData) => { const validate = (formData) => {
const errors = {}; const errors = {};
const emailRegEx = new RegExp( const firstname = formData.firstname.trim();
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, const lastname = formData.lastname.trim();
); const email = formData.email.trim();
const message = formData.message.trim();
if (!formData.firstname) { if (validator.isEmpty(firstname)) {
errors.firstname = 'Firstname is required'; errors.firstname = 'Firstname is required';
} else if (!validator.isLength(firstname, { min: 2, max: 50 })) {
errors.firstname = 'Firstname must be between 2 and 50 characters';
} }
if (!formData.lastname) { if (validator.isEmpty(lastname)) {
errors.lastname = 'Lastname is required'; errors.lastname = 'Lastname is required';
} else if (!validator.isLength(lastname, { min: 2, max: 60 })) {
errors.lastname = 'Lastname must be between 2 and 60 characters';
} }
if (!formData.email) { if (validator.isEmpty(email)) {
errors.email = 'Email is required'; errors.email = 'Email is required';
} else if (!emailRegEx.test(formData.email)) { } else if (!validator.isEmail(email)) {
errors.email = 'Email is invalid'; errors.email = 'Email is invalid';
} }
if (!formData.message) { if (validator.isEmpty(message)) {
errors.message = 'Message is required'; errors.message = 'Message is required';
} else if (!validator.isLength(message, { min: 10, max: 1000 })) {
errors.message = 'Message must be between 10 and 1000 characters';
} }
return errors; return errors;
@@ -0,0 +1,2 @@
# VITE_ als Präfix notwendig
VITE_SECRET_TOKEN="secret-token"
@@ -12,7 +12,7 @@
"react": "^19.3.1", "react": "^19.3.1",
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4" "react-router-dom": "^7.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
@@ -1615,9 +1615,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.4", "version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.5.tgz",
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "integrity": "sha512-dTgvcXDOrLPrlLHrcZAqWveiK1hMSqN+rGAJzegP8HtmNkfRxOoshGWBNX3ZJ0cZjOHkRdlUJtaIhZMLLyGyPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -1637,12 +1637,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.18.4", "version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.5.tgz",
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", "integrity": "sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.18.4" "react-router": "7.18.5"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@@ -14,7 +14,7 @@
"react": "^19.3.1", "react": "^19.3.1",
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4" "react-router-dom": "^7.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx'; import App from './App.jsx';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap'; // js import 'bootstrap/dist/js/bootstrap.bundle.js'; // js
// import './styles/main.scss'; // import './styles/main.scss';
const root = createRoot(document.getElementById('root')); const root = createRoot(document.getElementById('root'));
@@ -20,8 +20,13 @@ import UserPage from '../pages/UserPage';
// <RouterProvider router={router}>{/*<App />*/}</RouterProvider>; // <RouterProvider router={router}>{/*<App />*/}</RouterProvider>;
const checkAuth = () => { const checkAuth = () => {
// localStorage.setItem('token', 'secret-token'); // wird in localStorage nach erfolgreicher Anmeldung abgelegt
const passToken = import.meta.env.VITE_SECRET_TOKEN;
console.log(passToken);
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
return token && token === 'secret-token' ? true : false; return token && token === passToken ? true : false;
}; };
const ProtectedRoute = (props) => { const ProtectedRoute = (props) => {
@@ -55,7 +60,7 @@ const Router = () => {
<Route path="/user" element={<UserPage />}> <Route path="/user" element={<UserPage />}>
<Route index element={<h5>Index</h5>} /> <Route index element={<h5>Index</h5>} />
<Route path="profile" element={<h5>Profile</h5>} /> <Route path="profile/:userId" element={<UserProfilePage />} />
<Route path="dashboard" element={<h5>Dashboard</h5>} /> <Route path="dashboard" element={<h5>Dashboard</h5>} />
<Route path="settings" element={<h5>Settings</h5>} /> <Route path="settings" element={<h5>Settings</h5>} />
</Route> </Route>
@@ -13,7 +13,7 @@
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-hook-form": "^7.91.0", "react-hook-form": "^7.91.0",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4" "react-router-dom": "^7.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
@@ -1638,9 +1638,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.4", "version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.5.tgz",
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "integrity": "sha512-dTgvcXDOrLPrlLHrcZAqWveiK1hMSqN+rGAJzegP8HtmNkfRxOoshGWBNX3ZJ0cZjOHkRdlUJtaIhZMLLyGyPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -1660,12 +1660,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.18.4", "version": "7.18.5",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.5.tgz",
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", "integrity": "sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.18.4" "react-router": "7.18.5"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@@ -15,7 +15,7 @@
"react-dom": "^19.3.1", "react-dom": "^19.3.1",
"react-hook-form": "^7.91.0", "react-hook-form": "^7.91.0",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.4" "react-router-dom": "^7.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
@@ -24,7 +24,7 @@ const FormContact = (props) => {
// AJAX/FETCH send to server // AJAX/FETCH send to server
console.log('FormData: ', formData); console.log('FormData: ', formData);
setFormData(INITIAL_FORM_DATA); // reset setFormData(INITIAL_FORM_DATA); // reset
form.reset(); // not necessary in react // e.currentTarget.reset(); // not necessary in react
setErrors({}); setErrors({});
} else { } else {
console.error('Form submit error'); console.error('Form submit error');
@@ -39,7 +39,7 @@ const FormContact = (props) => {
setFormData((state) => ({ ...state, [name]: value })); setFormData((state) => ({ ...state, [name]: value }));
// console.log({ [e.target.name]: e.target.value }); // console.log({ [e.target.name]: e.target.value });
// console.log('formData: ', formData); console.log('formData: ', formData);
}; };
return ( return (
@@ -72,6 +72,7 @@ const FormContact = (props) => {
name="lastname" name="lastname"
id="input-lastname" id="input-lastname"
onChange={handleChange} onChange={handleChange}
value={formData.lastname}
className={`form-control ${errors.lastname ? 'is-invalid' : ''}`} className={`form-control ${errors.lastname ? 'is-invalid' : ''}`}
/> />
<div className="invalid-feedback">{errors?.lastname || 'Error'}</div> <div className="invalid-feedback">{errors?.lastname || 'Error'}</div>
@@ -89,6 +90,7 @@ const FormContact = (props) => {
id="input-email" id="input-email"
name="email" name="email"
onChange={handleChange} onChange={handleChange}
value={formData.email}
className={`form-control ${errors.email ? 'is-invalid' : ''}`} className={`form-control ${errors.email ? 'is-invalid' : ''}`}
/> />
<div className="invalid-feedback">{errors?.email || 'Error'}</div> <div className="invalid-feedback">{errors?.email || 'Error'}</div>
@@ -107,6 +109,7 @@ const FormContact = (props) => {
cols="30" cols="30"
rows="5" rows="5"
onChange={handleChange} onChange={handleChange}
value={formData.message}
className={`form-control ${errors.message ? 'is-invalid' : ''}`}></textarea> className={`form-control ${errors.message ? 'is-invalid' : ''}`}></textarea>
<div className="invalid-feedback">{errors?.message || 'Error'}</div> <div className="invalid-feedback">{errors?.message || 'Error'}</div>
</div> </div>
@@ -1,27 +1,41 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
// import validate from '../../../utils/validators/validate';
const FormContact = (props) => { const FormContact = (props) => {
// const { } = props; // const { } = props;
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm(); } = useForm();
// EventHandlers // EventHandlers =======
const onSubmit = (data) => { const onSubmit = (data) => {
// Event wird nur getriggert, wenn kein Fehler
console.log('submitted'); console.log('submitted');
console.log(); console.log('Form data:', data);
if (errors) {
console.log(errors); // 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 ( return (
<form className="m-form-contact form-contact" noValidate={true} onSubmit={handleSubmit(onSubmit)}> <form className="m-form-contact form-contact" noValidate onSubmit={handleSubmit(onSubmit)}>
<div className="row"> <div className="row">
<div className="col-12 col-sm-6"> <div className="col-12 col-md">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="input-firstname" className="form-label"> <label htmlFor="input-firstname" className="form-label">
Firstname Firstname
@@ -31,17 +45,17 @@ const FormContact = (props) => {
name="firstname" name="firstname"
id="input-firstname" id="input-firstname"
{...register('firstname', { {...register('firstname', {
required: 'Firstname is required', required: 'Firstname is invalid',
mixLength: { value: 2, message: 'Firstname must have at least 2 characters.' }, minLength: { value: 2, message: 'Firstname must have at least 2 Characters' },
maxLength: 60, maxLength: 60,
})} })}
className={`form-control ${errors.firstname ? 'is-invalid' : ''}`} className={`form-control input-firstname ${errors.firstname ? 'is-invalid' : ''}`}
/> />
<div className="valid-feedback">OK</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.firstname?.message || 'Error'}</div> <div className="invalid-feedback">{errors?.firstname?.message || 'Error'}</div>
</div> </div>
</div> </div>
<div className="col-12 col-sm-6"> <div className="col-12 col-md">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="input-lastname" className="form-label"> <label htmlFor="input-lastname" className="form-label">
Lastname Lastname
@@ -50,38 +64,37 @@ const FormContact = (props) => {
type="text" type="text"
name="lastname" name="lastname"
id="input-lastname" id="input-lastname"
{...register('lastname', { {...register('lastname', { required: 'Lastname is invalid' })}
required: 'Lastname is required', className={`form-control input-lastname ${errors.lastname ? 'is-invalid' : ''}`}
mixLength: { value: 2, message: 'Lastname must have at least 2 characters.' },
maxLength: 80,
})}
className={`form-control ${errors.lastname ? 'is-invalid' : ''}`}
/> />
<div className="invalid-feedback">{errors?.lastname?.message || 'Error'}</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.lastname?.message}</div>
</div> </div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col"> <div className="col">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="input-email" className="form-label"> <label htmlFor="input-email" className="form-label">
Email E-Mail
</label> </label>
<input <input
type="email" type="email"
id="input-email"
name="email" name="email"
id="input-email"
{...register('email', { {...register('email', {
required: 'Email is not valid', required: 'Email is required!!!',
pattern: pattern: { value: /^\S+@\S+\.\S+$/, message: 'Invalid email' },
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
})} })}
className={`form-control ${errors.email ? 'is-invalid' : ''}`} className={`form-control input-email ${errors.email ? 'is-invalid' : ''}`}
/> />
<div className="invalid-feedback">{errors?.email?.message || 'Error'}</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">Email is required</div>
</div> </div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col"> <div className="col">
<div className="mb-3"> <div className="mb-3">
@@ -90,24 +103,27 @@ const FormContact = (props) => {
</label> </label>
<textarea <textarea
name="message" name="message"
cols={4}
rows={6}
id="textarea-message" id="textarea-message"
cols="30"
rows="5"
{...register('message', { {...register('message', {
required: 'Message invalid', required: 'Message invalid',
minLength: { value: 10, message: 'Message to short' }, minLength: { value: 10, message: 'Message to short' },
maxLength: { value: 1000, message: 'Message to long' },
})} })}
className={`form-control ${errors.message ? 'is-invalid' : ''}`}></textarea> className={`form-control textarea-message ${errors.message ? 'is-invalid' : ''}`}></textarea>
<div className="invalid-feedback">{errors?.message?.message || 'Error'}</div> <div className="valid-feedback">OK</div>
<div className="invalid-feedback">{errors?.message?.message}</div>
</div> </div>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col"> <div className="col">
<button type="submit" className="btn btn-dark button-send-contact"> <div className="mb-3">
Send <button type="submit" className="btn btn-dark button-form-send">
</button> Send Message
</button>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -1,4 +1,4 @@
const validate = (formData) => { const validate = (formData, requiredFiedls = []) => {
const errors = {}; const errors = {};
const emailRegExp = new RegExp( const emailRegExp = new RegExp(