Compare commits
12 Commits
9af4ddaf18
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| a46cb3b9e7 | |||
| d8e8062990 | |||
| 528ed8ac40 | |||
| 470fe75d86 | |||
| cfd3a77472 | |||
| d49e62a280 | |||
| a66b150d97 | |||
| f7a0a3753c | |||
| d308f80341 | |||
| 636730d4b2 | |||
| 968e1e7727 | |||
| c79f81ec7d |
@@ -158,6 +158,7 @@ keys/
|
|||||||
# Own files
|
# Own files
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
PayPal-QR-Code.png
|
PayPal-QR-Code.png
|
||||||
|
.docker/volumes
|
||||||
|
|
||||||
# only in development branch
|
# only in development branch
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
@@ -20,7 +20,7 @@ Ticket intake and validation app with a React frontend and an Express + MySQL ba
|
|||||||
|
|
||||||
### Production Tech Stack
|
### Production Tech Stack
|
||||||
|
|
||||||
> **Note**: For production, check the [prod branch](prod) which contains an VPN git submodule of wg-easy to run the app securely on a private network. It also contains a dnsmasq container to resolve the backend service name from the frontend container. The main branch is meant for local development and testing, so it doesn't include those components to keep things simple.
|
> **Note** For production, check the [prod branch](https://git.the1s.de/theis.gaedigk/ca-lose/src/branch/prod/) which contains an VPN git submodule of wg-easy to run the app securely on a private network. It also contains a dnsmasq container to resolve the backend service name from the frontend container. The main branch is meant for local development and testing, so it doesn't include those components to keep things simple.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -11,14 +11,16 @@ const pool = mysql
|
|||||||
})
|
})
|
||||||
.promise();
|
.promise();
|
||||||
|
|
||||||
export const getUser = async () => {
|
export const getInfo = async () => {
|
||||||
const [rows] = await pool.query("SELECT username FROM users");
|
const [rows] = await pool.query("SELECT username FROM users;");
|
||||||
|
const [rows2] = await pool.query("SELECT name FROM prize_draws;");
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0 && rows2.length > 0) {
|
||||||
const users = rows.map((r) => r.username);
|
const users = rows.map((r) => r.username);
|
||||||
return { users };
|
const prize_draws = rows2.map((n) => n.name);
|
||||||
|
return { users, prize_draws };
|
||||||
} else {
|
} else {
|
||||||
return { users: [], message: "No data found" };
|
return { message: "No data found" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,11 +45,13 @@ export const confirmUser = async (username) => {
|
|||||||
const [createTable] = await pool.query(
|
const [createTable] = await pool.query(
|
||||||
`CREATE TABLE IF NOT EXISTS ?? (
|
`CREATE TABLE IF NOT EXISTS ?? (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Verlosung VARCHAR(100) NOT NULL,
|
||||||
Vorname VARCHAR(100) NOT NULL,
|
Vorname VARCHAR(100) NOT NULL,
|
||||||
Nachname Varchar(100) NOT NULL,
|
Nachname Varchar(100) NOT NULL,
|
||||||
EMail Varchar(100) NOT NULL,
|
EMail Varchar(100) NOT NULL,
|
||||||
Telefonnummer Varchar(100) NOT NULL,
|
Telefonnummer Varchar(100) NOT NULL,
|
||||||
Lose INT NOT NULL,
|
Lose INT NOT NULL,
|
||||||
|
Schokolade BOOLEAN NOT NULL,
|
||||||
Firmenname Varchar(100),
|
Firmenname Varchar(100),
|
||||||
Vorname_Geschaeftlich Varchar(100),
|
Vorname_Geschaeftlich Varchar(100),
|
||||||
Nachname_Geschaeftlich Varchar(100),
|
Nachname_Geschaeftlich Varchar(100),
|
||||||
@@ -80,7 +84,7 @@ export const confirmUser = async (username) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newEntry = async (formData, username) => {
|
export const newEntry = async (formData, username, prizeDraw) => {
|
||||||
const confirmation = await confirmUser(username);
|
const confirmation = await confirmUser(username);
|
||||||
|
|
||||||
if (!confirmation || !confirmation.success) {
|
if (!confirmation || !confirmation.success) {
|
||||||
@@ -90,14 +94,16 @@ export const newEntry = async (formData, username) => {
|
|||||||
const tableName = confirmation.tableName;
|
const tableName = confirmation.tableName;
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO ?? (Vorname, Nachname, EMail, Telefonnummer, Lose, Firmenname, Vorname_Geschaeftlich, Nachname_Geschaeftlich, EMail_Geschaeftlich, Telefonnummer_Geschaeftlich, Strasse_Hausnr, Plz_Ort, Zahlungsmethode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO ?? (Verlosung, Vorname, Nachname, EMail, Telefonnummer, Lose, Schokolade, Firmenname, Vorname_Geschaeftlich, Nachname_Geschaeftlich, EMail_Geschaeftlich, Telefonnummer_Geschaeftlich, Strasse_Hausnr, Plz_Ort, Zahlungsmethode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
tableName,
|
tableName,
|
||||||
|
prizeDraw,
|
||||||
formData.firstName,
|
formData.firstName,
|
||||||
formData.lastName,
|
formData.lastName,
|
||||||
formData.email,
|
formData.email,
|
||||||
formData.phoneNumber,
|
formData.phoneNumber,
|
||||||
formData.tickets,
|
formData.tickets,
|
||||||
|
formData.chocolates,
|
||||||
formData.companyName,
|
formData.companyName,
|
||||||
formData.cmpFirstName,
|
formData.cmpFirstName,
|
||||||
formData.cpmLastName,
|
formData.cpmLastName,
|
||||||
|
|||||||
@@ -3,24 +3,33 @@ import dotenv from "dotenv";
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import { getUser, newEntry, confirmUser } from "./frontend.data.js";
|
import { getInfo, newEntry, confirmUser } from "./frontend.data.js";
|
||||||
|
|
||||||
router.post("/new-entry", async (req, res) => {
|
router.post("/new-entry", async (req, res) => {
|
||||||
const username = req.query.username;
|
const username = req.query.username;
|
||||||
const result = await newEntry(req.body, username);
|
const draw = req.query.draw;
|
||||||
|
|
||||||
|
const result = await newEntry(req.body, username, draw);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(500).json({ message: "Form Data Invalid" });
|
return res.status(500).json({ message: "Form Data Invalid" });
|
||||||
}
|
}
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/users", async (req, res) => {
|
router.get("/info", async (req, res) => {
|
||||||
const users = await getUser();
|
const info = await getInfo();
|
||||||
res.json(users);
|
|
||||||
|
if (!info) {
|
||||||
|
return res.status(500).json({ message: "Server error" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(info);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/confirm-user", async (req, res) => {
|
router.get("/confirm-user", async (req, res) => {
|
||||||
const username = req.query.username;
|
const username = req.query.username;
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
return res.status(400).json({ message: "Username is required" });
|
return res.status(400).json({ message: "Username is required" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
USE ca_lose;
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
username VARCHAR(100) NOT NULL UNIQUE,
|
username VARCHAR(100) NOT NULL UNIQUE,
|
||||||
@@ -6,9 +8,16 @@ CREATE TABLE users (
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE prize_draws (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
/* This scheme does not have to be implemented manually. It always will be generated by the backend */
|
/* This scheme does not have to be implemented manually. It always will be generated by the backend */
|
||||||
CREATE TABLE xx_DD_MM_YYYY (
|
CREATE TABLE xx_DD_MM_YYYY (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Verlosung VARCHAR(100) NOT NULL,
|
||||||
Vorname VARCHAR(100) NOT NULL,
|
Vorname VARCHAR(100) NOT NULL,
|
||||||
Nachname Varchar(100) NOT NULL,
|
Nachname Varchar(100) NOT NULL,
|
||||||
EMail Varchar(100) NOT NULL,
|
EMail Varchar(100) NOT NULL,
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
interface SelectUserModalProps {
|
interface SelectUserModalProps {
|
||||||
showSelectUser: boolean;
|
showSelectUser: boolean;
|
||||||
setShowSelectUser: (value: boolean) => void;
|
setShowSelectUser: (value: boolean) => void;
|
||||||
usernameData: { users: string[] };
|
info: { users: string[]; prize_draws: string[] };
|
||||||
usernameDataIsLoading: boolean;
|
infoIsLoading: boolean;
|
||||||
selectedUser: string | null;
|
selectedUser: string | null;
|
||||||
|
selectedDraw: string | null;
|
||||||
handleUserSelection: (value: string | null) => void;
|
handleUserSelection: (value: string | null) => void;
|
||||||
|
handleDrawSelection: (value: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectUserModal = (props: SelectUserModalProps) => {
|
export const SelectUserModal = (props: SelectUserModalProps) => {
|
||||||
@@ -25,8 +27,8 @@ export const SelectUserModal = (props: SelectUserModalProps) => {
|
|||||||
<Typography>{t("user")}</Typography>
|
<Typography>{t("user")}</Typography>
|
||||||
{/* User selection */}
|
{/* User selection */}
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={props.usernameData?.users ?? []}
|
options={props.info?.users ?? []}
|
||||||
loading={props.usernameDataIsLoading}
|
loading={props.infoIsLoading}
|
||||||
loadingText={t("loading")}
|
loadingText={t("loading")}
|
||||||
value={props.selectedUser}
|
value={props.selectedUser}
|
||||||
onChange={(_, value) => props.handleUserSelection(value)}
|
onChange={(_, value) => props.handleUserSelection(value)}
|
||||||
@@ -34,6 +36,17 @@ export const SelectUserModal = (props: SelectUserModalProps) => {
|
|||||||
variant="soft"
|
variant="soft"
|
||||||
sx={{ borderRadius: "10px" }}
|
sx={{ borderRadius: "10px" }}
|
||||||
/>
|
/>
|
||||||
|
{/* Prize selection */}
|
||||||
|
<Autocomplete
|
||||||
|
options={props.info?.prize_draws ?? []}
|
||||||
|
loading={props.infoIsLoading}
|
||||||
|
loadingText={t("loading")}
|
||||||
|
value={props.selectedDraw}
|
||||||
|
onChange={(_, value) => props.handleDrawSelection(value)}
|
||||||
|
placeholder={t("prize_draws")}
|
||||||
|
variant="soft"
|
||||||
|
sx={{ borderRadius: "10px" }}
|
||||||
|
/>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const createFormSchema = (
|
|||||||
email: z.email(t("email-error")),
|
email: z.email(t("email-error")),
|
||||||
phoneNumber: z.string(t("phone-error")).refine(validator.isMobilePhone),
|
phoneNumber: z.string(t("phone-error")).refine(validator.isMobilePhone),
|
||||||
tickets: z.number(t("ticket-error")).min(1),
|
tickets: z.number(t("ticket-error")).min(1),
|
||||||
|
chocolates: z.boolean(t("chocolates-error")),
|
||||||
companyName: invoice
|
companyName: invoice
|
||||||
? z.string().min(1, t("name-error"))
|
? z.string().min(1, t("name-error"))
|
||||||
: z.string().optional(),
|
: z.string().optional(),
|
||||||
|
|||||||
@@ -21,51 +21,31 @@ import PersonIcon from "@mui/icons-material/Person";
|
|||||||
import QrCodeIcon from "@mui/icons-material/QrCode";
|
import QrCodeIcon from "@mui/icons-material/QrCode";
|
||||||
import TranslateIcon from "@mui/icons-material/Translate";
|
import TranslateIcon from "@mui/icons-material/Translate";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { confirmUser, fetchUsers } from "../utils/api/users";
|
import { confirmUser, fetchInfo } from "../utils/api/users";
|
||||||
import { QRcodeModal } from "../components/modals/QR-CodeModal";
|
import { QRcodeModal } from "../components/modals/QR-CodeModal";
|
||||||
import { SelectUserModal } from "../components/modals/SelectUserModal";
|
import { SelectUserModal } from "../components/modals/SelectUserModal";
|
||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { changeTranslation } from "../utils/uxFncs";
|
import { changeTranslation } from "../utils/uxFncs";
|
||||||
import { createFormSchema } from "../config/interfaces.config";
|
import { createFormSchema } from "../config/interfaces.config";
|
||||||
import type { ZodObject, ZodRawShape } from "zod";
|
import { validateFieldWithZod } from "../utils/uxFncs";
|
||||||
import { TextField } from "../components/TextField";
|
import { TextField } from "../components/TextField";
|
||||||
|
import { PAYMENT_METHODS, PAYMENT_LABELS } from "../utils/uxFncs";
|
||||||
const PAYMENT_METHODS = ["bar", "paypal", "andere"] as const;
|
|
||||||
const PAYMENT_LABELS: Record<string, string> = {
|
|
||||||
bar: "Cash",
|
|
||||||
paypal: "PayPal",
|
|
||||||
andere: "Transfer",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a single field against the full Zod schema.
|
|
||||||
* Returns the first error message for that field, or undefined if valid.
|
|
||||||
*/
|
|
||||||
function validateFieldWithZod(
|
|
||||||
schema: ZodObject<ZodRawShape>,
|
|
||||||
fieldName: string,
|
|
||||||
allValues: Record<string, unknown>,
|
|
||||||
): string | undefined {
|
|
||||||
const result = schema.safeParse(allValues);
|
|
||||||
if (result.success) return undefined;
|
|
||||||
const issue = result.error.issues.find(
|
|
||||||
(i) => i.path.length === 1 && i.path[0] === fieldName,
|
|
||||||
);
|
|
||||||
return issue?.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MainForm = () => {
|
export const MainForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// States
|
||||||
const [invoice, setInvoice] = useState(false);
|
const [invoice, setInvoice] = useState(false);
|
||||||
const [msg, setMsg] = useState<Message | null>(null);
|
const [msg, setMsg] = useState<Message | null>(null);
|
||||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||||
|
const [selectedDraw, setSelectedDraw] = useState<string | null>(null);
|
||||||
const [showSelectUser, setShowSelectUser] = useState(false);
|
const [showSelectUser, setShowSelectUser] = useState(false);
|
||||||
const [QRmodal, setQRmodal] = useState(false);
|
const [QRmodal, setQRmodal] = useState(false);
|
||||||
|
|
||||||
const formSchema = createFormSchema(t, invoice);
|
const formSchema = createFormSchema(t, invoice);
|
||||||
|
|
||||||
|
// Validates the fields and makes sure that the form is maching the zod schema
|
||||||
const makeFieldValidator = (fieldName: string) => ({
|
const makeFieldValidator = (fieldName: string) => ({
|
||||||
onSubmit: ({
|
onSubmit: ({
|
||||||
fieldApi,
|
fieldApi,
|
||||||
@@ -85,13 +65,15 @@ export const MainForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Creates a form with tanstack form
|
||||||
const { Field, Subscribe, handleSubmit, setFieldValue } = useForm({
|
const { Field, Subscribe, handleSubmit, setFieldValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
tickets: 1,
|
tickets: 0,
|
||||||
|
chocolates: false,
|
||||||
companyName: "",
|
companyName: "",
|
||||||
cmpFirstName: "",
|
cmpFirstName: "",
|
||||||
cpmLastName: "",
|
cpmLastName: "",
|
||||||
@@ -108,6 +90,7 @@ export const MainForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This function returns the errors for one field
|
||||||
const getErrors = (field: {
|
const getErrors = (field: {
|
||||||
state: { meta: { errorMap: Record<string, unknown> } };
|
state: { meta: { errorMap: Record<string, unknown> } };
|
||||||
}) => {
|
}) => {
|
||||||
@@ -122,8 +105,14 @@ export const MainForm = () => {
|
|||||||
return [...blurErrors, ...submitErrors].filter(Boolean);
|
return [...blurErrors, ...submitErrors].filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// useEffect that only executes on mount and is checking if there is a user selcted in the cookies
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedUser = Cookies.get("selectedUser");
|
const savedUser = Cookies.get("selectedUser");
|
||||||
|
const savedDraw = Cookies.get("selectedDraw");
|
||||||
|
if (savedDraw) {
|
||||||
|
setSelectedDraw(savedDraw);
|
||||||
|
}
|
||||||
|
|
||||||
if (savedUser) {
|
if (savedUser) {
|
||||||
setSelectedUser(savedUser);
|
setSelectedUser(savedUser);
|
||||||
} else {
|
} else {
|
||||||
@@ -135,17 +124,20 @@ export const MainForm = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: usernameData, isLoading: usernameDataIsLoading } = useQuery({
|
// Fetch users tanstack query
|
||||||
queryKey: ["users"],
|
const { data: info, isLoading: infoIsLoading } = useQuery({
|
||||||
queryFn: fetchUsers,
|
queryKey: ["info"],
|
||||||
|
queryFn: fetchInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Query for validating selcted user
|
||||||
const { data: userData } = useQuery({
|
const { data: userData } = useQuery({
|
||||||
queryKey: ["user", selectedUser],
|
queryKey: ["user", selectedUser],
|
||||||
enabled: !!selectedUser,
|
enabled: !!selectedUser,
|
||||||
queryFn: () => confirmUser(selectedUser),
|
queryFn: () => confirmUser(selectedUser),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tanstack query (mutate) for sending the form
|
||||||
const { mutate: mutateForm, isPending: mutateFormIsPending } = useMutation({
|
const { mutate: mutateForm, isPending: mutateFormIsPending } = useMutation({
|
||||||
mutationFn: (values: FormData) => submitFormData(values, selectedUser),
|
mutationFn: (values: FormData) => submitFormData(values, selectedUser),
|
||||||
onSuccess: (_, values) => {
|
onSuccess: (_, values) => {
|
||||||
@@ -164,6 +156,7 @@ export const MainForm = () => {
|
|||||||
|
|
||||||
const nextID = userData?.nextID ?? "N/A";
|
const nextID = userData?.nextID ?? "N/A";
|
||||||
|
|
||||||
|
// function for selecting user
|
||||||
const handleUserSelection = (username: string | null) => {
|
const handleUserSelection = (username: string | null) => {
|
||||||
if (username == null || username == "") {
|
if (username == null || username == "") {
|
||||||
return;
|
return;
|
||||||
@@ -171,15 +164,26 @@ export const MainForm = () => {
|
|||||||
setSelectedUser(username);
|
setSelectedUser(username);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// function for selecting draw
|
||||||
|
const handleDrawSelection = (draw: string | null) => {
|
||||||
|
if (draw == null || draw == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedDraw(draw);
|
||||||
|
Cookies.set("selectedDraw", draw);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SelectUserModal
|
<SelectUserModal
|
||||||
showSelectUser={showSelectUser}
|
showSelectUser={showSelectUser}
|
||||||
setShowSelectUser={setShowSelectUser}
|
setShowSelectUser={setShowSelectUser}
|
||||||
usernameData={usernameData}
|
info={info}
|
||||||
usernameDataIsLoading={usernameDataIsLoading}
|
infoIsLoading={infoIsLoading}
|
||||||
selectedUser={selectedUser}
|
selectedUser={selectedUser}
|
||||||
|
selectedDraw={selectedDraw}
|
||||||
handleUserSelection={handleUserSelection}
|
handleUserSelection={handleUserSelection}
|
||||||
|
handleDrawSelection={handleDrawSelection}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QRcodeModal setQRmodal={setQRmodal} QRmodal={QRmodal} />
|
<QRcodeModal setQRmodal={setQRmodal} QRmodal={QRmodal} />
|
||||||
@@ -313,7 +317,7 @@ export const MainForm = () => {
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{/* Tickets + Invoice toggle */}
|
{/* Tickets + Invoice + Chocolate toggle */}
|
||||||
<div className="grid grid-cols-2 gap-3 items-end">
|
<div className="grid grid-cols-2 gap-3 items-end">
|
||||||
<FormControl required>
|
<FormControl required>
|
||||||
<FormLabel>{t("tickets")}</FormLabel>
|
<FormLabel>{t("tickets")}</FormLabel>
|
||||||
@@ -327,12 +331,14 @@ export const MainForm = () => {
|
|||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={field.state.value ?? ""}
|
value={
|
||||||
onBlur={field.handleBlur}
|
field.state.value === 0 ? "" : field.state.value
|
||||||
onChange={(e) =>
|
|
||||||
field.handleChange(Number(e.target.value))
|
|
||||||
}
|
}
|
||||||
slotProps={{ input: { min: 1 } }}
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
field.handleChange(val === "" ? 0 : Number(val));
|
||||||
|
}}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
sx={{ borderRadius: "10px" }}
|
sx={{ borderRadius: "10px" }}
|
||||||
/>
|
/>
|
||||||
@@ -346,7 +352,7 @@ export const MainForm = () => {
|
|||||||
}}
|
}}
|
||||||
</Field>
|
</Field>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="flex items-center pb-2">
|
<div className="flex flex-col gap-2 pb-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={invoice}
|
checked={invoice}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -365,6 +371,16 @@ export const MainForm = () => {
|
|||||||
label={t("invoice")}
|
label={t("invoice")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
<Field name="chocolates">
|
||||||
|
{(field) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={!!field.state.value}
|
||||||
|
onChange={(e) => field.handleChange(e.target.checked)}
|
||||||
|
label={t("chocolates")}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { API_BASE } from "../../config/api.config";
|
import { API_BASE } from "../../config/api.config";
|
||||||
import type { FormData } from "../../config/interfaces.config";
|
import type { FormData } from "../../config/interfaces.config";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
export const submitFormData = async (
|
export const submitFormData = async (
|
||||||
data: FormData,
|
data: FormData,
|
||||||
@@ -7,10 +8,16 @@ export const submitFormData = async (
|
|||||||
) => {
|
) => {
|
||||||
console.warn("submitFormData is fetching!");
|
console.warn("submitFormData is fetching!");
|
||||||
|
|
||||||
|
const draw = Cookies.get("selectedDraw");
|
||||||
|
|
||||||
|
if (!draw) {
|
||||||
|
throw new Error("You need to set an prize draw!");
|
||||||
|
}
|
||||||
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds
|
// await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/default/new-entry?username=${username}`,
|
`${API_BASE}/default/new-entry?username=${username}&draw=${draw}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { API_BASE } from "../../config/api.config";
|
import { API_BASE } from "../../config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
export const fetchUsers = async () => {
|
export const fetchInfo = async () => {
|
||||||
console.warn("fetchUsers is fetching!");
|
console.warn("fetchInfo is fetching!");
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/default/users`);
|
const response = await fetch(`${API_BASE}/default/info`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -14,11 +14,12 @@
|
|||||||
"user": "Benutzer",
|
"user": "Benutzer",
|
||||||
"next-id": "Nächste Eintragsnummer: ",
|
"next-id": "Nächste Eintragsnummer: ",
|
||||||
"form-submitted-successfully": "Formular erfolgreich übermittelt!",
|
"form-submitted-successfully": "Formular erfolgreich übermittelt!",
|
||||||
"form-submission-failed": "Formularübermittlung fehlgeschlagen.",
|
"form-submission-failed": "Formularübermittlung fehlgeschlagen. Haben Sie eine Verlosung ausgewählt?",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"cash": "Bar",
|
"cash": "Bar",
|
||||||
"paypal": "PayPal",
|
"paypal": "PayPal",
|
||||||
|
"prize_draws": "Verlosungen",
|
||||||
"transfer": "Andere (notieren)",
|
"transfer": "Andere (notieren)",
|
||||||
"ticket-payment_one": "Sie haben erfolgreich {{count}} Los gekauft.",
|
"ticket-payment_one": "Sie haben erfolgreich {{count}} Los gekauft.",
|
||||||
"ticket-payment_other": "Sie haben erfolgreich {{count}} Lose gekauft.",
|
"ticket-payment_other": "Sie haben erfolgreich {{count}} Lose gekauft.",
|
||||||
@@ -34,5 +35,7 @@
|
|||||||
"name-error": "Sie müssen einen Namen eingeben!",
|
"name-error": "Sie müssen einen Namen eingeben!",
|
||||||
"email-error": "Sie müssen eine gültige E-Mail Adresse eingeben!",
|
"email-error": "Sie müssen eine gültige E-Mail Adresse eingeben!",
|
||||||
"phone-error": "Sie müssen eine gültige Telefonnummer eingeben!",
|
"phone-error": "Sie müssen eine gültige Telefonnummer eingeben!",
|
||||||
"footer-headline": "Dieses System wurde vollständig konzipiert und entwickelt von Theis Gaedigk. - Portfolio: "
|
"footer-headline": "Dieses System wurde vollständig konzipiert und entwickelt von Theis Gaedigk. - Portfolio: ",
|
||||||
|
"chocolates": "Schokoladen",
|
||||||
|
"chocolates-error": "Dieses Feld kann nur wahr oder falsch sein."
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"user": "User",
|
"user": "User",
|
||||||
"next-id": "Next Entry Number: ",
|
"next-id": "Next Entry Number: ",
|
||||||
"form-submitted-successfully": "Form submitted successfully!",
|
"form-submitted-successfully": "Form submitted successfully!",
|
||||||
"orm-submission-failed": "Form submission failed.",
|
"form-submission-failed": "Form submission failed. Have you set an prize draw?",
|
||||||
|
"prize_draws": "Prize draws",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"cash": "Cash",
|
"cash": "Cash",
|
||||||
@@ -35,5 +36,7 @@
|
|||||||
"name-error": "You have to enter a name!",
|
"name-error": "You have to enter a name!",
|
||||||
"email-error": "You have to enter a valid E-Mail adress!",
|
"email-error": "You have to enter a valid E-Mail adress!",
|
||||||
"phone-error": "You have to enter a vaild phone number!",
|
"phone-error": "You have to enter a vaild phone number!",
|
||||||
"footer-headline": "This system was fully designed and developed by Theis Gaedigk. - Portfolio: "
|
"footer-headline": "This system was fully designed and developed by Theis Gaedigk. - Portfolio: ",
|
||||||
|
"chocolates": "Chocolates",
|
||||||
|
"chocolates-error": "This field can only be true or false."
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import i18n from "./i18n";
|
import i18n from "./i18n";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { ZodObject } from "zod";
|
||||||
|
import type { ZodRawShape } from "zod";
|
||||||
|
|
||||||
export const changeTranslation = () => {
|
export const changeTranslation = () => {
|
||||||
const clientLng = i18n.language;
|
const clientLng = i18n.language;
|
||||||
@@ -14,3 +16,27 @@ export const changeTranslation = () => {
|
|||||||
alert("Cannot change language.");
|
alert("Cannot change language.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a single field against the full Zod schema.
|
||||||
|
* Returns the first error message for that field, or undefined if valid.
|
||||||
|
*/
|
||||||
|
export function validateFieldWithZod(
|
||||||
|
schema: ZodObject<ZodRawShape>,
|
||||||
|
fieldName: string,
|
||||||
|
allValues: Record<string, unknown>,
|
||||||
|
): string | undefined {
|
||||||
|
const result = schema.safeParse(allValues);
|
||||||
|
if (result.success) return undefined;
|
||||||
|
const issue = result.error.issues.find(
|
||||||
|
(i) => i.path.length === 1 && i.path[0] === fieldName,
|
||||||
|
);
|
||||||
|
return issue?.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAYMENT_METHODS = ["bar", "paypal", "andere"] as const;
|
||||||
|
export const PAYMENT_LABELS: Record<string, string> = {
|
||||||
|
bar: "Cash",
|
||||||
|
paypal: "PayPal",
|
||||||
|
andere: "Transfer",
|
||||||
|
};
|
||||||
+1
-1
Submodule wg-easy-ca-lose updated: 9581e6eacb...8c70c24205
Reference in New Issue
Block a user