Compare commits
19 Commits
216a1cb1d4
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 96f4ca6fd9 | |||
| e3e1037a85 | |||
| 9445f5d417 | |||
| 308b21ae6c | |||
| 697e84b5ea | |||
| 09bbec6ce3 | |||
| adbf60df6c | |||
| acc1ecc88c | |||
| ed48d7552e | |||
| 9f22f4aa35 | |||
| 0b1e46779e | |||
| 0a7f83f6e8 | |||
| 4970f80b70 | |||
| 2feb1ec8c8 | |||
| ef4734f886 | |||
| 82edf655a3 | |||
| 81a32faea3 | |||
| 8396840149 | |||
| e2ed58499a |
@@ -12,7 +12,7 @@ const pool = mysql
|
||||
.promise();
|
||||
|
||||
export const getUser = async () => {
|
||||
const [rows] = await pool.query("SELECT username FROM usersNEW");
|
||||
const [rows] = await pool.query("SELECT username FROM users");
|
||||
|
||||
if (rows.length > 0) {
|
||||
const users = rows.map((r) => r.username);
|
||||
@@ -23,7 +23,7 @@ export const getUser = async () => {
|
||||
};
|
||||
|
||||
export const confirmUser = async (username) => {
|
||||
const [rows] = await pool.query("SELECT * FROM usersNEW WHERE username = ?", [
|
||||
const [rows] = await pool.query("SELECT * FROM users WHERE username = ?", [
|
||||
username,
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ services:
|
||||
hostname: lose-verkaufen
|
||||
build: ./frontend
|
||||
networks:
|
||||
- ca-lose-internal
|
||||
ca-lose-internal:
|
||||
ipv4_address: 172.25.0.2
|
||||
proxynet:
|
||||
ipv4_address: 172.20.0.61
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
container_name: ca-lose-backend
|
||||
hostname: backend
|
||||
build: ./backend
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
@@ -19,11 +23,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 +40,65 @@ 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
|
||||
proxynet:
|
||||
ipv4_address: 172.20.0.60
|
||||
|
||||
# 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 +110,6 @@ networks:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.25.0.0/24
|
||||
gateway: 172.25.0.1
|
||||
proxynet:
|
||||
external: true
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<title>Lose verkaufen</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainForm />} />
|
||||
<Route path="/success" element={<SuccessPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitMessage, setSubmitMessage] = useState<string | null>(null);
|
||||
|
||||
const updateField =
|
||||
(field: keyof typeof formValues) =>
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormValues((prev) => ({ ...prev, [field]: event.target.value }));
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const nextErrors: Record<string, string> = {};
|
||||
|
||||
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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<Box className="min-h-screen bg-neutral-900 flex justify-center items-start py-10 px-4">
|
||||
<Box
|
||||
component="form"
|
||||
action=""
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full max-w-md bg-white shadow-sm rounded-md"
|
||||
sx={{ display: "flex", flexDirection: "column", gap: 2, p: 3 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
id="first-name"
|
||||
label={t("first_name")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.firstName}
|
||||
onChange={updateField("firstName")}
|
||||
error={Boolean(errors.firstName)}
|
||||
helperText={errors.firstName}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="last-name"
|
||||
label={t("last_name")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.lastName}
|
||||
onChange={updateField("lastName")}
|
||||
error={Boolean(errors.lastName)}
|
||||
helperText={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
required
|
||||
id="email"
|
||||
label={t("email")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="email"
|
||||
value={formValues.email}
|
||||
onChange={updateField("email")}
|
||||
error={Boolean(errors.email)}
|
||||
helperText={errors.email}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="phone-number"
|
||||
label={t("phone-number")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="tel"
|
||||
value={formValues.phoneNumber}
|
||||
onChange={updateField("phoneNumber")}
|
||||
error={Boolean(errors.phoneNumber)}
|
||||
helperText={errors.phoneNumber}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
id="tickets"
|
||||
label={t("tickets")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="number"
|
||||
value={formValues.tickets}
|
||||
onChange={updateField("tickets")}
|
||||
error={Boolean(errors.tickets)}
|
||||
helperText={errors.tickets}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={invoice}
|
||||
onChange={(event) => setInvoice(event.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={t("invoice")}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{invoice && (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<TextField
|
||||
required
|
||||
id="company-name"
|
||||
label={t("company_name")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.companyName}
|
||||
onChange={updateField("companyName")}
|
||||
error={Boolean(errors.companyName)}
|
||||
helperText={errors.companyName}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
id="first-name_invoice"
|
||||
label={t("first_name")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.invoiceFirstName}
|
||||
onChange={updateField("invoiceFirstName")}
|
||||
error={Boolean(errors.invoiceFirstName)}
|
||||
helperText={errors.invoiceFirstName}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="last-name_invoice"
|
||||
label={t("last_name")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.invoiceLastName}
|
||||
onChange={updateField("invoiceLastName")}
|
||||
error={Boolean(errors.invoiceLastName)}
|
||||
helperText={errors.invoiceLastName}
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
required
|
||||
id="street"
|
||||
label={t("street")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.street}
|
||||
onChange={updateField("street")}
|
||||
error={Boolean(errors.street)}
|
||||
helperText={errors.street}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="postal-code"
|
||||
label={t("postal_code")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.postalCode}
|
||||
onChange={updateField("postalCode")}
|
||||
error={Boolean(errors.postalCode)}
|
||||
helperText={errors.postalCode}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="phone-number_invoice"
|
||||
label={t("phone_number")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="tel"
|
||||
value={formValues.invoicePhoneNumber}
|
||||
onChange={updateField("invoicePhoneNumber")}
|
||||
error={Boolean(errors.invoicePhoneNumber)}
|
||||
helperText={errors.invoicePhoneNumber}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="email_invoice"
|
||||
label={t("email")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="email"
|
||||
value={formValues.invoiceEmail}
|
||||
onChange={updateField("invoiceEmail")}
|
||||
error={Boolean(errors.invoiceEmail)}
|
||||
helperText={errors.invoiceEmail}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack direction="row" spacing={2} flexWrap="wrap" pl={1}>
|
||||
{/* Payment methods - only one must be selected */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={paymentMethod === "cash"}
|
||||
onChange={() =>
|
||||
setPaymentMethod((current) =>
|
||||
current === "cash" ? null : "cash"
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("cash")}
|
||||
sx={{ color: errors.paymentMethod ? "error.main" : undefined }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={paymentMethod === "paypal"}
|
||||
onChange={() =>
|
||||
setPaymentMethod((current) =>
|
||||
current === "paypal" ? null : "paypal"
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("paypal")}
|
||||
sx={{ color: errors.paymentMethod ? "error.main" : undefined }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={paymentMethod === "transfer"}
|
||||
onChange={() =>
|
||||
setPaymentMethod((current) =>
|
||||
current === "transfer" ? null : "transfer"
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
label={t("transfer")}
|
||||
sx={{ color: errors.paymentMethod ? "error.main" : undefined }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
required
|
||||
id="code"
|
||||
label={t("code")}
|
||||
variant="filled"
|
||||
size="small"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={formValues.code}
|
||||
onChange={updateField("code")}
|
||||
error={Boolean(errors.code)}
|
||||
helperText={errors.code}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
sx={{ mt: 1 }}
|
||||
fullWidth
|
||||
disabled={submitting}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
{submitMessage && (
|
||||
<Alert
|
||||
severity={submitMessage.includes("failed") ? "error" : "success"}
|
||||
>
|
||||
{submitMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
Chip,
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
import { submitFormData } from "../utils/sender";
|
||||
import Cookies from "js-cookie";
|
||||
import * as React from "react";
|
||||
|
||||
interface Message {
|
||||
type: "error" | "info" | "success" | "warning";
|
||||
@@ -49,7 +51,7 @@ export const MainForm = () => {
|
||||
// Fetch user data or any other data needed for the form
|
||||
try {
|
||||
const fetchUsers = async () => {
|
||||
const response = await fetch("/backend/default/users");
|
||||
const response = await fetch("http://localhost:8004/default/users");
|
||||
const data = await response.json();
|
||||
setUsers(data.users);
|
||||
};
|
||||
@@ -69,7 +71,7 @@ export const MainForm = () => {
|
||||
setSelectedUser(cookieUser);
|
||||
confirmUser(cookieUser);
|
||||
}
|
||||
}, []);
|
||||
}, [isLoading]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
@@ -78,7 +80,7 @@ export const MainForm = () => {
|
||||
const confirmUser = async (selectedUser: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/backend/default/confirm-user?username=${selectedUser}`
|
||||
`http://localhost:8004/default/confirm-user?username=${selectedUser}`,
|
||||
);
|
||||
const data = await response.json();
|
||||
setNextID(data.nextID);
|
||||
@@ -99,11 +101,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",
|
||||
@@ -117,11 +115,33 @@ export const MainForm = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4">
|
||||
<Box
|
||||
className="bg-gray-100 py-10 px-4"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
className="w-full max-w-md p-6 rounded-lg"
|
||||
sx={{ backgroundColor: "#fff" }}
|
||||
elevation={6}
|
||||
className="w-full rounded-2xl"
|
||||
sx={{
|
||||
backgroundColor: "#fff",
|
||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
||||
width: "100%",
|
||||
maxWidth: {
|
||||
xs: 360, // kompakte Handy-Ansicht
|
||||
sm: 420, // kleine Tablets / große Phones
|
||||
md: 480, // Desktop, bleibt angenehm schmal
|
||||
},
|
||||
padding: {
|
||||
xs: "1.5rem",
|
||||
sm: "2rem",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -153,10 +173,14 @@ export const MainForm = () => {
|
||||
color="primary"
|
||||
sx={{
|
||||
alignSelf: "flex-start",
|
||||
fontWeight: "bold",
|
||||
fontSize: "1rem",
|
||||
py: 2,
|
||||
px: 1,
|
||||
fontWeight: 500,
|
||||
fontSize: "0.9rem",
|
||||
mt: 0.5,
|
||||
mb: 0.5,
|
||||
py: 0.5,
|
||||
px: 1.25,
|
||||
borderRadius: "999px",
|
||||
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -238,7 +262,14 @@ export const MainForm = () => {
|
||||
|
||||
{/* Invoice Fields */}
|
||||
{invoice && (
|
||||
<Box className="flex flex-col gap-3 pt-2 border-t border-gray-200">
|
||||
<Box
|
||||
className="flex flex-col gap-2 pt-3 mt-2"
|
||||
sx={{
|
||||
borderTop: "2px solid",
|
||||
borderColor: "primary.light",
|
||||
borderRadius: "0",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
id="company-name"
|
||||
@@ -321,26 +352,118 @@ export const MainForm = () => {
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Payment Methods */}
|
||||
<Box className="flex justify-center gap-4 pt-2">
|
||||
<FormControlLabel control={<Checkbox />} label={t("cash")} />
|
||||
<FormControlLabel control={<Checkbox />} label={t("paypal")} />
|
||||
<FormControlLabel control={<Checkbox />} label={t("transfer")} />
|
||||
<Box className="flex flex-col gap-2 mt-2">
|
||||
<Typography
|
||||
component="label"
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
color: "text.secondary",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{t("select-payment-method")} *
|
||||
</Typography>
|
||||
<Box className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant={
|
||||
formData.paymentMethod === "bar" ? "contained" : "outlined"
|
||||
}
|
||||
onClick={() =>
|
||||
setFormData({ ...formData, paymentMethod: "bar" })
|
||||
}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: "100px",
|
||||
py: 1.5,
|
||||
borderRadius: "12px",
|
||||
textTransform: "none",
|
||||
fontWeight: formData.paymentMethod === "bar" ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t("cash")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
formData.paymentMethod === "paypal" ? "contained" : "outlined"
|
||||
}
|
||||
onClick={() =>
|
||||
setFormData({ ...formData, paymentMethod: "paypal" })
|
||||
}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: "100px",
|
||||
py: 1.5,
|
||||
borderRadius: "12px",
|
||||
textTransform: "none",
|
||||
fontWeight: formData.paymentMethod === "paypal" ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t("paypal")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
formData.paymentMethod === "andere" ? "contained" : "outlined"
|
||||
}
|
||||
onClick={() =>
|
||||
setFormData({ ...formData, paymentMethod: "andere" })
|
||||
}
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: "100px",
|
||||
py: 1.5,
|
||||
borderRadius: "12px",
|
||||
textTransform: "none",
|
||||
fontWeight: formData.paymentMethod === "andere" ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{t("transfer")}
|
||||
</Button>
|
||||
</Box>
|
||||
{!formData.paymentMethod && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
style={{
|
||||
opacity: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
position: "absolute",
|
||||
}}
|
||||
required
|
||||
value={formData.paymentMethod}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !formData.paymentMethod}
|
||||
fullWidth
|
||||
size="large"
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.5,
|
||||
mt: 3,
|
||||
py: 2,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: "bold",
|
||||
borderRadius: "12px",
|
||||
fontSize: "1rem",
|
||||
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
|
||||
boxShadow: "0 4px 14px 0 rgba(25, 118, 210, 0.39)",
|
||||
"&:hover": {
|
||||
background: "linear-gradient(135deg, #1565c0 0%, #0d47a1 100%)",
|
||||
boxShadow: "0 6px 20px 0 rgba(25, 118, 210, 0.5)",
|
||||
},
|
||||
"&:disabled": {
|
||||
background: "#e0e0e0",
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
210
frontend/src/pages/SuccessPage.tsx
Normal file
210
frontend/src/pages/SuccessPage.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Box, Paper, Typography, Chip, Button } 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<string | null>(null);
|
||||
const [tickets, setNumberOfTickets] = useState<number>(0);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
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);
|
||||
|
||||
setTimeout(() => setAnimate(true), 100);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (seconds === 0) {
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [seconds]);
|
||||
|
||||
return (
|
||||
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4">
|
||||
<Paper
|
||||
elevation={3}
|
||||
className="w-full max-w-md p-8 rounded-lg"
|
||||
sx={{
|
||||
backgroundColor: "#fff",
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Animated Success Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
mb: 3,
|
||||
transition: "all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||
transform: animate ? "scale(1)" : "scale(0)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<CircleCheck size={80} className="text-green-500" strokeWidth={2.5} />
|
||||
</Box>
|
||||
|
||||
{/* Success Message */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
gutterBottom
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: "#2e7d32",
|
||||
mb: 2,
|
||||
transition: "all 0.5s ease-in-out 0.2s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{t("form-submitted-successfully")}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: "#666",
|
||||
mb: 3,
|
||||
transition: "all 0.5s ease-in-out 0.3s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{t("ticket-payment", { count: tickets })}
|
||||
</Typography>
|
||||
|
||||
{/* Tickets Display */}
|
||||
{tickets > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
mb: 2,
|
||||
transition: "all 0.5s ease-in-out 0.35s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`${tickets} ${tickets === 1 ? t("ticket") : t("tickets")}`}
|
||||
color="secondary"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "1rem",
|
||||
py: 2.5,
|
||||
px: 2,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Order ID Display */}
|
||||
{orderId && (
|
||||
<Box
|
||||
sx={{
|
||||
mb: 3,
|
||||
transition: "all 0.5s ease-in-out 0.4s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: "#888",
|
||||
mb: 1,
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{t("entry-id")}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`#${orderId}`}
|
||||
color="primary"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "1.25rem",
|
||||
py: 3,
|
||||
px: 2,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Return button */}
|
||||
<Box
|
||||
sx={{
|
||||
mb: 3,
|
||||
transition: "all 0.5s ease-in-out 0.4s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
href="/"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "1.25rem",
|
||||
py: 3,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
{seconds + " " + t("return-to-homepage")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Additional Info */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 4,
|
||||
pt: 3,
|
||||
borderTop: "1px solid #e0e0e0",
|
||||
transition: "all 0.5s ease-in-out 0.5s",
|
||||
transform: animate ? "translateY(0)" : "translateY(20px)",
|
||||
opacity: animate ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: "#666",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{t("thank-you")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "4px",
|
||||
background: "linear-gradient(90deg, #4caf50 0%, #81c784 100%)",
|
||||
transition: "all 0.8s ease-in-out 0.6s",
|
||||
transform: animate ? "scaleX(1)" : "scaleX(0)",
|
||||
transformOrigin: "left",
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@
|
||||
"street": "Straße + Haus Nr.",
|
||||
"postal-code": "Plz + Stadt",
|
||||
"email": "E-Mail",
|
||||
"submit": "Kaufen",
|
||||
"submit": "Abschicken",
|
||||
"failed-to-load-users": "Das Laden der Benutzer ist fehlgeschlagen.",
|
||||
"user": "Benutzer",
|
||||
"next-id": "Nächste Eintragsnummer: ",
|
||||
@@ -18,5 +18,11 @@
|
||||
"error": "Fehler",
|
||||
"cash": "Bar",
|
||||
"paypal": "PayPal",
|
||||
"transfer": "Überweisung"
|
||||
"transfer": "Andere (notieren)",
|
||||
"ticket-payment_one": "Sie haben erfolgreich {{count}} Los gekauft.",
|
||||
"ticket-payment_other": "Sie haben erfolgreich {{count}} 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.",
|
||||
"select-payment-method": "Zahlungsmethode auswählen",
|
||||
"return-to-homepage": "Zurück"
|
||||
}
|
||||
@@ -2,13 +2,12 @@
|
||||
"first-name": "First Name",
|
||||
"last-name": "Last Name",
|
||||
"phone-number": "Phone Number",
|
||||
"tickets": "Tickets",
|
||||
"invoice": "Invoice",
|
||||
"company-name": "Company Name",
|
||||
"street": "Street + House No.",
|
||||
"postal-code": "Postal Code + City",
|
||||
"email": "Email",
|
||||
"submit": "Buy",
|
||||
"submit": "Submit Form",
|
||||
"failed-to-load-users": "Failed to load users.",
|
||||
"user": "User",
|
||||
"next-id": "Next Entry Number: ",
|
||||
@@ -18,5 +17,13 @@
|
||||
"error": "Error",
|
||||
"cash": "Cash",
|
||||
"paypal": "PayPal",
|
||||
"transfer": "Bank Transfer"
|
||||
"transfer": "Other (note down)",
|
||||
"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.",
|
||||
"select-payment-method": "Select Payment Method",
|
||||
"return-to-homepage": "Return"
|
||||
}
|
||||
@@ -15,14 +15,18 @@ interface FormData {
|
||||
}
|
||||
|
||||
export const submitFormData = async (data: FormData, username: string) => {
|
||||
console.log(data);
|
||||
try {
|
||||
const response = await fetch(`/backend/default/new-entry?username=${username}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`http://localhost:8004/default/new-entry?username=${username}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
Reference in New Issue
Block a user