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:
Your Name
2026-04-04 20:01:03 +02:00
parent 0f9b1fe260
commit 7e9edc2992
20 changed files with 1567 additions and 1091 deletions

View File

@@ -2,18 +2,22 @@
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap> <sitemap>
<loc>https://dociva.io/sitemaps/static.xml</loc> <loc>https://dociva.io/sitemaps/static.xml</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
</sitemap> </sitemap>
<sitemap> <sitemap>
<loc>https://dociva.io/sitemaps/blog.xml</loc> <loc>https://dociva.io/sitemaps/blog.xml</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
</sitemap> </sitemap>
<sitemap> <sitemap>
<loc>https://dociva.io/sitemaps/tools.xml</loc> <loc>https://dociva.io/sitemaps/tools.xml</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
</sitemap> </sitemap>
<sitemap> <sitemap>
<loc>https://dociva.io/sitemaps/seo.xml</loc> <loc>https://dociva.io/sitemaps/seo.xml</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
</sitemap>
<sitemap>
<loc>https://dociva.io/sitemaps/comparisons.xml</loc>
<lastmod>2026-04-04</lastmod>
</sitemap> </sitemap>
</sitemapindex> </sitemapindex>

View File

@@ -2,31 +2,31 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://dociva.io/blog/how-to-compress-pdf-online</loc> <loc>https://dociva.io/blog/how-to-compress-pdf-online</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/blog/convert-images-without-losing-quality</loc> <loc>https://dociva.io/blog/convert-images-without-losing-quality</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/blog/ocr-extract-text-from-images</loc> <loc>https://dociva.io/blog/ocr-extract-text-from-images</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/blog/merge-split-pdf-files</loc> <loc>https://dociva.io/blog/merge-split-pdf-files</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/blog/ai-chat-with-pdf-documents</loc> <loc>https://dociva.io/blog/ai-chat-with-pdf-documents</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://dociva.io/compare/compress-pdf-vs-ilovepdf</loc>
<lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dociva.io/compare/merge-pdf-vs-smallpdf</loc>
<lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dociva.io/compare/pdf-to-word-vs-adobe-acrobat</loc>
<lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dociva.io/compare/compress-image-vs-tinypng</loc>
<lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dociva.io/compare/ocr-vs-adobe-scan</loc>
<lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

File diff suppressed because it is too large Load Diff

View File

@@ -2,61 +2,61 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://dociva.io/</loc> <loc>https://dociva.io/</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>daily</changefreq> <changefreq>daily</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/tools</loc> <loc>https://dociva.io/tools</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/about</loc> <loc>https://dociva.io/about</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.4</priority> <priority>0.4</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/contact</loc> <loc>https://dociva.io/contact</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.4</priority> <priority>0.4</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/privacy</loc> <loc>https://dociva.io/privacy</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>yearly</changefreq> <changefreq>yearly</changefreq>
<priority>0.3</priority> <priority>0.3</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/terms</loc> <loc>https://dociva.io/terms</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>yearly</changefreq> <changefreq>yearly</changefreq>
<priority>0.3</priority> <priority>0.3</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/pricing</loc> <loc>https://dociva.io/pricing</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.7</priority> <priority>0.7</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/pricing-transparency</loc> <loc>https://dociva.io/pricing-transparency</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.7</priority> <priority>0.7</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/blog</loc> <loc>https://dociva.io/blog</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url> <url>
<loc>https://dociva.io/developers</loc> <loc>https://dociva.io/developers</loc>
<lastmod>2026-04-03</lastmod> <lastmod>2026-04-04</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.5</priority> <priority>0.5</priority>
</url> </url>

View File

@@ -53,6 +53,10 @@ export default function App() {
const location = useLocation(); const location = useLocation();
const refreshUser = useAuthStore((state) => state.refreshUser); const refreshUser = useAuthStore((state) => state.refreshUser);
const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const isMarketingLayout =
location.pathname === '/' ||
['/about', '/contact', '/pricing', '/tools', '/developers', '/pricing-transparency'].includes(location.pathname) ||
location.pathname.startsWith('/compare/');
useEffect(() => { useEffect(() => {
initAnalytics(); initAnalytics();
@@ -103,7 +107,7 @@ export default function App() {
<div className="flex min-h-screen flex-col bg-slate-50 transition-colors duration-300 dark:bg-slate-950"> <div className="flex min-h-screen flex-col bg-slate-50 transition-colors duration-300 dark:bg-slate-950">
<Header /> <Header />
<main className="container mx-auto flex-1 px-4 py-8 sm:px-6 lg:px-8"> <main className={isMarketingLayout ? 'flex-1' : 'container mx-auto flex-1 px-4 py-8 sm:px-6 lg:px-8'}>
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback={<LoadingFallback />}> <Suspense fallback={<LoadingFallback />}>
<Routes> <Routes>

View File

@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FileText } from 'lucide-react'; import { ArrowRight, FileText, Layers3 } from 'lucide-react';
const FOOTER_TOOLS = { const FOOTER_TOOLS = {
PDF: [ PDF: [
@@ -49,99 +49,87 @@ export default function Footer() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<footer className="border-t border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-900"> <footer className="border-t border-slate-200/80 bg-white/80 backdrop-blur-sm dark:border-slate-700/60 dark:bg-slate-950/80">
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{/* Tool link grid */} <div className="marketing-panel overflow-hidden px-6 py-8 sm:px-8 sm:py-10">
<div className="mb-8 grid gap-8 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-10 xl:grid-cols-[1.15fr,1.85fr]">
{Object.entries(FOOTER_TOOLS).map(([category, tools]) => ( <div>
<div key={category}> <div className="flex items-center gap-3">
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-slate-900 dark:text-white"> <div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-500 via-sky-500 to-accent-500 shadow-lg shadow-primary-200/70 dark:shadow-primary-950/40">
{category} <Layers3 className="h-5 w-5 text-white" />
</h3> </div>
<ul className="space-y-2"> <div>
{tools.map((tool) => ( <p className="text-xl font-black tracking-tight text-slate-950 dark:text-white">
<li key={tool.slug}> {t('common.appName')}
<Link </p>
to={(tool as { slug: string; isLanding?: boolean; isComparison?: boolean }).isComparison ? `/compare/${tool.slug}` : (tool as { slug: string; isLanding?: boolean }).isLanding ? `/${tool.slug}` : `/tools/${tool.slug}`} <p className="text-sm text-slate-500 dark:text-slate-400">
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400" {t('common.siteTagline', 'Online PDF and file workflows')}
> </p>
{tool.label} </div>
</Link> </div>
</li>
))} <p className="mt-6 max-w-md text-sm leading-7 text-slate-600 dark:text-slate-300">
</ul> {t(
'common.footerDescription',
'Convert, compress, edit, and automate document work in one browser-based workspace built for speed, clarity, and secure processing.'
)}
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Link
to="/tools"
className="inline-flex items-center gap-2 rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-600 dark:bg-white dark:text-slate-950 dark:hover:bg-primary-300"
>
{t('common.allTools')}
<ArrowRight className="h-4 w-4" />
</Link>
<Link
to="/developers"
className="inline-flex items-center rounded-full border border-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-white dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-900"
>
{t('common.developers')}
</Link>
</div>
</div> </div>
))}
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{Object.entries(FOOTER_TOOLS).map(([category, tools]) => (
<div key={category}>
<h3 className="mb-4 text-xs font-bold uppercase tracking-[0.22em] text-slate-500 dark:text-slate-400">
{category}
</h3>
<ul className="space-y-2.5">
{tools.map((tool) => (
<li key={tool.slug}>
<Link
to={(tool as { slug: string; isLanding?: boolean; isComparison?: boolean }).isComparison ? `/compare/${tool.slug}` : (tool as { slug: string; isLanding?: boolean }).isLanding ? `/${tool.slug}` : `/tools/${tool.slug}`}
className="text-sm text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
>
{tool.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div> </div>
{/* Bottom bar */} <div className="mt-6 flex flex-col gap-4 border-t border-slate-200/80 pt-6 dark:border-slate-700/60 lg:flex-row lg:items-center lg:justify-between">
<div className="border-t border-slate-200 pt-6 dark:border-slate-700"> <div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row"> <FileText className="h-4 w-4" />
{/* Brand */} <span>© {new Date().getFullYear()} {t('common.appName')}</span>
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-400"> </div>
<FileText className="h-5 w-5" />
<span className="text-sm font-medium">
© {new Date().getFullYear()} {t('common.appName')}
</span>
</div>
{/* Links */} <div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-6"> <Link to="/privacy" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.privacy')}</Link>
<Link <Link to="/terms" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.terms')}</Link>
to="/privacy" <Link to="/pricing" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.pricing')}</Link>
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400" <Link to="/pricing-transparency" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.pricingTransparency')}</Link>
> <Link to="/about" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.about')}</Link>
{t('common.privacy')} <Link to="/contact" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.contact')}</Link>
</Link> <Link to="/blog" className="text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400">{t('common.blog')}</Link>
<Link
to="/terms"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.terms')}
</Link>
<Link
to="/tools"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.allTools')}
</Link>
<Link
to="/about"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.about')}
</Link>
<Link
to="/contact"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.contact')}
</Link>
<Link
to="/pricing"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.pricing')}
</Link>
<Link
to="/pricing-transparency"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.pricingTransparency')}
</Link>
<Link
to="/blog"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.blog')}
</Link>
<Link
to="/developers"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.developers')}
</Link>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import { Link, NavLink, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound, Coins, ArrowRight } from 'lucide-react'; import { ChevronDown, Coins, ArrowRight, Layers3, Menu, Moon, Sparkles, Sun, UserRound, X } from 'lucide-react';
import { useAuthStore } from '@/stores/authStore'; import { useAuthStore } from '@/stores/authStore';
import { ensureLanguageResources } from '@/i18n'; import { ensureLanguageResources } from '@/i18n';
interface LangOption { interface LangOption {
code: string; code: string;
label: string; label: string;
@@ -16,6 +17,14 @@ const languages: LangOption[] = [
{ code: 'fr', label: 'Français', flag: '🇫🇷' }, { code: 'fr', label: 'Français', flag: '🇫🇷' },
]; ];
const NAV_LINKS = [
{ to: '/tools', key: 'common.allTools', fallback: 'All tools' },
{ to: '/pricing', key: 'common.pricing', fallback: 'Pricing' },
{ to: '/developers', key: 'common.developers', fallback: 'Developers' },
{ to: '/about', key: 'common.about', fallback: 'About' },
{ to: '/contact', key: 'common.contact', fallback: 'Contact' },
] as const;
function useDarkMode() { function useDarkMode() {
const [isDark, setIsDark] = useState(() => { const [isDark, setIsDark] = useState(() => {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@@ -41,6 +50,7 @@ function useDarkMode() {
export default function Header() { export default function Header() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { isDark, toggle: toggleDark } = useDarkMode(); const { isDark, toggle: toggleDark } = useDarkMode();
const location = useLocation();
const user = useAuthStore((state) => state.user); const user = useAuthStore((state) => state.user);
const credits = useAuthStore((state) => state.credits); const credits = useAuthStore((state) => state.credits);
const [langOpen, setLangOpen] = useState(false); const [langOpen, setLangOpen] = useState(false);
@@ -60,96 +70,99 @@ export default function Header() {
return () => document.removeEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick);
}, []); }, []);
useEffect(() => {
setMobileOpen(false);
setLangOpen(false);
}, [location.pathname]);
const switchLang = async (code: string) => { const switchLang = async (code: string) => {
const resolved = await ensureLanguageResources(code); const resolved = await ensureLanguageResources(code);
void i18n.changeLanguage(resolved); void i18n.changeLanguage(resolved);
setLangOpen(false); setLangOpen(false);
}; };
const desktopNavClassName = ({ isActive }: { isActive: boolean }) =>
[
'rounded-full px-4 py-2 text-sm font-semibold transition-all duration-200',
isActive
? 'bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-950'
: 'text-slate-600 hover:bg-white hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white',
].join(' ');
const mobileNavClassName = ({ isActive }: { isActive: boolean }) =>
[
'block rounded-2xl px-4 py-3 text-sm font-semibold transition-colors',
isActive
? 'bg-primary-600 text-white shadow-sm shadow-primary-200 dark:shadow-primary-950/30'
: 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800',
].join(' ');
return ( return (
<header className="sticky top-0 z-50 border-b border-slate-200/80 bg-white/85 backdrop-blur-xl dark:border-slate-700/60 dark:bg-slate-900/85"> <header className="sticky top-0 z-50 border-b border-slate-200/70 bg-white/78 backdrop-blur-2xl dark:border-slate-700/60 dark:bg-slate-950/78">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"> <div className="mx-auto flex h-20 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
{/* Logo */} <div className="flex items-center gap-8">
<Link to="/" className="flex items-center gap-2.5 group"> <Link to="/" className="group flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-primary-600 shadow-sm shadow-primary-200 group-hover:bg-primary-700 transition-colors dark:shadow-primary-900/40"> <div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-500 via-sky-500 to-accent-500 shadow-lg shadow-primary-200/70 transition-transform duration-300 group-hover:-translate-y-0.5 dark:shadow-primary-950/40">
<FileText className="h-5 w-5 text-white" /> <Layers3 className="h-5 w-5 text-white" />
</div> </div>
<span className="text-lg font-extrabold tracking-tight text-slate-900 dark:text-white"> <div>
{t('common.appName')} <span className="block text-lg font-black tracking-tight text-slate-950 dark:text-white">
</span> {t('common.appName')}
</Link> </span>
<span className="hidden text-xs font-medium text-slate-500 dark:text-slate-400 sm:block">
{t('common.siteTagline', 'Online PDF and file workflows')}
</span>
</div>
</Link>
{/* Desktop Navigation */} <nav className="hidden items-center gap-1 rounded-full border border-slate-200/80 bg-white/80 p-1 shadow-sm lg:flex dark:border-slate-700/70 dark:bg-slate-900/70">
<nav className="hidden items-center gap-1 md:flex"> {NAV_LINKS.map((link) => (
<Link <NavLink key={link.to} to={link.to} className={desktopNavClassName}>
to="/" {t(link.key, link.fallback)}
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white" </NavLink>
> ))}
{t('common.home')} </nav>
</Link> </div>
<Link
to="/pricing"
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white"
>
{t('common.pricing')}
</Link>
<Link
to="/developers"
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white"
>
{t('common.developers')}
</Link>
<Link
to="/about"
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white"
>
{t('common.about')}
</Link>
</nav>
{/* Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Account / credits pill */}
<Link <Link
to="/account" to="/account"
className="hidden max-w-[200px] items-center gap-2 rounded-xl border border-slate-200 px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 md:flex dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800" className="hidden max-w-[220px] items-center gap-2 rounded-full border border-slate-200/80 bg-white/70 px-3.5 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-white lg:flex dark:border-slate-700/70 dark:bg-slate-900/70 dark:text-slate-200 dark:hover:bg-slate-900"
> >
<UserRound className="h-4 w-4 flex-shrink-0" /> <UserRound className="h-4 w-4 shrink-0" />
<span className="truncate">{user?.email || t('common.account')}</span> <span className="truncate">{user?.email || t('common.account')}</span>
{user && credits && ( {user && credits ? (
<span className="flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"> <span className="flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-bold text-primary-700 dark:bg-primary-900/40 dark:text-primary-300">
<Coins className="h-3 w-3" /> <Coins className="h-3 w-3" />
{credits.credits_remaining} {credits.credits_remaining}
</span> </span>
)} ) : null}
</Link> </Link>
{/* CTA — Start Free */} {!user ? (
{!user && (
<Link <Link
to="/account" to="/account"
className="hidden md:inline-flex items-center gap-1.5 rounded-xl bg-primary-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm shadow-primary-200 transition-all hover:bg-primary-700 hover:shadow-md hover:-translate-y-px active:translate-y-0 dark:shadow-primary-900/40" className="hidden items-center gap-2 rounded-full bg-slate-950 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:-translate-y-0.5 hover:bg-primary-600 lg:inline-flex dark:bg-white dark:text-slate-950 dark:hover:bg-primary-300"
> >
<Sparkles className="h-4 w-4" />
{t('home.startFree', 'Start Free')} {t('home.startFree', 'Start Free')}
<ArrowRight className="h-3.5 w-3.5" /> <ArrowRight className="h-3.5 w-3.5" />
</Link> </Link>
)} ) : null}
{/* Dark Mode Toggle */}
<button <button
onClick={toggleDark} onClick={toggleDark}
className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800" className="flex items-center justify-center rounded-full border border-transparent p-2.5 text-slate-500 transition-colors hover:border-slate-200 hover:bg-white dark:text-slate-400 dark:hover:border-slate-700 dark:hover:bg-slate-900"
aria-label={isDark ? t('common.lightMode') : t('common.darkMode')} aria-label={isDark ? t('common.lightMode') : t('common.darkMode')}
title={isDark ? t('common.lightMode') : t('common.darkMode')} title={isDark ? t('common.lightMode') : t('common.darkMode')}
> >
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />} {isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button> </button>
{/* Language Dropdown */}
<div className="relative" ref={langRef}> <div className="relative" ref={langRef}>
<button <button
onClick={() => setLangOpen((v) => !v)} onClick={() => setLangOpen((value) => !value)}
className="flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800" className="flex items-center gap-1.5 rounded-full border border-transparent px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:border-slate-200 hover:bg-white dark:text-slate-300 dark:hover:border-slate-700 dark:hover:bg-slate-900"
aria-label={t('common.language')} aria-label={t('common.language')}
aria-expanded={langOpen} aria-expanded={langOpen}
aria-haspopup="listbox" aria-haspopup="listbox"
@@ -159,36 +172,34 @@ export default function Header() {
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${langOpen ? 'rotate-180' : ''}`} /> <ChevronDown className={`h-4 w-4 transition-transform duration-200 ${langOpen ? 'rotate-180' : ''}`} />
</button> </button>
{/* Dropdown Menu */} {langOpen ? (
{langOpen && ( <div className="absolute end-0 top-full z-50 mt-2 w-48 origin-top-right rounded-2xl border border-slate-200 bg-white p-1.5 shadow-xl shadow-slate-200/70 dark:border-slate-700 dark:bg-slate-900 dark:shadow-slate-950/30">
<div className="absolute end-0 top-full z-50 mt-2 w-44 origin-top-right animate-in fade-in slide-in-from-top-2 rounded-xl border border-slate-200 bg-white p-1 shadow-lg dark:border-slate-700 dark:bg-slate-800">
{languages.map((lang) => ( {languages.map((lang) => (
<button <button
key={lang.code} key={lang.code}
onClick={() => void switchLang(lang.code)} onClick={() => void switchLang(lang.code)}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${ className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors ${
lang.code === i18n.language lang.code === i18n.language
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400' ? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700' : 'text-slate-600 hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800'
}`} }`}
role="option" role="option"
aria-selected={lang.code === i18n.language} aria-selected={lang.code === i18n.language}
> >
<span className="text-lg leading-none">{lang.flag}</span> <span className="text-lg leading-none">{lang.flag}</span>
<span>{lang.label}</span> <span>{lang.label}</span>
{lang.code === i18n.language && ( {lang.code === i18n.language ? (
<span className="ms-auto text-primary-600 dark:text-primary-400"></span> <span className="ms-auto text-primary-600 dark:text-primary-400"></span>
)} ) : null}
</button> </button>
))} ))}
</div> </div>
)} ) : null}
</div> </div>
{/* Mobile Menu Toggle */}
<button <button
onClick={() => setMobileOpen((v) => !v)} onClick={() => setMobileOpen((value) => !value)}
className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 md:hidden dark:text-slate-400 dark:hover:bg-slate-800" className="flex items-center justify-center rounded-full border border-transparent p-2.5 text-slate-500 transition-colors hover:border-slate-200 hover:bg-white lg:hidden dark:text-slate-400 dark:hover:border-slate-700 dark:hover:bg-slate-900"
aria-label="Menu" aria-label="Menu"
> >
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />} {mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
@@ -196,62 +207,40 @@ export default function Header() {
</div> </div>
</div> </div>
{/* Mobile Navigation */} {mobileOpen ? (
{mobileOpen && ( <nav className="border-t border-slate-200/70 bg-white/92 px-4 pb-5 pt-3 lg:hidden dark:border-slate-700/60 dark:bg-slate-950/92">
<nav className="border-t border-slate-200 bg-white px-4 pb-4 pt-2 md:hidden dark:border-slate-700 dark:bg-slate-900"> <div className="mx-auto flex max-w-7xl flex-col gap-2">
<Link {NAV_LINKS.map((link) => (
to="/" <NavLink key={link.to} to={link.to} className={mobileNavClassName}>
onClick={() => setMobileOpen(false)} {t(link.key, link.fallback)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800" </NavLink>
> ))}
{t('common.home')}
</Link>
<Link
to="/pricing"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.pricing')}
</Link>
<Link
to="/about"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.about')}
</Link>
<Link
to="/account"
onClick={() => setMobileOpen(false)}
className="flex items-center justify-between rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
<span>{user?.email || t('common.account')}</span>
{user && credits && (
<span className="flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-xs font-semibold text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
<Coins className="h-3 w-3" />
{credits.credits_remaining}
</span>
)}
</Link>
<Link
to="/developers"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.developers')}
</Link>
{!user && (
<Link <Link
to="/account" to="/account"
onClick={() => setMobileOpen(false)} className="flex items-center justify-between rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
className="mt-2 flex items-center justify-center gap-2 rounded-xl bg-primary-600 px-4 py-3 text-sm font-semibold text-white hover:bg-primary-700"
> >
{t('home.startFree', 'Start Free')} <span>{user?.email || t('common.account')}</span>
<ArrowRight className="h-4 w-4" /> {user && credits ? (
<span className="flex items-center gap-1 rounded-full bg-primary-100 px-2 py-0.5 text-[11px] font-bold text-primary-700 dark:bg-primary-900/40 dark:text-primary-300">
<Coins className="h-3 w-3" />
{credits.credits_remaining}
</span>
) : null}
</Link> </Link>
)}
{!user ? (
<Link
to="/account"
className="mt-1 flex items-center justify-center gap-2 rounded-2xl bg-slate-950 px-4 py-3 text-sm font-semibold text-white dark:bg-white dark:text-slate-950"
>
{t('home.startFree', 'Start Free')}
<ArrowRight className="h-4 w-4" />
</Link>
) : null}
</div>
</nav> </nav>
)} ) : null}
</header> </header>
); );
} }

View File

@@ -0,0 +1,25 @@
import type { ReactNode } from 'react';
interface MarketingPageLayoutProps {
hero?: ReactNode;
children: ReactNode;
className?: string;
bodyClassName?: string;
}
export default function MarketingPageLayout({
hero,
children,
className,
bodyClassName,
}: MarketingPageLayoutProps) {
const rootClassName = ['marketing-shell relative isolate', className].filter(Boolean).join(' ');
const contentClassName = ['relative', bodyClassName].filter(Boolean).join(' ');
return (
<div className={rootClassName}>
{hero}
<div className={contentClassName}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,82 @@
import {
ArrowUpDown,
Barcode,
Code,
Crop,
Droplets,
Eraser,
FileDown,
FileImage,
FileOutput,
FileText,
Film,
GitBranch,
Hash,
Image,
ImageIcon,
Languages,
Layers,
ListOrdered,
Lock,
MessageSquare,
Minimize2,
PenLine,
Presentation,
QrCode,
RotateCw,
ScanText,
Scaling,
Scissors,
Sheet,
Table,
Unlock,
Wrench,
} from 'lucide-react';
const ICON_MAP = {
ArrowUpDown,
Barcode,
Code,
Crop,
Droplets,
Eraser,
FileDown,
FileImage,
FileOutput,
FileText,
Film,
GitBranch,
Hash,
Image,
ImageIcon,
Languages,
Layers,
ListOrdered,
Lock,
MessageSquare,
Minimize2,
PenLine,
Presentation,
QrCode,
RotateCw,
ScanText,
Scaling,
Scissors,
Sheet,
Table,
Unlock,
Wrench,
} as const;
interface ManifestToolIconProps {
iconName: string;
className?: string;
}
export default function ManifestToolIcon({
iconName,
className = 'h-6 w-6',
}: ManifestToolIconProps) {
const Icon = ICON_MAP[iconName as keyof typeof ICON_MAP] ?? FileText;
return <Icon className={className} />;
}

View File

@@ -0,0 +1,43 @@
interface SectionIntroProps {
eyebrow?: string;
title: string;
description?: string;
align?: 'left' | 'center';
className?: string;
titleClassName?: string;
descriptionClassName?: string;
}
export default function SectionIntro({
eyebrow,
title,
description,
align = 'left',
className,
titleClassName,
descriptionClassName,
}: SectionIntroProps) {
const alignmentClassName = align === 'center' ? 'mx-auto max-w-3xl text-center' : 'max-w-3xl';
return (
<div className={[alignmentClassName, className].filter(Boolean).join(' ')}>
{eyebrow ? <p className="section-kicker">{eyebrow}</p> : null}
<h2 className={[
'mt-3 text-3xl font-black tracking-tight text-slate-950 dark:text-white sm:text-4xl',
titleClassName,
].filter(Boolean).join(' ')}>
{title}
</h2>
{description ? (
<p
className={[
'mt-4 text-base leading-7 text-slate-600 dark:text-slate-300 sm:text-lg',
descriptionClassName,
].filter(Boolean).join(' ')}
>
{description}
</p>
) : null}
</div>
);
}

View File

@@ -58,15 +58,15 @@ describe('Tool Manifest ↔ SEO Data sync', () => {
}); });
}); });
describe('Tool Manifest ↔ HomePage ICON_MAP sync', () => { describe('Tool Manifest ↔ ManifestToolIcon ICON_MAP sync', () => {
const homePageSource = readFileSync( const iconSource = readFileSync(
resolve(__dirname, '../pages/HomePage.tsx'), resolve(__dirname, '../components/shared/ManifestToolIcon.tsx'),
'utf-8' 'utf-8'
); );
// Extract icon names from the ICON_MAP object literal // Extract icon names from the ICON_MAP object literal
// Match from "= {" to "};" to skip the type annotation that also contains braces // Match from "= {" to "};" to skip the type annotation that also contains braces
const iconMapMatch = homePageSource.match(/ICON_MAP[^=]+=\s*\{([\s\S]+?)\};/); const iconMapMatch = iconSource.match(/ICON_MAP[^=]+=\s*\{([\s\S]+?)\}\s*as\s+const/);
const iconMapKeys = new Set( const iconMapKeys = new Set(
iconMapMatch iconMapMatch
? iconMapMatch[1] ? iconMapMatch[1]

View File

@@ -590,6 +590,11 @@ export function getHomepageTools(section: 'pdf' | 'other'): readonly ToolEntry[]
return TOOL_MANIFEST.filter((t) => t.homepage && t.homepageSection === section); return TOOL_MANIFEST.filter((t) => t.homepage && t.homepageSection === section);
} }
/** Tools grouped by portfolio category */
export function getToolsByCategory(category: ToolCategory): readonly ToolEntry[] {
return TOOL_MANIFEST.filter((t) => t.category === category);
}
/** Lookup a single tool by slug */ /** Lookup a single tool by slug */
export function getToolEntry(slug: string): ToolEntry | undefined { export function getToolEntry(slug: string): ToolEntry | undefined {
return TOOL_MANIFEST.find((t) => t.slug === slug); return TOOL_MANIFEST.find((t) => t.slug === slug);

View File

@@ -2,13 +2,37 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import SEOHead from '@/components/seo/SEOHead'; import SEOHead from '@/components/seo/SEOHead';
import { generateWebPage, getSiteOrigin } from '@/utils/seo'; 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'; 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() { export default function AboutPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const toolCategories = t('pages.about.toolCategories', { returnObjects: true }) as string[];
return ( return (
<> <>
@@ -23,87 +47,106 @@ export default function AboutPage() {
})} })}
/> />
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-6xl">
<h1 className="mb-8 text-3xl font-bold text-slate-900 dark:text-white"> {/* Hero Banner */}
{t('pages.about.title')} <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">
</h1> <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" />
{/* Mission */} <h1 className="relative text-3xl font-extrabold uppercase tracking-wide sm:text-4xl lg:text-5xl">
<section className="mb-10"> {t('pages.about.heroTitle', 'Empowering Document Productivity Worldwide')}
<div className="flex items-center gap-3 mb-3"> </h1>
<Target className="h-6 w-6 text-primary-600 dark:text-primary-400" /> <p className="relative mt-4 max-w-2xl text-lg leading-relaxed text-white/90">
<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">
{t('pages.about.missionText')} {t('pages.about.missionText')}
</p> </p>
</section> </section>
{/* Technology */} {/* Our Team */}
<section className="mb-10"> <section className="mb-16">
<div className="flex items-center gap-3 mb-3"> <h2 className="mb-8 text-2xl font-bold text-slate-900 dark:text-white">
<Cpu className="h-6 w-6 text-primary-600 dark:text-primary-400" /> {t('pages.about.teamTitle', 'Our Team')}
<h2 className="text-xl font-semibold text-slate-900 dark:text-white"> </h2>
{t('pages.about.technologyTitle')} <div className="flex flex-wrap gap-8">
</h2> {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> </div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.technologyText')}
</p>
</section> </section>
{/* Security */} {/* Stats */}
<section className="mb-10"> <section className="mb-16">
<div className="flex items-center gap-3 mb-3"> <div className="grid gap-6 sm:grid-cols-3">
<Shield className="h-6 w-6 text-primary-600 dark:text-primary-400" /> {STATS.map((stat, idx) => (
<h2 className="text-xl font-semibold text-slate-900 dark:text-white"> <div
{t('pages.about.securityTitle')} key={idx}
</h2> 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> </div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.securityText')}
</p>
</section> </section>
{/* File Privacy */} {/* Timeline */}
<section className="mb-10"> <section className="mb-16">
<div className="flex items-center gap-3 mb-3"> <div className="relative flex items-center justify-between overflow-x-auto py-8">
<Lock className="h-6 w-6 text-primary-600 dark:text-primary-400" /> {/* Line */}
<h2 className="text-xl font-semibold text-slate-900 dark:text-white"> <div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-primary-200 dark:bg-primary-800" />
{t('pages.about.privacyTitle')} {TIMELINE.map((event, idx) => (
</h2> <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> </div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.privacyText', { minutes: FILE_RETENTION_MINUTES })}
</p>
</section> </section>
{/* What We Offer */} {/* Our Values */}
<section className="mb-10"> <section className="mb-16">
<div className="flex items-center gap-3 mb-3"> <h2 className="mb-8 text-center text-2xl font-bold text-slate-900 dark:text-white">
<Wrench className="h-6 w-6 text-primary-600 dark:text-primary-400" /> {t('pages.about.valuesTitle', 'Our Values')}
<h2 className="text-xl font-semibold text-slate-900 dark:text-white"> </h2>
{t('pages.about.toolsTitle')} <div className="grid gap-6 sm:grid-cols-3">
</h2> {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> </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> </section>
{/* CTA */} {/* CTA */}
<div className="rounded-xl border border-slate-200 bg-white p-6 text-center dark:border-slate-700 dark:bg-slate-800"> <div className="rounded-2xl 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"> <p className="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('pages.about.ctaText', 'Have questions? Get in touch.')}
{t('common.contact')}
</Link>
</p> </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>
</div> </div>
</> </>

View File

@@ -607,7 +607,7 @@ export default function AccountPage() {
</section> </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="border-b border-slate-200 px-6 py-5 dark:border-slate-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<FolderClock className="h-5 w-5 text-primary-600 dark:text-primary-400" /> <FolderClock className="h-5 w-5 text-primary-600 dark:text-primary-400" />

View File

@@ -1,22 +1,49 @@
import { useDeferredValue, useMemo, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ArrowRight, Search } from 'lucide-react';
import SEOHead from '@/components/seo/SEOHead'; import SEOHead from '@/components/seo/SEOHead';
import BreadcrumbNav from '@/components/seo/BreadcrumbNav'; import BreadcrumbNav from '@/components/seo/BreadcrumbNav';
import ManifestToolIcon from '@/components/shared/ManifestToolIcon';
import { TOOLS_SEO } from '@/config/seoData'; import { TOOLS_SEO } from '@/config/seoData';
import { TOOL_MANIFEST } from '@/config/toolManifest';
import { generateBreadcrumbs, generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo'; 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() { export default function AllToolsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const path = '/tools'; const path = '/tools';
const url = `${origin}${path}`; 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) => ({ const filteredTools = useMemo(() => {
category, let tools = TOOLS_SEO;
items: TOOLS_SEO.filter((tool) => tool.category === category), if (activeTab !== 'All') {
})).filter((group) => group.items.length > 0); 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 = [ const jsonLd = [
generateCollectionPage({ generateCollectionPage({
@@ -45,54 +72,92 @@ export default function AllToolsPage() {
jsonLd={jsonLd} jsonLd={jsonLd}
/> />
<div className="mx-auto max-w-6xl space-y-10"> <div className="mx-auto max-w-6xl space-y-8">
<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"> {/* Header */}
<section>
<BreadcrumbNav <BreadcrumbNav
className="mb-6" className="mb-4"
items={[ items={[
{ label: t('common.home'), to: '/' }, { label: t('common.home'), to: '/' },
{ label: t('common.allTools') }, { label: t('common.allTools') },
]} ]}
/> />
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl"> <h1 className="text-4xl font-extrabold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
{t('pages.toolsHub.title')} {t('pages.toolsHub.title', 'All PDF Tools')}
</h1> </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> </section>
{groupedTools.map((group) => ( {/* Search + category tabs */}
<section <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
key={group.category} <div className="relative flex-1">
className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70" <Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400" />
> <input
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white"> type="text"
{t(`pages.toolsHub.categories.${group.category}`)} value={query}
</h2> 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"> {/* Tools grid */}
{group.items.map((tool) => ( <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<Link {filteredTools.map((tool) => {
key={tool.slug} const manifest = getManifestEntry(tool.slug);
to={`/tools/${tool.slug}`} return (
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" <Link
> key={tool.slug}
<p className="text-sm font-medium uppercase tracking-wide text-primary-600 dark:text-primary-400"> to={`/tools/${tool.slug}`}
{group.category} 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"
</p> >
<h3 className="mt-2 text-lg font-semibold text-slate-900 dark:text-white"> <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`)} {t(`tools.${tool.i18nKey}.title`)}
</h3> </h3>
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-400"> </div>
{t(`tools.${tool.i18nKey}.shortDesc`)} <p className="flex-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
</p> {t(`tools.${tool.i18nKey}.shortDesc`)}
</Link> </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">
</div> <ArrowRight className="h-4 w-4" />
</section> </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> </div>
</> </>
); );

View File

@@ -1,7 +1,21 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async'; 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 { isAxiosError } from 'axios';
import { toast } from 'sonner'; import { toast } from 'sonner';
import SEOHead from '@/components/seo/SEOHead'; import SEOHead from '@/components/seo/SEOHead';
@@ -9,11 +23,27 @@ import { generateWebPage, getSiteOrigin } from '@/utils/seo';
import { getApiClient } from '@/services/api'; import { getApiClient } from '@/services/api';
const CONTACT_EMAIL = 'support@dociva.io'; 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_BASE = import.meta.env.VITE_API_URL || '';
const api = getApiClient(); const api = getApiClient();
type Category = 'general' | 'bug' | 'feature'; 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() { export default function ContactPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
@@ -21,6 +51,7 @@ export default function ContactPage() {
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [openFaq, setOpenFaq] = useState<number | null>(null);
const placeholderKey = `pages.contact.${category}Placeholder` as const; 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="mx-auto max-w-6xl">
<div className="mb-8 text-center"> {/* Page header */}
<h1 className="text-3xl font-bold text-slate-800 dark:text-slate-100"> <div className="mb-10">
{t('pages.contact.title')} <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> </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')} {t('pages.contact.subtitle')}
</p> </p>
</div> </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"> {/* Two-column layout */}
<h2 className="text-lg font-semibold text-slate-700 dark:text-slate-200"> <div className="grid gap-10 lg:grid-cols-2">
{t('pages.contact.formTitle')} {/* Left column — Contact form */}
</h2>
{/* Category */}
<div> <div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"> <form onSubmit={handleSubmit} className="space-y-5">
{t('pages.contact.categoryLabel')} {/* Category */}
</label> <select
<select value={category}
value={category} onChange={(e) => setCategory(e.target.value as 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"
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="general">{t('pages.contact.categories.general')}</option> <option value="bug">{t('pages.contact.categories.bug')}</option>
<option value="bug">{t('pages.contact.categories.bug')}</option> <option value="feature">{t('pages.contact.categories.feature')}</option>
<option value="feature">{t('pages.contact.categories.feature')}</option> </select>
</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> </div>
{/* Name */} {/* Right column — Contact info cards */}
<div> <div className="space-y-5">
<label htmlFor="name" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"> {/* Email card */}
{t('common.name')} <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">
</label> <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
<input <Mail className="h-6 w-6 text-primary-600 dark:text-primary-400" />
id="name" </div>
name="name" <div>
type="text" <p className="font-semibold text-slate-900 dark:text-white">{t('pages.contact.emailLabel', 'Email:')}</p>
required <a
placeholder={t('pages.contact.namePlaceholder')} href={`mailto:${CONTACT_EMAIL}`}
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" className="text-sm text-slate-600 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
/> >
</div> {CONTACT_EMAIL}
</a>
{/* Email */} </div>
<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}
</div> </div>
)}
{/* Submit */} {/* Phone card */}
<button <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">
type="submit" <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
disabled={loading} <Phone className="h-6 w-6 text-primary-600 dark:text-primary-400" />
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" </div>
> <div>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} <p className="font-semibold text-slate-900 dark:text-white">{t('pages.contact.phoneLabel', 'Phone:')}</p>
{loading ? t('common.sending', 'Sending...') : t('common.send')} <p className="text-sm text-slate-600 dark:text-slate-400">{CONTACT_PHONE}</p>
</button> </div>
</form> </div>
{/* Direct email fallback */} {/* Office card */}
<div className="mt-6 text-center text-sm text-slate-500 dark:text-slate-400"> <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">
<p> <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30">
{t('pages.contact.directEmail')}{' '} <MapPin className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<a </div>
href={`mailto:${CONTACT_EMAIL}`} <div>
className="inline-flex items-center gap-1 font-medium text-primary-600 hover:underline dark:text-primary-400" <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>
<Mail className="h-4 w-4" /> </div>
{CONTACT_EMAIL} </div>
</a>
</p> {/* Social links */}
<p className="mt-1">{t('pages.contact.responseTime')}</p> <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> </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> </div>
</> </>
); );

View File

@@ -1,93 +1,92 @@
import { useDeferredValue } from 'react'; import { useDeferredValue } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom'; import { Link, 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 { 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, ArrowRight,
Star,
CheckCircle2, CheckCircle2,
Download,
Globe,
Layers,
Lock,
MousePointerClick,
Search,
ShieldCheck,
Sparkles,
Star,
UploadCloud,
X,
Zap,
} from 'lucide-react'; } from 'lucide-react';
import ToolCard from '@/components/shared/ToolCard'; import MarketingPageLayout from '@/components/layout/MarketingPageLayout';
import HeroUploadZone from '@/components/shared/HeroUploadZone';
import AdSlot from '@/components/layout/AdSlot'; 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 SocialProofStrip from '@/components/shared/SocialProofStrip';
import { getHomepageTools, type ToolEntry } from '@/config/toolManifest'; import ToolCard from '@/components/shared/ToolCard';
import SEOHead from '@/components/seo/SEOHead';
// Map icon names from manifest to lucide components import { TOOL_MANIFEST, getHomepageTools, type ToolEntry } from '@/config/toolManifest';
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = { import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo';
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}`} />;
}
interface ToolInfo { interface ToolInfo {
key: string; key: string;
path: string; path: string;
icon: React.ReactNode; icon: React.ReactNode;
bgColor: string; bgColor: string;
iconName: string;
iconColor: string;
} }
function manifestToToolInfo(tools: readonly ToolEntry[]): ToolInfo[] { function manifestToToolInfo(tools: readonly ToolEntry[]): ToolInfo[] {
return tools.map((t) => ({ return tools.map((t) => ({
key: t.i18nKey, key: t.i18nKey,
path: `/tools/${t.slug}`, path: `/tools/${t.slug}`,
icon: renderToolIcon(t), icon: <ManifestToolIcon iconName={t.iconName} className={`h-6 w-6 ${t.iconColor}`} />,
bgColor: t.bgColor, bgColor: t.bgColor,
iconName: t.iconName,
iconColor: t.iconColor,
})); }));
} }
const pdfTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('pdf')); const pdfTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('pdf'));
const otherTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('other')); 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 = [ const HOW_IT_WORKS = [
{ {
step: '01', step: '01',
@@ -127,6 +126,25 @@ export default function HomePage() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || ''; const query = searchParams.get('q') || '';
const deferredQuery = useDeferredValue(query.trim().toLowerCase()); 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) => { const matchesTool = (tool: ToolInfo) => {
if (!deferredQuery) { if (!deferredQuery) {
@@ -151,7 +169,95 @@ export default function HomePage() {
}; };
return ( 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 <SEOHead
title={t('common.appName')} title={t('common.appName')}
description={t('home.heroSub')} description={t('home.heroSub')}
@@ -165,77 +271,31 @@ export default function HomePage() {
]} ]}
/> />
{/* ── Hero Section ──────────────────────────────────────────── */} <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<section className="hero-gradient-bg relative overflow-hidden py-16 sm:py-24 px-4 mb-10 rounded-b-[3rem]"> <AdSlot slot="home-top" format="horizontal" className="mb-8" />
{/* Decorative blobs */} </div>
<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="relative max-w-4xl mx-auto text-center"> <section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
{/* Animated badge */} <SocialProofStrip className="mb-12" />
<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>
<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]"> <SectionIntro
{t('home.hero')} align="center"
</h1> eyebrow={t('home.howItWorksLabel', 'Simple process')}
<p className="mx-auto max-w-2xl text-lg text-slate-500 dark:text-slate-400 mb-4 leading-relaxed"> title={t('home.howItWorksTitle', 'Convert and edit in three simple steps')}
{t('home.heroSub')} description={t(
</p> 'home.howItWorksSubtitle',
'No account, no installation, and no friction. Upload, choose the right workflow, and download.'
{/* Trust strip */} )}
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 mb-10"> className="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>
<div className="relative grid gap-6 sm:grid-cols-3"> <div className="relative grid gap-6 sm:grid-cols-3">
{HOW_IT_WORKS.map(({ step, icon: Icon, titleKey, titleDefault, descKey, descDefault, color, glow }, idx) => ( {HOW_IT_WORKS.map(({ step, icon: Icon, titleKey, titleDefault, descKey, descDefault, color, glow }, idx) => (
<div key={step} className="relative"> <div key={step} className="relative">
{/* Connector line (between steps, hidden on mobile) */}
{idx < HOW_IT_WORKS.length - 1 && ( {idx < HOW_IT_WORKS.length - 1 && (
<div className="step-connector" /> <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"> <div className="marketing-card flex flex-col items-center text-center p-7">
{/* Numbered icon */}
<div className={`relative mb-5 flex h-16 w-16 items-center justify-center rounded-2xl ${color} shadow-lg ${glow} text-white`}> <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" /> <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"> <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> </div>
</section> </section>
{/* ── Search & Tools ────────────────────────────────────────── */} <section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
<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="marketing-panel p-6 sm:p-8 lg:p-10">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div className="grid gap-8 xl:grid-cols-[280px_1fr]">
<div> <div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white"> <SectionIntro
{t('common.search')} eyebrow={t('common.search')}
</h2> title={t('home.toolsDirectoryTitle', 'Find the right tool faster')}
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400"> description={t(
{t('home.searchToolsPlaceholder')} 'home.toolsDirectorySubtitle',
</p> 'Search by task, format, or output and jump directly into the workflow you need.'
</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"
/> />
</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 ────────────────────────────────────────── */} <label className="relative mt-6 block">
<section className="mb-12"> <Search className="pointer-events-none absolute start-4 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<div className="mb-6 flex items-center justify-between"> <input
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-200"> value={query}
{t('home.pdfTools')} onChange={(event) => updateQuery(event.target.value)}
</h2> placeholder={t('home.searchToolsPlaceholder')}
<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"> 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"
{t('common.allTools')} />
<ArrowRight className="h-3.5 w-3.5" /> </label>
</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>
<h2 className="mb-6 text-xl font-bold text-slate-800 dark:text-slate-200"> {query ? (
{t('home.otherTools', 'Other Tools')} <button
</h2> type="button"
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-12"> onClick={() => updateQuery('')}
{filteredOtherTools.map((tool) => ( 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"
<ToolCard >
key={tool.key} <X className="h-4 w-4" />
to={tool.path} {t('common.clear')}
icon={tool.icon} </button>
title={t(`tools.${tool.key}.title`)} ) : null}
description={t(`tools.${tool.key}.shortDesc`)}
bgColor={tool.bgColor}
/>
))}
</div>
{filteredPdfTools.length + filteredOtherTools.length === 0 && ( <div className="mt-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
<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"> {stats.map((stat) => (
<p className="text-base font-medium text-slate-700 dark:text-slate-200"> <div key={stat.label} className="metric-card">
{t('home.noSearchResults')} <p className="text-xs font-bold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500">
</p> {stat.label}
</div> </p>
)} <p className="mt-2 text-2xl font-black text-slate-950 dark:text-white">{stat.value}</p>
</section> </div>
{/* ── 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>
))} ))}
</ul> </div>
</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> </div>
</section> </section>
{/* ── Developer API Banner ──────────────────────────────────── */} <section className="mx-auto max-w-7xl px-4 pb-14 sm:px-6 lg:px-8">
<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"> <SectionIntro
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between"> align="center"
<div className="max-w-2xl"> eyebrow={t('home.whyChooseLabel', 'Why Dociva')}
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400"> title={t('home.featuresTitle', 'A clearer, faster way to work with files')}
{t('common.developers')} 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> </p>
<h2 className="mt-2 text-2xl font-bold text-slate-900 dark:text-white"> <h2 className="mt-3 text-3xl font-black tracking-tight text-white sm:text-4xl">
{t('pages.developers.ctaTitle')} {t('home.ctaBannerTitle', 'Ready to convert your files?')}
</h2> </h2>
<p className="mt-2 text-slate-500 dark:text-slate-400"> <p className="mt-4 text-lg leading-8 text-primary-100">
{t('pages.developers.ctaSubtitle')} {t('home.ctaBannerSubtitle', 'Join thousands of users who convert, compress, and edit their files every day — completely free.')}
</p> </p>
</div> <div className="mt-8 flex flex-wrap items-center justify-center gap-4">
<div className="flex flex-col gap-3 sm:flex-row"> <Link
<Link to="/tools"
to="/developers" 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"
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('home.ctaBrowseTools', 'Browse All Tools')}
{t('pages.developers.openDocs')} <ArrowRight className="h-4 w-4" />
<ArrowRight className="h-4 w-4" /> </Link>
</Link> <Link
<Link to="/account"
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"
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('home.ctaCreateAccount', 'Create Free Account')}
{t('pages.developers.getApiKey')} </Link>
</Link> </div>
</div> </div>
</div> </div>
<AdSlot slot="home-bottom" className="mt-12" />
</section> </section>
</MarketingPageLayout>
{/* ── 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" />
</>
); );
} }

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import SEOHead from '@/components/seo/SEOHead'; import SEOHead from '@/components/seo/SEOHead';
import { generateWebPage, getSiteOrigin } from '@/utils/seo'; 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 { useAuthStore } from '@/stores/authStore';
import SocialProofStrip from '@/components/shared/SocialProofStrip'; import SocialProofStrip from '@/components/shared/SocialProofStrip';
import { getApiClient } from '@/services/api'; import { getApiClient } from '@/services/api';
@@ -15,35 +15,48 @@ interface PlanFeature {
key: string; key: string;
free: boolean | string; free: boolean | string;
pro: boolean | string; pro: boolean | string;
enterprise: boolean | string;
} }
const FEATURES: PlanFeature[] = [ const FEATURES: PlanFeature[] = [
{ key: 'credits', free: '50 credits/30 days', pro: '500 credits/30 days' }, { key: 'credits', free: '50 credits/30 days', pro: '500 credits/30 days', enterprise: 'Unlimited' },
{ key: 'apiAccess', free: false, pro: true }, { key: 'apiAccess', free: false, pro: true, enterprise: true },
{ key: 'apiRequests', free: '—', pro: '1,000/month' }, { key: 'apiRequests', free: '—', pro: '1,000/month', enterprise: 'Unlimited' },
{ key: 'maxFileSize', free: '50 MB', pro: '100 MB' }, { key: 'maxFileSize', free: '50 MB', pro: '100 MB', enterprise: '500 MB' },
{ key: 'historyRetention', free: '25 files', pro: '250 files' }, { key: 'historyRetention', free: '25 files', pro: '250 files', enterprise: 'Unlimited' },
{ key: 'allTools', free: true, pro: true }, { key: 'allTools', free: true, pro: true, enterprise: true },
{ key: 'aiTools', free: true, pro: true }, { key: 'aiTools', free: true, pro: true, enterprise: true },
{ key: 'priorityProcessing', free: false, pro: true }, { key: 'priorityProcessing', free: false, pro: true, enterprise: true },
{ key: 'noAds', free: false, pro: true }, { key: 'noAds', free: false, pro: true, enterprise: true },
{ key: 'emailSupport', free: false, pro: 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() { export default function PricingPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const [loading, setLoading] = useState(false); 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 // Track interest in paid plan
try { try {
await api.post('/internal/admin/plan-interest/record', { plan: 'pro', billing }); await api.post('/internal/admin/plan-interest/record', { plan, billing });
} catch { } catch {
// Non-critical — don't block the flow // Non-critical — don't block the flow
} }
if (plan === 'enterprise') {
window.location.href = '/contact';
return;
}
if (!user) { if (!user) {
window.location.href = '/account?redirect=pricing'; window.location.href = '/account?redirect=pricing';
return; return;
@@ -53,42 +66,64 @@ export default function PricingPage() {
const { data } = await api.post(`${API_BASE}/stripe/create-checkout-session`, { billing }); const { data } = await api.post(`${API_BASE}/stripe/create-checkout-session`, { billing });
if (data.url) window.location.href = data.url; if (data.url) window.location.href = data.url;
} catch { } catch {
// Stripe not configured yet — show message
alert(t('pages.pricing.stripeNotReady', 'Payment system is being set up. Please try again later.')); alert(t('pages.pricing.stripeNotReady', 'Payment system is being set up. Please try again later.'));
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
function renderValue(val: boolean | string) { const prices = billing === 'yearly' ? YEARLY_PRICES : MONTHLY_PRICES;
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>;
}
return ( return (
<> <>
<SEOHead <SEOHead
title={t('pages.pricing.title', 'Pricing')} 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" path="/pricing"
jsonLd={generateWebPage({ jsonLd={generateWebPage({
name: t('pages.pricing.title', 'Pricing'), 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`, url: `${siteOrigin}/pricing`,
})} })}
/> />
<div className="mx-auto max-w-5xl"> <div className="mx-auto max-w-6xl">
{/* Header */} {/* Header + billing toggle */}
<div className="mb-12 text-center"> <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')} {t('pages.pricing.title', 'Simple, Transparent Pricing')}
</h1> </h1>
<p className="mx-auto max-w-2xl text-lg text-slate-600 dark:text-slate-400"> <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> </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="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 flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex gap-3"> <div className="flex gap-3">
@@ -120,88 +155,76 @@ export default function PricingPage() {
<SocialProofStrip /> <SocialProofStrip />
</div> </div>
{/* Plan Cards */} {/* 3-tier Plan Cards */}
<div className="mb-16 grid gap-8 md:grid-cols-2"> <div className="mb-16 grid gap-8 md:grid-cols-3">
{/* Free Plan */} {/* 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="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 flex items-center gap-3"> <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">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-700"> <h2 className="text-lg font-bold text-primary-700 dark:text-primary-300">
<Zap className="h-6 w-6 text-slate-600 dark:text-slate-300" /> {t('pages.pricing.freePlan', 'Free')}
</div> </h2>
<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> </div>
<div className="mb-6"> <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> <span className="text-slate-500 dark:text-slate-400"> / {t('pages.pricing.month', 'month')}</span>
</div> </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) => ( {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"> <li key={f.key} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
<Check className="h-4 w-4 shrink-0 text-green-500" /> <Check className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
{t(`pages.pricing.features.${f.key}`, f.key)} <span>
{typeof f.free === 'string' && ( {t(`pages.pricing.features.${f.key}`, f.key)}
<span className="ml-auto text-xs font-medium text-slate-500">({f.free})</span> {typeof f.free === 'string' && (
)} <span className="ml-1 text-xs text-slate-500">({f.free})</span>
)}
</span>
</li> </li>
))} ))}
</ul> </ul>
<Link <Link
to="/" 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> </Link>
</div> </div>
{/* Pro Plan */} {/* Pro Plan */}
<div className="relative rounded-2xl border-2 border-primary-500 bg-white p-8 shadow-lg dark:bg-slate-800"> <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-primary-600 px-4 py-1 text-xs font-bold text-white"> <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')} {t('pages.pricing.popular', 'MOST POPULAR')}
</div> </div>
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 rounded-xl bg-gradient-to-r from-primary-600 to-primary-500 py-3 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-100 dark:bg-primary-900/30"> <h2 className="text-lg font-bold text-white">
<Crown className="h-6 w-6 text-primary-600 dark:text-primary-400" /> {t('pages.pricing.proPlan', 'Pro')}
</div> </h2>
<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> </div>
<div className="mb-6"> <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> <span className="text-slate-500 dark:text-slate-400"> / {t('pages.pricing.month', 'month')}</span>
</div> </div>
<ul className="mb-8 space-y-3"> <ul className="mb-8 flex-1 space-y-3">
{FEATURES.map((f) => ( {FEATURES.filter((f) => f.pro !== false).map((f) => (
<li key={f.key} className="flex items-center gap-3 text-sm text-slate-700 dark:text-slate-300"> <li key={f.key} className="flex items-start gap-3 text-sm text-slate-700 dark:text-slate-300">
<Check className="h-4 w-4 shrink-0 text-primary-500" /> <Check className="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
{t(`pages.pricing.features.${f.key}`, f.key)} <span>
{typeof f.pro === 'string' && ( {t(`pages.pricing.features.${f.key}`, f.key)}
<span className="ml-auto text-xs font-medium text-primary-600 dark:text-primary-400">({f.pro})</span> {typeof f.pro === 'string' && (
)} <span className="ml-1 text-xs text-primary-600 dark:text-primary-400">({f.pro})</span>
)}
</span>
</li> </li>
))} ))}
</ul> </ul>
<button <button
onClick={() => handleUpgrade('monthly')} onClick={() => handleUpgrade('pro')}
disabled={loading || user?.plan === '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" 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' ? ( ) : user?.plan === 'pro' ? (
t('pages.pricing.currentPlan', 'Current Plan') t('pages.pricing.currentPlan', 'Current Plan')
) : ( ) : (
t('pages.pricing.upgradeToPro', 'Upgrade to Pro') t('pages.pricing.startFreeTrial', 'Start Your Free Trial')
)} )}
</button> </button>
<p className="mt-2 text-center text-xs text-slate-500 dark:text-slate-400"> </div>
{t('pages.pricing.securePayment', 'Secure payment via Stripe')}
</p> {/* 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>
</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"> <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"> <div className="max-w-3xl">
<h2 className="text-2xl font-bold text-slate-900 dark:text-white"> <h2 className="text-2xl font-bold text-slate-900 dark:text-white">
@@ -244,39 +300,16 @@ export default function PricingPage() {
</div> </div>
</section> </section>
{/* Comparison Table */} {/* Bottom trust badges */}
<div className="mb-16 overflow-hidden rounded-2xl border border-slate-200 dark:border-slate-700"> <div className="mb-16 flex flex-wrap items-center justify-center gap-8 text-sm text-slate-500 dark:text-slate-400">
<table className="w-full text-sm"> <div className="flex items-center gap-2">
<thead> <Shield className="h-5 w-5" />
<tr className="border-b border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50"> {t('pages.pricing.securePayment', 'Secure Payment')}
<th className="px-6 py-4 text-left font-semibold text-slate-700 dark:text-slate-200"> </div>
{t('pages.pricing.feature', 'Feature')} <div className="flex items-center gap-2">
</th> <Zap className="h-5 w-5" />
<th className="px-6 py-4 text-center font-semibold text-slate-700 dark:text-slate-200"> {t('pages.pricing.moneyBack', '30-Day Money Back Guarantee')}
{t('pages.pricing.freePlan', 'Free')} </div>
</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>
</div> </div>
{/* FAQ */} {/* FAQ */}

View File

@@ -169,6 +169,40 @@
linear-gradient(180deg, #0f172a 0%, #0f172a 100%); linear-gradient(180deg, #0f172a 0%, #0f172a 100%);
} }
.marketing-shell {
background:
radial-gradient(circle at top left, rgba(59, 130, 246, 0.1), transparent 30%),
radial-gradient(circle at top right, rgba(14, 165, 233, 0.08), transparent 26%),
linear-gradient(180deg, rgba(248, 250, 252, 0.88) 0%, rgba(255, 255, 255, 0.92) 26%, #ffffff 100%);
}
.dark .marketing-shell {
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.18), transparent 32%),
radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 28%),
linear-gradient(180deg, rgba(2, 6, 23, 0.96) 0%, rgba(15, 23, 42, 0.98) 26%, #020617 100%);
}
.marketing-panel {
@apply rounded-[2rem] border border-slate-200/80 bg-white/90 shadow-sm backdrop-blur-sm dark:border-slate-700/70 dark:bg-slate-900/70;
}
.marketing-card {
@apply rounded-[1.75rem] border border-slate-200/80 bg-white/90 p-6 shadow-sm transition-all duration-300 dark:border-slate-700/70 dark:bg-slate-900/75;
}
.marketing-card:hover {
@apply -translate-y-1 shadow-lg shadow-slate-200/70 dark:shadow-slate-950/30;
}
.metric-card {
@apply rounded-3xl border border-slate-200/80 bg-white/85 p-5 shadow-sm backdrop-blur-sm dark:border-slate-700/70 dark:bg-slate-900/70;
}
.section-kicker {
@apply text-xs font-bold uppercase tracking-[0.24em] text-primary-600 dark:text-primary-400;
}
/* ────────────────────────────────────────────────────────────────────────── /* ──────────────────────────────────────────────────────────────────────────
Shimmer loading effect Shimmer loading effect
────────────────────────────────────────────────────────────────────────── */ ────────────────────────────────────────────────────────────────────────── */