diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 11ecc57c..2ee6cd1a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@headlessui/react": "^2.2.4", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", @@ -21,9 +22,11 @@ "next": "^16.1.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.72.0", "react-icons": "^5.6.0", "tailwind-merge": "^3.3.0", - "three": "^0.182.0" + "three": "^0.182.0", + "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -540,6 +543,18 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1929,6 +1944,12 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", @@ -6165,6 +6186,23 @@ "react": "^19.2.4" } }, + "node_modules/react-hook-form": { + "version": "7.72.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.0.tgz", + "integrity": "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 47e59f2c..b7cc7bf4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@headlessui/react": "^2.2.4", + "@hookform/resolvers": "^5.2.2", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", @@ -22,9 +23,11 @@ "next": "^16.1.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-hook-form": "^7.72.0", "react-icons": "^5.6.0", "tailwind-merge": "^3.3.0", - "three": "^0.182.0" + "three": "^0.182.0", + "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/frontend/src/app/contact/page.tsx b/frontend/src/app/contact/page.tsx index 97bb7830..089447b3 100644 --- a/frontend/src/app/contact/page.tsx +++ b/frontend/src/app/contact/page.tsx @@ -2,23 +2,26 @@ import { useState } from "react"; import Link from "next/link"; -import { ChevronLeft, Send, CheckCircle, AlertCircle, MessageSquare, Twitter, Github } from "lucide-react"; +import { ChevronLeft, Send, MessageSquare, Twitter, Github } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + import Footer from "@/component/Footer"; import Header from "@/component/Header"; import PageBackground from "@/component/PageBackground"; - -const CATEGORIES = ["Technical", "Account", "Trading", "Other"] as const; -type Category = (typeof CATEGORIES)[number]; - -interface FormState { - name: string; - email: string; - subject: string; - category: Category; - message: string; -} - -const INITIAL: FormState = { name: "", email: "", subject: "", category: "Technical", message: "" }; +import { + FormInput, + FormTextarea, + FormSelect, + SubmitButton, + FormSuccessBanner, + FormErrorBanner, +} from "@/component/FormField"; +import { + contactSchema, + type ContactFormData, + CONTACT_CATEGORIES, +} from "@/lib/validations"; const SOCIAL_LINKS = [ { label: "Telegram", href: "https://t.me/+hR9dZKau8f84YTk0", icon: MessageSquare }, @@ -27,250 +30,201 @@ const SOCIAL_LINKS = [ ]; export default function ContactPage() { - const [form, setForm] = useState(INITIAL); - const [errors, setErrors] = useState>({}); const [status, setStatus] = useState<"idle" | "success" | "error">("idle"); - const [loading, setLoading] = useState(false); - - function validate(): boolean { - const e: Partial = {}; - if (!form.name.trim()) e.name = "Name is required"; - if (!form.email.trim()) e.email = "Email is required"; - else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) e.email = "Invalid email address"; - if (!form.subject.trim()) e.subject = "Subject is required"; - if (!form.message.trim()) e.message = "Message is required"; - else if (form.message.trim().length < 20) e.message = "Message must be at least 20 characters"; - setErrors(e); - return Object.keys(e).length === 0; - } - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!validate()) return; - setLoading(true); - // Simulate submission — replace with real API call - await new Promise((r) => setTimeout(r, 1200)); - setLoading(false); - // Randomly succeed for demo; swap with real error handling - setStatus("success"); - setForm(INITIAL); - setErrors({}); - } - - function field(key: keyof FormState) { - return { - value: form[key], - onChange: (e: React.ChangeEvent) => - setForm((f) => ({ ...f, [key]: e.target.value })), - }; - } - const inputCls = (key: keyof FormState) => - `w-full rounded-xl border bg-[#0f172a]/80 px-4 py-3 text-sm text-white placeholder-[#475569] outline-none transition focus:ring-2 focus:ring-[#4FD1C5]/60 ${ - errors[key] ? "border-red-500/60" : "border-white/10 focus:border-[#4FD1C5]/40" - }`; + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + reset, + } = useForm({ + resolver: zodResolver(contactSchema), + defaultValues: { category: "Technical" }, + mode: "onTouched", + }); + + const onSubmit = async (data: ContactFormData) => { + try { + // Simulate API call — replace with real endpoint + await new Promise((r) => setTimeout(r, 1200)); + console.log("Contact submitted:", data); + setStatus("success"); + reset(); + } catch { + setStatus("error"); + } + }; return ( -
- - -
-
- -
- {/* Header card */} -
-
-
-

Support

-

Contact Us

-

- Have a question or issue? Fill out the form below and we'll get back to you as soon as possible. -

-
- - - Back to home - + +
+ +
+ {/* Header card */} +
+
+
+

+ Support +

+

+ Contact Us +

+

+ Have a question or issue? Fill out the form below and we'll + get back to you as soon as possible. +

- -
- {/* Form */} -
- {status === "success" ? ( -
- -

Message Sent!

-

- Thanks for reaching out. We typically respond within 24–48 hours. -

- -
- ) : status === "error" ? ( -
- -

Something went wrong

-

Please try again or reach us directly via social media.

- + + + Back to home + +
+ +
+ {/* ── Form ── */} +
+ {status === "success" ? ( + setStatus("idle")} + resetLabel="Send another message" + /> + ) : status === "error" ? ( + setStatus("idle")} + /> + ) : ( +
+
+ +
- ) : ( - -
-
- - - {errors.name &&

{errors.name}

} -
-
- - - {errors.email &&

{errors.email}

} -
-
-
-
- - -
-
- - - {errors.subject &&

{errors.subject}

} -
-
- -
- -