diff --git a/backend/routes/default/frontend.data.js b/backend/routes/default/frontend.data.js index 7e3eb62..833f8e7 100644 --- a/backend/routes/default/frontend.data.js +++ b/backend/routes/default/frontend.data.js @@ -28,6 +28,9 @@ export const confirmUser = async (username) => { ]); if (rows.length > 0) { + const { first_name, last_name } = rows[0]; + const fullname = first_name + " " + last_name; + // creating userTicketTable const d = new Date(); @@ -37,8 +40,6 @@ export const confirmUser = async (username) => { const date = `${day}_${month}_${year}`; const tableName = `${username}_${date}`; - console.log(tableName); - const [createTable] = await pool.query( `CREATE TABLE IF NOT EXISTS ?? ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -70,9 +71,9 @@ export const confirmUser = async (username) => { nextID = rows.length > 0 ? rows[0].id + 1 : 1; }; await getNextID(); - return { success: true, nextID, tableName }; + return { success: true, nextID, tableName, fullname }; } else { - return { success: false, message: "Table creation failed" }; + return { success: false, message: "Table creation failed", fullname }; } } else { return null; diff --git a/backend/routes/default/frontend.route.js b/backend/routes/default/frontend.route.js index 5db5ac1..e380a7d 100644 --- a/backend/routes/default/frontend.route.js +++ b/backend/routes/default/frontend.route.js @@ -11,8 +11,6 @@ router.post("/new-entry", async (req, res) => { if (!result.success) { return res.status(500).json({ message: "Form Data Invalid" }); } - console.log(req.body); - console.log(username); res.sendStatus(204); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95e7dff..e676de0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,8 +16,10 @@ "@mui/joy": "^5.0.0-beta.52", "@mui/material": "^9.0.1", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-query": "^5.100.10", "i18next": "^26.0.10", "js-cookie": "^3.0.5", + "k6": "^0.0.0", "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", @@ -1766,6 +1768,32 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tanstack/query-core": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -3042,6 +3070,12 @@ "node": ">=6" } }, + "node_modules/k6": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/k6/-/k6-0.0.0.tgz", + "integrity": "sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g==", + "license": "AGPL-3.0" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4178,23 +4212,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ed61e3e..8e872d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,8 +18,10 @@ "@mui/joy": "^5.0.0-beta.52", "@mui/material": "^9.0.1", "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-query": "^5.100.10", "i18next": "^26.0.10", "js-cookie": "^3.0.5", + "k6": "^0.0.0", "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", diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 63fd9f8..2146915 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,9 +3,14 @@ import { createRoot } from "react-dom/client"; import "./utils/i18n/index.ts"; import "./index.css"; import App from "./App.tsx"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); createRoot(document.getElementById("root")!).render( - + + + , ); diff --git a/frontend/src/pages/MainForm.tsx b/frontend/src/pages/MainForm.tsx index 2205bd9..23cfef6 100644 --- a/frontend/src/pages/MainForm.tsx +++ b/frontend/src/pages/MainForm.tsx @@ -18,14 +18,16 @@ import { Modal, ModalDialog, ModalClose, + CircularProgress, } from "@mui/joy"; -import { submitFormData } from "../utils/sender"; -import { API_BASE } from "../config/api.config"; +import { submitFormData } from "../utils/api/form"; import type { FormData, Message } from "../config/interfaces.config"; import PersonIcon from "@mui/icons-material/Person"; import QrCodeIcon from "@mui/icons-material/QrCode"; import TranslateIcon from "@mui/icons-material/Translate"; import qrCode from "../assets/PayPal-QR-Code.png"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { confirmUser, fetchUsers } from "../utils/api/users"; const PAYMENT_METHODS = ["bar", "paypal", "andere"] as const; const PAYMENT_LABELS: Record = { @@ -82,13 +84,12 @@ const Field = ({ export const MainForm = () => { const { t, i18n } = useTranslation(); + const queryClient = useQueryClient(); const [invoice, setInvoice] = useState(false); const [msg, setMsg] = useState(null); - const [isLoading, setIsLoading] = useState(false); const [nextID, setNextID] = useState(null); - const [users, setUsers] = useState([]); - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUser, setSelectedUser] = useState(""); const [formData, setFormData] = useState(DEFAULT_FORM); const [showSelectUser, setShowSelectUser] = useState(false); const [QRmodal, setQRmodal] = useState(false); @@ -97,23 +98,62 @@ export const MainForm = () => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; - const confirmUser = async (username: string) => { - try { - const res = await fetch( - `${API_BASE}/default/confirm-user?username=${username}`, - ); - const data = await res.json(); - setNextID(data.nextID); - } catch (error) { - console.error("Error confirming user:", error); + useEffect(() => { + const savedUser = Cookies.get("selectedUser"); + if (savedUser) { + setSelectedUser(savedUser); } - }; + }, []); + + const { data: usernameData, isLoading: usernameDataIsLoading } = useQuery({ + queryKey: ["users"], + queryFn: fetchUsers, + }); + + const { data: userData, isSuccess: userDataIsSuccess } = useQuery({ + queryKey: ["user", selectedUser], + enabled: !!selectedUser, + queryFn: () => confirmUser(selectedUser), + }); + + const { + mutate: mutateForm, + isSuccess: mutateFormIsSuccess, + isPending: mutateFormIsPending, + isError: mutateFormIsError, + } = useMutation({ + mutationFn: () => submitFormData(formData, selectedUser), + }); + + // Redirecting to success page if mutation was successful + useEffect(() => { + if (mutateFormIsSuccess) { + queryClient.invalidateQueries({ queryKey: ["user", selectedUser] }); + document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`; + } + + if (mutateFormIsError) { + queryClient.invalidateQueries({ queryKey: ["user", selectedUser] }); + setMsg({ + type: "danger", + headline: t("error"), + text: t("form-submission-failed"), + }); + } + }, [mutateFormIsSuccess, mutateFormIsError]); + + // Setting the nextID after a user is selected + useEffect(() => { + if (!userData) return; + setNextID(userData.nextID); + }, [userDataIsSuccess]); const handleUserSelection = (username: string | null) => { - if (!username) return; + if (username == null || username == "") { + return; + } + setSelectedUser(username); - confirmUser(username); - Cookies.set("selectedUser", username); }; const changeTranslation = () => { @@ -140,46 +180,6 @@ export const MainForm = () => { } }, [formData.paymentMethod]); - 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 { - const result = await submitFormData(formData, selectedUser || ""); - if (result.success) { - document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`; - } else { - setMsg({ - type: "danger", - headline: t("error"), - text: result.error || t("form-submission-failed"), - }); - } - } finally { - setIsLoading(false); - } - }; - // Shorthand so we don't repeat formData + onChange on every Field usage const fieldProps = { formData, onChange: handleChange }; @@ -191,15 +191,14 @@ export const MainForm = () => { {t("user")} {/* User selection */} handleUserSelection(value)} placeholder={t("user")} variant="soft" sx={{ borderRadius: "10px" }} - onKeyDown={(e) => { - if (e.key === "Enter") e.preventDefault(); - }} /> @@ -250,12 +249,23 @@ export const MainForm = () => { + + {`${t("greeting")} ${userData?.fullname ?? t("loading")}`} +
{ e.preventDefault(); - handleSubmit(); + mutateForm(); }} className="flex flex-col gap-4" > @@ -408,25 +418,29 @@ export const MainForm = () => { )} - {/* Submit button */} - + {mutateFormIsPending ? ( +
+ +
+ ) : ( + + )} {/* Alert message */} {msg && ( diff --git a/frontend/src/pages/SuccessPage.tsx b/frontend/src/pages/SuccessPage.tsx index 4687f06..c991964 100644 --- a/frontend/src/pages/SuccessPage.tsx +++ b/frontend/src/pages/SuccessPage.tsx @@ -36,7 +36,7 @@ export const SuccessPage = () => { }); return ( -
+
{ {/* Headline */}
- + {t("form-submitted-successfully")}
@@ -110,8 +113,18 @@ export const SuccessPage = () => { {/* Order ID chip */} {orderId && ( -
- +
+ {t("entry-id")} { borderRadius: "12px", fontWeight: 700, background: "linear-gradient(135deg, #2563eb, #1d4ed8)", - "&:hover": { background: "linear-gradient(135deg, #1d4ed8, #1e40af)" }, + "&:hover": { + background: "linear-gradient(135deg, #1d4ed8, #1e40af)", + }, }} > {seconds}s — {t("return-to-homepage")} @@ -146,11 +161,11 @@ export const SuccessPage = () => {
{/* Thank-you note */} -
- +
+ {t("thank-you")}
diff --git a/frontend/src/utils/api/form.ts b/frontend/src/utils/api/form.ts new file mode 100644 index 0000000..0229d7f --- /dev/null +++ b/frontend/src/utils/api/form.ts @@ -0,0 +1,26 @@ +import { API_BASE } from "../../config/api.config"; +import type { FormData } from "../../config/interfaces.config"; + +export const submitFormData = async (data: FormData, username: string) => { + console.warn("submitFormData is fetching!"); + + await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds + + const response = await fetch( + `${API_BASE}/default/new-entry?username=${username}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }, + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Form submission failed"); + } + + return; +}; diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users.ts new file mode 100644 index 0000000..4e0e705 --- /dev/null +++ b/frontend/src/utils/api/users.ts @@ -0,0 +1,21 @@ +import { API_BASE } from "../../config/api.config"; +import Cookies from "js-cookie"; + +export const fetchUsers = async () => { + console.warn("fetchUsers is fetching!"); + + const response = await fetch(`${API_BASE}/default/users`); + const data = await response.json(); + + return data; +}; + +export const confirmUser = async (username: string) => { + console.warn("confirmUser is fetching!"); + const response = await fetch( + `${API_BASE}/default/confirm-user?username=${username}`, + ); + const data = await response.json(); + Cookies.set("selectedUser", username); + return data; +}; diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json index cdaf863..0416628 100644 --- a/frontend/src/utils/i18n/locales/de/de.json +++ b/frontend/src/utils/i18n/locales/de/de.json @@ -14,7 +14,7 @@ "user": "Benutzer", "next-id": "Nächste Eintragsnummer: ", "form-submitted-successfully": "Formular erfolgreich übermittelt!", - "orm-submission-failed": "Formularübermittlung fehlgeschlagen.", + "form-submission-failed": "Formularübermittlung fehlgeschlagen.", "success": "Erfolg", "error": "Fehler", "cash": "Bar", @@ -26,5 +26,7 @@ "thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los.", "select-payment-method": "Zahlungsmethode auswählen", "return-to-homepage": "Zurück", - "qr-text": "PayPal QR-Code der Claudius Akademie" + "qr-text": "PayPal QR-Code der Claudius Akademie", + "loading": "Lädt...", + "greeting": "Hallo," } \ 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 4bde327..5e47dcc 100644 --- a/frontend/src/utils/i18n/locales/en/en.json +++ b/frontend/src/utils/i18n/locales/en/en.json @@ -27,5 +27,7 @@ "thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket.", "select-payment-method": "Select Payment Method", "return-to-homepage": "Return", - "qr-text": "PayPal QR-Code from the Claudius Akademie" + "qr-text": "PayPal QR-Code from the Claudius Akademie", + "loading": "Loading...", + "greeting": "Hello," } \ No newline at end of file diff --git a/frontend/src/utils/sender.ts b/frontend/src/utils/sender.ts deleted file mode 100644 index 724447d..0000000 --- a/frontend/src/utils/sender.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { API_BASE } from "../config/api.config"; -import type { FormData } from "../config/interfaces.config"; - -export const submitFormData = async ( - data: FormData, - username: string | null, -) => { - if (username == null) { - return { success: false, errorCode: "x001" }; - } - - try { - const response = await fetch( - `${API_BASE}/default/new-entry?username=${username}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }, - ); - - if (!response.ok) { - const errorText = await response.text(); - return { success: false, error: `Server error: ${errorText}` }; - } - - return { success: true }; - } catch (error) { - return { success: false, error: (error as Error).message }; - } -}; diff --git a/frontend/tests/test.local.js b/frontend/tests/test.local.js new file mode 100644 index 0000000..9531c16 --- /dev/null +++ b/frontend/tests/test.local.js @@ -0,0 +1,13 @@ +import http from "k6/http"; +import { sleep } from "k6"; + +export const options = { + vus: 100, // amount of users + duration: "60s", // duration of the test +}; + +export default function () { + http.get("http://localhost:8004/default/confirm-user?username=TheisGaedigk"); + http.get("http://localhost:8004/default/users"); + sleep(1); +} diff --git a/frontend/tests/test.server.js b/frontend/tests/test.server.js new file mode 100644 index 0000000..3e4b4f1 --- /dev/null +++ b/frontend/tests/test.server.js @@ -0,0 +1,15 @@ +// Before running: Establish VPN connection first + +import http from "k6/http"; +import { sleep } from "k6"; + +export const options = { + vus: 100, // amount of users + duration: "60s", // duration of the test +}; + +export default function () { + http.get("http://backend:8004/default/confirm-user?username=TheisGaedigk"); + http.get("http://backend:8004/default/users"); + sleep(0.5); +}