feat: update MainForm component for improved form handling and validation; add i18next support for localization
This commit is contained in:
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
@@ -15,10 +15,11 @@
|
|||||||
"@mui/material": "^7.3.6",
|
"@mui/material": "^7.3.6",
|
||||||
"@mui/styled-engine-sc": "^7.3.6",
|
"@mui/styled-engine-sc": "^7.3.6",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"i18next": "^25.7.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-i18next": "^16.2.0",
|
"react-i18next": "^16.5.3",
|
||||||
"react-router-dom": "^7.11.0",
|
"react-router-dom": "^7.11.0",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -2031,6 +2033,13 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-cookie": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -3263,7 +3272,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4"
|
"@babel/runtime": "^7.28.4"
|
||||||
},
|
},
|
||||||
@@ -4071,9 +4079,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-i18next": {
|
"node_modules/react-i18next": {
|
||||||
"version": "16.5.2",
|
"version": "16.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.3.tgz",
|
||||||
"integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
|
"integrity": "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
|
|||||||
@@ -16,19 +16,21 @@
|
|||||||
"@mui/icons-material": "^7.3.6",
|
"@mui/icons-material": "^7.3.6",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/material": "^7.3.6",
|
||||||
"@mui/styled-engine-sc": "^7.3.6",
|
"@mui/styled-engine-sc": "^7.3.6",
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"i18next": "^25.7.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-i18next": "^16.2.0",
|
"react-i18next": "^16.5.3",
|
||||||
"react-router-dom": "^7.11.0",
|
"react-router-dom": "^7.11.0",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7"
|
||||||
"@tailwindcss/vite": "^4.1.11"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
434
frontend/src/pages/MainForm copy.tsx
Normal file
434
frontend/src/pages/MainForm copy.tsx
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
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("http://localhost:8004/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
TextField,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -10,425 +8,123 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
const phonePattern = /^[+]?[- 0-9()]{7,}$/;
|
interface Message {
|
||||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
type: "error" | "info" | "success" | "warning";
|
||||||
|
headline: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const MainForm = () => {
|
export const MainForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [invoice, setInvoice] = useState(false);
|
const [invoice, setInvoice] = useState(false);
|
||||||
const [paymentMethod, setPaymentMethod] = useState<
|
const [msg, setMsg] = useState<Message | null>(null);
|
||||||
"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 =
|
const handleSubmit = () => {
|
||||||
(field: keyof typeof formValues) =>
|
// Form submission logic goes here
|
||||||
(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("http://localhost:8004/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 (
|
return (
|
||||||
<Box className="min-h-screen bg-neutral-900 flex justify-center items-start py-10 px-4">
|
<>
|
||||||
<Box
|
<form action="" method="post">
|
||||||
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
|
<TextField
|
||||||
required
|
required
|
||||||
id="first-name"
|
id="first-name"
|
||||||
label={t("first_name")}
|
label={t("first_name")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={formValues.firstName}
|
|
||||||
onChange={updateField("firstName")}
|
|
||||||
error={Boolean(errors.firstName)}
|
|
||||||
helperText={errors.firstName}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="last-name"
|
id="last-name"
|
||||||
label={t("last_name")}
|
label={t("last_name")}
|
||||||
variant="filled"
|
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="email" label={t("email")} variant="filled" />
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="phone-number"
|
id="phone-number"
|
||||||
label={t("phone_number")}
|
label={t("phone_number")}
|
||||||
variant="filled"
|
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
|
<TextField
|
||||||
required
|
required
|
||||||
id="tickets"
|
id="tickets"
|
||||||
label={t("tickets")}
|
label={t("tickets")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
type="number"
|
|
||||||
value={formValues.tickets}
|
|
||||||
onChange={updateField("tickets")}
|
|
||||||
error={Boolean(errors.tickets)}
|
|
||||||
helperText={errors.tickets}
|
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={invoice}
|
checked={invoice}
|
||||||
onChange={(event) => setInvoice(event.target.checked)}
|
onChange={(e) => setInvoice(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={t("invoice")}
|
label={t("invoice")}
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{invoice && (
|
{invoice && (
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="company-name"
|
id="company-name"
|
||||||
label={t("company_name")}
|
label={t("company_name")}
|
||||||
variant="filled"
|
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
|
<TextField
|
||||||
required
|
required
|
||||||
id="first-name_invoice"
|
id="first-name_invoice"
|
||||||
label={t("first_name")}
|
label={t("first_name")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={formValues.invoiceFirstName}
|
|
||||||
onChange={updateField("invoiceFirstName")}
|
|
||||||
error={Boolean(errors.invoiceFirstName)}
|
|
||||||
helperText={errors.invoiceFirstName}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="last-name_invoice"
|
id="last-name_invoice"
|
||||||
label={t("last_name")}
|
label={t("last_name")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={formValues.invoiceLastName}
|
|
||||||
onChange={updateField("invoiceLastName")}
|
|
||||||
error={Boolean(errors.invoiceLastName)}
|
|
||||||
helperText={errors.invoiceLastName}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="street"
|
id="street"
|
||||||
label={t("street")}
|
label={t("street")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={formValues.street}
|
|
||||||
onChange={updateField("street")}
|
|
||||||
error={Boolean(errors.street)}
|
|
||||||
helperText={errors.street}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="postal-code"
|
id="postal-code"
|
||||||
label={t("postal_code")}
|
label={t("postal_code")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
value={formValues.postalCode}
|
|
||||||
onChange={updateField("postalCode")}
|
|
||||||
error={Boolean(errors.postalCode)}
|
|
||||||
helperText={errors.postalCode}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="phone-number_invoice"
|
id="phone-number_invoice"
|
||||||
label={t("phone_number")}
|
label={t("phone_number")}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
margin="dense"
|
|
||||||
type="tel"
|
|
||||||
value={formValues.invoicePhoneNumber}
|
|
||||||
onChange={updateField("invoicePhoneNumber")}
|
|
||||||
error={Boolean(errors.invoicePhoneNumber)}
|
|
||||||
helperText={errors.invoicePhoneNumber}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
id="email_invoice"
|
id="email_invoice"
|
||||||
label={t("email")}
|
label={t("email")}
|
||||||
variant="filled"
|
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 */}
|
{/* Payment methods - only one must be selected */}
|
||||||
<FormControlLabel
|
<FormControlLabel control={<Checkbox />} label={t("cash")} />
|
||||||
control={
|
<FormControlLabel control={<Checkbox />} label={t("paypal")} />
|
||||||
<Checkbox
|
<FormControlLabel control={<Checkbox />} label={t("transfer")} />
|
||||||
checked={paymentMethod === "cash"}
|
<TextField required id="code" label={t("code")} variant="filled" />
|
||||||
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
|
<Button
|
||||||
type="submit"
|
onClick={() => {
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="large"
|
|
||||||
sx={{ mt: 1 }}
|
|
||||||
fullWidth
|
|
||||||
disabled={submitting}
|
|
||||||
>
|
>
|
||||||
{t("submit")}
|
{t("submit")}
|
||||||
</Button>
|
</Button>
|
||||||
{submitMessage && (
|
{msg && (
|
||||||
<Alert
|
<Alert severity={msg.type}>
|
||||||
severity={submitMessage.includes("failed") ? "error" : "success"}
|
{msg.headline}: {msg.text}
|
||||||
>
|
|
||||||
{submitMessage}
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</form>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"phone-number": "Telefonnummer"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"phone_number": "Telefonnummer"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user