feat(about): add contact section

This commit is contained in:
2026-06-07 12:49:53 +03:30
parent e8eff6c2cb
commit b4e06b641d
2 changed files with 249 additions and 0 deletions

View File

@@ -64,6 +64,49 @@
"Project access and user roles help limit what each person can see or use.", "Project access and user roles help limit what each person can see or use.",
"Exports and logs are designed to make review easier, not to hide important details." "Exports and logs are designed to make review easier, not to hide important details."
], ],
"contact": {
"eyebrow": "Contact",
"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.",
"formTitle": "Send a note",
"fields": {
"firstName": "First name",
"lastName": "Last name",
"email": "Email",
"mobile": "Mobile",
"message": "Message"
},
"placeholders": {
"firstName": "Your first name",
"lastName": "Your last name",
"email": "you@example.com",
"mobile": "09...",
"message": "Tell us what you need help with..."
},
"submit": "Prepare email",
"channels": [
{
"label": "Telegram support",
"value": "qlockify_support",
"href": "https://t.me/qlockify_support"
},
{
"label": "Telegram channel",
"value": "qlockify",
"href": "https://t.me/qlockify"
},
{
"label": "Support email",
"value": "qlockify@gmail.com",
"href": "mailto:qlockify@gmail.com"
},
{
"label": "Mobile (message or call)",
"value": "09938228438",
"href": "tel:09938228438"
}
]
},
"cta": { "cta": {
"title": "Start with one workspace and make time easier to explain.", "title": "Start with one workspace and make time easier to explain.",
"description": "Open Qlockify, track a real workday, and compare the report with the way your team reviews work today.", "description": "Open Qlockify, track a real workday, and compare the report with the way your team reviews work today.",
@@ -135,6 +178,49 @@
"دسترسی پروژه و نقش کاربران کمک می‌کند هر فرد فقط چیزی را ببیند یا استفاده کند که باید.", "دسترسی پروژه و نقش کاربران کمک می‌کند هر فرد فقط چیزی را ببیند یا استفاده کند که باید.",
"خروجی‌ها و لاگ‌ها برای ساده‌تر کردن بررسی طراحی شده‌اند، نه برای پنهان کردن جزئیات مهم." "خروجی‌ها و لاگ‌ها برای ساده‌تر کردن بررسی طراحی شده‌اند، نه برای پنهان کردن جزئیات مهم."
], ],
"contact": {
"eyebrow": "تماس",
"title": "کمک می‌خواهید یا می‌خواهید درباره Qlockify صحبت کنیم؟",
"description": "یک پیام بفرستید یا از مسیرهای پشتیبانی زیر با ما در ارتباط باشید. فرم یک پیش‌نویس ایمیل با پیام شما آماده می‌کند تا قبل از ارسال آن را بررسی کنید.",
"formTitle": "ارسال پیام",
"fields": {
"firstName": "نام",
"lastName": "نام خانوادگی",
"email": "ایمیل",
"mobile": "موبایل",
"message": "پیام"
},
"placeholders": {
"firstName": "نام شما",
"lastName": "نام خانوادگی شما",
"email": "you@example.com",
"mobile": "09...",
"message": "بگویید برای چه چیزی به کمک نیاز دارید..."
},
"submit": "آماده‌سازی ایمیل",
"channels": [
{
"label": "پشتیبانی تلگرام",
"value": "qlockify_support",
"href": "https://t.me/qlockify_support"
},
{
"label": "کانال تلگرام",
"value": "qlockify",
"href": "https://t.me/qlockify"
},
{
"label": "ایمیل پشتیبانی",
"value": "qlockify@gmail.com",
"href": "mailto:qlockify@gmail.com"
},
{
"label": "موبایل (پیام یا تماس)",
"value": "09938228438",
"href": "tel:09938228438"
}
]
},
"cta": { "cta": {
"title": "با یک ورک‌اسپیس شروع کنید و توضیح زمان را ساده‌تر کنید.", "title": "با یک ورک‌اسپیس شروع کنید و توضیح زمان را ساده‌تر کنید.",
"description": "Qlockify را باز کنید، یک روز کاری واقعی را ثبت کنید و گزارش آن را با روش فعلی مرور کار در تیم مقایسه کنید.", "description": "Qlockify را باز کنید، یک روز کاری واقعی را ثبت کنید و گزارش آن را با روش فعلی مرور کار در تیم مقایسه کنید.",

View File

@@ -1,7 +1,9 @@
import { useState, type FormEvent } from "react"
import { Link, useNavigate } from "react-router-dom" import { Link, useNavigate } from "react-router-dom"
import { import {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
AtSign,
BarChart3, BarChart3,
CheckCircle2, CheckCircle2,
Command, Command,
@@ -9,7 +11,11 @@ import {
Globe2, Globe2,
Layers3, Layers3,
LockKeyhole, LockKeyhole,
Mail,
MessageCircle,
Moon, Moon,
Phone,
Send,
ShieldCheck, ShieldCheck,
Sparkles, Sparkles,
Sun, Sun,
@@ -19,6 +25,8 @@ import {
} from "lucide-react" } from "lucide-react"
import { Button } from "../components/ui/button" import { Button } from "../components/ui/button"
import { Input } from "../components/ui/input"
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 aboutContent from "../content/about.json" import aboutContent from "../content/about.json"
@@ -29,11 +37,19 @@ type AboutContent = typeof aboutContent.en
const sectionIcons = [Sparkles, ShieldCheck, Layers3] const sectionIcons = [Sparkles, ShieldCheck, Layers3]
const principleIcons = [TimerReset, Waypoints, BarChart3] const principleIcons = [TimerReset, Waypoints, BarChart3]
const capabilityIcons = [TimerReset, Users, FileText, LockKeyhole] const capabilityIcons = [TimerReset, Users, FileText, LockKeyhole]
const contactIcons = [MessageCircle, MessageCircle, Mail, Phone]
export default function About() { export default function About() {
const navigate = useNavigate() const navigate = useNavigate()
const { t, lang, setLanguage } = useTranslation() const { t, lang, setLanguage } = useTranslation()
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const [contactForm, setContactForm] = useState({
firstName: "",
lastName: "",
email: "",
mobile: "",
message: "",
})
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")
@@ -42,6 +58,27 @@ export default function About() {
(theme === "system" && document.documentElement.classList.contains("dark")) (theme === "system" && document.documentElement.classList.contains("dark"))
const ctaTarget = isAuthenticated ? "/timesheet" : "/auth" const ctaTarget = isAuthenticated ? "/timesheet" : "/auth"
const updateContactField = (field: keyof typeof contactForm, value: string) => {
setContactForm((current) => ({ ...current, [field]: value }))
}
const submitContactForm = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const subject = encodeURIComponent("Qlockify contact request")
const body = encodeURIComponent(
[
`First name: ${contactForm.firstName}`,
`Last name: ${contactForm.lastName}`,
`Email: ${contactForm.email}`,
`Mobile: ${contactForm.mobile}`,
"",
"Message:",
contactForm.message,
].join("\n"),
)
window.location.href = `mailto:qlockify@gmail.com?subject=${subject}&body=${body}`
}
return ( return (
<div className="scroll-smooth min-h-screen overflow-x-hidden bg-[radial-gradient(circle_at_top,#e0f2fe_0%,#f8fafc_36%,#eef2ff_100%)] text-slate-950 dark:bg-[radial-gradient(circle_at_top,#082f49_0%,#020617_40%,#020617_100%)] dark:text-slate-50"> <div className="scroll-smooth min-h-screen overflow-x-hidden bg-[radial-gradient(circle_at_top,#e0f2fe_0%,#f8fafc_36%,#eef2ff_100%)] text-slate-950 dark:bg-[radial-gradient(circle_at_top,#082f49_0%,#020617_40%,#020617_100%)] dark:text-slate-50">
<div className="landing-aurora pointer-events-none fixed inset-0 opacity-80" /> <div className="landing-aurora pointer-events-none fixed inset-0 opacity-80" />
@@ -291,6 +328,132 @@ export default function About() {
</div> </div>
</section> </section>
<section className="grid gap-6 py-8 lg:grid-cols-[0.95fr_1.05fr]">
<div className="animate-landing-rise rounded-[2rem] border border-white/70 bg-white/80 p-7 shadow-[0_30px_80px_-50px_rgba(15,23,42,0.6)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/65">
<div className="text-sm font-semibold uppercase tracking-[0.2em] text-cyan-700 dark:text-cyan-300">
{content.contact.eyebrow}
</div>
<h2 className="mt-4 text-4xl font-semibold tracking-[-0.05em] text-slate-950 dark:text-white">
{content.contact.title}
</h2>
<p className="mt-4 text-base leading-8 text-slate-600 dark:text-slate-300">
{content.contact.description}
</p>
<div className="mt-7 grid gap-3">
{content.contact.channels.map((channel, index) => {
const Icon = contactIcons[index] ?? AtSign
return (
<a
key={channel.label}
href={channel.href}
target={channel.href.startsWith("http") ? "_blank" : undefined}
rel={channel.href.startsWith("http") ? "noreferrer" : undefined}
className="group flex items-center justify-between gap-4 rounded-2xl border border-slate-200/80 bg-white/80 p-4 text-start shadow-sm transition hover:border-cyan-300 hover:bg-cyan-50/70 dark:border-slate-800 dark:bg-slate-900/75 dark:hover:border-cyan-500/40 dark:hover:bg-cyan-950/30"
>
<span className="flex min-w-0 items-center gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-slate-950 text-white dark:bg-cyan-400 dark:text-slate-950">
<Icon className="h-5 w-5" />
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-slate-950 dark:text-white">
{channel.label}
</span>
<span className="block truncate text-sm text-slate-600 dark:text-slate-300">
{channel.value}
</span>
</span>
</span>
{lang === "fa" ? (
<ArrowLeft className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:text-cyan-600 dark:group-hover:text-cyan-300" />
) : (
<ArrowRight className="h-4 w-4 shrink-0 text-slate-400 transition group-hover:text-cyan-600 dark:group-hover:text-cyan-300" />
)}
</a>
)
})}
</div>
</div>
<form
onSubmit={submitContactForm}
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">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-slate-950 text-white dark:bg-cyan-400 dark:text-slate-950">
<Send className="h-5 w-5" />
</span>
<h2 className="text-2xl font-semibold tracking-[-0.04em] text-slate-950 dark:text-white">
{content.contact.formTitle}
</h2>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
{content.contact.fields.firstName}
<Input
value={contactForm.firstName}
onChange={(event) => updateContactField("firstName", event.target.value)}
placeholder={content.contact.placeholders.firstName}
className="mt-2 h-12 rounded-2xl"
required
/>
</label>
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
{content.contact.fields.lastName}
<Input
value={contactForm.lastName}
onChange={(event) => updateContactField("lastName", event.target.value)}
placeholder={content.contact.placeholders.lastName}
className="mt-2 h-12 rounded-2xl"
required
/>
</label>
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
{content.contact.fields.email}
<Input
type="email"
value={contactForm.email}
onChange={(event) => updateContactField("email", event.target.value)}
placeholder={content.contact.placeholders.email}
className="mt-2 h-12 rounded-2xl"
required
/>
</label>
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
{content.contact.fields.mobile}
<Input
value={contactForm.mobile}
onChange={(event) => updateContactField("mobile", event.target.value)}
placeholder={content.contact.placeholders.mobile}
className="mt-2 h-12 rounded-2xl"
required
/>
</label>
</div>
<label className="mt-4 block text-sm font-medium text-slate-700 dark:text-slate-300">
{content.contact.fields.message}
<TextAreaInput
value={contactForm.message}
onChange={(event) => updateContactField("message", event.target.value)}
placeholder={content.contact.placeholders.message}
className="mt-2 min-h-40 rounded-2xl"
required
/>
</label>
<Button
type="submit"
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" />
{content.contact.submit}
</Button>
</form>
</section>
<section className="py-8"> <section className="py-8">
<div className="relative overflow-hidden rounded-[2.5rem] border border-slate-950/5 bg-slate-950 px-6 py-10 text-white shadow-[0_40px_100px_-40px_rgba(15,23,42,0.8)] dark:border-white/10 sm:px-10"> <div className="relative overflow-hidden rounded-[2.5rem] border border-slate-950/5 bg-slate-950 px-6 py-10 text-white shadow-[0_40px_100px_-40px_rgba(15,23,42,0.8)] dark:border-white/10 sm:px-10">
<div className="pointer-events-none absolute inset-y-0 right-0 w-[45%] bg-[radial-gradient(circle_at_top_right,rgba(34,211,238,0.35),transparent_55%),radial-gradient(circle_at_bottom_right,rgba(16,185,129,0.24),transparent_45%)]" /> <div className="pointer-events-none absolute inset-y-0 right-0 w-[45%] bg-[radial-gradient(circle_at_top_right,rgba(34,211,238,0.35),transparent_55%),radial-gradient(circle_at_bottom_right,rgba(16,185,129,0.24),transparent_45%)]" />