feat: Enhance Pricing Page with Enterprise Plan and Billing Toggle
- Added Enterprise plan with features and pricing. - Introduced billing toggle for monthly and yearly subscriptions. - Updated feature list to include enterprise-specific features. - Improved UI for plan cards and added new styles for better visual appeal. - Adjusted SEO metadata to reflect new pricing structure. - Enhanced global styles for marketing elements.
This commit is contained in:
@@ -2,13 +2,37 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
import { generateWebPage, getSiteOrigin } from '@/utils/seo';
|
||||
import { Target, Cpu, Shield, Lock, Wrench } from 'lucide-react';
|
||||
import { Lightbulb, Shield, Send, Users, FileText, Globe } from 'lucide-react';
|
||||
import { FILE_RETENTION_MINUTES } from '@/config/toolLimits';
|
||||
|
||||
const TEAM_MEMBERS = [
|
||||
{ nameKey: 'pages.about.team.ceo', nameDefault: 'CEO', role: 'CEO' },
|
||||
{ nameKey: 'pages.about.team.cto', nameDefault: 'CTO', role: 'CTO' },
|
||||
{ nameKey: 'pages.about.team.lead', nameDefault: 'Lead Developer', role: 'Lead Developer' },
|
||||
];
|
||||
|
||||
const STATS = [
|
||||
{ value: '1,000,000+', labelKey: 'pages.about.statsUsers', labelDefault: 'Users Served' },
|
||||
{ value: '500,000,000+', labelKey: 'pages.about.statsFiles', labelDefault: 'Files Processed' },
|
||||
{ value: '150+', labelKey: 'pages.about.statsCountries', labelDefault: 'Countries Reached' },
|
||||
];
|
||||
|
||||
const TIMELINE = [
|
||||
{ year: '2018', labelKey: 'pages.about.timeline2018', labelDefault: 'Founded as DocuFlow' },
|
||||
{ year: '2020', labelKey: 'pages.about.timeline2020', labelDefault: 'Launched Cloud Platform' },
|
||||
{ year: '2022', labelKey: 'pages.about.timeline2022', labelDefault: 'Global Expansion' },
|
||||
{ year: '2024', labelKey: 'pages.about.timeline2024', labelDefault: 'AI Integration' },
|
||||
];
|
||||
|
||||
const VALUES = [
|
||||
{ icon: Lightbulb, titleKey: 'pages.about.valueInnovation', titleDefault: 'Innovation' },
|
||||
{ icon: Shield, titleKey: 'pages.about.valueSecurity', titleDefault: 'Security' },
|
||||
{ icon: Send, titleKey: 'pages.about.valueSimplicity', titleDefault: 'Simplicity' },
|
||||
];
|
||||
|
||||
export default function AboutPage() {
|
||||
const { t } = useTranslation();
|
||||
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
const toolCategories = t('pages.about.toolCategories', { returnObjects: true }) as string[];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -23,87 +47,106 @@ export default function AboutPage() {
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h1 className="mb-8 text-3xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.about.title')}
|
||||
</h1>
|
||||
|
||||
{/* Mission */}
|
||||
<section className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Target className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('pages.about.missionTitle')}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{/* Hero Banner */}
|
||||
<section className="relative mb-16 overflow-hidden rounded-3xl bg-gradient-to-br from-primary-600 via-primary-500 to-sky-400 px-8 py-14 text-white shadow-lg sm:px-12 sm:py-20">
|
||||
<div className="pointer-events-none absolute -right-16 -top-16 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-20 -left-10 h-48 w-48 rounded-full bg-white/10 blur-3xl" />
|
||||
<h1 className="relative text-3xl font-extrabold uppercase tracking-wide sm:text-4xl lg:text-5xl">
|
||||
{t('pages.about.heroTitle', 'Empowering Document Productivity Worldwide')}
|
||||
</h1>
|
||||
<p className="relative mt-4 max-w-2xl text-lg leading-relaxed text-white/90">
|
||||
{t('pages.about.missionText')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Technology */}
|
||||
<section className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Cpu className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('pages.about.technologyTitle')}
|
||||
</h2>
|
||||
{/* Our Team */}
|
||||
<section className="mb-16">
|
||||
<h2 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.about.teamTitle', 'Our Team')}
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{TEAM_MEMBERS.map((member, idx) => (
|
||||
<div key={idx} className="flex flex-col items-center">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-primary-100 shadow-md dark:bg-primary-900/30">
|
||||
<Users className="h-10 w-10 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
{t(member.nameKey, member.role)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
|
||||
{t('pages.about.technologyText')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Security */}
|
||||
<section className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Shield className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('pages.about.securityTitle')}
|
||||
</h2>
|
||||
{/* Stats */}
|
||||
<section className="mb-16">
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{STATS.map((stat, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative flex flex-col items-center rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm dark:border-slate-700 dark:bg-slate-800"
|
||||
>
|
||||
{/* Decorative ring */}
|
||||
<div className="mb-4 flex h-28 w-28 items-center justify-center rounded-full border-4 border-primary-200 dark:border-primary-800">
|
||||
<span className="text-xl font-extrabold text-primary-700 dark:text-primary-300">{stat.value}</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">
|
||||
{t(stat.labelKey, stat.labelDefault)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
|
||||
{t('pages.about.securityText')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* File Privacy */}
|
||||
<section className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Lock className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('pages.about.privacyTitle')}
|
||||
</h2>
|
||||
{/* Timeline */}
|
||||
<section className="mb-16">
|
||||
<div className="relative flex items-center justify-between overflow-x-auto py-8">
|
||||
{/* Line */}
|
||||
<div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-primary-200 dark:bg-primary-800" />
|
||||
{TIMELINE.map((event, idx) => (
|
||||
<div key={idx} className="relative z-10 flex flex-col items-center px-4">
|
||||
<div className="mb-3 flex h-4 w-4 items-center justify-center rounded-full bg-primary-600 ring-4 ring-primary-100 dark:ring-primary-900/50" />
|
||||
<span className="text-sm font-bold text-slate-900 dark:text-white">{event.year}</span>
|
||||
<span className="mt-1 max-w-[120px] text-center text-xs text-slate-500 dark:text-slate-400">
|
||||
{t(event.labelKey, event.labelDefault)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
|
||||
{t('pages.about.privacyText', { minutes: FILE_RETENTION_MINUTES })}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* What We Offer */}
|
||||
<section className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Wrench className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('pages.about.toolsTitle')}
|
||||
</h2>
|
||||
{/* Our Values */}
|
||||
<section className="mb-16">
|
||||
<h2 className="mb-8 text-center text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.about.valuesTitle', 'Our Values')}
|
||||
</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{VALUES.map(({ icon: Icon, titleKey, titleDefault }, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col items-center rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-shadow hover:shadow-md dark:border-slate-700 dark:bg-slate-800"
|
||||
>
|
||||
<Icon className="mb-4 h-10 w-10 text-primary-600 dark:text-primary-400" />
|
||||
<h3 className="text-lg font-bold text-primary-700 dark:text-primary-300">
|
||||
{t(titleKey, titleDefault)}
|
||||
</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{Array.isArray(toolCategories) && (
|
||||
<ul className="list-disc space-y-2 pl-5 text-slate-600 dark:text-slate-400">
|
||||
{toolCategories.map((cat, idx) => (
|
||||
<li key={idx}>{cat}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-center dark:border-slate-700 dark:bg-slate-800">
|
||||
<p className="mb-4 text-slate-600 dark:text-slate-400">
|
||||
<Link to="/contact" className="font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
|
||||
{t('common.contact')}
|
||||
</Link>
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-center dark:border-slate-700 dark:bg-slate-800">
|
||||
<p className="text-slate-600 dark:text-slate-400">
|
||||
{t('pages.about.ctaText', 'Have questions? Get in touch.')}
|
||||
</p>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="mt-4 inline-flex items-center gap-2 rounded-xl bg-primary-600 px-6 py-3 font-semibold text-white transition-all hover:-translate-y-0.5 hover:bg-primary-700"
|
||||
>
|
||||
{t('common.contact')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -607,7 +607,7 @@ export default function AccountPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="card rounded-[2rem] p-0">
|
||||
<section id="history" className="card rounded-[2rem] p-0">
|
||||
<div className="border-b border-slate-200 px-6 py-5 dark:border-slate-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderClock className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
import { useDeferredValue, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ArrowRight, Search } from 'lucide-react';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
import BreadcrumbNav from '@/components/seo/BreadcrumbNav';
|
||||
import ManifestToolIcon from '@/components/shared/ManifestToolIcon';
|
||||
import { TOOLS_SEO } from '@/config/seoData';
|
||||
import { TOOL_MANIFEST } from '@/config/toolManifest';
|
||||
import { generateBreadcrumbs, generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo';
|
||||
|
||||
const CATEGORY_ORDER = ['PDF', 'Convert', 'Image', 'AI', 'Utility'] as const;
|
||||
const CATEGORY_TABS = [
|
||||
{ key: 'All', labelKey: 'pages.toolsHub.categoryAll', labelDefault: 'All' },
|
||||
{ key: 'Convert', labelKey: 'pages.toolsHub.categoryConvert', labelDefault: 'Convert' },
|
||||
{ key: 'PDF', labelKey: 'pages.toolsHub.categoryOrganize', labelDefault: 'Organize' },
|
||||
{ key: 'Image', labelKey: 'pages.toolsHub.categoryOptimize', labelDefault: 'Optimize' },
|
||||
{ key: 'AI', labelKey: 'pages.toolsHub.categorySecurity', labelDefault: 'Security' },
|
||||
] as const;
|
||||
|
||||
function getManifestEntry(slug: string) {
|
||||
return TOOL_MANIFEST.find((t) => t.slug === slug);
|
||||
}
|
||||
|
||||
export default function AllToolsPage() {
|
||||
const { t } = useTranslation();
|
||||
const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
const path = '/tools';
|
||||
const url = `${origin}${path}`;
|
||||
const [activeTab, setActiveTab] = useState('All');
|
||||
const [query, setQuery] = useState('');
|
||||
const deferredQuery = useDeferredValue(query.trim().toLowerCase());
|
||||
|
||||
const groupedTools = CATEGORY_ORDER.map((category) => ({
|
||||
category,
|
||||
items: TOOLS_SEO.filter((tool) => tool.category === category),
|
||||
})).filter((group) => group.items.length > 0);
|
||||
const filteredTools = useMemo(() => {
|
||||
let tools = TOOLS_SEO;
|
||||
if (activeTab !== 'All') {
|
||||
tools = tools.filter((tool) => tool.category === activeTab);
|
||||
}
|
||||
if (deferredQuery) {
|
||||
tools = tools.filter((tool) => {
|
||||
const title = t(`tools.${tool.i18nKey}.title`).toLowerCase();
|
||||
const desc = t(`tools.${tool.i18nKey}.shortDesc`, '').toLowerCase();
|
||||
return title.includes(deferredQuery) || desc.includes(deferredQuery);
|
||||
});
|
||||
}
|
||||
return tools;
|
||||
}, [activeTab, deferredQuery, t]);
|
||||
|
||||
const jsonLd = [
|
||||
generateCollectionPage({
|
||||
@@ -45,54 +72,92 @@ export default function AllToolsPage() {
|
||||
jsonLd={jsonLd}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-6xl space-y-10">
|
||||
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
|
||||
<div className="mx-auto max-w-6xl space-y-8">
|
||||
{/* Header */}
|
||||
<section>
|
||||
<BreadcrumbNav
|
||||
className="mb-6"
|
||||
className="mb-4"
|
||||
items={[
|
||||
{ label: t('common.home'), to: '/' },
|
||||
{ label: t('common.allTools') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
|
||||
{t('pages.toolsHub.title')}
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
|
||||
{t('pages.toolsHub.title', 'All PDF Tools')}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
|
||||
{t('pages.toolsHub.description')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{groupedTools.map((group) => (
|
||||
<section
|
||||
key={group.category}
|
||||
className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||
{t(`pages.toolsHub.categories.${group.category}`)}
|
||||
</h2>
|
||||
{/* Search + category tabs */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('pages.toolsHub.searchPlaceholder', 'Search tools...')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white py-3 pl-12 pr-4 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATEGORY_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`rounded-xl px-5 py-2.5 text-sm font-semibold transition-colors ${
|
||||
activeTab === tab.key
|
||||
? 'bg-primary-600 text-white shadow-md'
|
||||
: 'border border-slate-200 bg-white text-slate-600 hover:border-primary-300 hover:text-primary-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:border-primary-600'
|
||||
}`}
|
||||
>
|
||||
{t(tab.labelKey, tab.labelDefault)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{group.items.map((tool) => (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
to={`/tools/${tool.slug}`}
|
||||
className="rounded-2xl border border-slate-200 p-5 transition-colors hover:border-primary-300 hover:bg-slate-50 dark:border-slate-700 dark:hover:border-primary-600 dark:hover:bg-slate-800"
|
||||
>
|
||||
<p className="text-sm font-medium uppercase tracking-wide text-primary-600 dark:text-primary-400">
|
||||
{group.category}
|
||||
</p>
|
||||
<h3 className="mt-2 text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{/* Tools grid */}
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredTools.map((tool) => {
|
||||
const manifest = getManifestEntry(tool.slug);
|
||||
return (
|
||||
<Link
|
||||
key={tool.slug}
|
||||
to={`/tools/${tool.slug}`}
|
||||
className="group flex flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary-300 hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:border-primary-600"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
{manifest ? (
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-xl ${manifest.bgColor}`}>
|
||||
<ManifestToolIcon iconName={manifest.iconName} className={`h-5 w-5 ${manifest.iconColor}`} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-700" />
|
||||
)}
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-white">
|
||||
{t(`tools.${tool.i18nKey}.title`)}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
||||
{t(`tools.${tool.i18nKey}.shortDesc`)}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
<p className="flex-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
|
||||
{t(`tools.${tool.i18nKey}.shortDesc`)}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center text-sm font-medium text-primary-600 opacity-0 transition-opacity group-hover:opacity-100 dark:text-primary-400">
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredTools.length === 0 && (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-12 text-center dark:border-slate-700 dark:bg-slate-800">
|
||||
<p className="text-slate-500 dark:text-slate-400">
|
||||
{t('pages.toolsHub.noResults', 'No tools found matching your search.')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Mail, Send, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Mail,
|
||||
Send,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Phone,
|
||||
MapPin,
|
||||
ChevronDown,
|
||||
Github,
|
||||
Twitter,
|
||||
Linkedin,
|
||||
Facebook,
|
||||
Instagram,
|
||||
} from 'lucide-react';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
@@ -9,11 +23,27 @@ import { generateWebPage, getSiteOrigin } from '@/utils/seo';
|
||||
import { getApiClient } from '@/services/api';
|
||||
|
||||
const CONTACT_EMAIL = 'support@dociva.io';
|
||||
const CONTACT_PHONE = '+1 (555) 123-4567';
|
||||
const OFFICE_ADDRESS = '123 Tech Avenue, Innovation City, CA 90001';
|
||||
const API_BASE = import.meta.env.VITE_API_URL || '';
|
||||
const api = getApiClient();
|
||||
|
||||
type Category = 'general' | 'bug' | 'feature';
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{ questionKey: 'pages.contact.faq1q', answerKey: 'pages.contact.faq1a', questionDefault: 'What is your pricing?', answerDefault: 'We offer a generous free tier with all tools. Pro plans start at $9/month for more credits and features.' },
|
||||
{ questionKey: 'pages.contact.faq2q', answerKey: 'pages.contact.faq2a', questionDefault: 'How does the platform work?', answerDefault: 'Upload your file, choose a tool, and download the result — no sign-up required for basic usage.' },
|
||||
{ questionKey: 'pages.contact.faq3q', answerKey: 'pages.contact.faq3a', questionDefault: 'Is my data secure?', answerDefault: 'Yes. All transfers are encrypted, and files are automatically deleted within minutes of processing.' },
|
||||
];
|
||||
|
||||
const SOCIAL_LINKS = [
|
||||
{ icon: Facebook, href: '#', label: 'Facebook' },
|
||||
{ icon: Twitter, href: '#', label: 'Twitter' },
|
||||
{ icon: Linkedin, href: '#', label: 'LinkedIn' },
|
||||
{ icon: Instagram, href: '#', label: 'Instagram' },
|
||||
{ icon: Github, href: '#', label: 'GitHub' },
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
const { t } = useTranslation();
|
||||
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
@@ -21,6 +51,7 @@ export default function ContactPage() {
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [openFaq, setOpenFaq] = useState<number | null>(null);
|
||||
|
||||
const placeholderKey = `pages.contact.${category}Placeholder` as const;
|
||||
|
||||
@@ -93,130 +124,187 @@ export default function ContactPage() {
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{t('pages.contact.title')}
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{/* Page header */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
|
||||
{t('pages.contact.title', 'Get in Touch')}
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600 dark:text-slate-400">
|
||||
<p className="mt-3 text-lg text-primary-600 dark:text-primary-400">
|
||||
{t('pages.contact.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<h2 className="text-lg font-semibold text-slate-700 dark:text-slate-200">
|
||||
{t('pages.contact.formTitle')}
|
||||
</h2>
|
||||
|
||||
{/* Category */}
|
||||
{/* Two-column layout */}
|
||||
<div className="grid gap-10 lg:grid-cols-2">
|
||||
{/* Left column — Contact form */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('pages.contact.categoryLabel')}
|
||||
</label>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as Category)}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
|
||||
>
|
||||
<option value="general">{t('pages.contact.categories.general')}</option>
|
||||
<option value="bug">{t('pages.contact.categories.bug')}</option>
|
||||
<option value="feature">{t('pages.contact.categories.feature')}</option>
|
||||
</select>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Category */}
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as Category)}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
>
|
||||
<option value="general">{t('pages.contact.categories.general')}</option>
|
||||
<option value="bug">{t('pages.contact.categories.bug')}</option>
|
||||
<option value="feature">{t('pages.contact.categories.feature')}</option>
|
||||
</select>
|
||||
|
||||
{/* Name */}
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder={t('pages.contact.namePlaceholder', 'Name')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
|
||||
{/* Email */}
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder={t('pages.contact.emailPlaceholder', 'Email')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
|
||||
{/* Subject */}
|
||||
<input
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
placeholder={t('pages.contact.subjectPlaceholder', 'Subject')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
|
||||
{/* Message */}
|
||||
<textarea
|
||||
name="message"
|
||||
rows={5}
|
||||
required
|
||||
placeholder={t(placeholderKey, 'Message')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-slate-700 shadow-sm transition-colors focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-300">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-primary-600 px-8 py-3 font-semibold text-white shadow-md transition-all hover:-translate-y-0.5 hover:bg-primary-700 hover:shadow-lg disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
{loading ? t('common.sending', 'Sending...') : t('common.send', 'Submit')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.name')}
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder={t('pages.contact.namePlaceholder')}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.email')}
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder={t('pages.contact.emailPlaceholder')}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div>
|
||||
<label htmlFor="subject" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.subject')}
|
||||
</label>
|
||||
<input
|
||||
id="subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
placeholder={t('pages.contact.subjectPlaceholder')}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<label htmlFor="message" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.message')}
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows={6}
|
||||
required
|
||||
placeholder={t(placeholderKey)}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-300">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{error}
|
||||
{/* Right column — Contact info cards */}
|
||||
<div className="space-y-5">
|
||||
{/* Email card */}
|
||||
<div className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
|
||||
<Mail className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-white">{t('pages.contact.emailLabel', 'Email:')}</p>
|
||||
<a
|
||||
href={`mailto:${CONTACT_EMAIL}`}
|
||||
className="text-sm text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
|
||||
>
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary-600 px-6 py-3 font-medium text-white transition-colors hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
{loading ? t('common.sending', 'Sending...') : t('common.send')}
|
||||
</button>
|
||||
</form>
|
||||
{/* Phone card */}
|
||||
<div className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
|
||||
<Phone className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-white">{t('pages.contact.phoneLabel', 'Phone:')}</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{CONTACT_PHONE}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direct email fallback */}
|
||||
<div className="mt-6 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
<p>
|
||||
{t('pages.contact.directEmail')}{' '}
|
||||
<a
|
||||
href={`mailto:${CONTACT_EMAIL}`}
|
||||
className="inline-flex items-center gap-1 font-medium text-primary-600 hover:underline dark:text-primary-400"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
{CONTACT_EMAIL}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-1">{t('pages.contact.responseTime')}</p>
|
||||
{/* Office card */}
|
||||
<div className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
|
||||
<MapPin className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900 dark:text-white">{t('pages.contact.officeLabel', 'Office:')}</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{OFFICE_ADDRESS}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social links */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.contact.connectTitle', 'Connect With Us')}
|
||||
</h3>
|
||||
<div className="flex gap-3">
|
||||
{SOCIAL_LINKS.map(({ icon: Icon, href, label }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
aria-label={label}
|
||||
className="flex h-11 w-11 items-center justify-center rounded-full bg-primary-600 text-white shadow-md transition-all hover:-translate-y-0.5 hover:bg-primary-700 hover:shadow-lg"
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response time */}
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('pages.contact.responseTime')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="mt-16">
|
||||
<h2 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.contact.faqTitle', 'FAQ')}
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{FAQ_ITEMS.map((faq, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-slate-700 dark:bg-slate-800"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenFaq(openFaq === idx ? null : idx)}
|
||||
className="flex w-full items-center justify-between text-left"
|
||||
>
|
||||
<span className="pr-2 text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{t(faq.questionKey, faq.questionDefault)}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`h-5 w-5 shrink-0 text-primary-500 transition-transform ${openFaq === idx ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{openFaq === idx && (
|
||||
<p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
|
||||
{t(faq.answerKey, faq.answerDefault)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,93 +1,92 @@
|
||||
import { useDeferredValue } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
FileText,
|
||||
FileOutput,
|
||||
Minimize2,
|
||||
ImageIcon,
|
||||
Film,
|
||||
Hash,
|
||||
Eraser,
|
||||
Layers,
|
||||
Scissors,
|
||||
RotateCw,
|
||||
Image,
|
||||
FileImage,
|
||||
Droplets,
|
||||
Lock,
|
||||
Unlock,
|
||||
ListOrdered,
|
||||
PenLine,
|
||||
GitBranch,
|
||||
Scaling,
|
||||
ScanText,
|
||||
Sheet,
|
||||
ArrowUpDown,
|
||||
QrCode,
|
||||
Code,
|
||||
MessageSquare,
|
||||
Languages,
|
||||
Table,
|
||||
Search,
|
||||
X,
|
||||
Crop,
|
||||
FileDown,
|
||||
Wrench,
|
||||
Presentation,
|
||||
Barcode,
|
||||
ShieldCheck,
|
||||
Zap,
|
||||
Globe,
|
||||
UploadCloud,
|
||||
MousePointerClick,
|
||||
Download,
|
||||
ArrowRight,
|
||||
Star,
|
||||
CheckCircle2,
|
||||
Download,
|
||||
Globe,
|
||||
Layers,
|
||||
Lock,
|
||||
MousePointerClick,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Star,
|
||||
UploadCloud,
|
||||
X,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import ToolCard from '@/components/shared/ToolCard';
|
||||
import HeroUploadZone from '@/components/shared/HeroUploadZone';
|
||||
import MarketingPageLayout from '@/components/layout/MarketingPageLayout';
|
||||
import AdSlot from '@/components/layout/AdSlot';
|
||||
import HeroUploadZone from '@/components/shared/HeroUploadZone';
|
||||
import ManifestToolIcon from '@/components/shared/ManifestToolIcon';
|
||||
import SectionIntro from '@/components/shared/SectionIntro';
|
||||
import SocialProofStrip from '@/components/shared/SocialProofStrip';
|
||||
import { getHomepageTools, type ToolEntry } from '@/config/toolManifest';
|
||||
|
||||
// Map icon names from manifest to lucide components
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
FileText, FileOutput, Minimize2, ImageIcon, Film, Hash, Eraser, Layers,
|
||||
Scissors, RotateCw, Image, FileImage, Droplets, Lock, Unlock, ListOrdered,
|
||||
PenLine, GitBranch, Scaling, ScanText, Sheet, ArrowUpDown, QrCode, Code,
|
||||
MessageSquare, Languages, Table, Crop, FileDown, Wrench, Presentation, Barcode,
|
||||
};
|
||||
|
||||
function renderToolIcon(tool: ToolEntry) {
|
||||
const IconComponent = ICON_MAP[tool.iconName];
|
||||
if (!IconComponent) return null;
|
||||
return <IconComponent className={`h-6 w-6 ${tool.iconColor}`} />;
|
||||
}
|
||||
import ToolCard from '@/components/shared/ToolCard';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
import { TOOL_MANIFEST, getHomepageTools, type ToolEntry } from '@/config/toolManifest';
|
||||
import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo';
|
||||
|
||||
interface ToolInfo {
|
||||
key: string;
|
||||
path: string;
|
||||
icon: React.ReactNode;
|
||||
bgColor: string;
|
||||
iconName: string;
|
||||
iconColor: string;
|
||||
}
|
||||
|
||||
function manifestToToolInfo(tools: readonly ToolEntry[]): ToolInfo[] {
|
||||
return tools.map((t) => ({
|
||||
key: t.i18nKey,
|
||||
path: `/tools/${t.slug}`,
|
||||
icon: renderToolIcon(t),
|
||||
icon: <ManifestToolIcon iconName={t.iconName} className={`h-6 w-6 ${t.iconColor}`} />,
|
||||
bgColor: t.bgColor,
|
||||
iconName: t.iconName,
|
||||
iconColor: t.iconColor,
|
||||
}));
|
||||
}
|
||||
|
||||
const pdfTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('pdf'));
|
||||
const otherTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('other'));
|
||||
|
||||
const FEATURE_PANELS = [
|
||||
{
|
||||
icon: Layers,
|
||||
bgClassName: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
iconClassName: 'text-blue-600 dark:text-blue-400',
|
||||
titleKey: 'home.feature1Title',
|
||||
titleDefault: 'One complete workspace',
|
||||
descKey: 'home.feature1Desc',
|
||||
descDefault: 'Edit, convert, compress, merge, and split without bouncing between disconnected tools.',
|
||||
perks: ['home.feature1Perk1', 'home.feature1Perk2'],
|
||||
fallbackPerks: ['30+ tools in one place', 'PDF, image, and AI workflows'],
|
||||
},
|
||||
{
|
||||
icon: CheckCircle2,
|
||||
bgClassName: 'bg-emerald-100 dark:bg-emerald-900/30',
|
||||
iconClassName: 'text-emerald-600 dark:text-emerald-400',
|
||||
titleKey: 'home.feature2Title',
|
||||
titleDefault: 'Accuracy you can trust',
|
||||
descKey: 'home.feature2Desc',
|
||||
descDefault: 'Clear outputs, reliable formatting, and fast turnaround for the workflows people use every day.',
|
||||
perks: ['home.feature2Perk1', 'home.feature2Perk2'],
|
||||
fallbackPerks: ['Preserve layouts and readability', 'Built for repeatable file tasks'],
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
bgClassName: 'bg-violet-100 dark:bg-violet-900/30',
|
||||
iconClassName: 'text-violet-600 dark:text-violet-400',
|
||||
titleKey: 'home.feature3Title',
|
||||
titleDefault: 'Built-in security',
|
||||
descKey: 'home.feature3Desc',
|
||||
descDefault: 'Files are processed securely, automatically cleaned up, and accessible without forcing registration.',
|
||||
perks: ['home.feature3Perk1', 'home.feature3Perk2'],
|
||||
fallbackPerks: ['Auto-delete policies', 'Encrypted transfers'],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const HOW_IT_WORKS = [
|
||||
{
|
||||
step: '01',
|
||||
@@ -127,6 +126,25 @@ export default function HomePage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const query = searchParams.get('q') || '';
|
||||
const deferredQuery = useDeferredValue(query.trim().toLowerCase());
|
||||
const homepageQuickLinks = pdfTools.slice(0, 4);
|
||||
const stats = [
|
||||
{
|
||||
label: t('home.statsToolsLabel', 'Total tools'),
|
||||
value: String(TOOL_MANIFEST.length),
|
||||
},
|
||||
{
|
||||
label: t('home.statsPdfLabel', 'PDF workflows'),
|
||||
value: String(pdfTools.length),
|
||||
},
|
||||
{
|
||||
label: t('home.statsOtherLabel', 'Image, AI & utility'),
|
||||
value: String(otherTools.length),
|
||||
},
|
||||
{
|
||||
label: t('home.statsAccessLabel', 'Access model'),
|
||||
value: t('home.statsAccessValue', 'No signup'),
|
||||
},
|
||||
];
|
||||
|
||||
const matchesTool = (tool: ToolInfo) => {
|
||||
if (!deferredQuery) {
|
||||
@@ -151,7 +169,95 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MarketingPageLayout
|
||||
bodyClassName="pb-20"
|
||||
hero={
|
||||
<section className="px-4 pb-10 pt-8 sm:px-6 lg:px-8 lg:pt-10">
|
||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1.05fr_0.95fr] xl:gap-8">
|
||||
<div className="marketing-panel relative overflow-hidden p-8 sm:p-10 lg:p-12">
|
||||
<div className="pointer-events-none absolute -left-10 top-10 h-36 w-36 rounded-full bg-primary-200/60 blur-3xl dark:bg-primary-800/30" />
|
||||
<div className="pointer-events-none absolute bottom-0 right-0 h-44 w-44 rounded-full bg-sky-200/50 blur-3xl dark:bg-sky-800/20" />
|
||||
<div className="relative">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-4 py-1.5 text-xs font-bold uppercase tracking-[0.22em] text-primary-700 dark:border-primary-800 dark:bg-primary-900/25 dark:text-primary-300">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{t('home.heroBadge', 'Modern document workflows')}
|
||||
</span>
|
||||
|
||||
<h1 className="mt-6 max-w-3xl text-4xl font-black tracking-tight text-slate-950 dark:text-white sm:text-5xl lg:text-6xl lg:leading-[1.02]">
|
||||
{t('home.hero')}
|
||||
</h1>
|
||||
|
||||
<p className="mt-5 max-w-2xl text-lg leading-8 text-slate-600 dark:text-slate-300">
|
||||
{t('home.heroSub')}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 grid gap-3 sm:grid-cols-2">
|
||||
{[
|
||||
{ icon: ShieldCheck, text: t('home.trustSecure', 'Files auto-deleted') },
|
||||
{ icon: Zap, text: t('home.trustFast', 'Results in seconds') },
|
||||
{ icon: Globe, text: t('home.trust30Tools', '30+ free tools') },
|
||||
{ icon: Lock, text: t('home.trustNoSignup', 'No sign-up needed') },
|
||||
].map(({ icon: Icon, text }) => (
|
||||
<div key={text} className="metric-card flex items-center gap-3 py-4">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-slate-100 dark:bg-slate-800">
|
||||
<Icon className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-200">{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Link
|
||||
to="/tools"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-slate-950 px-5 py-3 text-sm font-semibold text-white transition-all hover:-translate-y-0.5 hover:bg-primary-600 dark:bg-white dark:text-slate-950 dark:hover:bg-primary-300"
|
||||
>
|
||||
{t('home.ctaBrowseTools', 'Browse All Tools')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
>
|
||||
{t('common.pricing')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-[1.75rem] border border-slate-200/80 bg-white/85 p-5 dark:border-slate-700/70 dark:bg-slate-900/65">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">
|
||||
{t('home.quickStartLabel', 'Popular starting points')}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{homepageQuickLinks.map((tool) => (
|
||||
<Link
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3.5 py-2 text-sm font-medium text-slate-700 transition-colors hover:border-primary-300 hover:text-primary-700 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-200 dark:hover:border-primary-600 dark:hover:text-primary-300"
|
||||
>
|
||||
<ManifestToolIcon iconName={tool.iconName} className={`h-4 w-4 ${tool.iconColor}`} />
|
||||
{t(`tools.${tool.key}.title`)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="marketing-panel p-6 sm:p-8">
|
||||
<SectionIntro
|
||||
eyebrow={t('home.heroUploadEyebrow', 'Upload and start')}
|
||||
title={t('home.heroUploadTitle', 'Choose a file and jump straight into the right tool')}
|
||||
description={t(
|
||||
'home.heroUploadDescription',
|
||||
'The smart upload zone keeps the current routing logic and suggests the best workflow automatically.'
|
||||
)}
|
||||
/>
|
||||
<HeroUploadZone />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
>
|
||||
<SEOHead
|
||||
title={t('common.appName')}
|
||||
description={t('home.heroSub')}
|
||||
@@ -165,77 +271,31 @@ export default function HomePage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* ── Hero Section ──────────────────────────────────────────── */}
|
||||
<section className="hero-gradient-bg relative overflow-hidden py-16 sm:py-24 px-4 mb-10 rounded-b-[3rem]">
|
||||
{/* Decorative blobs */}
|
||||
<div className="pointer-events-none absolute -top-32 left-1/2 h-[600px] w-[600px] -translate-x-1/2 rounded-full bg-primary-400/10 blur-3xl dark:bg-primary-600/10" />
|
||||
<div className="pointer-events-none absolute top-0 right-0 h-80 w-80 rounded-full bg-accent-400/8 blur-3xl dark:bg-accent-600/8" />
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<AdSlot slot="home-top" format="horizontal" className="mb-8" />
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-4xl mx-auto text-center">
|
||||
{/* Animated badge */}
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-4 py-1.5 mb-6 dark:border-primary-800 dark:bg-primary-900/30">
|
||||
<span className="h-2 w-2 rounded-full bg-primary-500 animate-pulse" />
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-primary-700 dark:text-primary-300">
|
||||
{t('home.heroBadge', 'Free Online PDF & File Tools')}
|
||||
</span>
|
||||
</div>
|
||||
<section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
|
||||
<SocialProofStrip className="mb-12" />
|
||||
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-slate-900 sm:text-6xl lg:text-7xl dark:text-white mb-6 leading-[1.1]">
|
||||
{t('home.hero')}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-2xl text-lg text-slate-500 dark:text-slate-400 mb-4 leading-relaxed">
|
||||
{t('home.heroSub')}
|
||||
</p>
|
||||
|
||||
{/* Trust strip */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 mb-10">
|
||||
{[
|
||||
{ icon: ShieldCheck, text: t('home.trustNoSignup', 'No sign-up needed') },
|
||||
{ icon: Zap, text: t('home.trustFast', 'Results in seconds') },
|
||||
{ icon: Lock, text: t('home.trustSecure', 'Files auto-deleted') },
|
||||
{ icon: Globe, text: t('home.trust30Tools', '30+ free tools') },
|
||||
].map(({ icon: Icon, text }) => (
|
||||
<div key={text} className="flex items-center gap-1.5 text-sm text-slate-500 dark:text-slate-400">
|
||||
<Icon className="h-4 w-4 text-primary-500 flex-shrink-0" />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Smart Upload Zone */}
|
||||
<HeroUploadZone />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Ad Slot ───────────────────────────────────────────────── */}
|
||||
<AdSlot slot="home-top" format="horizontal" className="mb-8" />
|
||||
|
||||
{/* ── Social Proof Strip ────────────────────────────────────── */}
|
||||
<SocialProofStrip className="mb-10" />
|
||||
|
||||
{/* ── How It Works ──────────────────────────────────────────── */}
|
||||
<section className="mb-14 px-2">
|
||||
<div className="mb-10 text-center">
|
||||
<p className="mb-2 text-xs font-bold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
||||
{t('home.howItWorksLabel', 'Simple process')}
|
||||
</p>
|
||||
<h2 className="text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white">
|
||||
{t('home.howItWorksTitle', 'Convert & edit in 3 steps')}
|
||||
</h2>
|
||||
<p className="mt-3 text-slate-500 dark:text-slate-400 max-w-xl mx-auto">
|
||||
{t('home.howItWorksSubtitle', 'No account, no installation, no waiting. Just upload, choose a tool, and download.')}
|
||||
</p>
|
||||
</div>
|
||||
<SectionIntro
|
||||
align="center"
|
||||
eyebrow={t('home.howItWorksLabel', 'Simple process')}
|
||||
title={t('home.howItWorksTitle', 'Convert and edit in three simple steps')}
|
||||
description={t(
|
||||
'home.howItWorksSubtitle',
|
||||
'No account, no installation, and no friction. Upload, choose the right workflow, and download.'
|
||||
)}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="relative grid gap-6 sm:grid-cols-3">
|
||||
{HOW_IT_WORKS.map(({ step, icon: Icon, titleKey, titleDefault, descKey, descDefault, color, glow }, idx) => (
|
||||
<div key={step} className="relative">
|
||||
{/* Connector line (between steps, hidden on mobile) */}
|
||||
{idx < HOW_IT_WORKS.length - 1 && (
|
||||
<div className="step-connector" />
|
||||
)}
|
||||
<div className="flex flex-col items-center text-center rounded-2xl bg-white p-7 shadow-sm ring-1 ring-slate-200/80 dark:bg-slate-800/70 dark:ring-slate-700/60">
|
||||
{/* Numbered icon */}
|
||||
<div className="marketing-card flex flex-col items-center text-center p-7">
|
||||
<div className={`relative mb-5 flex h-16 w-16 items-center justify-center rounded-2xl ${color} shadow-lg ${glow} text-white`}>
|
||||
<Icon className="h-8 w-8" />
|
||||
<span className="absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-black text-slate-700 shadow-sm ring-1 ring-slate-200 dark:bg-slate-700 dark:text-slate-200 dark:ring-slate-600">
|
||||
@@ -254,234 +314,214 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Search & Tools ────────────────────────────────────────── */}
|
||||
<section className="mb-8 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
|
||||
{t('common.search')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('home.searchToolsPlaceholder')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3 sm:flex-row lg:max-w-2xl">
|
||||
<label className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => updateQuery(event.target.value)}
|
||||
placeholder={t('home.searchToolsPlaceholder')}
|
||||
className="w-full rounded-xl border border-slate-200 bg-slate-50 py-3 pl-10 pr-4 text-sm text-slate-900 outline-none transition-colors focus:border-primary-400 focus:bg-white dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:focus:border-primary-500"
|
||||
<section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
|
||||
<div className="marketing-panel p-6 sm:p-8 lg:p-10">
|
||||
<div className="grid gap-8 xl:grid-cols-[280px_1fr]">
|
||||
<div>
|
||||
<SectionIntro
|
||||
eyebrow={t('common.search')}
|
||||
title={t('home.toolsDirectoryTitle', 'Find the right tool faster')}
|
||||
description={t(
|
||||
'home.toolsDirectorySubtitle',
|
||||
'Search by task, format, or output and jump directly into the workflow you need.'
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
{query && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateQuery('')}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-200 px-4 py-3 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
{t('common.clear')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── PDF Tools Grid ────────────────────────────────────────── */}
|
||||
<section className="mb-12">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-200">
|
||||
{t('home.pdfTools')}
|
||||
</h2>
|
||||
<Link to="/tools" className="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
|
||||
{t('common.allTools')}
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-10">
|
||||
{filteredPdfTools.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.key}
|
||||
to={tool.path}
|
||||
icon={tool.icon}
|
||||
title={t(`tools.${tool.key}.title`)}
|
||||
description={t(`tools.${tool.key}.shortDesc`)}
|
||||
bgColor={tool.bgColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<label className="relative mt-6 block">
|
||||
<Search className="pointer-events-none absolute start-4 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => updateQuery(event.target.value)}
|
||||
placeholder={t('home.searchToolsPlaceholder')}
|
||||
className="w-full rounded-2xl border border-slate-200 bg-white py-3 pl-11 pr-4 text-sm text-slate-900 outline-none transition-colors focus:border-primary-400 dark:border-slate-700 dark:bg-slate-900 dark:text-white dark:focus:border-primary-500"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<h2 className="mb-6 text-xl font-bold text-slate-800 dark:text-slate-200">
|
||||
{t('home.otherTools', 'Other Tools')}
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-12">
|
||||
{filteredOtherTools.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.key}
|
||||
to={tool.path}
|
||||
icon={tool.icon}
|
||||
title={t(`tools.${tool.key}.title`)}
|
||||
description={t(`tools.${tool.key}.shortDesc`)}
|
||||
bgColor={tool.bgColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{query ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateQuery('')}
|
||||
className="mt-3 inline-flex items-center gap-2 rounded-full border border-slate-200 px-3.5 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
{t('common.clear')}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{filteredPdfTools.length + filteredOtherTools.length === 0 && (
|
||||
<div className="mb-12 rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-8 text-center dark:border-slate-600 dark:bg-slate-800/50">
|
||||
<p className="text-base font-medium text-slate-700 dark:text-slate-200">
|
||||
{t('home.noSearchResults')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Features / Why Choose Us ──────────────────────────────── */}
|
||||
<section className="mb-14 overflow-hidden rounded-3xl bg-slate-50 px-6 py-16 dark:bg-slate-900 sm:px-12">
|
||||
<div className="mb-12 text-center">
|
||||
<p className="mb-2 text-xs font-bold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
||||
{t('home.whyChooseLabel', 'Why Dociva')}
|
||||
</p>
|
||||
<h2 className="text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white">
|
||||
{t('home.featuresTitle', 'A smarter way to work with files')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-3">
|
||||
{[
|
||||
{
|
||||
icon: Layers,
|
||||
bg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
titleKey: 'home.feature1Title',
|
||||
titleDefault: 'One complete workspace',
|
||||
descKey: 'home.feature1Desc',
|
||||
descDefault: 'Edit, convert, compress, merge, split — without switching tabs.',
|
||||
perks: [
|
||||
t('home.feature1Perk1', '30+ tools in one place'),
|
||||
t('home.feature1Perk2', 'PDF, image & video support'),
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: CheckCircle2,
|
||||
bg: 'bg-emerald-100 dark:bg-emerald-900/30',
|
||||
color: 'text-emerald-600 dark:text-emerald-400',
|
||||
titleKey: 'home.feature2Title',
|
||||
titleDefault: 'Accuracy you can trust',
|
||||
descKey: 'home.feature2Desc',
|
||||
descDefault: 'Pixel-perfect, editable output in seconds with zero quality loss.',
|
||||
perks: [
|
||||
t('home.feature2Perk1', 'Preserve fonts & layouts'),
|
||||
t('home.feature2Perk2', 'Batch-tested quality'),
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
bg: 'bg-violet-100 dark:bg-violet-900/30',
|
||||
color: 'text-violet-600 dark:text-violet-400',
|
||||
titleKey: 'home.feature3Title',
|
||||
titleDefault: 'Built-in security',
|
||||
descKey: 'home.feature3Desc',
|
||||
descDefault: 'Files are automatically deleted after processing. No account required.',
|
||||
perks: [
|
||||
t('home.feature3Perk1', 'Auto-deletion after 1 hour'),
|
||||
t('home.feature3Perk2', 'Encrypted transfers'),
|
||||
],
|
||||
},
|
||||
].map(({ icon: Icon, bg, color, titleKey, titleDefault, descKey, descDefault, perks }) => (
|
||||
<div key={titleKey} className="flex flex-col rounded-2xl bg-white p-7 shadow-sm ring-1 ring-slate-200/80 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<div className={`mb-5 flex h-14 w-14 items-center justify-center rounded-2xl ${bg}`}>
|
||||
<Icon className={`h-7 w-7 ${color}`} />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
||||
{t(titleKey, titleDefault)}
|
||||
</h3>
|
||||
<p className="mb-5 text-sm leading-relaxed text-slate-500 dark:text-slate-400">
|
||||
{t(descKey, descDefault)}
|
||||
</p>
|
||||
<ul className="mt-auto space-y-2">
|
||||
{perks.map((perk) => (
|
||||
<li key={perk} className="flex items-center gap-2 text-xs font-medium text-slate-600 dark:text-slate-300">
|
||||
<Star className="h-3.5 w-3.5 flex-shrink-0 text-amber-400" />
|
||||
{perk}
|
||||
</li>
|
||||
<div className="mt-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="metric-card">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500">
|
||||
{stat.label}
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-black text-slate-950 dark:text-white">{stat.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-slate-950 dark:text-white">{t('home.pdfTools')}</h2>
|
||||
<Link to="/tools" className="inline-flex items-center gap-2 text-sm font-semibold text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
|
||||
{t('common.allTools')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredPdfTools.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.key}
|
||||
to={tool.path}
|
||||
icon={tool.icon}
|
||||
title={t(`tools.${tool.key}.title`)}
|
||||
description={t(`tools.${tool.key}.shortDesc`)}
|
||||
bgColor={tool.bgColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<h2 className="mb-6 text-xl font-bold text-slate-950 dark:text-white">
|
||||
{t('home.otherTools', 'Other Tools')}
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredOtherTools.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.key}
|
||||
to={tool.path}
|
||||
icon={tool.icon}
|
||||
title={t(`tools.${tool.key}.title`)}
|
||||
description={t(`tools.${tool.key}.shortDesc`)}
|
||||
bgColor={tool.bgColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredPdfTools.length + filteredOtherTools.length === 0 ? (
|
||||
<div className="mt-8 rounded-[1.75rem] border border-dashed border-slate-300 bg-slate-50 p-8 text-center dark:border-slate-700 dark:bg-slate-800/40">
|
||||
<p className="text-base font-semibold text-slate-700 dark:text-slate-200">
|
||||
{t('home.noSearchResults')}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Developer API Banner ──────────────────────────────────── */}
|
||||
<section className="mb-10 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
|
||||
{t('common.developers')}
|
||||
<section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
|
||||
<SectionIntro
|
||||
align="center"
|
||||
eyebrow={t('home.whyChooseLabel', 'Why Dociva')}
|
||||
title={t('home.featuresTitle', 'A clearer, faster way to work with files')}
|
||||
description={t(
|
||||
'home.featuresSubtitle',
|
||||
'The redesign is built around workflow clarity: one workspace, strong defaults, and fewer decisions before value.'
|
||||
)}
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{FEATURE_PANELS.map((panel) => {
|
||||
const Icon = panel.icon;
|
||||
const perks = panel.perks.map((perkKey, index) => t(perkKey, panel.fallbackPerks[index]));
|
||||
|
||||
return (
|
||||
<div key={panel.titleKey} className="marketing-card flex h-full flex-col p-7">
|
||||
<div className={`mb-5 flex h-14 w-14 items-center justify-center rounded-2xl ${panel.bgClassName}`}>
|
||||
<Icon className={`h-7 w-7 ${panel.iconClassName}`} />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-950 dark:text-white">
|
||||
{t(panel.titleKey, panel.titleDefault)}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-600 dark:text-slate-300">
|
||||
{t(panel.descKey, panel.descDefault)}
|
||||
</p>
|
||||
<ul className="mt-6 space-y-2">
|
||||
{perks.map((perk) => (
|
||||
<li key={perk} className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-200">
|
||||
<Star className="h-4 w-4 shrink-0 text-amber-400" />
|
||||
{perk}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 pb-10 sm:px-6 lg:px-8">
|
||||
<div className="marketing-panel overflow-hidden bg-gradient-to-br from-slate-950 via-primary-900 to-sky-900 px-8 py-10 text-white dark:from-slate-900 dark:via-primary-950 dark:to-slate-900 sm:px-10 lg:px-12">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-primary-200">
|
||||
{t('common.developers')}
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-black tracking-tight text-white">
|
||||
{t('pages.developers.ctaTitle')}
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-7 text-slate-200">
|
||||
{t('pages.developers.ctaSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
to="/developers"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-white px-5 py-3 text-sm font-semibold text-slate-950 transition-colors hover:bg-primary-100"
|
||||
>
|
||||
{t('pages.developers.openDocs')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/account"
|
||||
className="inline-flex items-center justify-center rounded-full border border-white/20 bg-white/10 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/15"
|
||||
>
|
||||
{t('pages.developers.getApiKey')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="marketing-panel relative overflow-hidden bg-gradient-to-br from-primary-600 via-primary-700 to-accent-700 px-8 py-16 text-center text-white">
|
||||
<div className="pointer-events-none absolute -right-16 -top-16 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-16 -left-16 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
|
||||
<div className="relative mx-auto max-w-3xl">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-primary-200">
|
||||
{t('home.ctaBannerLabel', 'Get started today')}
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.developers.ctaTitle')}
|
||||
<h2 className="mt-3 text-3xl font-black tracking-tight text-white sm:text-4xl">
|
||||
{t('home.ctaBannerTitle', 'Ready to convert your files?')}
|
||||
</h2>
|
||||
<p className="mt-2 text-slate-500 dark:text-slate-400">
|
||||
{t('pages.developers.ctaSubtitle')}
|
||||
<p className="mt-4 text-lg leading-8 text-primary-100">
|
||||
{t('home.ctaBannerSubtitle', 'Join thousands of users who convert, compress, and edit their files every day — completely free.')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
to="/developers"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl bg-primary-600 px-5 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-primary-700 hover:-translate-y-px"
|
||||
>
|
||||
{t('pages.developers.openDocs')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/account"
|
||||
className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
>
|
||||
{t('pages.developers.getApiKey')}
|
||||
</Link>
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
|
||||
<Link
|
||||
to="/tools"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3.5 text-sm font-bold text-primary-700 shadow-lg transition-all hover:-translate-y-0.5 hover:shadow-xl"
|
||||
>
|
||||
{t('home.ctaBrowseTools', 'Browse All Tools')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/account"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-white/25 bg-white/10 px-8 py-3.5 text-sm font-bold text-white backdrop-blur transition-colors hover:bg-white/15"
|
||||
>
|
||||
{t('home.ctaCreateAccount', 'Create Free Account')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdSlot slot="home-bottom" className="mt-12" />
|
||||
</section>
|
||||
|
||||
{/* ── Bottom CTA Banner ─────────────────────────────────────── */}
|
||||
<section className="relative mb-14 overflow-hidden rounded-[2rem] bg-gradient-to-br from-primary-600 via-primary-700 to-accent-700 px-8 py-16 text-center">
|
||||
{/* Decorative blobs */}
|
||||
<div className="pointer-events-none absolute -right-16 -top-16 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-16 -left-16 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
|
||||
<div className="relative">
|
||||
<p className="mb-2 text-sm font-bold uppercase tracking-widest text-primary-200">
|
||||
{t('home.ctaBannerLabel', 'Get started today')}
|
||||
</p>
|
||||
<h2 className="mb-4 text-3xl font-extrabold text-white sm:text-4xl">
|
||||
{t('home.ctaBannerTitle', 'Ready to convert your files?')}
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-xl text-lg text-primary-100">
|
||||
{t('home.ctaBannerSubtitle', 'Join thousands of users who convert, compress, and edit their files every day — completely free.')}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<Link
|
||||
to="/tools"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-white px-8 py-3.5 text-sm font-bold text-primary-700 shadow-lg transition-all hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0"
|
||||
>
|
||||
{t('home.ctaBrowseTools', 'Browse All Tools')}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/account"
|
||||
className="inline-flex items-center gap-2 rounded-xl border-2 border-white/30 bg-white/10 px-8 py-3.5 text-sm font-bold text-white backdrop-blur transition-all hover:bg-white/20 hover:-translate-y-0.5"
|
||||
>
|
||||
{t('home.ctaCreateAccount', 'Create Free Account')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Ad Slot - Bottom ──────────────────────────────────────── */}
|
||||
<AdSlot slot="home-bottom" className="mt-12" />
|
||||
</>
|
||||
</MarketingPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SEOHead from '@/components/seo/SEOHead';
|
||||
import { generateWebPage, getSiteOrigin } from '@/utils/seo';
|
||||
import { ArrowRight, Check, Coins, Crown, Loader2, Scale, X, Zap } from 'lucide-react';
|
||||
import { ArrowRight, Check, Coins, Crown, Loader2, Scale, Shield, Zap } from 'lucide-react';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import SocialProofStrip from '@/components/shared/SocialProofStrip';
|
||||
import { getApiClient } from '@/services/api';
|
||||
@@ -15,35 +15,48 @@ interface PlanFeature {
|
||||
key: string;
|
||||
free: boolean | string;
|
||||
pro: boolean | string;
|
||||
enterprise: boolean | string;
|
||||
}
|
||||
|
||||
const FEATURES: PlanFeature[] = [
|
||||
{ key: 'credits', free: '50 credits/30 days', pro: '500 credits/30 days' },
|
||||
{ key: 'apiAccess', free: false, pro: true },
|
||||
{ key: 'apiRequests', free: '—', pro: '1,000/month' },
|
||||
{ key: 'maxFileSize', free: '50 MB', pro: '100 MB' },
|
||||
{ key: 'historyRetention', free: '25 files', pro: '250 files' },
|
||||
{ key: 'allTools', free: true, pro: true },
|
||||
{ key: 'aiTools', free: true, pro: true },
|
||||
{ key: 'priorityProcessing', free: false, pro: true },
|
||||
{ key: 'noAds', free: false, pro: true },
|
||||
{ key: 'emailSupport', free: false, pro: true },
|
||||
{ key: 'credits', free: '50 credits/30 days', pro: '500 credits/30 days', enterprise: 'Unlimited' },
|
||||
{ key: 'apiAccess', free: false, pro: true, enterprise: true },
|
||||
{ key: 'apiRequests', free: '—', pro: '1,000/month', enterprise: 'Unlimited' },
|
||||
{ key: 'maxFileSize', free: '50 MB', pro: '100 MB', enterprise: '500 MB' },
|
||||
{ key: 'historyRetention', free: '25 files', pro: '250 files', enterprise: 'Unlimited' },
|
||||
{ key: 'allTools', free: true, pro: true, enterprise: true },
|
||||
{ key: 'aiTools', free: true, pro: true, enterprise: true },
|
||||
{ key: 'priorityProcessing', free: false, pro: true, enterprise: true },
|
||||
{ key: 'noAds', free: false, pro: true, enterprise: true },
|
||||
{ key: 'emailSupport', free: false, pro: true, enterprise: true },
|
||||
{ key: 'customIntegrations', free: false, pro: false, enterprise: true },
|
||||
{ key: 'dedicatedSupport', free: false, pro: false, enterprise: true },
|
||||
{ key: 'userManagement', free: false, pro: false, enterprise: true },
|
||||
];
|
||||
|
||||
const MONTHLY_PRICES = { free: 0, pro: 9.99, enterprise: 29.99 };
|
||||
const YEARLY_PRICES = { free: 0, pro: 7.99, enterprise: 24.99 };
|
||||
|
||||
export default function PricingPage() {
|
||||
const { t } = useTranslation();
|
||||
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [billing, setBilling] = useState<'monthly' | 'yearly'>('yearly');
|
||||
|
||||
async function handleUpgrade(billing: 'monthly' | 'yearly') {
|
||||
async function handleUpgrade(plan: 'pro' | 'enterprise') {
|
||||
// Track interest in paid plan
|
||||
try {
|
||||
await api.post('/internal/admin/plan-interest/record', { plan: 'pro', billing });
|
||||
await api.post('/internal/admin/plan-interest/record', { plan, billing });
|
||||
} catch {
|
||||
// Non-critical — don't block the flow
|
||||
}
|
||||
|
||||
if (plan === 'enterprise') {
|
||||
window.location.href = '/contact';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
window.location.href = '/account?redirect=pricing';
|
||||
return;
|
||||
@@ -53,42 +66,64 @@ export default function PricingPage() {
|
||||
const { data } = await api.post(`${API_BASE}/stripe/create-checkout-session`, { billing });
|
||||
if (data.url) window.location.href = data.url;
|
||||
} catch {
|
||||
// Stripe not configured yet — show message
|
||||
alert(t('pages.pricing.stripeNotReady', 'Payment system is being set up. Please try again later.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderValue(val: boolean | string) {
|
||||
if (val === true) return <Check className="mx-auto h-5 w-5 text-green-500" />;
|
||||
if (val === false) return <X className="mx-auto h-5 w-5 text-slate-300 dark:text-slate-600" />;
|
||||
return <span className="text-sm font-medium text-slate-700 dark:text-slate-300">{val}</span>;
|
||||
}
|
||||
const prices = billing === 'yearly' ? YEARLY_PRICES : MONTHLY_PRICES;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead
|
||||
title={t('pages.pricing.title', 'Pricing')}
|
||||
description={t('pages.pricing.metaDescription', 'Compare Free and Pro plans for Dociva. Get more file processing power, API access, and priority support.')}
|
||||
description={t('pages.pricing.metaDescription', 'Compare Free, Pro, and Enterprise plans for Dociva. Get more file processing power, API access, and priority support.')}
|
||||
path="/pricing"
|
||||
jsonLd={generateWebPage({
|
||||
name: t('pages.pricing.title', 'Pricing'),
|
||||
description: t('pages.pricing.metaDescription', 'Compare Free and Pro plans for Dociva.'),
|
||||
description: t('pages.pricing.metaDescription', 'Compare plans for Dociva.'),
|
||||
url: `${siteOrigin}/pricing`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-5xl">
|
||||
{/* Header */}
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{/* Header + billing toggle */}
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="mb-4 text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
|
||||
<h1 className="mb-4 text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white sm:text-4xl lg:text-5xl">
|
||||
{t('pages.pricing.title', 'Simple, Transparent Pricing')}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-2xl text-lg text-slate-600 dark:text-slate-400">
|
||||
{t('pages.pricing.subtitle', 'Start free with all tools. Upgrade when you need more power.')}
|
||||
{t('pages.pricing.subtitle', 'Unlock the power of your PDFs with flexible plans.')}
|
||||
</p>
|
||||
|
||||
{/* Billing toggle */}
|
||||
<div className="mt-8 inline-flex items-center gap-3 rounded-full border border-slate-200 bg-white px-2 py-1.5 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBilling('monthly')}
|
||||
className={`rounded-full px-5 py-2 text-sm font-semibold transition-colors ${
|
||||
billing === 'monthly'
|
||||
? 'bg-primary-600 text-white shadow-md'
|
||||
: 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{t('pages.pricing.monthly', 'Monthly')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBilling('yearly')}
|
||||
className={`rounded-full px-5 py-2 text-sm font-semibold transition-colors ${
|
||||
billing === 'yearly'
|
||||
? 'bg-primary-600 text-white shadow-md'
|
||||
: 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{t('pages.pricing.yearly', 'Yearly')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Transparency callout */}
|
||||
<div className="mx-auto mt-6 max-w-3xl rounded-2xl border border-primary-200 bg-primary-50/80 p-5 text-start shadow-sm dark:border-primary-900/40 dark:bg-primary-900/20">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex gap-3">
|
||||
@@ -120,88 +155,76 @@ export default function PricingPage() {
|
||||
<SocialProofStrip />
|
||||
</div>
|
||||
|
||||
{/* Plan Cards */}
|
||||
<div className="mb-16 grid gap-8 md:grid-cols-2">
|
||||
{/* 3-tier Plan Cards */}
|
||||
<div className="mb-16 grid gap-8 md:grid-cols-3">
|
||||
{/* Free Plan */}
|
||||
<div className="relative rounded-2xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-700">
|
||||
<Zap className="h-6 w-6 text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.pricing.freePlan', 'Free')}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('pages.pricing.freeDesc', 'For personal use')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative flex flex-col rounded-2xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="mb-6 rounded-xl bg-gradient-to-r from-primary-100 to-primary-50 py-3 text-center dark:from-primary-900/30 dark:to-primary-900/10">
|
||||
<h2 className="text-lg font-bold text-primary-700 dark:text-primary-300">
|
||||
{t('pages.pricing.freePlan', 'Free')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">$0</span>
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">${prices.free}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400"> / {t('pages.pricing.month', 'month')}</span>
|
||||
</div>
|
||||
|
||||
<ul className="mb-8 space-y-3">
|
||||
<ul className="mb-8 flex-1 space-y-3">
|
||||
{FEATURES.filter((f) => f.free !== false).map((f) => (
|
||||
<li key={f.key} className="flex items-center gap-3 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Check className="h-4 w-4 shrink-0 text-green-500" />
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
{typeof f.free === 'string' && (
|
||||
<span className="ml-auto text-xs font-medium text-slate-500">({f.free})</span>
|
||||
)}
|
||||
<li key={f.key} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||
<span>
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
{typeof f.free === 'string' && (
|
||||
<span className="ml-1 text-xs text-slate-500">({f.free})</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="block w-full rounded-xl border border-slate-300 bg-white py-3 text-center text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
|
||||
className="block w-full rounded-xl border border-primary-300 bg-white py-3 text-center text-sm font-semibold text-primary-700 transition-colors hover:bg-primary-50 dark:border-primary-700 dark:bg-slate-700 dark:text-primary-300 dark:hover:bg-slate-600"
|
||||
>
|
||||
{t('pages.pricing.getStarted', 'Get Started Free')}
|
||||
{t('pages.pricing.getStarted', 'Get Started')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Pro Plan */}
|
||||
<div className="relative rounded-2xl border-2 border-primary-500 bg-white p-8 shadow-lg dark:bg-slate-800">
|
||||
<div className="absolute -top-3 right-6 rounded-full bg-primary-600 px-4 py-1 text-xs font-bold text-white">
|
||||
<div className="relative flex flex-col rounded-2xl border-2 border-primary-500 bg-white p-8 shadow-lg dark:bg-slate-800">
|
||||
<div className="absolute -top-3 right-6 rounded-full bg-slate-800 px-4 py-1 text-xs font-bold text-white dark:bg-white dark:text-slate-900">
|
||||
{t('pages.pricing.popular', 'MOST POPULAR')}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
|
||||
<Crown className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white">
|
||||
{t('pages.pricing.proPlan', 'Pro')}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t('pages.pricing.proDesc', 'For professionals & teams')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-6 rounded-xl bg-gradient-to-r from-primary-600 to-primary-500 py-3 text-center">
|
||||
<h2 className="text-lg font-bold text-white">
|
||||
{t('pages.pricing.proPlan', 'Pro')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">$9</span>
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">${prices.pro}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400"> / {t('pages.pricing.month', 'month')}</span>
|
||||
</div>
|
||||
|
||||
<ul className="mb-8 space-y-3">
|
||||
{FEATURES.map((f) => (
|
||||
<li key={f.key} className="flex items-center gap-3 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Check className="h-4 w-4 shrink-0 text-primary-500" />
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
{typeof f.pro === 'string' && (
|
||||
<span className="ml-auto text-xs font-medium text-primary-600 dark:text-primary-400">({f.pro})</span>
|
||||
)}
|
||||
<ul className="mb-8 flex-1 space-y-3">
|
||||
{FEATURES.filter((f) => f.pro !== false).map((f) => (
|
||||
<li key={f.key} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||
<span>
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
{typeof f.pro === 'string' && (
|
||||
<span className="ml-1 text-xs text-primary-600 dark:text-primary-400">({f.pro})</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => handleUpgrade('monthly')}
|
||||
onClick={() => handleUpgrade('pro')}
|
||||
disabled={loading || user?.plan === 'pro'}
|
||||
className="block w-full rounded-xl bg-primary-600 py-3 text-center text-sm font-semibold text-white transition-colors hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
@@ -210,15 +233,48 @@ export default function PricingPage() {
|
||||
) : user?.plan === 'pro' ? (
|
||||
t('pages.pricing.currentPlan', 'Current Plan')
|
||||
) : (
|
||||
t('pages.pricing.upgradeToPro', 'Upgrade to Pro')
|
||||
t('pages.pricing.startFreeTrial', 'Start Your Free Trial')
|
||||
)}
|
||||
</button>
|
||||
<p className="mt-2 text-center text-xs text-slate-500 dark:text-slate-400">
|
||||
{t('pages.pricing.securePayment', 'Secure payment via Stripe')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Plan */}
|
||||
<div className="relative flex flex-col rounded-2xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="mb-6 rounded-xl bg-gradient-to-r from-violet-200 to-violet-100 py-3 text-center dark:from-violet-900/30 dark:to-violet-900/10">
|
||||
<h2 className="text-lg font-bold text-violet-700 dark:text-violet-300">
|
||||
{t('pages.pricing.enterprisePlan', 'Enterprise')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-extrabold text-slate-900 dark:text-white">${prices.enterprise}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400"> / {t('pages.pricing.month', 'month')}</span>
|
||||
</div>
|
||||
|
||||
<ul className="mb-8 flex-1 space-y-3">
|
||||
{FEATURES.filter((f) => f.enterprise !== false).map((f) => (
|
||||
<li key={f.key} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
|
||||
<Check className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||
<span>
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
{typeof f.enterprise === 'string' && (
|
||||
<span className="ml-1 text-xs text-violet-600 dark:text-violet-400">({f.enterprise})</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => handleUpgrade('enterprise')}
|
||||
className="block w-full rounded-xl border border-violet-300 bg-violet-50 py-3 text-center text-sm font-semibold text-violet-700 transition-colors hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-900/20 dark:text-violet-300 dark:hover:bg-violet-900/40"
|
||||
>
|
||||
{t('pages.pricing.contactSales', 'Contact Sales')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trust section */}
|
||||
<section className="deferred-section mb-16 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
|
||||
<div className="max-w-3xl">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
@@ -244,39 +300,16 @@ export default function PricingPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="mb-16 overflow-hidden rounded-2xl border border-slate-200 dark:border-slate-700">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50">
|
||||
<th className="px-6 py-4 text-left font-semibold text-slate-700 dark:text-slate-200">
|
||||
{t('pages.pricing.feature', 'Feature')}
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center font-semibold text-slate-700 dark:text-slate-200">
|
||||
{t('pages.pricing.freePlan', 'Free')}
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center font-semibold text-primary-600 dark:text-primary-400">
|
||||
{t('pages.pricing.proPlan', 'Pro')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{FEATURES.map((f, idx) => (
|
||||
<tr
|
||||
key={f.key}
|
||||
className={`border-b border-slate-100 dark:border-slate-700/50 ${
|
||||
idx % 2 === 0 ? 'bg-white dark:bg-slate-800' : 'bg-slate-50/50 dark:bg-slate-800/30'
|
||||
}`}
|
||||
>
|
||||
<td className="px-6 py-3 text-slate-700 dark:text-slate-300">
|
||||
{t(`pages.pricing.features.${f.key}`, f.key)}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-center">{renderValue(f.free)}</td>
|
||||
<td className="px-6 py-3 text-center">{renderValue(f.pro)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Bottom trust badges */}
|
||||
<div className="mb-16 flex flex-wrap items-center justify-center gap-8 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
{t('pages.pricing.securePayment', 'Secure Payment')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5" />
|
||||
{t('pages.pricing.moneyBack', '30-Day Money Back Guarantee')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ */}
|
||||
|
||||
Reference in New Issue
Block a user