diff --git a/messages/en.json b/messages/en.json index 7ef2725..ac0aaf1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -118,5 +118,19 @@ "serverError": "Error on the server side", "noPassword": "Weak password, please avoid obvious passwords", "errorOccured": "An unexpected error occurred" + }, + "contacts": { + "serverError": "Error on the server side", + "addressTitle": "Address", + "address": "Bishkek, Turusbekov str. 109/1, office 108", + "phoneTitle": "Phone", + "title": "We're in touch", + "name": "Name", + "surname": "Last name", + "phone": "Phone number", + "message": "Message", + "checkbox": "I agree to the processing of my personal data", + "send": "Send a message", + "successMessage": "Your message has been sent" } } diff --git a/messages/kg.json b/messages/kg.json index f5a2087..83970be 100644 --- a/messages/kg.json +++ b/messages/kg.json @@ -118,5 +118,19 @@ "serverError": "Сервер тараптагы ката", "noPassword": "Начар сырсөз, ачык сырсөздөрдү колдонбоңуз", "errorOccured": "Күтүлбөгөн ката кетти" + }, + "contacts": { + "serverError": "Сервер тараптагы ката", + "addressTitle": "Дарек", + "address": "Бишкек ш., Турусбеков көч. 109/1, офис 108", + "phoneTitle": "Телефон", + "title": "Биз байланыштабыз", + "name": "Аты-жөнү", + "familia": "фамилиясы", + "phone": "Телефон номери", + "message": "Кабар", + "checkbox": "Мен жеке маалыматтарымды иштетүүгө макулмун", + "send": "Билдирүү жөнөтүү", + "successMessage": "Сиздин билдирүү жөнөтүлдү" } } diff --git a/messages/ru.json b/messages/ru.json index 4235b19..369b834 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -109,5 +109,19 @@ "serverError": "Ошибка на стороне сервера", "noPassword": "Слабый пароль, прошу избегайте очевидных паролей", "errorOccured": "Произошла непредвиденная ошибка" + }, + "contacts": { + "serverError": "Ошибка на стороне сервера", + "addressTitle": "Адрес", + "address": "г. Бишкек, ул. Турусбекова 109/1, офис 108", + "phoneTitle": "Телефон", + "title": "Мы на связи", + "name": "Имя", + "surname": "Фамилия", + "phone": "Номер телефона", + "message": "Сообщение", + "checkbox": "Я согласен (-на) на обработку моих личных данных", + "send": "Отправить сообщение", + "successMessage": "Ваше сообщение отправлено" } } diff --git a/package-lock.json b/package-lock.json index 8b8c886..428dc0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.52.2", + "react-toastify": "^10.0.5", "sass": "^1.77.8", "tailwind-merge": "^2.5.2", "zod": "^3.23.8", @@ -4224,6 +4225,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 93513d0..1969e2e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.52.2", + "react-toastify": "^10.0.5", "sass": "^1.77.8", "tailwind-merge": "^2.5.2", "zod": "^3.23.8", diff --git a/src/app/[locale]/contacts/page.tsx b/src/app/[locale]/contacts/page.tsx index 547f929..af4eac1 100644 --- a/src/app/[locale]/contacts/page.tsx +++ b/src/app/[locale]/contacts/page.tsx @@ -1,8 +1,58 @@ +"use client"; +import { apiInstance } from "@/shared/config/apiConfig"; import { Container, Title } from "@/shared/ui"; - -// import ContactForm from "@/widgets/forms/ContactForm"; +import Loader from "@/shared/ui/Loader/Loader"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AxiosError } from "axios"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "react-toastify"; +import { z } from "zod"; export default function Contacts() { + const t = useTranslations("contacts"); + const [error, setError] = useState(""); + const [loader, setLoader] = useState(false); + + const appealFormScheme = z.object({ + first_name: z.string(), + last_name: z.string(), + email: z.string(), + phone_number: z.string(), + message: z.string(), + }); + + type FormFields = z.infer; + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(appealFormScheme), + }); + + const onSubmit = async (data: FormFields) => { + try { + setError(""); + setLoader(true); + + const response = await apiInstance.post("/appeal_response/", data); + console.log(response); + toast.success(t("successMessage")); + } catch (error) { + if (error instanceof AxiosError) { + if ( + [500, 501, 502, 503, 504].includes(error.response?.status as number) + ) { + toast.error(t("serverError")); + } + } + } finally { + setLoader(false); + } + }; return (
@@ -12,13 +62,11 @@ export default function Contacts() { >
- - <p className="text-grey-text"> - г. Бишкек, ул. Турусбекова 109/1, офис 108 - </p> + <Title text={t("addressTitle")} className="font-extrabold mb-2" /> + <p className="text-grey-text">{t("address")}</p> </div> <div className="max-w-[384px] max-h-[106px] flex flex-col items-center px-7"> - <Title text="Телефон" className="font-extrabold mb-2" /> + <Title text={t("phoneTitle")} className="font-extrabold mb-2" /> <p className="text-grey-text">(0312) 39 40 38</p> </div> <div className="max-w-[384px] max-h-[106px] flex flex-col items-center px-7"> @@ -26,36 +74,38 @@ export default function Contacts() { <p className="text-grey-text">kyrgyzstan@transparency.org</p> </div> </section> - {/* <ContactForm /> */} - <form className="w-[480px] my-0 mx-auto"> + <form + onSubmit={handleSubmit(onSubmit)} + className="w-[480px] my-0 mx-auto" + > <div className="flex items-center flex-col mt-[96px] mb-[96px]"> - <h1 className="text-gray-900 text-[36px]">Мы на связи</h1> + <h1 className="text-gray-900 text-[36px]">{t("title")}</h1> <div className="flex flex-col items-center gap-6 mt-[64px]"> <div className="flex gap-8 w-full"> <div className="w-[224px] flex flex-col"> <label htmlFor="firstName" className="mb-[6px]"> - Имя + {t("name")} </label> <input type="text" - name="firstName" className="w-full h-[48px] bg-white py-3 px-4 rounded-md shadow-sm border border-gray" - placeholder="Имя" + placeholder={t("name")} id="firstName" required + {...register("first_name", { required: true })} /> </div> <div className="w-[224px] flex flex-col"> <label htmlFor="lastName" className="mb-[6px]"> - Фамилия + {t("surname")} </label> <input type="text" - name="lastName" className="w-full h-[48px] bg-white py-3 px-4 rounded-md shadow-sm border border-gray" - placeholder="Фамилия" + placeholder={t("surname")} id="lastName" required + {...register("last_name", { required: true })} /> </div> </div> @@ -65,36 +115,36 @@ export default function Contacts() { </label> <input type="text" - name="email" placeholder="user@gmail.com" className="w-full h-[48px] bg-white py-3 px-4 rounded-md shadow-sm border border-gray" id="email" + {...register("email", { required: true })} /> </div> <div className="w-full"> <label htmlFor="phoneNumber" className="mb-[6px]"> - Номер телефона + {t("phone")} </label> <input type="text" - name="phoneNumber" className="w-full h-[48px] bg-white py-3 px-4 rounded-md shadow-sm border border-gray" placeholder="+996" id="phoneNumber" required + {...register("phone_number", { required: true })} /> </div> <div className="w-full"> <label htmlFor="message" className="mb-[6px]"> - Сообщение + {t("message")} </label> <textarea className="h-[120px] resize-none w-full bg-white py-3 px-4 rounded-md shadow-sm border border-gray" id="message" - name="message" required + {...register("message", { required: true })} ></textarea> </div> @@ -110,16 +160,24 @@ export default function Contacts() { htmlFor="consentCheckbox" className="text-[#667085] ml-[10px]" > - Я согласен (-на) на обработку моих личных данных + {t("checkbox")} </label> </div> + {errors.root && ( + <p className="text-sm leading-5 text-red-500"> + {errors.root.message} + </p> + )} + {error ? ( + <p className="text-red-500 leading-5 text-sm">{error}</p> + ) : null} <div className="w-full"> <button type="submit" className="bg-blue text-white w-full h-[48px] rounded-md " > - Отправить сообщение + {loader ? <Loader /> : t("send")} </button> </div> </div> diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index a48fbd6..dae6d13 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -5,6 +5,8 @@ import { NextIntlClientProvider, useMessages } from "next-intl"; import Footer from "../../widgets/Footer/Footer"; import Navbar from "@/widgets/Navbar/Navbar"; import { Providers } from "./Providers"; +import "react-toastify/dist/ReactToastify.css"; +import { ToastContainer } from "react-toastify"; const inter = Inter({ subsets: ["latin"] }); @@ -29,6 +31,7 @@ export default function RootLayout({ <Providers> <Navbar /> <div>{children}</div> + <ToastContainer /> <Footer /> </Providers> </NextIntlClientProvider>