diff --git a/src/App.tsx b/src/App.tsx index ea48da3..17d49a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ import Timesheet from "./pages/Timesheet" import Logs from "./pages/Logs" import NotificationsPage from "./pages/Notifications" import RateLimitPage from "./pages/RateLimit" +import Landing from "./pages/Landing" import { isRateLimitActive } from "./lib/rateLimit" const MainLayout = () => { @@ -47,7 +48,7 @@ const MainLayout = () => { ); }; -const RootRedirect = () => { +const AppRedirect = () => { if (isRateLimitActive()) { return } @@ -77,7 +78,8 @@ const router = createBrowserRouter([ ), children: [ - { path: "/", element: }, + { path: "/", element: }, + { path: "/app", element: }, { path: "/auth", element: }, { path: "/terms", element: }, { path: "/rate-limit", element: }, diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 726d94b..1b2ba64 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -14,7 +14,7 @@ import { History, Tags, } from 'lucide-react'; -import { useWorkspace } from '../context/WorkspaceContext'; +import { useOptionalWorkspace } from '../context/WorkspaceContext'; import { useTranslation } from '../hooks/useTranslation'; import { canWorkspace, WORKSPACE_LOGS_VIEW } from '../lib/permissions'; @@ -27,7 +27,8 @@ export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) => const [isCollapsed, setIsCollapsed] = useState(false); const { t, lang } = useTranslation(); - const { activeWorkspace } = useWorkspace(); + const workspaceContext = useOptionalWorkspace(); + const activeWorkspace = workspaceContext?.activeWorkspace ?? null; const isRtl = lang === 'fa'; const canViewLogs = canWorkspace(activeWorkspace?.my_role, WORKSPACE_LOGS_VIEW); diff --git a/src/context/WorkspaceContext.tsx b/src/context/WorkspaceContext.tsx index 809915e..8d57b40 100644 --- a/src/context/WorkspaceContext.tsx +++ b/src/context/WorkspaceContext.tsx @@ -15,13 +15,15 @@ interface WorkspaceContextType { isLoading: boolean } -const WorkspaceContext = createContext(undefined) - -export const useWorkspace = () => { - const context = useContext(WorkspaceContext) - if (!context) throw new Error("useWorkspace must be used within a WorkspaceProvider") - return context -} +const WorkspaceContext = createContext(undefined) + +export const useWorkspace = () => { + const context = useContext(WorkspaceContext) + if (!context) throw new Error("useWorkspace must be used within a WorkspaceProvider") + return context +} + +export const useOptionalWorkspace = () => useContext(WorkspaceContext) export const WorkspaceProvider = ({ children }: { children: ReactNode }) => { const { t } = useTranslation() diff --git a/src/index.css b/src/index.css index 259812c..b8dd787 100644 --- a/src/index.css +++ b/src/index.css @@ -83,6 +83,93 @@ line-height: 1.75rem !important; } } + + @keyframes landing-rise { + from { + opacity: 0; + transform: translateY(28px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes landing-float { + 0%, + 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-14px); + } + } + + @keyframes landing-grid { + from { + transform: translate3d(0, 0, 0); + } + to { + transform: translate3d(0, 36px, 0); + } + } + + @keyframes landing-aurora { + 0%, + 100% { + opacity: 0.8; + transform: translate3d(0, 0, 0) scale(1); + } + 50% { + opacity: 1; + transform: translate3d(0, -1%, 0) scale(1.04); + } + } + + @keyframes landing-shimmer { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 100% 50%; + } + } + + .animate-landing-rise { + animation: landing-rise 0.9s cubic-bezier(0.22, 1, 0.36, 1) both; + } + + .animate-landing-float { + animation: landing-float 6s ease-in-out infinite; + } + + .landing-hero-grid { + background-image: + linear-gradient(to right, rgba(15, 23, 42, 0.06) 1px, transparent 1px), + linear-gradient(to bottom, rgba(15, 23, 42, 0.06) 1px, transparent 1px); + background-size: 72px 72px; + mask-image: radial-gradient(circle at top, rgba(0, 0, 0, 0.95), transparent 78%); + animation: landing-grid 16s linear infinite; + } + + .dark .landing-hero-grid { + background-image: + linear-gradient(to right, rgba(148, 163, 184, 0.09) 1px, transparent 1px), + linear-gradient(to bottom, rgba(148, 163, 184, 0.09) 1px, transparent 1px); + } + + .landing-aurora { + background: + radial-gradient(circle at 10% 10%, rgba(34, 211, 238, 0.18), transparent 34%), + radial-gradient(circle at 85% 18%, rgba(245, 158, 11, 0.18), transparent 28%), + radial-gradient(circle at 58% 34%, rgba(20, 184, 166, 0.12), transparent 30%); + animation: landing-aurora 14s ease-in-out infinite; + } + + .landing-shimmer { + background-size: 200% 200%; + animation: landing-shimmer 7s linear infinite; + } } @@ -120,6 +207,10 @@ scrollbar-color: #cbd5e1 transparent; } +html { + scroll-behavior: smooth; +} + .dark * { scrollbar-color: #334155 transparent; } diff --git a/src/locales/en.ts b/src/locales/en.ts index a3f9bdc..256ba8c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -291,20 +291,108 @@ export const en = { next: "Next", }, - sidebar: { - timesheet: "Timesheet", - reports: "Reports", - logs: "Logs", + sidebar: { + timesheet: "Timesheet", + reports: "Reports", + logs: "Logs", workspaces: 'Workspaces', clients: 'Clients', projects: "Projects", tags: "Tags", expand: 'Expand', - collapse: 'Collapse', - }, - - ordering: { - createdAtDesc: "Newest First", + collapse: 'Collapse', + }, + + landing: { + brandLabel: "Operating system for time", + eyebrow: "Built for high-discipline teams that need clean time intelligence", + nav: { + demo: "Product demo", + features: "Core capabilities", + workflow: "How it works", + }, + actions: { + switchToEnglish: "English", + switchToPersian: "فارسی", + signIn: "Sign in", + openApp: "Open app", + openWorkspace: "Open workspace", + startNow: "Start tracking with control", + watchDemo: "See the product demo", + readTerms: "Read terms", + }, + hero: { + titleTop: "Turn every working hour into a reliable operating signal.", + titleAccent: "Qlockify makes time visible, accountable, and billable.", + description: + "A focused workspace for modern teams that need fast time capture, trustworthy project tracking, structured reports, and a log trail that management can actually use.", + }, + metrics: { + capture: "cleaner billable capture", + visibility: "faster reporting visibility", + decision: "from raw entries to management context", + }, + trust: { + first: "Precise timers with manual control when needed", + second: "Workspace permissions, logs, and rate-aware reporting", + third: "Built for agencies, consultancies, product teams, and operators", + }, + capabilities: { + time: { + title: "Capture work without friction", + description: + "Start a timer, adjust historical entries, and keep project and tag context attached to every hour without slowing the team down.", + }, + reports: { + title: "Read the business in minutes", + description: + "See daily output, billable performance, project distribution, and exportable report packs without spreadsheet cleanup.", + }, + control: { + title: "Keep operations explainable", + description: + "Track who changed what, keep workspace roles explicit, and give management a cleaner operational trail than ad hoc chat or manual files.", + }, + }, + demo: { + timerTag: "Live timer", + timerTitle: "Current execution window", + timerText: "Design system refinement synced to the correct project, tags, and billable rate.", + panelLabel: "Interactive product preview", + panelTitle: "One surface for tracking, reporting, and operational clarity", + runningCard: "Active entry", + currentTask: "Enterprise landing page rollout", + currentTaskMeta: "Project: Qlockify Marketing · Tags: Design, Review, Delivery", + billableLabel: "Live billable rate", + reportCard: "Daily report trend", + opsCard: "Operational health", + opsLabels: ["Coverage", "Team focus", "Billing readiness"], + logCard: "Recent workspace activity", + logItems: [ + { title: "Rate updated for product design", meta: "Owner action · 3 minutes ago" }, + { title: "Client-facing project moved to archived", meta: "Admin action · 18 minutes ago" }, + { title: "Historic tag preserved on edited entry", meta: "Member action · 41 minutes ago" }, + ], + outcomeTag: "Management result", + outcomeText: "Less ambiguity at month end, fewer missing billable hours, and faster operational reviews.", + }, + workflowTag: "Operational workflow", + workflowTitle: "A tighter loop from raw effort to usable management data.", + workflowDescription: + "Qlockify is designed to keep the path short: capture accurately, structure context once, and reuse the result everywhere from timesheets to reports to workspace-level decisions.", + workflow: { + capture: "Capture time at the source with project, tags, and billing context attached immediately.", + structure: "Keep every workspace action, membership change, and rate update visible and reviewable.", + improve: "Review daily and monthly performance with reports that are ready to export or act on.", + }, + finalCtaTag: "Ready for production teams", + finalCtaTitle: "If your team sells expertise or ships client work, your time system should look this serious.", + finalCtaDescription: + "Open the app, create a workspace, and see how fast your reporting discipline improves when the product stops leaking context.", + }, + + ordering: { + createdAtDesc: "Newest First", createdAt: "Olders First", updatedAtDesc: "Recently Updated", name: "Name (A-Z)", diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 85025a6..9648716 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -300,7 +300,95 @@ export const fa = { collapse: 'جمع کردن', }, - ordering: { + landing: { + brandLabel: "زیرساخت عملیاتی زمان", + eyebrow: "طراحی‌شده برای تیم‌های دقیق که به داده زمانی قابل اتکا نیاز دارند", + nav: { + demo: "دموی محصول", + features: "قابلیت‌ها", + workflow: "فرآیند کار", + }, + actions: { + switchToEnglish: "English", + switchToPersian: "فارسی", + signIn: "ورود", + openApp: "ورود به اپ", + openWorkspace: "باز کردن ورک‌اسپیس", + startNow: "شروع با کنترل کامل", + watchDemo: "مشاهده دموی محصول", + readTerms: "مطالعه قوانین", + }, + hero: { + titleTop: "هر ساعت کاری را به یک سیگنال عملیاتی قابل اعتماد تبدیل کنید.", + titleAccent: "Qlockify زمان را شفاف، پاسخ‌گو و قابل‌صورتحساب می‌کند.", + description: + "یک محیط متمرکز برای تیم‌های مدرن که به ثبت سریع زمان، رهگیری دقیق پروژه، گزارش‌های قابل اتکا و لاگ عملیاتی واقعی برای مدیریت نیاز دارند.", + }, + metrics: { + capture: "ثبت تمیزتر ساعات قابل‌صورتحساب", + visibility: "دسترسی سریع‌تر به دید گزارش‌دهی", + decision: "از ورودی خام تا تصمیم مدیریتی", + }, + trust: { + first: "تایمر دقیق با امکان ویرایش دستی در زمان لازم", + second: "دسترسی‌ها، لاگ‌ها و گزارش‌های مبتنی بر نرخ", + third: "مناسب آژانس‌ها، شرکت‌های مشاوره، تیم‌های محصول و عملیات", + }, + capabilities: { + time: { + title: "ثبت کار بدون اصطکاک", + description: + "تایمر را شروع کنید، ورودی‌های گذشته را اصلاح کنید و پروژه و تگ را بدون ایجاد اصطکاک برای تیم به هر ساعت متصل نگه دارید.", + }, + reports: { + title: "کسب‌وکار را در چند دقیقه بخوانید", + description: + "خروجی روزانه، عملکرد قابل‌صورتحساب، توزیع پروژه‌ها و بسته‌های گزارشی قابل خروجی را بدون پاک‌سازی دستی فایل‌ها ببینید.", + }, + control: { + title: "عملیات را قابل توضیح نگه دارید", + description: + "ببینید چه کسی چه چیزی را تغییر داده، نقش‌ها را شفاف نگه دارید و برای مدیریت یک رد عملیاتی تمیزتر از چت و فایل دستی بسازید.", + }, + }, + demo: { + timerTag: "تایمر زنده", + timerTitle: "بازه اجرای فعلی", + timerText: "بهبود دیزاین سیستم، متصل به پروژه درست، تگ‌های صحیح و نرخ قابل‌صورتحساب.", + panelLabel: "پیش‌نمایش تعاملی محصول", + panelTitle: "یک سطح واحد برای رهگیری، گزارش‌دهی و شفافیت عملیاتی", + runningCard: "ورودی فعال", + currentTask: "پیاده‌سازی لندینگ سازمانی", + currentTaskMeta: "پروژه: بازاریابی Qlockify · تگ‌ها: طراحی، بازبینی، تحویل", + billableLabel: "نرخ زنده قابل‌صورتحساب", + reportCard: "روند گزارش روزانه", + opsCard: "سلامت عملیات", + opsLabels: ["پوشش", "تمرکز تیم", "آمادگی صورتحساب"], + logCard: "آخرین فعالیت‌های ورک‌اسپیس", + logItems: [ + { title: "نرخ تیم طراحی محصول به‌روزرسانی شد", meta: "اقدام مالک · ۳ دقیقه پیش" }, + { title: "پروژه مشتری‌محور بایگانی شد", meta: "اقدام ادمین · ۱۸ دقیقه پیش" }, + { title: "تگ تاریخی روی ورودی ویرایش‌شده حفظ شد", meta: "اقدام عضو · ۴۱ دقیقه پیش" }, + ], + outcomeTag: "خروجی مدیریتی", + outcomeText: "ابهام کمتر در پایان ماه، ساعات قابل‌صورتحساب از‌دست‌رفته کمتر و بازبینی عملیاتی سریع‌تر.", + }, + workflowTag: "فرآیند عملیاتی", + workflowTitle: "از تلاش خام تا داده مدیریتی قابل استفاده، با یک حلقه کوتاه‌تر.", + workflowDescription: + "Qlockify مسیر را کوتاه نگه می‌دارد: دقیق ثبت کنید، یک‌بار بستر را درست بسازید و همان نتیجه را در تایم‌شیت، گزارش و تصمیم‌گیری مدیریتی مصرف کنید.", + workflow: { + capture: "زمان را در مبدأ، همراه با پروژه، تگ و بستر مالی ثبت کنید.", + structure: "هر تغییر در اعضا، نرخ‌ها و تنظیمات ورک‌اسپیس را قابل مشاهده و قابل بررسی نگه دارید.", + improve: "عملکرد روزانه و ماهانه را با گزارش‌هایی بخوانید که آماده خروجی و اقدام هستند.", + }, + finalCtaTag: "آماده برای تیم‌های جدی", + finalCtaTitle: "اگر تیم شما تخصص می‌فروشد یا پروژه مشتری تحویل می‌دهد، سیستم زمان شما هم باید همین‌قدر جدی باشد.", + finalCtaDescription: + "اپ را باز کنید، ورک‌اسپیس بسازید و ببینید وقتی محصول نشت بستر را متوقف می‌کند، انضباط گزارش‌دهی چقدر سریع بهتر می‌شود.", + }, + + ordering: { createdAtDesc: "جدیدترین", createdAt: "قدیمی‌ترین", updatedAtDesc: "اخیراً بروزرسانی شده", diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx new file mode 100644 index 0000000..821c983 --- /dev/null +++ b/src/pages/Landing.tsx @@ -0,0 +1,418 @@ +import { useMemo } from "react" +import { Link, useNavigate } from "react-router-dom" +import { + ArrowRight, + BarChart3, + CheckCircle2, + Clock3, + Command, + Globe2, + Layers3, + Moon, + ShieldCheck, + Sparkles, + Sun, + TimerReset, + Waypoints, +} from "lucide-react" + +import { Button } from "../components/ui/button" +import { useTheme } from "../components/ThemeProvider" +import { useTranslation } from "../hooks/useTranslation" +import { cn } from "../lib/utils" + +const formatNumber = (value: number, lang: "en" | "fa") => + new Intl.NumberFormat(lang === "fa" ? "fa-IR" : "en-US").format(value) + +export default function Landing() { + const navigate = useNavigate() + const { t, lang, setLanguage } = useTranslation() + const { theme, setTheme } = useTheme() + + const isAuthenticated = typeof window !== "undefined" && !!localStorage.getItem("accessToken") + const isDarkMode = + theme === "dark" || + (theme === "system" && document.documentElement.classList.contains("dark")) + + const metrics = useMemo( + () => [ + { + value: lang === "fa" ? "۹۸٪" : "98%", + label: t.landing.metrics.capture, + tone: "from-cyan-500/20 to-cyan-500/5 text-cyan-700 dark:text-cyan-200", + }, + { + value: lang === "fa" ? "۴.۶×" : "4.6x", + label: t.landing.metrics.visibility, + tone: "from-emerald-500/20 to-emerald-500/5 text-emerald-700 dark:text-emerald-200", + }, + { + value: lang === "fa" ? "< ۲m" : "< 2m", + label: t.landing.metrics.decision, + tone: "from-amber-500/20 to-amber-500/5 text-amber-700 dark:text-amber-200", + }, + ], + [lang, t.landing.metrics], + ) + + const capabilityCards = useMemo( + () => [ + { + icon: TimerReset, + title: t.landing.capabilities.time.title, + description: t.landing.capabilities.time.description, + }, + { + icon: BarChart3, + title: t.landing.capabilities.reports.title, + description: t.landing.capabilities.reports.description, + }, + { + icon: ShieldCheck, + title: t.landing.capabilities.control.title, + description: t.landing.capabilities.control.description, + }, + ], + [t.landing.capabilities], + ) + + const workflow = useMemo( + () => [ + t.landing.workflow.capture, + t.landing.workflow.structure, + t.landing.workflow.improve, + ], + [t.landing.workflow], + ) + + const ctaTarget = isAuthenticated ? "/timesheet" : "/auth" + + return ( +
+
+
+
+
+ +
+
+ + + + +
+ + +
+
+ +
+
+
+
+ + {t.landing.eyebrow} +
+

+ {t.landing.hero.titleTop} + + {t.landing.hero.titleAccent} + +

+

+ {t.landing.hero.description} +

+
+ +
+ + + {t.landing.actions.watchDemo} + +
+ +
+ {metrics.map((metric) => ( +
+
{metric.value}
+
{metric.label}
+
+ ))} +
+ +
+ + + {t.landing.trust.first} + + + + {t.landing.trust.second} + + + + {t.landing.trust.third} + +
+
+ +
+
+
+
+ +
+
+
{t.landing.demo.timerTag}
+
{t.landing.demo.timerTitle}
+
+
+
+ {lang === "fa" ? "۰۲:۴۶:۱۸" : "02:46:18"} +
+
{t.landing.demo.timerText}
+
+ +
+
+
+
+ {t.landing.demo.panelLabel} +
+
+ {t.landing.demo.panelTitle} +
+
+
+ {lang === "fa" ? "زنده" : "Live"} +
+
+ +
+
+
+
+
+
{t.landing.demo.runningCard}
+
{t.landing.demo.currentTask}
+
{t.landing.demo.currentTaskMeta}
+
+
+
{t.landing.demo.billableLabel}
+
{lang === "fa" ? "۹۵ دلار" : "$95"}
+
+
+
+
+
+
+ +
+
+
+ + {t.landing.demo.reportCard} +
+
+ {[34, 56, 48, 72, 65, 88, 76].map((height, index) => ( +
+ ))} +
+
+ +
+
+ + {t.landing.demo.opsCard} +
+
+ {[82, 63, 91].map((value, index) => ( +
+
+ {t.landing.demo.opsLabels[index]} + {formatNumber(value, lang)}% +
+
+
+
+
+ ))} +
+
+
+
+ +
+
+
+ + {t.landing.demo.logCard} +
+
+ {t.landing.demo.logItems.map((item: { title: string; meta: string }, index: number) => ( +
+
+
+
{item.title}
+
{item.meta}
+
+
+
+
+ ))} +
+
+ +
+
{t.landing.demo.outcomeTag}
+
{lang === "fa" ? "۳۶٪" : "36%"}
+
{t.landing.demo.outcomeText}
+
+
+
+
+
+
+ +
+ {capabilityCards.map(({ icon: Icon, title, description }, index) => ( +
+
+ +
+

{title}

+

{description}

+
+ ))} +
+ +
+
+
+ {t.landing.workflowTag} +
+

+ {t.landing.workflowTitle} +

+

+ {t.landing.workflowDescription} +

+
+ +
+ {workflow.map((item, index) => ( +
+
+ {lang === "fa" ? `گام ${formatNumber(index + 1, lang)}` : `Step ${index + 1}`} +
+
+ {item} +
+
+ ))} +
+
+ +
+
+
+
+
+
{t.landing.finalCtaTag}
+

+ {t.landing.finalCtaTitle} +

+

{t.landing.finalCtaDescription}

+
+
+ + +
+
+
+
+
+
+ ) +}