Merge branch 'dev' into prod

This commit is contained in:
2026-01-21 14:07:26 +01:00
9 changed files with 262 additions and 464 deletions

View File

@@ -4,11 +4,13 @@ services:
hostname: lose-verkaufen hostname: lose-verkaufen
build: ./frontend build: ./frontend
networks: networks:
- ca-lose-internal ca-lose-internal:
ipv4_address: 172.25.0.2
restart: unless-stopped restart: unless-stopped
backend: backend:
container_name: ca-lose-backend container_name: ca-lose-backend
hostname: backend
build: ./backend build: ./backend
environment: environment:
NODE_ENV: production NODE_ENV: production
@@ -19,11 +21,13 @@ services:
depends_on: depends_on:
- database - database
networks: networks:
- ca-lose-internal ca-lose-internal:
ipv4_address: 172.25.0.3
restart: unless-stopped restart: unless-stopped
database: database:
container_name: ca-lose-mysql container_name: ca-lose-mysql
hostname: database
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -34,37 +38,63 @@ services:
- ca-lose_mysql:/var/lib/mysql - ca-lose_mysql:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
networks: 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: wireguard:
image: lscr.io/linuxserver/wireguard:latest image: ghcr.io/wg-easy/wg-easy:latest
container_name: ca-lose-wireguard container_name: ca-lose-wireguard
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE #optional - SYS_MODULE
environment: environment:
- PUID=1000 LANG: de
- PGID=1000 WG_HOST: dus3.the1s.de
- TZ=Etc/UTC WG_PORT: "51830"
- SERVERURL=dus3.the1s.de #optional PORT: "51821"
- SERVERPORT=51830 #optional WG_DEFAULT_ADDRESS: 10.14.14.x
- PEERS=2 #optional WG_DEFAULT_DNS: "172.25.0.53"
- PEERDNS=auto #optional WG_ALLOWED_IPS: 172.25.0.0/24
- INTERNAL_SUBNET=10.13.14.0 #optional WG_PERSISTENT_KEEPALIVE: "25"
- ALLOWEDIPS=10.13.14.0/24,172.25.0.0/24 #optional 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"
- PERSISTENTKEEPALIVE_PEERS= #optional 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"
- LOG_CONFS=true #optional
volumes: volumes:
- ./config:/config - wireguard-data:/etc/wireguard
- /lib/modules:/lib/modules #optional - /lib/modules:/lib/modules:ro
ports: ports:
- 51830:51830/udp - "51830:51830/udp"
sysctls: sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1 - net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped restart: unless-stopped
depends_on:
- dnsmasq
- frontend
networks: networks:
ca-lose-internal: ca-lose-internal:
ipv4_address: 172.25.0.10 ipv4_address: 172.25.0.10
proxynet:
ipv4_address: 172.20.0.50
volumes: volumes:
ca-lose_mysql: ca-lose_mysql:
@@ -76,3 +106,6 @@ networks:
ipam: ipam:
config: config:
- subnet: 172.25.0.0/24 - subnet: 172.25.0.0/24
gateway: 172.25.0.1
proxynet:
external: true

View File

@@ -17,7 +17,9 @@
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"i18next": "^25.7.4", "i18next": "^25.7.4",
"js-cookie": "^3.0.5", "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-dom": "^19.2.0",
"react-i18next": "^16.5.3", "react-i18next": "^16.5.3",
"react-router-dom": "^7.11.0", "react-router-dom": "^7.11.0",
@@ -3784,6 +3786,21 @@
"yallist": "^3.0.2" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

View File

@@ -19,7 +19,9 @@
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"i18next": "^25.7.4", "i18next": "^25.7.4",
"js-cookie": "^3.0.5", "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-dom": "^19.2.0",
"react-i18next": "^16.5.3", "react-i18next": "^16.5.3",
"react-router-dom": "^7.11.0", "react-router-dom": "^7.11.0",

View File

@@ -1,12 +1,14 @@
import "./App.css"; import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { MainForm } from "./pages/MainForm"; import { MainForm } from "./pages/MainForm";
import { SuccessPage } from "./pages/SuccessPage";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<MainForm />} /> <Route path="/" element={<MainForm />} />
<Route path="/success" element={<SuccessPage />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
); );

View File

@@ -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>
);
};

View File

@@ -69,7 +69,7 @@ export const MainForm = () => {
setSelectedUser(cookieUser); setSelectedUser(cookieUser);
confirmUser(cookieUser); confirmUser(cookieUser);
} }
}, []); }, [isLoading]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value }); setFormData({ ...formData, [e.target.name]: e.target.value });
@@ -99,11 +99,7 @@ export const MainForm = () => {
try { try {
const result = await submitFormData(formData, selectedUser || ""); const result = await submitFormData(formData, selectedUser || "");
if (result.success) { if (result.success) {
setMsg({ document.location.href = `/success?id=${nextID}&tickets=${formData.tickets}`;
type: "success",
headline: t("success"),
text: t("form-submitted-successfully"),
});
} else { } else {
setMsg({ setMsg({
type: "error", type: "error",

View File

@@ -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<string | null>(null);
const [tickets, setNumberOfTickets] = useState<number>(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 (
<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>
)}
{/* 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>
);
};

View File

@@ -18,5 +18,8 @@
"error": "Fehler", "error": "Fehler",
"cash": "Bar", "cash": "Bar",
"paypal": "PayPal", "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."
} }

View File

@@ -2,7 +2,6 @@
"first-name": "First Name", "first-name": "First Name",
"last-name": "Last Name", "last-name": "Last Name",
"phone-number": "Phone Number", "phone-number": "Phone Number",
"tickets": "Tickets",
"invoice": "Invoice", "invoice": "Invoice",
"company-name": "Company Name", "company-name": "Company Name",
"street": "Street + House No.", "street": "Street + House No.",
@@ -18,5 +17,11 @@
"error": "Error", "error": "Error",
"cash": "Cash", "cash": "Cash",
"paypal": "PayPal", "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."
} }