diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ff80c1b..aed72b7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,11 +4,13 @@ services: hostname: lose-verkaufen build: ./frontend networks: - - ca-lose-internal + ca-lose-internal: + ipv4_address: 172.25.0.2 restart: unless-stopped backend: container_name: ca-lose-backend + hostname: backend build: ./backend environment: NODE_ENV: production @@ -19,11 +21,13 @@ services: depends_on: - database networks: - - ca-lose-internal + ca-lose-internal: + ipv4_address: 172.25.0.3 restart: unless-stopped database: container_name: ca-lose-mysql + hostname: database image: mysql:8.0 restart: unless-stopped environment: @@ -34,37 +38,63 @@ services: - ca-lose_mysql:/var/lib/mysql - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro networks: - - ca-lose-internal + ca-lose-internal: + ipv4_address: 172.25.0.4 + # DNS Server für Hostname-Auflösung innerhalb des VPN + dnsmasq: + container_name: ca-lose-dns + image: andyshinn/dnsmasq:latest + restart: unless-stopped + cap_add: + - NET_ADMIN + command: > + --no-daemon + --log-queries + --address=/lose-verkaufen/172.25.0.2 + --address=/frontend/172.25.0.2 + --address=/backend/172.25.0.3 + --address=/database/172.25.0.4 + --address=/wg-admin/172.25.0.10 + networks: + ca-lose-internal: + ipv4_address: 172.25.0.53 + + # WireGuard VPN mit Web-UI (wg-easy) wireguard: - image: lscr.io/linuxserver/wireguard:latest + image: ghcr.io/wg-easy/wg-easy:latest container_name: ca-lose-wireguard cap_add: - NET_ADMIN - - SYS_MODULE #optional + - SYS_MODULE environment: - - PUID=1000 - - PGID=1000 - - TZ=Etc/UTC - - SERVERURL=dus3.the1s.de #optional - - SERVERPORT=51830 #optional - - PEERS=2 #optional - - PEERDNS=auto #optional - - INTERNAL_SUBNET=10.13.14.0 #optional - - ALLOWEDIPS=10.13.14.0/24,172.25.0.0/24 #optional - - PERSISTENTKEEPALIVE_PEERS= #optional - - LOG_CONFS=true #optional + LANG: de + WG_HOST: dus3.the1s.de + WG_PORT: "51830" + PORT: "51821" + WG_DEFAULT_ADDRESS: 10.14.14.x + WG_DEFAULT_DNS: "172.25.0.53" + WG_ALLOWED_IPS: 172.25.0.0/24 + WG_PERSISTENT_KEEPALIVE: "25" + WG_POST_UP: "iptables -t nat -A POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -A FORWARD -i wg0 -j DROP" + WG_POST_DOWN: "iptables -t nat -D POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -D FORWARD -i wg0 -j DROP" volumes: - - ./config:/config - - /lib/modules:/lib/modules #optional + - wireguard-data:/etc/wireguard + - /lib/modules:/lib/modules:ro ports: - - 51830:51830/udp + - "51830:51830/udp" sysctls: + - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 restart: unless-stopped + depends_on: + - dnsmasq + - frontend networks: ca-lose-internal: ipv4_address: 172.25.0.10 + proxynet: + ipv4_address: 172.20.0.50 volumes: ca-lose_mysql: @@ -76,3 +106,6 @@ networks: ipam: config: - subnet: 172.25.0.0/24 + gateway: 172.25.0.1 + proxynet: + external: true 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 ad0492c..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("/backend/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 5d960a6..0514abd 100644 --- a/frontend/src/pages/MainForm.tsx +++ b/frontend/src/pages/MainForm.tsx @@ -69,7 +69,7 @@ export const MainForm = () => { setSelectedUser(cookieUser); confirmUser(cookieUser); } - }, []); + }, [isLoading]); const handleChange = (e: React.ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value }); @@ -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