From 4d0a767eac348b939e7698dfbc804049f895fd91 Mon Sep 17 00:00:00 2001 From: Anuarbek Zhakhangir Date: Thu, 31 Jul 2025 19:53:19 +0900 Subject: [PATCH] feat: add Fight and Exhibition registration forms with input validation and file upload functionality --- package-lock.json | 7 ++ package.json | 1 + src/components/Events.tsx | 23 ++++- src/components/form/Fileupload.tsx | 4 +- src/components/form/Input.tsx | 8 +- src/hooks/usePhoneMask.ts | 26 +++++ src/pages/forms/exhibition.tsx | 117 ++++++++++++++++++++++- src/pages/forms/fight.tsx | 148 +++++++++++++++++++++++++++-- src/pages/index.tsx | 26 ++++- src/pages/thankyou.tsx | 27 ++++++ src/styles/globals.css | 1 - 11 files changed, 369 insertions(+), 19 deletions(-) create mode 100644 src/hooks/usePhoneMask.ts create mode 100644 src/pages/thankyou.tsx diff --git a/package-lock.json b/package-lock.json index 80eedac..ef31ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "next": "15.3.5", + "pocketbase": "^0.26.2", "react": "^19.0.0", "react-dom": "^19.0.0", "swiper": "^11.2.10" @@ -4995,6 +4996,12 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index e874700..92538ec 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "next": "15.3.5", + "pocketbase": "^0.26.2", "react": "^19.0.0", "react-dom": "^19.0.0", "swiper": "^11.2.10" diff --git a/src/components/Events.tsx b/src/components/Events.tsx index 4bd2d97..0ebe720 100644 --- a/src/components/Events.tsx +++ b/src/components/Events.tsx @@ -1,15 +1,25 @@ /* eslint-disable @next/next/no-img-element */ import { fluxgore, gothampro } from "@/utils/fonts"; import Button from "./Button"; +import { useRouter } from "next/router"; interface EventCardProps extends React.HTMLAttributes { image: string; title: string; description: string; link: string; + disabled: boolean; } function EventCard(props: EventCardProps) { + const router = useRouter(); + + const handleClick = () => { + if (!props.disabled) { + router.push(props.link); + } + }; + return (
-
@@ -77,6 +92,7 @@ function Events() {
diff --git a/src/components/form/Input.tsx b/src/components/form/Input.tsx index bd98e4d..440e080 100644 --- a/src/components/form/Input.tsx +++ b/src/components/form/Input.tsx @@ -5,7 +5,7 @@ import { gothampro } from "@/utils/fonts"; interface InputProps { label?: string; placeholder?: string; - type?: "text" | "email" | "password" | "number" | "tel"; + type?: "text" | "email" | "password" | "number" | "tel" | "date" | "datetime-local" | "time"; value?: string; onChange?: (e: React.ChangeEvent) => void; onBlur?: (e: React.FocusEvent) => void; @@ -15,6 +15,8 @@ interface InputProps { className?: string; id?: string; name?: string; + min?: string; + max?: string; } function Input({ @@ -30,6 +32,8 @@ function Input({ className = "", id, name, + min, + max, }: InputProps) { return (
@@ -52,6 +56,8 @@ function Input({ placeholder={placeholder} disabled={disabled} required={required} + min={min} + max={max} className={` w-full px-4 py-3 border border-gray-300 diff --git a/src/hooks/usePhoneMask.ts b/src/hooks/usePhoneMask.ts new file mode 100644 index 0000000..09e1465 --- /dev/null +++ b/src/hooks/usePhoneMask.ts @@ -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 }; +}; \ No newline at end of file diff --git a/src/pages/forms/exhibition.tsx b/src/pages/forms/exhibition.tsx index 5f98071..24f779a 100644 --- a/src/pages/forms/exhibition.tsx +++ b/src/pages/forms/exhibition.tsx @@ -2,25 +2,120 @@ import Checkbox from "@/components/form/Checkbox"; import Fileupload from "@/components/form/Fileupload"; import Input from "@/components/form/Input"; import Textarea from "@/components/form/Textarea"; +import { usePhoneMask } from "@/hooks/usePhoneMask"; import { fluxgore, gothampro } from "@/utils/fonts"; 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() { const [checkboxValues, setCheckboxValues] = useState([]); + const router = useRouter(); + const [formData, setFormData] = useState({ + name: "", + email: "", + carBrand: "", + carModel: "", + description: "", + }); + const [carPhotos, setCarPhotos] = useState([]); + 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 ( -
+
+ + Фестиваль технических видов спорта +

Регистрация на выставку

-
+
handleInputChange("name", e.target.value)} required /> @@ -28,6 +123,8 @@ function ExhibtionFormPage() { label="Телефон" placeholder="+7 (___) ___-__-__" type="tel" + value={phoneValue} + onChange={(e) => handlePhoneChange(e.target.value)} required /> @@ -35,6 +132,8 @@ function ExhibtionFormPage() { label="Почта" placeholder="example@email.com" type="email" + value={formData.email} + onChange={(e) => handleInputChange("email", e.target.value)} required /> @@ -42,6 +141,8 @@ function ExhibtionFormPage() { label="Марка автомобиля" placeholder="Введите марку автомобиля" type="text" + value={formData.carBrand} + onChange={(e) => handleInputChange("carBrand", e.target.value)} required /> @@ -49,12 +150,15 @@ function ExhibtionFormPage() { label="Модель" placeholder="Введите модель автомобиля" type="text" + value={formData.carModel} + onChange={(e) => handleInputChange("carModel", e.target.value)} required /> console.log("Selected files:", files)} + onFileSelect={(files) => setCarPhotos(files)} acceptedTypes={["image/*"]} maxFileSize={5} multiple={true} @@ -64,6 +168,8 @@ function ExhibtionFormPage() {