feat(about): add static public about page

This commit is contained in:
2026-06-06 23:32:55 +03:30
parent ef3eaf1206
commit 870d198cc8
3 changed files with 468 additions and 0 deletions

144
src/content/about.json Normal file
View File

@@ -0,0 +1,144 @@
{
"en": {
"eyebrow": "About Qlockify",
"hero": {
"title": "A focused operating layer for time, work, and accountability.",
"accent": "Qlockify turns daily work into context your team can trust.",
"description": "Qlockify is built for teams that need more than a stopwatch. It connects time entries to workspaces, projects, clients, tags, rates, exports, and activity logs so time becomes useful operational data."
},
"sections": [
{
"title": "Why it exists",
"description": "Most teams lose context between the moment work happens and the moment reports are reviewed. Qlockify keeps that context attached from the first timer click to the final exported report."
},
{
"title": "What it protects",
"description": "The product is designed to reduce missing billable work, unclear project history, manual spreadsheet cleanup, and undocumented workspace changes."
},
{
"title": "How it feels",
"description": "The interface stays minimal because time tracking should not become another heavy workflow. Teams can record work quickly while managers still receive structured, reviewable data."
}
],
"principles": [
{
"title": "Capture first",
"description": "Recording work should be fast enough to happen at the source, with project and billing context attached immediately."
},
{
"title": "Explain every change",
"description": "Workspace roles, rates, access changes, and report actions should remain visible instead of disappearing into chat or memory."
},
{
"title": "Make reports usable",
"description": "Reports should be ready for review, export, and decision-making without rebuilding them in spreadsheets."
}
],
"capabilities": [
{
"title": "Time tracking",
"description": "Run timers, adjust historical entries, and keep work tied to the correct project, client, and tags."
},
{
"title": "Workspace control",
"description": "Manage members, roles, project access, and rates without making everyday users deal with unnecessary complexity."
},
{
"title": "Operational reports",
"description": "Review daily summaries, project distribution, billable work, and exportable reports for management or client review."
},
{
"title": "Activity history",
"description": "Keep a clear trail of important actions so teams can understand what changed and when."
}
],
"audience": [
"Agencies",
"Consulting teams",
"Product teams",
"Operations teams",
"Client-service businesses"
],
"trust": [
"Data is organized around workspaces so each team can keep its own operational context.",
"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."
],
"cta": {
"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.",
"button": "Open Qlockify"
}
},
"fa": {
"eyebrow": "درباره Qlockify",
"hero": {
"title": "یک لایه عملیاتی متمرکز برای زمان، کار و پاسخ‌گویی.",
"accent": "Qlockify کار روزانه را به داده‌ای تبدیل می‌کند که تیم بتواند به آن اعتماد کند.",
"description": "Qlockify برای تیم‌هایی ساخته شده که فقط یک تایمر ساده نمی‌خواهند. این محصول ورودی‌های زمان را به ورک‌اسپیس، پروژه، مشتری، تگ، نرخ، خروجی گزارش و لاگ فعالیت‌ها وصل می‌کند تا زمان به داده عملیاتی قابل استفاده تبدیل شود."
},
"sections": [
{
"title": "چرا ساخته شده است",
"description": "بیشتر تیم‌ها بین لحظه انجام کار و زمان مرور گزارش‌ها، بستر و جزئیات مهم را از دست می‌دهند. Qlockify این بستر را از اولین کلیک تایمر تا گزارش خروجی نهایی نگه می‌دارد."
},
{
"title": "چه چیزی را حفظ می‌کند",
"description": "محصول برای کاهش کارکرد ثبت‌نشده، تاریخچه نامشخص پروژه، پاک‌سازی دستی فایل‌های گزارش و تغییرات ثبت‌نشده در ورک‌اسپیس طراحی شده است."
},
{
"title": "چه حسی دارد",
"description": "رابط کاربری مینیمال می‌ماند، چون ردیابی زمان نباید خودش به یک فرایند سنگین تبدیل شود. تیم می‌تواند سریع کار را ثبت کند و مدیر همچنان داده ساختارمند و قابل بررسی داشته باشد."
}
],
"principles": [
{
"title": "اول ثبت دقیق",
"description": "ثبت کار باید آن‌قدر سریع باشد که در همان لحظه انجام شود و پروژه و بستر مالی همان ابتدا به آن وصل شود."
},
{
"title": "هر تغییر قابل توضیح",
"description": "نقش‌ها، نرخ‌ها، دسترسی پروژه‌ها و عملیات گزارش باید قابل مشاهده بمانند، نه این‌که در چت یا حافظه افراد گم شوند."
},
{
"title": "گزارش قابل استفاده",
"description": "گزارش باید برای بررسی، خروجی گرفتن و تصمیم‌گیری آماده باشد، بدون این‌که دوباره در فایل‌های اکسل ساخته شود."
}
],
"capabilities": [
{
"title": "ردیابی زمان",
"description": "تایمر اجرا کنید، ورودی‌های گذشته را اصلاح کنید و کار را به پروژه، مشتری و تگ درست وصل نگه دارید."
},
{
"title": "کنترل ورک‌اسپیس",
"description": "اعضا، نقش‌ها، دسترسی پروژه‌ها و نرخ‌ها را مدیریت کنید، بدون این‌که کاربران روزمره درگیر پیچیدگی اضافه شوند."
},
{
"title": "گزارش عملیاتی",
"description": "خلاصه روزانه، توزیع پروژه‌ها، کارکرد قابل صورت‌حساب و گزارش‌های خروجی را برای مدیریت یا مرور مشتری بررسی کنید."
},
{
"title": "تاریخچه فعالیت‌ها",
"description": "رد روشنی از عملیات مهم نگه دارید تا تیم بداند چه چیزی، چه زمانی و توسط چه کسی تغییر کرده است."
}
],
"audience": [
"آژانس‌ها",
"تیم‌های مشاوره",
"تیم‌های محصول",
"تیم‌های عملیات",
"کسب‌وکارهای مشتری‌محور"
],
"trust": [
"داده‌ها حول ورک‌اسپیس سازمان‌دهی می‌شوند تا هر تیم بستر عملیاتی خودش را داشته باشد.",
"دسترسی پروژه و نقش کاربران کمک می‌کند هر فرد فقط چیزی را ببیند یا استفاده کند که باید.",
"خروجی‌ها و لاگ‌ها برای ساده‌تر کردن بررسی طراحی شده‌اند، نه برای پنهان کردن جزئیات مهم."
],
"cta": {
"title": "با یک ورک‌اسپیس شروع کنید و توضیح زمان را ساده‌تر کنید.",
"description": "Qlockify را باز کنید، یک روز کاری واقعی را ثبت کنید و گزارش آن را با روش فعلی مرور کار در تیم مقایسه کنید.",
"button": "باز کردن Qlockify"
}
}
}

323
src/pages/About.tsx Normal file
View File

@@ -0,0 +1,323 @@
import { Link, useNavigate } from "react-router-dom"
import {
ArrowLeft,
ArrowRight,
BarChart3,
CheckCircle2,
Command,
FileText,
Globe2,
Layers3,
LockKeyhole,
Moon,
ShieldCheck,
Sparkles,
Sun,
TimerReset,
Users,
Waypoints,
} from "lucide-react"
import { Button } from "../components/ui/button"
import { useTheme } from "../components/ThemeProvider"
import { useTranslation } from "../hooks/useTranslation"
import aboutContent from "../content/about.json"
import { cn } from "../lib/utils"
type AboutContent = typeof aboutContent.en
const sectionIcons = [Sparkles, ShieldCheck, Layers3]
const principleIcons = [TimerReset, Waypoints, BarChart3]
const capabilityIcons = [TimerReset, Users, FileText, LockKeyhole]
export default function About() {
const navigate = useNavigate()
const { t, lang, setLanguage } = useTranslation()
const { theme, setTheme } = useTheme()
const content = aboutContent[lang] as AboutContent
const isAuthenticated = typeof window !== "undefined" && !!localStorage.getItem("accessToken")
const isDarkMode =
theme === "dark" ||
(theme === "system" && document.documentElement.classList.contains("dark"))
const ctaTarget = isAuthenticated ? "/timesheet" : "/auth"
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="landing-aurora pointer-events-none fixed inset-0 opacity-80" />
<div className="landing-hero-grid pointer-events-none fixed inset-0 opacity-70 dark:opacity-40" />
<div className="pointer-events-none fixed left-[-12rem] top-20 h-80 w-80 rounded-full bg-cyan-400/20 blur-3xl dark:bg-cyan-500/10" />
<div className="pointer-events-none fixed right-[-10rem] top-52 h-72 w-72 rounded-full bg-emerald-300/20 blur-3xl dark:bg-emerald-400/10" />
<div className="relative mx-auto flex min-h-screen max-w-7xl flex-col px-4 pb-14 pt-5 sm:px-6 lg:px-8">
<header className="animate-landing-rise flex items-center justify-between rounded-full border border-white/70 bg-white/75 px-4 py-3 shadow-[0_20px_60px_-36px_rgba(15,23,42,0.45)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/55">
<button
type="button"
onClick={() => navigate("/")}
className="inline-flex items-center gap-3 rounded-full px-2 py-1 text-left"
>
<span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-slate-950 text-white shadow-lg shadow-cyan-500/20 dark:bg-white dark:text-slate-950">
<Command className="h-5 w-5" />
</span>
<span className="hidden sm:block">
<span className="block text-lg font-semibold">{t.title}</span>
</span>
</button>
<nav className="hidden items-center gap-2 md:flex">
<Link
to="/"
className="rounded-full px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white"
>
{lang === "fa" ? "خانه" : "Home"}
</Link>
<Link
to="/about"
className="rounded-full bg-slate-950 px-4 py-2 text-sm font-medium text-white shadow-sm dark:bg-white dark:text-slate-950"
>
{lang === "fa" ? "درباره ما" : "About us"}
</Link>
</nav>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setLanguage(lang === "fa" ? "en" : "fa")}
className="inline-flex h-11 items-center gap-2 rounded-full border border-slate-200/80 bg-white/80 px-4 text-sm font-medium text-slate-700 transition hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-950/80 dark:text-slate-200 dark:hover:bg-slate-900"
>
<Globe2 className="h-4 w-4" />
{lang === "fa" ? t.landing.actions.switchToEnglish : t.landing.actions.switchToPersian}
</button>
<button
type="button"
onClick={() => setTheme(isDarkMode ? "light" : "dark")}
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-slate-200/80 bg-white/80 text-slate-700 transition hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-950/80 dark:text-slate-200 dark:hover:bg-slate-900"
aria-label={isDarkMode ? t.lightMode : t.darkMode}
>
{isDarkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button>
</div>
</header>
<main className="flex-1">
<section className="grid items-center gap-10 py-12 lg:grid-cols-[1.05fr_0.95fr] lg:py-20">
<div className="space-y-7">
<div className="animate-landing-rise">
<div className="mb-5 inline-flex items-center gap-2 rounded-full border border-cyan-200/70 bg-white/75 px-4 py-2 text-sm font-medium text-cyan-900 shadow-sm backdrop-blur dark:border-cyan-500/20 dark:bg-cyan-500/10 dark:text-cyan-100">
<Sparkles className="h-4 w-4" />
{content.eyebrow}
</div>
<h1 className="max-w-4xl text-4xl font-semibold leading-[1.08] text-slate-950 sm:text-5xl lg:text-6xl dark:text-white">
{content.hero.title}
<span className="landing-shimmer mt-4 block bg-[linear-gradient(120deg,#0f172a_15%,#0891b2_48%,#0f766e_78%,#0f172a_100%)] bg-clip-text pb-4 text-transparent dark:bg-[linear-gradient(120deg,#ffffff_18%,#67e8f9_48%,#2dd4bf_78%,#ffffff_100%)]">
{content.hero.accent}
</span>
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600 dark:text-slate-300 sm:text-xl">
{content.hero.description}
</p>
</div>
<div className="animate-landing-rise flex flex-col gap-3 sm:flex-row [animation-delay:140ms]">
<Button
asChild
className="h-14 rounded-full bg-slate-950 px-7 text-base text-white shadow-[0_30px_80px_-28px_rgba(8,145,178,0.45)] hover:bg-slate-800 dark:bg-cyan-400 dark:text-slate-950 dark:hover:bg-cyan-300"
>
<Link to={ctaTarget}>
{content.cta.button}
<ArrowRight className={cn("ms-2 h-4 w-4", lang === "fa" && "rotate-180")} />
</Link>
</Button>
<Button
asChild
variant="outline"
className="h-14 rounded-full border-slate-200 bg-white/85 px-7 text-base text-slate-800 shadow-sm backdrop-blur hover:bg-white dark:border-slate-800 dark:bg-slate-950/70 dark:text-slate-100 dark:hover:bg-slate-900"
>
<Link to="/">
{lang === "fa" ? "بازگشت به صفحه اصلی" : "Back to home"}
{lang === "fa" ? <ArrowLeft className="ms-2 h-4 w-4" /> : <ArrowRight className="ms-2 h-4 w-4" />}
</Link>
</Button>
</div>
</div>
<div className="animate-landing-rise [animation-delay:180ms]">
<div className="relative overflow-hidden rounded-[2rem] border border-white/70 bg-white/80 p-5 shadow-[0_45px_110px_-48px_rgba(15,23,42,0.6)] backdrop-blur-2xl dark:border-white/10 dark:bg-slate-950/70 sm:p-6">
<div className="absolute inset-x-0 top-0 h-24 bg-[linear-gradient(90deg,rgba(34,211,238,0.16),rgba(16,185,129,0.12),rgba(245,158,11,0.12))]" />
<div className="relative">
<div className="mb-6 flex items-center justify-between">
<div>
<div className="text-xs uppercase tracking-[0.22em] text-slate-400 dark:text-slate-500">
{lang === "fa" ? "مدل محصول" : "Product model"}
</div>
<div className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950 dark:text-white">
{lang === "fa" ? "از ثبت تا تصمیم" : "From capture to decision"}
</div>
</div>
<div className="rounded-full border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-200">
{lang === "fa" ? "شفاف" : "Clear"}
</div>
</div>
<div className="space-y-3">
{content.sections.map((section, index) => {
const Icon = sectionIcons[index] ?? Sparkles
return (
<div
key={section.title}
className="rounded-[1.5rem] border border-slate-200/80 bg-white/80 p-4 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/80"
>
<div className="flex items-start gap-4">
<div className="flex h-11 w-11 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" />
</div>
<div>
<h2 className="text-lg font-semibold tracking-[-0.03em] text-slate-950 dark:text-white">
{section.title}
</h2>
<p className="mt-2 text-sm leading-7 text-slate-600 dark:text-slate-300">
{section.description}
</p>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
</section>
<section className="grid gap-4 py-8 md:grid-cols-3">
{content.principles.map((principle, index) => {
const Icon = principleIcons[index] ?? CheckCircle2
return (
<article
key={principle.title}
className="animate-landing-rise rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-[0_30px_80px_-50px_rgba(15,23,42,0.6)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/65"
style={{ animationDelay: `${index * 120}ms` }}
>
<div className="flex h-12 w-12 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" />
</div>
<h2 className="mt-5 text-2xl font-semibold tracking-[-0.04em] text-slate-950 dark:text-white">
{principle.title}
</h2>
<p className="mt-3 text-base leading-7 text-slate-600 dark:text-slate-300">
{principle.description}
</p>
</article>
)
})}
</section>
<section className="grid gap-6 py-8 lg:grid-cols-[0.9fr_1.1fr]">
<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">
{lang === "fa" ? "برای چه تیم‌هایی" : "Who it serves"}
</div>
<h2 className="mt-4 text-4xl font-semibold tracking-[-0.05em] text-slate-950 dark:text-white">
{lang === "fa" ? "برای تیم‌هایی که زمان را بخشی از عملیات می‌دانند." : "For teams that treat time as part of operations."}
</h2>
<div className="mt-6 flex flex-wrap gap-3">
{content.audience.map((item) => (
<span
key={item}
className="rounded-full border border-cyan-200/70 bg-cyan-50/70 px-4 py-2 text-sm font-medium text-cyan-900 backdrop-blur dark:border-cyan-500/20 dark:bg-cyan-500/10 dark:text-cyan-100"
>
{item}
</span>
))}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{content.capabilities.map((capability, index) => {
const Icon = capabilityIcons[index] ?? Waypoints
return (
<article
key={capability.title}
className="animate-landing-rise rounded-[2rem] border border-white/70 bg-gradient-to-br from-white/95 to-slate-50/80 p-6 shadow-[0_26px_70px_-48px_rgba(15,23,42,0.65)] backdrop-blur-xl dark:border-white/10 dark:from-slate-950/80 dark:to-slate-900/55"
style={{ animationDelay: `${index * 90}ms` }}
>
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-slate-950 text-white dark:bg-white dark:text-slate-950">
<Icon className="h-5 w-5" />
</div>
<h3 className="text-xl font-semibold tracking-[-0.04em] text-slate-950 dark:text-white">
{capability.title}
</h3>
</div>
<p className="mt-4 text-sm leading-7 text-slate-600 dark:text-slate-300">
{capability.description}
</p>
</article>
)
})}
</div>
</section>
<section className="py-8">
<div className="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="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="text-sm font-semibold uppercase tracking-[0.2em] text-cyan-700 dark:text-cyan-300">
{lang === "fa" ? "اعتماد و کنترل" : "Trust and control"}
</div>
<h2 className="mt-3 text-3xl font-semibold tracking-[-0.05em] text-slate-950 dark:text-white">
{lang === "fa" ? "جزئیات مهم باید قابل بررسی بمانند." : "Important details should remain reviewable."}
</h2>
</div>
</div>
<div className="grid gap-3 md:grid-cols-3">
{content.trust.map((item, index) => (
<div
key={item}
className="rounded-[1.5rem] border border-slate-200/80 bg-white/80 p-4 text-sm leading-7 text-slate-600 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/80 dark:text-slate-300"
>
<CheckCircle2 className="mb-4 h-5 w-5 text-emerald-500" />
{item}
<div className="mt-5 text-xs font-semibold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
{lang === "fa" ? `نکته ${new Intl.NumberFormat("fa-IR").format(index + 1)}` : `Note ${index + 1}`}
</div>
</div>
))}
</div>
</div>
</section>
<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="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="relative flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<div className="text-sm font-semibold uppercase tracking-[0.2em] text-cyan-300">
{lang === "fa" ? "شروع ساده" : "Simple start"}
</div>
<h2 className="mt-4 text-4xl font-semibold leading-[1.1] tracking-[-0.05em] sm:text-5xl">
{content.cta.title}
</h2>
<p className="mt-4 text-lg leading-8 text-slate-300">{content.cta.description}</p>
</div>
<Button
asChild
className="h-14 rounded-full bg-white px-7 text-base font-semibold text-slate-950 hover:bg-slate-100"
>
<Link to={ctaTarget}>
{content.cta.button}
<ArrowRight className={cn("ms-2 h-4 w-4", lang === "fa" && "rotate-180")} />
</Link>
</Button>
</div>
</div>
</section>
</main>
</div>
</div>
)
}

View File

@@ -11,6 +11,7 @@
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,