feat: add Fight and Exhibition registration forms with input validation and file upload functionality
This commit is contained in:
Generated
+7
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"swiper": "^11.2.10"
|
"swiper": "^11.2.10"
|
||||||
@@ -4995,6 +4996,12 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pocketbase": {
|
||||||
|
"version": "0.26.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.2.tgz",
|
||||||
|
"integrity": "sha512-WA8EOBc3QnSJh8rJ3iYoi9DmmPOMFIgVfAmIGux7wwruUEIzXgvrO4u0W2htfQjGIcyezJkdZOy5Xmh7SxAftw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"swiper": "^11.2.10"
|
"swiper": "^11.2.10"
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
image: string;
|
image: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
link: string;
|
link: string;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventCard(props: EventCardProps) {
|
function EventCard(props: EventCardProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
router.push(props.link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={props.id}
|
id={props.id}
|
||||||
@@ -35,7 +45,12 @@ function EventCard(props: EventCardProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center md:justify-end w-full md:w-1/3">
|
<div className="flex justify-center md:justify-end w-full md:w-1/3">
|
||||||
<Button variant="blue" shadowEnabled={false}>
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={props.disabled}
|
||||||
|
variant="blue"
|
||||||
|
shadowEnabled={false}
|
||||||
|
>
|
||||||
регистрация
|
регистрация
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,6 +92,7 @@ function Events() {
|
|||||||
|
|
||||||
<div className="flex flex-col space-y-4 md:space-y-7 mt-16 md:mt-36">
|
<div className="flex flex-col space-y-4 md:space-y-7 mt-16 md:mt-36">
|
||||||
<EventCard
|
<EventCard
|
||||||
|
disabled={true}
|
||||||
id="yuka"
|
id="yuka"
|
||||||
image="/events/yuka.png"
|
image="/events/yuka.png"
|
||||||
title="YUKA Drive Fest Джимхана"
|
title="YUKA Drive Fest Джимхана"
|
||||||
@@ -84,13 +100,15 @@ function Events() {
|
|||||||
link="#"
|
link="#"
|
||||||
/>
|
/>
|
||||||
<EventCard
|
<EventCard
|
||||||
|
disabled={false}
|
||||||
id="moscow_fight"
|
id="moscow_fight"
|
||||||
image="/events/moscow_fight.png"
|
image="/events/moscow_fight.png"
|
||||||
title="Дрифт«Битва за Москву»"
|
title="Дрифт«Битва за Москву»"
|
||||||
description="Любительский турнир по дрифту, который вырос из проекта «Дорога в дрифт», созданного в 2021 году для поиска новых талантов. За три года он превратился в полноценные соревнования с привлекательным призовым фондом. Во второй день фестиваля, 8 сентября, пройдет дрифт-гонка, где главным призом станет электромобиль «Москвич». Соревнования проводятся по традиционной олимпийской системе. Чтобы принять участие, необходимо подать заявку на сайте и дождаться приглашения от организаторов."
|
description="Любительский турнир по дрифту, который вырос из проекта «Дорога в дрифт», созданного в 2021 году для поиска новых талантов. За три года он превратился в полноценные соревнования с привлекательным призовым фондом. Во второй день фестиваля, 8 сентября, пройдет дрифт-гонка, где главным призом станет электромобиль «Москвич». Соревнования проводятся по традиционной олимпийской системе. Чтобы принять участие, необходимо подать заявку на сайте и дождаться приглашения от организаторов."
|
||||||
link="#"
|
link="/forms/fight"
|
||||||
/>
|
/>
|
||||||
<EventCard
|
<EventCard
|
||||||
|
disabled={true}
|
||||||
id="moto"
|
id="moto"
|
||||||
image="/events/moto.png"
|
image="/events/moto.png"
|
||||||
title="КуБок ШОС по Мотокроссу"
|
title="КуБок ШОС по Мотокроссу"
|
||||||
@@ -98,6 +116,7 @@ function Events() {
|
|||||||
link="#"
|
link="#"
|
||||||
/>
|
/>
|
||||||
<EventCard
|
<EventCard
|
||||||
|
disabled={true}
|
||||||
image="/events/cart.png"
|
image="/events/cart.png"
|
||||||
title="Кубок по Фиджитал картингу"
|
title="Кубок по Фиджитал картингу"
|
||||||
description="На Фестивале технических видов спорта 2025 впервые состоится Кубок по Фиджитал Картингу! Это уникальное состязание, где виртуальная реальность встречается с реальной трассой. Участники будут сражаться на симуляторах, а затем переносить свои навыки на настоящий картинг, демонстрируя невероятную адаптивность и мастерство."
|
description="На Фестивале технических видов спорта 2025 впервые состоится Кубок по Фиджитал Картингу! Это уникальное состязание, где виртуальная реальность встречается с реальной трассой. Участники будут сражаться на симуляторах, а затем переносить свои навыки на настоящий картинг, демонстрируя невероятную адаптивность и мастерство."
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ function Fileupload({
|
|||||||
className="ml-2 p-1 text-gray-400 hover:text-gray-600"
|
className="ml-2 p-1 text-gray-400 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-3 h-3"
|
className="w-4 h-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -281,7 +281,7 @@ function Fileupload({
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
d="M6 18L18 6M6 6l12 12"
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { gothampro } from "@/utils/fonts";
|
|||||||
interface InputProps {
|
interface InputProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
type?: "text" | "email" | "password" | "number" | "tel";
|
type?: "text" | "email" | "password" | "number" | "tel" | "date" | "datetime-local" | "time";
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
@@ -15,6 +15,8 @@ interface InputProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
min?: string;
|
||||||
|
max?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Input({
|
function Input({
|
||||||
@@ -30,6 +32,8 @@ function Input({
|
|||||||
className = "",
|
className = "",
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
}: InputProps) {
|
}: InputProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-2 w-full ${className}`}>
|
<div className={`flex flex-col gap-2 w-full ${className}`}>
|
||||||
@@ -52,6 +56,8 @@ function Input({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
className={`
|
className={`
|
||||||
w-full px-4 py-3
|
w-full px-4 py-3
|
||||||
border border-gray-300
|
border border-gray-300
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const usePhoneMask = (initialValue: string = '') => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
const formatPhoneNumber = useCallback((input: string) => {
|
||||||
|
// Remove all non-digit characters
|
||||||
|
const digits = input.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Format as +7 (XXX) XXX-XX-XX
|
||||||
|
if (digits.length === 0) return '';
|
||||||
|
if (digits.length <= 1) return '+7';
|
||||||
|
if (digits.length <= 4) return `+7 (${digits.slice(1)}`;
|
||||||
|
if (digits.length <= 7) return `+7 (${digits.slice(1, 4)}) ${digits.slice(4)}`;
|
||||||
|
if (digits.length <= 9) return `+7 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
|
||||||
|
return `+7 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 9)}-${digits.slice(9, 11)}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = useCallback((input: string) => {
|
||||||
|
const formatted = formatPhoneNumber(input);
|
||||||
|
setValue(formatted);
|
||||||
|
return formatted;
|
||||||
|
}, [formatPhoneNumber]);
|
||||||
|
|
||||||
|
return { value, handleChange, setValue };
|
||||||
|
};
|
||||||
@@ -2,25 +2,120 @@ import Checkbox from "@/components/form/Checkbox";
|
|||||||
import Fileupload from "@/components/form/Fileupload";
|
import Fileupload from "@/components/form/Fileupload";
|
||||||
import Input from "@/components/form/Input";
|
import Input from "@/components/form/Input";
|
||||||
import Textarea from "@/components/form/Textarea";
|
import Textarea from "@/components/form/Textarea";
|
||||||
|
import { usePhoneMask } from "@/hooks/usePhoneMask";
|
||||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const pb = new PocketBase(
|
||||||
|
"http://pocketbase-nkg4scskc4okw4w0cw4w88gk.176.114.67.63.sslip.io"
|
||||||
|
);
|
||||||
|
|
||||||
function ExhibtionFormPage() {
|
function ExhibtionFormPage() {
|
||||||
const [checkboxValues, setCheckboxValues] = useState<string[]>([]);
|
const [checkboxValues, setCheckboxValues] = useState<string[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
carBrand: "",
|
||||||
|
carModel: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
const [carPhotos, setCarPhotos] = useState<File[]>([]);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
// Add a key to force re-render of Fileupload component
|
||||||
|
const [fileUploadKey, setFileUploadKey] = useState(0);
|
||||||
|
|
||||||
|
const { value: phoneValue, handleChange: handlePhoneChange } = usePhoneMask();
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!checkboxValues.includes("terms")) {
|
||||||
|
alert("Необходимо согласие на обработку персональных данных");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formDataObject = {
|
||||||
|
name: formData.name,
|
||||||
|
phone: phoneValue,
|
||||||
|
email: formData.email,
|
||||||
|
carBrand: formData.carBrand,
|
||||||
|
carModel: formData.carModel,
|
||||||
|
description: formData.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
// Store all form data as JSON in the data field
|
||||||
|
data.append("data", JSON.stringify(formDataObject));
|
||||||
|
data.append("type", "exhibition");
|
||||||
|
|
||||||
|
// Add car photos to the images field (PocketBase will handle multiple files)
|
||||||
|
carPhotos.forEach((file) => {
|
||||||
|
data.append("images", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = await pb.collection("forms").create(data);
|
||||||
|
|
||||||
|
console.log("Form submitted successfully:", record);
|
||||||
|
console.log("Uploaded images:", record.images);
|
||||||
|
alert("Форма успешно отправлена!");
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
carBrand: "",
|
||||||
|
carModel: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
setCheckboxValues([]);
|
||||||
|
setCarPhotos([]);
|
||||||
|
// Force re-render of Fileupload component
|
||||||
|
setFileUploadKey((prev) => prev + 1);
|
||||||
|
handlePhoneChange(""); // Reset phone value
|
||||||
|
|
||||||
|
router.push("/thankyou");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting form:", error);
|
||||||
|
alert("Ошибка при отправке формы. Попробуйте еще раз.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#ffffff] relative p-6 md:p-8 lg:p-12 flex flex-col items-center justify-center md:h-full">
|
<div className="relative p-6 md:p-8 lg:p-12 flex flex-col items-center">
|
||||||
|
<Head>
|
||||||
|
<title>Фестиваль технических видов спорта</title>
|
||||||
|
</Head>
|
||||||
<h1
|
<h1
|
||||||
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative mb-8 md:mb-12`}
|
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative mb-8 md:mb-12`}
|
||||||
>
|
>
|
||||||
Регистрация на выставку
|
Регистрация на выставку
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="space-y-6 max-w-2xl w-full">
|
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Имя"
|
label="Имя"
|
||||||
placeholder="Введите ваше имя"
|
placeholder="Введите ваше имя"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -28,6 +123,8 @@ function ExhibtionFormPage() {
|
|||||||
label="Телефон"
|
label="Телефон"
|
||||||
placeholder="+7 (___) ___-__-__"
|
placeholder="+7 (___) ___-__-__"
|
||||||
type="tel"
|
type="tel"
|
||||||
|
value={phoneValue}
|
||||||
|
onChange={(e) => handlePhoneChange(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,6 +132,8 @@ function ExhibtionFormPage() {
|
|||||||
label="Почта"
|
label="Почта"
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
type="email"
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -42,6 +141,8 @@ function ExhibtionFormPage() {
|
|||||||
label="Марка автомобиля"
|
label="Марка автомобиля"
|
||||||
placeholder="Введите марку автомобиля"
|
placeholder="Введите марку автомобиля"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.carBrand}
|
||||||
|
onChange={(e) => handleInputChange("carBrand", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -49,12 +150,15 @@ function ExhibtionFormPage() {
|
|||||||
label="Модель"
|
label="Модель"
|
||||||
placeholder="Введите модель автомобиля"
|
placeholder="Введите модель автомобиля"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.carModel}
|
||||||
|
onChange={(e) => handleInputChange("carModel", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Fileupload
|
<Fileupload
|
||||||
|
key={fileUploadKey} // Force component re-render on reset
|
||||||
label="Фото автомобиля (до 3 файлов)"
|
label="Фото автомобиля (до 3 файлов)"
|
||||||
onFileSelect={(files) => console.log("Selected files:", files)}
|
onFileSelect={(files) => setCarPhotos(files)}
|
||||||
acceptedTypes={["image/*"]}
|
acceptedTypes={["image/*"]}
|
||||||
maxFileSize={5}
|
maxFileSize={5}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
@@ -64,6 +168,8 @@ function ExhibtionFormPage() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
label="Интересное об автомобиле"
|
label="Интересное об автомобиле"
|
||||||
placeholder="Расскажите что-то интересное о вашем автомобиле"
|
placeholder="Расскажите что-то интересное о вашем автомобиле"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
cols={50}
|
cols={50}
|
||||||
/>
|
/>
|
||||||
@@ -90,11 +196,12 @@ function ExhibtionFormPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
className={`${fluxgore.className} bg-[#1068B0] hover:bg-[#0d5a96] text-white px-9 py-4 text-base font-medium uppercase tracking-wide disabled:opacity-50 w-full`}
|
className={`${fluxgore.className} bg-[#1068B0] hover:bg-[#0d5a96] text-white px-9 py-4 text-base font-medium uppercase tracking-wide disabled:opacity-50 w-full`}
|
||||||
>
|
>
|
||||||
Отправить
|
{isSubmitting ? "Отправка..." : "Отправить"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-6
@@ -2,25 +2,137 @@ import Checkbox from "@/components/form/Checkbox";
|
|||||||
import Fileupload from "@/components/form/Fileupload";
|
import Fileupload from "@/components/form/Fileupload";
|
||||||
import Input from "@/components/form/Input";
|
import Input from "@/components/form/Input";
|
||||||
import Textarea from "@/components/form/Textarea";
|
import Textarea from "@/components/form/Textarea";
|
||||||
|
import { usePhoneMask } from "@/hooks/usePhoneMask";
|
||||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||||
|
import Head from "next/head";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const pb = new PocketBase(
|
||||||
|
"http://pocketbase-nkg4scskc4okw4w0cw4w88gk.176.114.67.63.sslip.io"
|
||||||
|
);
|
||||||
|
|
||||||
function FightFormPage() {
|
function FightFormPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [checkboxValues, setCheckboxValues] = useState<string[]>([]);
|
const [checkboxValues, setCheckboxValues] = useState<string[]>([]);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
lastName: "",
|
||||||
|
firstName: "",
|
||||||
|
middleName: "",
|
||||||
|
birthDate: "",
|
||||||
|
citizenship: "",
|
||||||
|
email: "",
|
||||||
|
carBrand: "",
|
||||||
|
carModel: "",
|
||||||
|
engine: "",
|
||||||
|
power: "",
|
||||||
|
additionalInfo: "",
|
||||||
|
});
|
||||||
|
const [carPhotos, setCarPhotos] = useState<File[]>([]);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
// Add a key to force re-render of Fileupload component
|
||||||
|
const [fileUploadKey, setFileUploadKey] = useState(0);
|
||||||
|
|
||||||
|
const { value: phoneValue, handleChange: handlePhoneChange } = usePhoneMask();
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!checkboxValues.includes("terms")) {
|
||||||
|
alert("Необходимо согласие на обработку персональных данных");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formDataObject = {
|
||||||
|
lastName: formData.lastName,
|
||||||
|
firstName: formData.firstName,
|
||||||
|
middleName: formData.middleName,
|
||||||
|
birthDate: formData.birthDate,
|
||||||
|
citizenship: formData.citizenship,
|
||||||
|
phone: phoneValue,
|
||||||
|
email: formData.email,
|
||||||
|
carBrand: formData.carBrand,
|
||||||
|
carModel: formData.carModel,
|
||||||
|
engine: formData.engine,
|
||||||
|
power: formData.power,
|
||||||
|
additionalInfo: formData.additionalInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
// Store all form data as JSON in the data field
|
||||||
|
data.append("data", JSON.stringify(formDataObject));
|
||||||
|
data.append("type", "fight");
|
||||||
|
|
||||||
|
// Add car photos to the images field (PocketBase will handle multiple files)
|
||||||
|
carPhotos.forEach((file) => {
|
||||||
|
data.append("images", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = await pb.collection("forms").create(data);
|
||||||
|
|
||||||
|
console.log("Form submitted successfully:", record);
|
||||||
|
console.log("Uploaded images:", record.images);
|
||||||
|
alert("Форма успешно отправлена!");
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
lastName: "",
|
||||||
|
firstName: "",
|
||||||
|
middleName: "",
|
||||||
|
birthDate: "",
|
||||||
|
citizenship: "",
|
||||||
|
email: "",
|
||||||
|
carBrand: "",
|
||||||
|
carModel: "",
|
||||||
|
engine: "",
|
||||||
|
power: "",
|
||||||
|
additionalInfo: "",
|
||||||
|
});
|
||||||
|
setCheckboxValues([]);
|
||||||
|
setCarPhotos([]);
|
||||||
|
// Force re-render of Fileupload component
|
||||||
|
setFileUploadKey((prev) => prev + 1);
|
||||||
|
handlePhoneChange(""); // Reset phone value
|
||||||
|
router.push("/thankyou");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting form:", error);
|
||||||
|
alert("Ошибка при отправке формы. Попробуйте еще раз.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#ffffff] relative p-6 md:p-8 lg:p-12 flex flex-col items-center justify-center">
|
<div className="relative p-6 md:p-8 lg:p-12 flex flex-col items-center justify-center">
|
||||||
|
<Head>
|
||||||
|
<title>Фестиваль технических видов спорта</title>
|
||||||
|
</Head>
|
||||||
<h1
|
<h1
|
||||||
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative mb-8 md:mb-12`}
|
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative mb-8 md:mb-12`}
|
||||||
>
|
>
|
||||||
Регистрация на Битву за Москву
|
Регистрация на Битву за Москву
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="space-y-6 max-w-2xl w-full">
|
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Фамилия"
|
label="Фамилия"
|
||||||
placeholder="Введите вашу фамилию"
|
placeholder="Введите вашу фамилию"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={(e) => handleInputChange("lastName", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -28,6 +140,8 @@ function FightFormPage() {
|
|||||||
label="Имя"
|
label="Имя"
|
||||||
placeholder="Введите ваше имя"
|
placeholder="Введите ваше имя"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={(e) => handleInputChange("firstName", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,13 +149,17 @@ function FightFormPage() {
|
|||||||
label="Отчество"
|
label="Отчество"
|
||||||
placeholder="Введите ваше отчество"
|
placeholder="Введите ваше отчество"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.middleName}
|
||||||
|
onChange={(e) => handleInputChange("middleName", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Дата рождения"
|
label="Дата рождения"
|
||||||
placeholder="дд.мм.гггг"
|
placeholder="дд.мм.гггг"
|
||||||
type="text"
|
type="date"
|
||||||
|
value={formData.birthDate}
|
||||||
|
onChange={(e) => handleInputChange("birthDate", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -49,6 +167,8 @@ function FightFormPage() {
|
|||||||
label="Гражданство"
|
label="Гражданство"
|
||||||
placeholder="Введите ваше гражданство"
|
placeholder="Введите ваше гражданство"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.citizenship}
|
||||||
|
onChange={(e) => handleInputChange("citizenship", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -56,6 +176,8 @@ function FightFormPage() {
|
|||||||
label="Телефон"
|
label="Телефон"
|
||||||
placeholder="+7 (___) ___-__-__"
|
placeholder="+7 (___) ___-__-__"
|
||||||
type="tel"
|
type="tel"
|
||||||
|
value={phoneValue}
|
||||||
|
onChange={(e) => handlePhoneChange(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -63,6 +185,8 @@ function FightFormPage() {
|
|||||||
label="Почта"
|
label="Почта"
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
type="email"
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -70,6 +194,8 @@ function FightFormPage() {
|
|||||||
label="Марка автомобиля"
|
label="Марка автомобиля"
|
||||||
placeholder="Введите марку автомобиля"
|
placeholder="Введите марку автомобиля"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.carBrand}
|
||||||
|
onChange={(e) => handleInputChange("carBrand", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -77,6 +203,8 @@ function FightFormPage() {
|
|||||||
label="Модель"
|
label="Модель"
|
||||||
placeholder="Введите модель автомобиля"
|
placeholder="Введите модель автомобиля"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.carModel}
|
||||||
|
onChange={(e) => handleInputChange("carModel", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -84,6 +212,8 @@ function FightFormPage() {
|
|||||||
label="Двигатель"
|
label="Двигатель"
|
||||||
placeholder="Введите тип двигателя"
|
placeholder="Введите тип двигателя"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.engine}
|
||||||
|
onChange={(e) => handleInputChange("engine", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -91,12 +221,15 @@ function FightFormPage() {
|
|||||||
label="Мощность"
|
label="Мощность"
|
||||||
placeholder="Введите мощность (л.с.)"
|
placeholder="Введите мощность (л.с.)"
|
||||||
type="text"
|
type="text"
|
||||||
|
value={formData.power}
|
||||||
|
onChange={(e) => handleInputChange("power", e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Fileupload
|
<Fileupload
|
||||||
|
key={fileUploadKey} // Force component re-render on reset
|
||||||
label="Фото автомобиля (до 3 файлов)"
|
label="Фото автомобиля (до 3 файлов)"
|
||||||
onFileSelect={(files) => console.log("Selected files:", files)}
|
onFileSelect={(files) => setCarPhotos(files)}
|
||||||
acceptedTypes={["image/*"]}
|
acceptedTypes={["image/*"]}
|
||||||
maxFileSize={5}
|
maxFileSize={5}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
@@ -106,6 +239,8 @@ function FightFormPage() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
label="Дополнительная информация"
|
label="Дополнительная информация"
|
||||||
placeholder="Дополнительная информация об автомобиле"
|
placeholder="Дополнительная информация об автомобиле"
|
||||||
|
value={formData.additionalInfo}
|
||||||
|
onChange={(e) => handleInputChange("additionalInfo", e.target.value)}
|
||||||
rows={4}
|
rows={4}
|
||||||
cols={50}
|
cols={50}
|
||||||
/>
|
/>
|
||||||
@@ -132,11 +267,12 @@ function FightFormPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
className={`${fluxgore.className} bg-[#1068B0] hover:bg-[#0d5a96] text-white px-9 py-4 text-base font-medium uppercase tracking-wide disabled:opacity-50 w-full`}
|
className={`${fluxgore.className} bg-[#1068B0] hover:bg-[#0d5a96] text-white px-9 py-4 text-base font-medium uppercase tracking-wide disabled:opacity-50 w-full`}
|
||||||
>
|
>
|
||||||
Отправить
|
{isSubmitting ? "Отправка..." : "Отправить"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-2
@@ -1,4 +1,15 @@
|
|||||||
import { CoverSoon } from "@/components/Cover";
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import Activities from "@/components/Activities";
|
||||||
|
import Cover from "@/components/Cover";
|
||||||
|
import Events from "@/components/Events";
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
import Info from "@/components/Info";
|
||||||
|
import Map from "@/components/Map";
|
||||||
|
import Navbar from "@/components/Navbar";
|
||||||
|
import Partners from "@/components/Partners";
|
||||||
|
import Scheme from "@/components/Scheme";
|
||||||
|
import Video from "@/components/Video";
|
||||||
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -7,7 +18,18 @@ export default function Home() {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>Фестиваль технических видов спорта</title>
|
<title>Фестиваль технических видов спорта</title>
|
||||||
</Head>
|
</Head>
|
||||||
<CoverSoon />
|
<Navbar />
|
||||||
|
<main className="flex-col min-h-full">
|
||||||
|
<Cover />
|
||||||
|
<Info />
|
||||||
|
<Video />
|
||||||
|
{/* <Scheme /> */}
|
||||||
|
<Events />
|
||||||
|
<Activities />
|
||||||
|
<Partners />
|
||||||
|
<Map />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||||
|
|
||||||
|
function ThankYouPage() {
|
||||||
|
return (
|
||||||
|
<div className="relative p-6 md:p-8 lg:p-12 flex flex-col items-center justify-center h-full">
|
||||||
|
<Head>
|
||||||
|
<title>Фестиваль технических видов спорта</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative mb-8 md:mb-12`}
|
||||||
|
>
|
||||||
|
СПАСИБО!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={`${gothampro.className} text-lg md:text-2xl text-center text-[#060606]`}
|
||||||
|
>
|
||||||
|
Ваша заявка отправлена. вам придет письмо на почту
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThankYouPage;
|
||||||
@@ -13,5 +13,4 @@
|
|||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: #0d0d0d;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user