feat: integrate Tailwind CSS and enhance MainForm component
- Added Tailwind CSS and related packages to the project. - Refactored App.css and index.css to use Tailwind for styling. - Enhanced MainForm component with improved form handling, validation, and layout using Material-UI and Tailwind CSS. - Implemented dynamic error handling and submission feedback. - Created a new Tailwind configuration file.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import mysql from "mysql2";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const pool = mysql
|
||||
.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
})
|
||||
.promise();
|
||||
|
||||
export const getAll25 = async () => {
|
||||
const [rows] = await pool.query("SELECT unique_key FROM users");
|
||||
if (rows.length > 0) {
|
||||
return rows;
|
||||
} else {
|
||||
return "No data found";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import express from "express";
|
||||
import dotenv from "dotenv";
|
||||
const router = express.Router();
|
||||
dotenv.config();
|
||||
dotenv.config();
|
||||
|
||||
import { getAll25 } from "./frontend.data.js";
|
||||
|
||||
router.post("/frontend", (req, res) => {
|
||||
console.log(req.body);
|
||||
res.status(200).json({ message: "Data received successfully" });
|
||||
console.log(getAll25());
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- "8004:8004"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DB_HOST: ca_lose
|
||||
DB_HOST: ca-lose-mysql
|
||||
DB_USER: root
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_NAME: ca_lose
|
||||
|
||||
659
frontend/package-lock.json
generated
659
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,11 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.2.0",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"styled-components": "^6.1.19"
|
||||
"styled-components": "^6.1.19",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"@tailwindcss/vite": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -37,4 +41,4 @@
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@import "tailwindcss";
|
||||
@@ -1,68 +1 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@import "tailwindcss";
|
||||
@@ -1,96 +1,434 @@
|
||||
import { TextField, FormControlLabel, Checkbox, Button } from "@mui/material";
|
||||
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 (
|
||||
<>
|
||||
<form action="" method="post">
|
||||
<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="first-name"
|
||||
label={t("first_name")}
|
||||
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="last-name"
|
||||
label={t("last_name")}
|
||||
variant="filled"
|
||||
/>
|
||||
<TextField required id="email" label={t("email")} variant="filled" />
|
||||
<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}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="tickets"
|
||||
label={t("tickets")}
|
||||
variant="filled"
|
||||
/>
|
||||
<FormControlLabel control={<Checkbox />} label={t("invoice")} />
|
||||
|
||||
<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}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="first-name_invoice"
|
||||
label={t("first_name")}
|
||||
variant="filled"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
id="last-name_invoice"
|
||||
label={t("last_name")}
|
||||
variant="filled"
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Payment methods - only one must be selected */}
|
||||
<FormControlLabel control={<Checkbox />} label={t("cash")} />
|
||||
<FormControlLabel control={<Checkbox />} label={t("paypal")} />
|
||||
<FormControlLabel control={<Checkbox />} label={t("transfer")} />
|
||||
<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" />
|
||||
<Button variant="contained">{t("submit")}</Button>
|
||||
</form>
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
// add other paths if needed
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user