From e2ed58499aec5fa2aaba1b934dcef6e8dd8b5278 Mon Sep 17 00:00:00 2001 From: Theis Date: Wed, 21 Jan 2026 10:26:39 +0100 Subject: [PATCH] feat: implement success page and update routing; add lucide icons and enhance form submission feedback --- frontend/package-lock.json | 19 +- frontend/package.json | 4 +- frontend/src/App.tsx | 2 + frontend/src/pages/MainForm copy.tsx | 434 --------------------- frontend/src/pages/MainForm.tsx | 8 +- frontend/src/pages/SuccessPage.tsx | 174 +++++++++ frontend/src/utils/i18n/locales/de/de.json | 5 +- frontend/src/utils/i18n/locales/en/en.json | 9 +- 8 files changed, 210 insertions(+), 445 deletions(-) delete mode 100644 frontend/src/pages/MainForm copy.tsx create mode 100644 frontend/src/pages/SuccessPage.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bab584c..7a7ed44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,9 @@ "@tailwindcss/vite": "^4.1.11", "i18next": "^25.7.4", "js-cookie": "^3.0.5", - "react": "^19.2.0", + "lucide": "^0.562.0", + "lucide-react": "^0.562.0", + "react": "^19.2.3", "react-dom": "^19.2.0", "react-i18next": "^16.5.3", "react-router-dom": "^7.11.0", @@ -3784,6 +3786,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.562.0.tgz", + "integrity": "sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==", + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4f390ed..f7844ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,9 @@ "@tailwindcss/vite": "^4.1.11", "i18next": "^25.7.4", "js-cookie": "^3.0.5", - "react": "^19.2.0", + "lucide": "^0.562.0", + "lucide-react": "^0.562.0", + "react": "^19.2.3", "react-dom": "^19.2.0", "react-i18next": "^16.5.3", "react-router-dom": "^7.11.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c8cd2e..b14f593 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,14 @@ import "./App.css"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import { MainForm } from "./pages/MainForm"; +import { SuccessPage } from "./pages/SuccessPage"; function App() { return ( } /> + } /> ); diff --git a/frontend/src/pages/MainForm copy.tsx b/frontend/src/pages/MainForm copy.tsx deleted file mode 100644 index ac52ff0..0000000 --- a/frontend/src/pages/MainForm copy.tsx +++ /dev/null @@ -1,434 +0,0 @@ -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 2b62ba1..550e13c 100644 --- a/frontend/src/pages/MainForm.tsx +++ b/frontend/src/pages/MainForm.tsx @@ -78,7 +78,7 @@ export const MainForm = () => { const confirmUser = async (selectedUser: string) => { try { const response = await fetch( - `http://localhost:8004/default/confirm-user?username=${selectedUser}` + `http://localhost:8004/default/confirm-user?username=${selectedUser}`, ); const data = await response.json(); setNextID(data.nextID); @@ -99,11 +99,7 @@ export const MainForm = () => { try { const result = await submitFormData(formData, selectedUser || ""); if (result.success) { - setMsg({ - type: "success", - headline: t("success"), - text: t("form-submitted-successfully"), - }); + document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`; } else { setMsg({ type: "error", diff --git a/frontend/src/pages/SuccessPage.tsx b/frontend/src/pages/SuccessPage.tsx new file mode 100644 index 0000000..ae54f10 --- /dev/null +++ b/frontend/src/pages/SuccessPage.tsx @@ -0,0 +1,174 @@ +import { Box, Paper, Typography, Chip } from "@mui/material"; +import { useEffect, useState } from "react"; +import { CircleCheck } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export const SuccessPage = () => { + const [orderId, setOrderId] = useState(null); + const [tickets, setNumberOfTickets] = useState(0); + const [animate, setAnimate] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const id = params.get("id"); + const numberOfTickets = params.get("tickets"); + + setOrderId(id); + setNumberOfTickets(numberOfTickets ? parseInt(numberOfTickets, 10) : 0); + + setTimeout(() => setAnimate(true), 100); + }, []); + + return ( + + + {/* Animated Success Icon */} + + + + + {/* Success Message */} + + {t("form-submitted-successfully")} + + + + {t("ticket-payment", { count: tickets })} + + + {/* Tickets Display */} + {tickets > 0 && ( + + + + )} + + {/* Order ID Display */} + {orderId && ( + + + {t("entry-id")} + + + + )} + + {/* Additional Info */} + + + {t("thank-you")} + + + + {/* Decorative Elements */} + + + + ); +}; diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json index fc36ebb..1e4a8df 100644 --- a/frontend/src/utils/i18n/locales/de/de.json +++ b/frontend/src/utils/i18n/locales/de/de.json @@ -18,5 +18,8 @@ "error": "Fehler", "cash": "Bar", "paypal": "PayPal", - "transfer": "Überweisung" + "transfer": "Überweisung", + "ticket-payment": "Sie haben erflogreich {count} {count, plural, one {Los} other {Lose}} gekauft.", + "entry-id": "Eintrags-ID", + "thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los." } \ 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 d006942..eded0df 100644 --- a/frontend/src/utils/i18n/locales/en/en.json +++ b/frontend/src/utils/i18n/locales/en/en.json @@ -2,7 +2,6 @@ "first-name": "First Name", "last-name": "Last Name", "phone-number": "Phone Number", - "tickets": "Tickets", "invoice": "Invoice", "company-name": "Company Name", "street": "Street + House No.", @@ -18,5 +17,11 @@ "error": "Error", "cash": "Cash", "paypal": "PayPal", - "transfer": "Bank Transfer" + "transfer": "Bank Transfer", + "ticket-payment_one": "You have successfully purchased {{count}} ticket.", + "ticket-payment_other": "You have successfully purchased {{count}} tickets.", + "ticket": "ticket", + "tickets": "tickets", + "entry-id": "Entry ID", + "thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket." } \ No newline at end of file