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