From 5c035ba1c0b015b19a5c6e3c4ac84ef31a3d163d Mon Sep 17 00:00:00 2001 From: Theis Date: Sun, 10 May 2026 19:39:32 +0200 Subject: [PATCH] add lucide-react dependency and update form handling in MainForm - Added lucide-react to package dependencies - Refactored MainForm to improve form handling and user selection - Removed SuccessPage component and related logic - Updated localization files to include new keys for invoice details - Created interfaces for form data and messages --- frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/src/config/interfaces.config.ts | 21 + frontend/src/pages/MainForm.tsx | 639 ++++++++------------- frontend/src/pages/SuccessPage.tsx | 296 ++++------ frontend/src/utils/i18n/locales/de/de.json | 1 + frontend/src/utils/i18n/locales/en/en.json | 1 + frontend/src/utils/sender.ts | 25 +- 8 files changed, 413 insertions(+), 581 deletions(-) create mode 100644 frontend/src/config/interfaces.config.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 59ee4ad..851608d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@tailwindcss/vite": "^4.3.0", "i18next": "^26.0.10", "js-cookie": "^3.0.5", + "lucide-react": "^1.14.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-i18next": "^17.0.7", @@ -3105,6 +3106,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "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 9566b6e..a4ee550 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@tailwindcss/vite": "^4.3.0", "i18next": "^26.0.10", "js-cookie": "^3.0.5", + "lucide-react": "^1.14.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-i18next": "^17.0.7", diff --git a/frontend/src/config/interfaces.config.ts b/frontend/src/config/interfaces.config.ts new file mode 100644 index 0000000..2a62639 --- /dev/null +++ b/frontend/src/config/interfaces.config.ts @@ -0,0 +1,21 @@ +export interface FormData { + firstName: string; + lastName: string; + email: string; + phoneNumber: string; + tickets: number; + companyName: string; + cmpFirstName: string; + cpmLastName: string; + cpmEmail: string; + cpmPhoneNumber: string; + street: string; + postalCode: string; + paymentMethod: string; +} + +export interface Message { + type: "primary" | "neutral" | "danger" | "success" | "warning"; + headline: string; + text: string; +} diff --git a/frontend/src/pages/MainForm.tsx b/frontend/src/pages/MainForm.tsx index 3464a5a..d6df471 100644 --- a/frontend/src/pages/MainForm.tsx +++ b/frontend/src/pages/MainForm.tsx @@ -1,120 +1,138 @@ -import { - TextField, - FormControlLabel, - Checkbox, - Button, - Alert, - CircularProgress, - Autocomplete, - Chip, - Box, - Paper, - Typography, - IconButton, -} from "@mui/material"; -import { useTranslation } from "../../node_modules/react-i18next"; +import { useTranslation } from "react-i18next"; import { useState, useEffect } from "react"; -import { submitFormData } from "../utils/sender"; -import Cookies from "../../node_modules/@types/js-cookie"; import * as React from "react"; -import TranslateIcon from "@mui/icons-material/Translate"; +import Cookies from "js-cookie"; +import { Languages } from "lucide-react"; +import { + Sheet, + Input, + Button, + Checkbox, + Chip, + IconButton, + Alert, + Typography, + FormControl, + FormLabel, + Autocomplete, +} from "@mui/joy"; +import { submitFormData } from "../utils/sender"; import { API_BASE } from "../config/api.config"; +import type { FormData, Message } from "../config/interfaces.config"; -interface Message { - type: "error" | "info" | "success" | "warning"; - headline: string; - text: string; -} +const PAYMENT_METHODS = ["bar", "paypal", "andere"] as const; +const PAYMENT_LABELS: Record = { + bar: "Cash", + paypal: "PayPal", + andere: "Transfer", +}; + +const DEFAULT_FORM: FormData = { + firstName: "", + lastName: "", + email: "", + phoneNumber: "", + tickets: 1, + companyName: "", + cmpFirstName: "", + cpmLastName: "", + cpmEmail: "", + cpmPhoneNumber: "", + street: "", + postalCode: "", + paymentMethod: "", +}; + +// ─── Field component lives OUTSIDE MainForm so React doesn't treat it as a +// new component type on every render, which would cause inputs to lose focus. +const Field = ({ + label, + name, + type = "text", + required = true, + formData, + onChange, +}: { + label: string; + name: keyof FormData; + type?: string; + required?: boolean; + formData: FormData; + onChange: (e: React.ChangeEvent) => void; +}) => ( + + {label} + + +); export const MainForm = () => { const { t, i18n } = useTranslation(); + const [invoice, setInvoice] = useState(false); const [msg, setMsg] = useState(null); const [isLoading, setIsLoading] = useState(false); const [nextID, setNextID] = useState(null); - const [formData, setFormData] = useState({ - firstName: "", - lastName: "", - email: "", - phoneNumber: "", - tickets: 1, - companyName: "", - cmpFirstName: "", - cpmLastName: "", - cpmEmail: "", - cpmPhoneNumber: "", - street: "", - postalCode: "", - paymentMethod: "", - }); const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); - - const changeTranslation = () => { - const clientLng = i18n.language; - - if (clientLng === "en") { - i18n.changeLanguage("de"); - } else if (clientLng === "de") { - i18n.changeLanguage("en"); - } else { - setMsg({ - type: "error", - headline: "Error", - text: "Cannot change langugage.", - }); - } - }; - - useEffect(() => { - // Fetch user data or any other data needed for the form - try { - const fetchUsers = async () => { - const response = await fetch(`${API_BASE}/default/users`); - const data = await response.json(); - setUsers(data.users); - }; - fetchUsers(); - console.log(users); - } catch (error) { - setMsg({ - type: "error", - headline: t("error"), - text: t("failed-to-load-users"), - }); - console.error("Error fetching users:", error); - } - - if (Cookies.get("selectedUser")) { - const cookieUser = Cookies.get("selectedUser")!; - setSelectedUser(cookieUser); - confirmUser(cookieUser); - } - }, [isLoading]); + const [formData, setFormData] = useState(DEFAULT_FORM); const handleChange = (e: React.ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; - const confirmUser = async (selectedUser: string) => { + const confirmUser = async (username: string) => { try { - const response = await fetch( - `${API_BASE}/default/confirm-user?username=${selectedUser}`, + const res = await fetch( + `${API_BASE}/default/confirm-user?username=${username}`, ); - const data = await response.json(); + const data = await res.json(); setNextID(data.nextID); } catch (error) { console.error("Error confirming user:", error); } }; - const handleUserSelection = (selectedUser: string | null) => { - if (!selectedUser) return; - setSelectedUser(selectedUser); - confirmUser(selectedUser); - Cookies.set("selectedUser", selectedUser); + const handleUserSelection = (username: string | null) => { + if (!username) return; + setSelectedUser(username); + confirmUser(username); + Cookies.set("selectedUser", username); }; + const toggleLanguage = () => { + i18n.changeLanguage(i18n.language === "en" ? "de" : "en"); + }; + + useEffect(() => { + (async () => { + try { + const res = await fetch(`${API_BASE}/default/users`); + const data = await res.json(); + setUsers(data.users); + } catch { + setMsg({ + type: "danger", + headline: t("error"), + text: t("failed-to-load-users"), + }); + } + })(); + + const cookieUser = Cookies.get("selectedUser"); + if (cookieUser) { + setSelectedUser(cookieUser); + confirmUser(cookieUser); + } + }, []); + const handleSubmit = async () => { setIsLoading(true); try { @@ -123,7 +141,7 @@ export const MainForm = () => { document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`; } else { setMsg({ - type: "error", + type: "danger", headline: t("error"), text: result.error || t("form-submission-failed"), }); @@ -133,44 +151,35 @@ export const MainForm = () => { } }; + // Shorthand so we don't repeat formData + onChange on every Field usage + const fieldProps = { formData, onChange: handleChange }; + return ( - - + - - changeTranslation()} - aria-label="translate" - > - - - + {/* Language toggle */} + + + +
{ e.preventDefault(); @@ -178,337 +187,185 @@ export const MainForm = () => { }} className="flex flex-col gap-4" > - {/* User Selection */} + {/* User selection */} ( - - )} - onChange={(_event, value) => handleUserSelection(value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.defaultMuiPrevented = true; - } + onChange={(_, value) => handleUserSelection(value)} + placeholder={t("user")} + variant="soft" + sx={{ borderRadius: "10px" }} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); }} /> - {/* Next ID Chip */} + {/* Next ID badge */} + > + #{nextID ?? "N/A"} + - {/* Name Fields - Two Columns */} - - - - + {/* Name row */} +
+ + +
- {/* Email */} - - - {/* Phone Number */} - + - {/* Tickets and Invoice Checkbox */} - - - setInvoice(e.target.checked)} - /> - } - label={t("invoice")} - className="justify-end" - /> - + {/* Tickets + Invoice toggle */} +
+ + {t("tickets")} + + +
+ setInvoice(e.target.checked)} + label={t("invoice")} + variant="outlined" + /> +
+
- {/* Invoice Fields */} + {/* Invoice details (conditional) */} {invoice && ( - - + + {t("invoice-details")} + + - - {/* Invoice Name Fields - Two Columns */} - - + - - - - - - + + - - - - - + )} - {/* Payment Methods */} - - - {t("select-payment-method")} * - - - - - - + + {/* Payment method selection */} + + {t("select-payment-method")} * +
+ {PAYMENT_METHODS.map((method) => ( + + ))} +
+ {/* Hidden required input to enforce payment selection on submit */} {!formData.paymentMethod && ( {}} style={{ opacity: 0, width: 0, height: 0, position: "absolute", }} - required - value={formData.paymentMethod} - onChange={() => {}} /> )} -
+ - {/* Submit Button */} + {/* Submit button */} - {/* Alert Message */} + {/* Alert message */} {msg && ( - - {msg.headline}: {msg.text} + + {msg.headline}: {msg.text} )} -
-
+ + ); }; diff --git a/frontend/src/pages/SuccessPage.tsx b/frontend/src/pages/SuccessPage.tsx index aa39c17..4687f06 100644 --- a/frontend/src/pages/SuccessPage.tsx +++ b/frontend/src/pages/SuccessPage.tsx @@ -1,23 +1,20 @@ -import { Box, Paper, Typography, Chip, Button } from "@mui/material"; import { useEffect, useState } from "react"; import { CircleCheck } from "lucide-react"; -import { useTranslation } from "../../node_modules/react-i18next"; +import { useTranslation } from "react-i18next"; +import { Sheet, Typography, Chip, Button } from "@mui/joy"; export const SuccessPage = () => { - const [orderId, setOrderId] = useState(null); - const [tickets, setNumberOfTickets] = useState(0); - const [animate, setAnimate] = useState(false); const { t } = useTranslation(); + const [orderId, setOrderId] = useState(null); + const [tickets, setTickets] = useState(0); + const [animate, setAnimate] = useState(false); const [seconds, setSeconds] = useState(30); 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); - + setOrderId(params.get("id")); + setTickets(parseInt(params.get("tickets") ?? "0", 10)); + // Small delay so the CSS transition actually plays setTimeout(() => setAnimate(true), 100); }, []); @@ -26,185 +23,138 @@ export const SuccessPage = () => { window.location.href = "/"; return; } - - const timer = setTimeout(() => setSeconds(seconds - 1), 1000); - + const timer = setTimeout(() => setSeconds((s) => s - 1), 1000); return () => clearTimeout(timer); }, [seconds]); + // Returns a style object that slides the element up + fades it in. + // Each section gets a slightly later delay for a staggered entrance. + const fadeUp = (delay: string): React.CSSProperties => ({ + transition: `opacity 0.5s ease-in-out ${delay}, transform 0.5s ease-in-out ${delay}`, + transform: animate ? "translateY(0)" : "translateY(20px)", + opacity: animate ? 1 : 0, + }); + 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")} - - - - )} - - {/* Return button */} - - - - - {/* Additional Info */} - - - {t("thank-you")} - - - - {/* Decorative Elements */} - - - + + {/* Animated success icon */} +
+ +
+ + {/* Headline */} +
+ + {t("form-submitted-successfully")} + +
+ + {/* Subtitle */} +
+ + {t("ticket-payment", { count: tickets })} + +
+ + {/* Tickets chip */} + {tickets > 0 && ( +
+ + {tickets} {tickets === 1 ? t("ticket") : t("tickets")} + +
+ )} + + {/* Order ID chip */} + {orderId && ( +
+ + {t("entry-id")} + + + #{orderId} + +
+ )} + + {/* Return button with countdown */} +
+ +
+ + {/* Thank-you note */} +
+ + {t("thank-you")} + +
+ + ); }; diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json index 6033c4b..e98ad0c 100644 --- a/frontend/src/utils/i18n/locales/de/de.json +++ b/frontend/src/utils/i18n/locales/de/de.json @@ -4,6 +4,7 @@ "phone-number": "Telefonnummer", "tickets": "Lose", "invoice": "Rechnung", + "invoice-details": "Rechnungsdetails", "company-name": "Firmenname", "street": "Straße + Haus Nr.", "postal-code": "Plz + Stadt", diff --git a/frontend/src/utils/i18n/locales/en/en.json b/frontend/src/utils/i18n/locales/en/en.json index e52ae2a..ccef4ad 100644 --- a/frontend/src/utils/i18n/locales/en/en.json +++ b/frontend/src/utils/i18n/locales/en/en.json @@ -3,6 +3,7 @@ "last-name": "Last Name", "phone-number": "Phone Number", "invoice": "Invoice", + "invoice-details": "Invoice Details", "company-name": "Company Name", "street": "Street + House No.", "postal-code": "Postal Code + City", diff --git a/frontend/src/utils/sender.ts b/frontend/src/utils/sender.ts index b61319c..724447d 100644 --- a/frontend/src/utils/sender.ts +++ b/frontend/src/utils/sender.ts @@ -1,23 +1,14 @@ import { API_BASE } from "../config/api.config"; +import type { FormData } from "../config/interfaces.config"; -interface FormData { - firstName: string; - lastName: string; - email: string; - phoneNumber: string; - tickets: number; - companyName: string; - cmpFirstName: string; - cpmLastName: string; - cpmEmail: string; - cpmPhoneNumber: string; - street: string; - postalCode: string; - paymentMethod: string; -} +export const submitFormData = async ( + data: FormData, + username: string | null, +) => { + if (username == null) { + return { success: false, errorCode: "x001" }; + } -export const submitFormData = async (data: FormData, username: string) => { - console.log(data); try { const response = await fetch( `${API_BASE}/default/new-entry?username=${username}`,