feat(about): submit contact form to api

This commit is contained in:
2026-06-07 14:09:54 +03:30
parent 69908887c1
commit 8abfcc9c2b
3 changed files with 80 additions and 20 deletions

41
src/api/contact.ts Normal file
View File

@@ -0,0 +1,41 @@
import { buildApiError, buildApiUrl } from "./client"
const normalizeDigits = (value: string) =>
value
.replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit)))
.replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit)))
export interface ContactSubmissionPayload {
first_name: string
last_name: string
email: string
mobile: string
message: string
}
export interface ContactSubmissionResponse extends ContactSubmissionPayload {
id: string
status: string
created_at: string
}
export const submitContactForm = async (
payload: ContactSubmissionPayload,
): Promise<ContactSubmissionResponse> => {
const response = await fetch(buildApiUrl("/api/contact/"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...payload,
mobile: normalizeDigits(payload.mobile),
}),
})
if (!response.ok) {
throw await buildApiError(response)
}
return response.json()
}

View File

@@ -67,7 +67,7 @@
"contact": { "contact": {
"eyebrow": "Contact", "eyebrow": "Contact",
"title": "Need help or want to talk about Qlockify?", "title": "Need help or want to talk about Qlockify?",
"description": "Send a note or reach out through the support channels below. The form opens an email draft with your message so you can review it before sending.", "description": "Send a note or reach out through the support channels below. Contact form submissions are stored securely so the team can review and follow up.",
"formTitle": "Send a note", "formTitle": "Send a note",
"fields": { "fields": {
"firstName": "First name", "firstName": "First name",
@@ -83,7 +83,10 @@
"mobile": "09...", "mobile": "09...",
"message": "Tell us what you need help with..." "message": "Tell us what you need help with..."
}, },
"submit": "Prepare email", "submit": "Send message",
"submitting": "Sending...",
"success": "Your message was saved. We will contact you soon.",
"error": "Could not send your message. Please try again.",
"channels": [ "channels": [
{ {
"label": "Telegram support", "label": "Telegram support",
@@ -181,7 +184,7 @@
"contact": { "contact": {
"eyebrow": "تماس", "eyebrow": "تماس",
"title": "کمک می‌خواهید یا می‌خواهید درباره Qlockify صحبت کنیم؟", "title": "کمک می‌خواهید یا می‌خواهید درباره Qlockify صحبت کنیم؟",
"description": "یک پیام بفرستید یا از مسیرهای پشتیبانی زیر با ما در ارتباط باشید. فرم یک پیش‌نویس ایمیل با پیام شما آماده می‌کند تا قبل از ارسال آن را بررسی کنید.", "description": "یک پیام بفرستید یا از مسیرهای پشتیبانی زیر با ما در ارتباط باشید. پیام‌های فرم تماس ذخیره می‌شوند تا تیم بتواند آن‌ها را بررسی و پیگیری کند.",
"formTitle": "ارسال پیام", "formTitle": "ارسال پیام",
"fields": { "fields": {
"firstName": "نام", "firstName": "نام",
@@ -197,7 +200,10 @@
"mobile": "09...", "mobile": "09...",
"message": "بگویید برای چه چیزی به کمک نیاز دارید..." "message": "بگویید برای چه چیزی به کمک نیاز دارید..."
}, },
"submit": "آماده‌سازی ایمیل", "submit": "ارسال پیام",
"submitting": "در حال ارسال...",
"success": "پیام شما ثبت شد. به‌زودی با شما تماس می‌گیریم.",
"error": "ارسال پیام انجام نشد. لطفا دوباره تلاش کنید.",
"channels": [ "channels": [
{ {
"label": "پشتیبانی تلگرام", "label": "پشتیبانی تلگرام",

View File

@@ -1,5 +1,6 @@
import { useState, type FormEvent } from "react" import { useState, type FormEvent } from "react"
import { Link, useNavigate } from "react-router-dom" import { Link, useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { import {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
@@ -29,6 +30,7 @@ import { Input } from "../components/ui/input"
import { TextAreaInput } from "../components/ui/TextAreaInput" import { TextAreaInput } from "../components/ui/TextAreaInput"
import { useTheme } from "../components/ThemeProvider" import { useTheme } from "../components/ThemeProvider"
import { useTranslation } from "../hooks/useTranslation" import { useTranslation } from "../hooks/useTranslation"
import { submitContactForm } from "../api/contact"
import aboutContent from "../content/about.json" import aboutContent from "../content/about.json"
import { cn } from "../lib/utils" import { cn } from "../lib/utils"
@@ -50,6 +52,7 @@ export default function About() {
mobile: "", mobile: "",
message: "", message: "",
}) })
const [isContactSubmitting, setIsContactSubmitting] = useState(false)
const content = aboutContent[lang] as AboutContent const content = aboutContent[lang] as AboutContent
const isAuthenticated = typeof window !== "undefined" && !!localStorage.getItem("accessToken") const isAuthenticated = typeof window !== "undefined" && !!localStorage.getItem("accessToken")
@@ -62,21 +65,30 @@ export default function About() {
setContactForm((current) => ({ ...current, [field]: value })) setContactForm((current) => ({ ...current, [field]: value }))
} }
const submitContactForm = (event: FormEvent<HTMLFormElement>) => { const handleContactSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
const subject = encodeURIComponent("Qlockify contact request") setIsContactSubmitting(true)
const body = encodeURIComponent( try {
[ await submitContactForm({
`First name: ${contactForm.firstName}`, first_name: contactForm.firstName.trim(),
`Last name: ${contactForm.lastName}`, last_name: contactForm.lastName.trim(),
`Email: ${contactForm.email}`, email: contactForm.email.trim(),
`Mobile: ${contactForm.mobile}`, mobile: contactForm.mobile.trim(),
"", message: contactForm.message.trim(),
"Message:", })
contactForm.message, setContactForm({
].join("\n"), firstName: "",
) lastName: "",
window.location.href = `mailto:qlockify@gmail.com?subject=${subject}&body=${body}` email: "",
mobile: "",
message: "",
})
toast.success(content.contact.success)
} catch (error) {
toast.error(error instanceof Error ? error.message : content.contact.error)
} finally {
setIsContactSubmitting(false)
}
} }
return ( return (
@@ -377,7 +389,7 @@ export default function About() {
</div> </div>
<form <form
onSubmit={submitContactForm} onSubmit={handleContactSubmit}
className="animate-landing-rise rounded-[2rem] border border-white/70 bg-white/85 p-7 shadow-[0_30px_80px_-50px_rgba(15,23,42,0.6)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/70" className="animate-landing-rise rounded-[2rem] border border-white/70 bg-white/85 p-7 shadow-[0_30px_80px_-50px_rgba(15,23,42,0.6)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/70"
> >
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 flex items-center gap-3">
@@ -446,10 +458,11 @@ export default function About() {
<Button <Button
type="submit" type="submit"
disabled={isContactSubmitting}
className="mt-6 h-14 w-full rounded-full bg-slate-950 px-7 text-base text-white hover:bg-slate-800 dark:bg-cyan-400 dark:text-slate-950 dark:hover:bg-cyan-300" className="mt-6 h-14 w-full rounded-full bg-slate-950 px-7 text-base text-white hover:bg-slate-800 dark:bg-cyan-400 dark:text-slate-950 dark:hover:bg-cyan-300"
> >
<Send className="me-2 h-4 w-4" /> <Send className="me-2 h-4 w-4" />
{content.contact.submit} {isContactSubmitting ? content.contact.submitting : content.contact.submit}
</Button> </Button>
</form> </form>
</section> </section>