diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 35ee7b1..bab584c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,10 +15,11 @@ "@mui/material": "^7.3.6", "@mui/styled-engine-sc": "^7.3.6", "@tailwindcss/vite": "^4.1.11", + "i18next": "^25.7.4", "js-cookie": "^3.0.5", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-i18next": "^16.2.0", + "react-i18next": "^16.5.3", "react-router-dom": "^7.11.0", "styled-components": "^6.1.19", "tailwind-merge": "^3.3.1", @@ -27,6 +28,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/js-cookie": "^3.0.6", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -2031,6 +2033,13 @@ "@types/react": "*" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3263,7 +3272,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -4071,9 +4079,9 @@ } }, "node_modules/react-i18next": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz", - "integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==", + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.3.tgz", + "integrity": "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", diff --git a/frontend/package.json b/frontend/package.json index 22c92d6..4f390ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,19 +16,21 @@ "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@mui/styled-engine-sc": "^7.3.6", + "@tailwindcss/vite": "^4.1.11", + "i18next": "^25.7.4", "js-cookie": "^3.0.5", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-i18next": "^16.2.0", + "react-i18next": "^16.5.3", "react-router-dom": "^7.11.0", "styled-components": "^6.1.19", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", - "tailwindcss-animate": "^1.0.7", - "@tailwindcss/vite": "^4.1.11" + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/js-cookie": "^3.0.6", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -41,4 +43,4 @@ "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } -} \ No newline at end of file +} diff --git a/frontend/src/pages/MainForm copy.tsx b/frontend/src/pages/MainForm copy.tsx new file mode 100644 index 0000000..ac52ff0 --- /dev/null +++ b/frontend/src/pages/MainForm copy.tsx @@ -0,0 +1,434 @@ +import { + Box, + Stack, + TextField, + FormControlLabel, + Checkbox, + Button, + Alert, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; + +const phonePattern = /^[+]?[- 0-9()]{7,}$/; +const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export const MainForm = () => { + const { t } = useTranslation(); + const [invoice, setInvoice] = useState(false); + const [paymentMethod, setPaymentMethod] = useState< + "cash" | "paypal" | "transfer" | null + >(null); + const [formValues, setFormValues] = useState({ + firstName: "", + lastName: "", + email: "", + phoneNumber: "", + tickets: "", + code: "", + companyName: "", + invoiceFirstName: "", + invoiceLastName: "", + street: "", + postalCode: "", + invoicePhoneNumber: "", + invoiceEmail: "", + }); + const [errors, setErrors] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitMessage, setSubmitMessage] = useState(null); + + const updateField = + (field: keyof typeof formValues) => + (event: React.ChangeEvent) => { + setFormValues((prev) => ({ ...prev, [field]: event.target.value })); + }; + + const validate = () => { + const nextErrors: Record = {}; + + if (!formValues.firstName.trim()) nextErrors.firstName = "Required"; + if (!formValues.lastName.trim()) nextErrors.lastName = "Required"; + + if (!formValues.email.trim()) nextErrors.email = "Required"; + else if (!emailPattern.test(formValues.email)) + nextErrors.email = "Invalid email"; + + if (!formValues.phoneNumber.trim()) nextErrors.phoneNumber = "Required"; + else if (!phonePattern.test(formValues.phoneNumber)) + nextErrors.phoneNumber = "Invalid phone number"; + + const ticketsNumber = Number(formValues.tickets); + if (!formValues.tickets.trim()) nextErrors.tickets = "Required"; + else if (!Number.isFinite(ticketsNumber) || ticketsNumber <= 0) + nextErrors.tickets = "Must be a positive number"; + + if (!paymentMethod) nextErrors.paymentMethod = "Select a payment method"; + + if (!formValues.code.trim()) nextErrors.code = "Required"; + + if (invoice) { + if (!formValues.companyName.trim()) nextErrors.companyName = "Required"; + if (!formValues.invoiceFirstName.trim()) + nextErrors.invoiceFirstName = "Required"; + if (!formValues.invoiceLastName.trim()) + nextErrors.invoiceLastName = "Required"; + if (!formValues.street.trim()) nextErrors.street = "Required"; + if (!formValues.postalCode.trim()) nextErrors.postalCode = "Required"; + + if (!formValues.invoicePhoneNumber.trim()) + nextErrors.invoicePhoneNumber = "Required"; + else if (!phonePattern.test(formValues.invoicePhoneNumber)) + nextErrors.invoicePhoneNumber = "Invalid phone number"; + + if (!formValues.invoiceEmail.trim()) nextErrors.invoiceEmail = "Required"; + else if (!emailPattern.test(formValues.invoiceEmail)) + nextErrors.invoiceEmail = "Invalid email"; + } + + setErrors(nextErrors); + return Object.keys(nextErrors).length === 0; + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setSubmitMessage(null); + + const isValid = validate(); + if (!isValid) return; + + setSubmitting(true); + try { + const payload = { + invoice, + paymentMethod, + firstName: formValues.firstName.trim(), + lastName: formValues.lastName.trim(), + email: formValues.email.trim(), + phoneNumber: formValues.phoneNumber.trim(), + tickets: Number(formValues.tickets), + code: Number(formValues.code), + invoiceDetails: invoice + ? { + companyName: formValues.companyName.trim(), + firstName: formValues.invoiceFirstName.trim(), + lastName: formValues.invoiceLastName.trim(), + street: formValues.street.trim(), + postalCode: formValues.postalCode.trim(), + phoneNumber: formValues.invoicePhoneNumber.trim(), + email: formValues.invoiceEmail.trim(), + } + : null, + }; + + const response = await fetch("http://localhost:8004/default/frontend", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + + setSubmitMessage("Submitted successfully."); + } catch (error) { + setSubmitMessage("Submit failed. Please try again."); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + + + + + + + + + setInvoice(event.target.checked)} + /> + } + label={t("invoice")} + /> + + + {invoice && ( + + + + + + + + + + + + )} + + + {/* Payment methods - only one must be selected */} + + setPaymentMethod((current) => + current === "cash" ? null : "cash" + ) + } + /> + } + label={t("cash")} + sx={{ color: errors.paymentMethod ? "error.main" : undefined }} + /> + + setPaymentMethod((current) => + current === "paypal" ? null : "paypal" + ) + } + /> + } + label={t("paypal")} + sx={{ color: errors.paymentMethod ? "error.main" : undefined }} + /> + + setPaymentMethod((current) => + current === "transfer" ? null : "transfer" + ) + } + /> + } + label={t("transfer")} + sx={{ color: errors.paymentMethod ? "error.main" : undefined }} + /> + + + + + {submitMessage && ( + + {submitMessage} + + )} + + + ); +}; diff --git a/frontend/src/pages/MainForm.tsx b/frontend/src/pages/MainForm.tsx index 441468f..28897a3 100644 --- a/frontend/src/pages/MainForm.tsx +++ b/frontend/src/pages/MainForm.tsx @@ -1,6 +1,4 @@ import { - Box, - Stack, TextField, FormControlLabel, Checkbox, @@ -10,425 +8,123 @@ import { import { useTranslation } from "react-i18next"; import { useState } from "react"; -const phonePattern = /^[+]?[- 0-9()]{7,}$/; -const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +interface Message { + type: "error" | "info" | "success" | "warning"; + headline: string; + text: string; +} export const MainForm = () => { const { t } = useTranslation(); const [invoice, setInvoice] = useState(false); - const [paymentMethod, setPaymentMethod] = useState< - "cash" | "paypal" | "transfer" | null - >(null); - const [formValues, setFormValues] = useState({ - firstName: "", - lastName: "", - email: "", - phoneNumber: "", - tickets: "", - code: "", - companyName: "", - invoiceFirstName: "", - invoiceLastName: "", - street: "", - postalCode: "", - invoicePhoneNumber: "", - invoiceEmail: "", - }); - const [errors, setErrors] = useState>({}); - const [submitting, setSubmitting] = useState(false); - const [submitMessage, setSubmitMessage] = useState(null); + const [msg, setMsg] = useState(null); - const updateField = - (field: keyof typeof formValues) => - (event: React.ChangeEvent) => { - setFormValues((prev) => ({ ...prev, [field]: event.target.value })); - }; - - const validate = () => { - const nextErrors: Record = {}; - - if (!formValues.firstName.trim()) nextErrors.firstName = "Required"; - if (!formValues.lastName.trim()) nextErrors.lastName = "Required"; - - if (!formValues.email.trim()) nextErrors.email = "Required"; - else if (!emailPattern.test(formValues.email)) - nextErrors.email = "Invalid email"; - - if (!formValues.phoneNumber.trim()) nextErrors.phoneNumber = "Required"; - else if (!phonePattern.test(formValues.phoneNumber)) - nextErrors.phoneNumber = "Invalid phone number"; - - const ticketsNumber = Number(formValues.tickets); - if (!formValues.tickets.trim()) nextErrors.tickets = "Required"; - else if (!Number.isFinite(ticketsNumber) || ticketsNumber <= 0) - nextErrors.tickets = "Must be a positive number"; - - if (!paymentMethod) nextErrors.paymentMethod = "Select a payment method"; - - if (!formValues.code.trim()) nextErrors.code = "Required"; - - if (invoice) { - if (!formValues.companyName.trim()) nextErrors.companyName = "Required"; - if (!formValues.invoiceFirstName.trim()) - nextErrors.invoiceFirstName = "Required"; - if (!formValues.invoiceLastName.trim()) - nextErrors.invoiceLastName = "Required"; - if (!formValues.street.trim()) nextErrors.street = "Required"; - if (!formValues.postalCode.trim()) nextErrors.postalCode = "Required"; - - if (!formValues.invoicePhoneNumber.trim()) - nextErrors.invoicePhoneNumber = "Required"; - else if (!phonePattern.test(formValues.invoicePhoneNumber)) - nextErrors.invoicePhoneNumber = "Invalid phone number"; - - if (!formValues.invoiceEmail.trim()) nextErrors.invoiceEmail = "Required"; - else if (!emailPattern.test(formValues.invoiceEmail)) - nextErrors.invoiceEmail = "Invalid email"; - } - - setErrors(nextErrors); - return Object.keys(nextErrors).length === 0; - }; - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setSubmitMessage(null); - - const isValid = validate(); - if (!isValid) return; - - setSubmitting(true); - try { - const payload = { - invoice, - paymentMethod, - firstName: formValues.firstName.trim(), - lastName: formValues.lastName.trim(), - email: formValues.email.trim(), - phoneNumber: formValues.phoneNumber.trim(), - tickets: Number(formValues.tickets), - code: Number(formValues.code), - invoiceDetails: invoice - ? { - companyName: formValues.companyName.trim(), - firstName: formValues.invoiceFirstName.trim(), - lastName: formValues.invoiceLastName.trim(), - street: formValues.street.trim(), - postalCode: formValues.postalCode.trim(), - phoneNumber: formValues.invoicePhoneNumber.trim(), - email: formValues.invoiceEmail.trim(), - } - : null, - }; - - const response = await fetch("http://localhost:8004/default/frontend", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`Request failed with status ${response.status}`); - } - - setSubmitMessage("Submitted successfully."); - } catch (error) { - setSubmitMessage("Submit failed. Please try again."); - } finally { - setSubmitting(false); - } + const handleSubmit = () => { + // Form submission logic goes here }; return ( - - - - - - - + <> +
+ + - - - - setInvoice(event.target.checked)} - /> - } - label={t("invoice")} - /> - - + + setInvoice(e.target.checked)} + /> + } + label={t("invoice")} + /> {invoice && ( - + <> - - - - + + - + )} - - - {/* Payment methods - only one must be selected */} - - setPaymentMethod((current) => - current === "cash" ? null : "cash" - ) - } - /> - } - label={t("cash")} - sx={{ color: errors.paymentMethod ? "error.main" : undefined }} - /> - - setPaymentMethod((current) => - current === "paypal" ? null : "paypal" - ) - } - /> - } - label={t("paypal")} - sx={{ color: errors.paymentMethod ? "error.main" : undefined }} - /> - - setPaymentMethod((current) => - current === "transfer" ? null : "transfer" - ) - } - /> - } - label={t("transfer")} - sx={{ color: errors.paymentMethod ? "error.main" : undefined }} - /> - - - + {/* Payment methods - only one must be selected */} + } label={t("cash")} /> + } label={t("paypal")} /> + } label={t("transfer")} /> + - {submitMessage && ( - - {submitMessage} + {msg && ( + + {msg.headline}: {msg.text} )} - - + + ); }; diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json index e69de29..b2110cd 100644 --- a/frontend/src/utils/i18n/locales/de/de.json +++ b/frontend/src/utils/i18n/locales/de/de.json @@ -0,0 +1,3 @@ +{ + "phone-number": "Telefonnummer" +} \ No newline at end of file diff --git a/frontend/src/utils/i18n/locales/en/en.json b/frontend/src/utils/i18n/locales/en/en.json index e69de29..8c511e6 100644 --- a/frontend/src/utils/i18n/locales/en/en.json +++ b/frontend/src/utils/i18n/locales/en/en.json @@ -0,0 +1,3 @@ +{ + "phone_number": "Telefonnummer" +}