feat: comprehensive SaaS UI redesign — Hero mesh, ToolCard accent, How-it-Works, bottom CTA banner, Header CTA

Agent-Logs-Url: https://github.com/aborayan2022/SaaS-PDF/sessions/b8e294e5-c1b0-4395-a003-cfa8f003bf27

Co-authored-by: aborayan2022 <119736744+aborayan2022@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-03 00:32:09 +00:00
committed by GitHub
parent cf03d963fc
commit f55d726df2
7 changed files with 506 additions and 586 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound, Coins } from 'lucide-react';
import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound, Coins, ArrowRight } from 'lucide-react';
import { useAuthStore } from '@/stores/authStore';
import { ensureLanguageResources } from '@/i18n';
interface LangOption {
@@ -67,49 +67,54 @@ export default function Header() {
};
return (
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg dark:border-slate-700 dark:bg-slate-900/80">
<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">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600 dark:text-primary-400">
<FileText className="h-7 w-7" />
<span>{t('common.appName')}</span>
<Link to="/" className="flex items-center gap-2.5 group">
<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">
<FileText className="h-5 w-5 text-white" />
</div>
<span className="text-lg font-extrabold tracking-tight text-slate-900 dark:text-white">
{t('common.appName')}
</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden items-center gap-6 md:flex">
<nav className="hidden items-center gap-1 md:flex">
<Link
to="/"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
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.home')}
</Link>
<Link
to="/about"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
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.about')}
</Link>
<Link
to="/account"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
>
{t('common.account')}
{t('common.pricing')}
</Link>
<Link
to="/developers"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
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">
{/* Account / credits pill */}
<Link
to="/account"
className="hidden max-w-[220px] 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-[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"
>
<UserRound className="h-4 w-4" />
<UserRound className="h-4 w-4 flex-shrink-0" />
<span className="truncate">{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">
@@ -119,6 +124,17 @@ export default function Header() {
)}
</Link>
{/* CTA — Start Free */}
{!user && (
<Link
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"
>
{t('home.startFree', 'Start Free')}
<ArrowRight className="h-3.5 w-3.5" />
</Link>
)}
{/* Dark Mode Toggle */}
<button
onClick={toggleDark}
@@ -190,6 +206,13 @@ export default function Header() {
>
{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)}
@@ -217,6 +240,16 @@ export default function Header() {
>
{t('common.developers')}
</Link>
{!user && (
<Link
to="/account"
onClick={() => setMobileOpen(false)}
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')}
<ArrowRight className="h-4 w-4" />
</Link>
)}
</nav>
)}
</header>

View File

@@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { UploadCloud, Sparkles, PenLine } from 'lucide-react';
import { UploadCloud, PenLine, ChevronRight, FileCheck } from 'lucide-react';
import ToolSelectorModal from '@/components/shared/ToolSelectorModal';
import { useFileStore } from '@/stores/fileStore';
import { getToolsForFile, detectFileCategory, getCategoryLabel } from '@/utils/fileRouting';
@@ -24,6 +24,15 @@ const ACCEPTED_TYPES = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
};
const FORMAT_BADGES = [
{ label: 'PDF', color: 'bg-red-50 text-red-700 ring-red-100 dark:bg-red-900/20 dark:text-red-400 dark:ring-red-800/40' },
{ label: 'Word', color: 'bg-blue-50 text-blue-700 ring-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:ring-blue-800/40' },
{ label: 'JPG', color: 'bg-amber-50 text-amber-700 ring-amber-100 dark:bg-amber-900/20 dark:text-amber-400 dark:ring-amber-800/40' },
{ label: 'PNG', color: 'bg-green-50 text-green-700 ring-green-100 dark:bg-green-900/20 dark:text-green-400 dark:ring-green-800/40' },
{ label: 'WebP', color: 'bg-violet-50 text-violet-700 ring-violet-100 dark:bg-violet-900/20 dark:text-violet-400 dark:ring-violet-800/40' },
{ label: 'MP4', color: 'bg-slate-100 text-slate-600 ring-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-600' },
];
export default function HeroUploadZone() {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -81,6 +90,16 @@ export default function HeroUploadZone() {
setMatchedTools([]);
}, []);
const iconGlowClass = isDragActive
? 'bg-primary-300/50 scale-125'
: 'bg-primary-100/0 group-hover:bg-primary-200/40 group-hover:scale-110';
const iconContainerClass = isDragActive
? 'bg-primary-100 shadow-primary-200 dark:bg-primary-900/50'
: 'bg-primary-50 shadow-sm dark:bg-slate-700/80';
const uploadIconClass = isDragActive
? 'text-primary-600 dark:text-primary-400'
: 'text-primary-400 group-hover:text-primary-600 dark:text-primary-500 dark:group-hover:text-primary-400';
return (
<>
<div className="mx-auto mt-8 max-w-2xl">
@@ -90,38 +109,30 @@ export default function HeroUploadZone() {
>
<input {...getInputProps()} />
{/* Animated icon */}
<div
className={`mb-5 flex h-20 w-20 items-center justify-center rounded-2xl shadow-sm transition-all duration-300 group-hover:-translate-y-2 ${
isDragActive
? 'bg-primary-100 shadow-glow dark:bg-primary-900/40'
: 'bg-white dark:bg-slate-700/60'
}`}
>
<UploadCloud
className={`h-10 w-10 transition-colors duration-300 ${
isDragActive
? 'text-primary-600 dark:text-primary-400'
: 'text-slate-400 group-hover:text-primary-500 dark:text-slate-400 dark:group-hover:text-primary-400'
}`}
/>
{/* Cloud icon with animated ring */}
<div className="relative mb-6">
{/* Outer glow ring */}
<div className={`absolute inset-0 rounded-3xl blur-xl transition-all duration-500 ${iconGlowClass}`} />
<div className={`relative flex h-20 w-20 items-center justify-center rounded-2xl transition-all duration-300 group-hover:-translate-y-2 group-hover:shadow-lg ${iconContainerClass}`}>
<UploadCloud className={`h-10 w-10 transition-colors duration-300 ${uploadIconClass}`} />
</div>
</div>
{/* Heading */}
<h3 className="mb-1.5 text-lg font-semibold text-slate-800 dark:text-slate-100">
<h3 className="mb-2 text-xl font-bold text-slate-800 dark:text-slate-100">
{isDragActive
? t('home.dropFileHere', 'Drop your file here…')
: t('home.dragDropTitle', 'Drag & drop your file here')}
</h3>
<p className="mb-6 text-sm text-slate-500 dark:text-slate-400">
<p className="mb-7 text-sm text-slate-500 dark:text-slate-400">
{t('common.dragDrop', 'or click the button to browse from your device')}
</p>
{/* CTA Buttons */}
<div className="mb-5 flex gap-3 justify-center z-10 relative flex-wrap">
<div className="relative z-10 mb-6 flex flex-wrap items-center justify-center gap-3">
<button
type="button"
className="px-6 py-2.5 bg-primary-600 hover:bg-primary-700 active:scale-95 text-white font-semibold rounded-full shadow-md hover:shadow-lg transition-all duration-200"
className="inline-flex items-center gap-2 rounded-xl bg-primary-600 px-7 py-3 text-sm font-semibold text-white shadow-md shadow-primary-200 transition-all duration-200 hover:bg-primary-700 hover:shadow-lg hover:-translate-y-px active:translate-y-0 dark:shadow-primary-900/40"
onClick={(e) => {
e.stopPropagation();
const input = document.createElement('input');
@@ -135,46 +146,57 @@ export default function HeroUploadZone() {
input.click();
}}
>
<FileCheck className="h-4 w-4" />
{t('home.uploadCta', 'Choose File')}
</button>
<button
onClick={(e) => {
e.stopPropagation();
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf';
input.onchange = (ev) => {
const fileInput = ev.target as HTMLInputElement;
const f = fileInput.files?.[0];
if (f) {
setStoreFile(f);
navigate('/tools/pdf-editor');
}
};
input.click();
}}
className="px-6 py-2.5 bg-slate-900 hover:bg-slate-700 active:scale-95 text-white font-semibold rounded-full shadow-md hover:shadow-lg transition-all duration-200 flex items-center gap-2 dark:bg-slate-700 dark:hover:bg-slate-600"
type="button"
onClick={(e) => {
e.stopPropagation();
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf';
input.onchange = (ev) => {
const fileInput = ev.target as HTMLInputElement;
const f = fileInput.files?.[0];
if (f) {
setStoreFile(f);
navigate('/tools/pdf-editor');
}
};
input.click();
}}
className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-6 py-3 text-sm font-semibold text-slate-700 shadow-sm transition-all duration-200 hover:border-slate-300 hover:bg-slate-50 hover:shadow-md hover:-translate-y-px active:translate-y-0 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:border-slate-500 dark:hover:bg-slate-700"
>
<PenLine className="h-4 w-4" />
{t('home.editNow')}
</button>
</div>
{/* Supported formats */}
{/* Divider */}
<div className="mb-5 flex items-center gap-3 w-full max-w-xs">
<div className="flex-1 h-px bg-slate-200 dark:bg-slate-700" />
<span className="text-xs text-slate-400 dark:text-slate-500 font-medium">
{t('home.supportedFormats', 'Supported formats')}
</span>
<div className="flex-1 h-px bg-slate-200 dark:bg-slate-700" />
</div>
{/* Coloured format badges */}
<div className="flex flex-wrap items-center justify-center gap-2">
{['PDF', 'Word', 'JPG', 'PNG', 'WebP', 'MP4'].map((format) => (
{FORMAT_BADGES.map(({ label, color }) => (
<span
key={format}
className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600 dark:bg-slate-700 dark:text-slate-300"
key={label}
className={`rounded-lg px-3 py-1 text-xs font-semibold ring-1 ${color}`}
>
{format}
{label}
</span>
))}
</div>
{/* File size hint */}
{/* Size hint */}
<p className="mt-4 flex items-center justify-center gap-1.5 text-xs text-slate-400 dark:text-slate-500">
<Sparkles className="h-3.5 w-3.5" />
<ChevronRight className="h-3 w-3" />
{t('home.uploadSubtitle')}
</p>
</div>

View File

@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import type { ReactNode } from 'react';
import { ArrowRight } from 'lucide-react';
interface ToolCardProps {
/** Tool route path */
@@ -23,18 +24,28 @@ export default function ToolCard({
}: ToolCardProps) {
return (
<Link to={to} className="group block h-full">
<div className="flex h-full flex-col gap-3 rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:ring-primary-300 dark:bg-slate-800 dark:ring-slate-700 dark:hover:ring-primary-500">
<div className="flex items-center gap-4">
<div
className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl transition-colors ${bgColor} dark:bg-slate-700 dark:group-hover:bg-slate-600`}
>
{icon}
<div className="relative flex h-full flex-col gap-3 overflow-hidden rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200/80 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg hover:shadow-slate-200/60 hover:ring-primary-200 dark:bg-slate-800/80 dark:ring-slate-700 dark:hover:ring-primary-700/60 dark:hover:shadow-slate-900/60">
{/* Top color accent bar — slides in on hover */}
<div className="absolute inset-x-0 top-0 h-[3px] origin-left scale-x-0 rounded-t-2xl bg-gradient-to-r from-primary-500 to-accent-500 transition-transform duration-300 group-hover:scale-x-100" />
<div className="flex items-start justify-between gap-3">
{/* Icon + title */}
<div className="flex items-center gap-3">
<div
className={`flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl ring-1 ring-black/5 transition-transform duration-200 group-hover:scale-110 ${bgColor} dark:ring-white/5 dark:brightness-90`}
>
{icon}
</div>
<h3 className="text-sm font-bold leading-snug text-slate-800 transition-colors group-hover:text-primary-700 dark:text-slate-100 dark:group-hover:text-primary-400">
{title}
</h3>
</div>
<h3 className="text-base font-bold text-slate-900 transition-colors group-hover:text-primary-600 dark:text-slate-100 dark:group-hover:text-primary-400">
{title}
</h3>
{/* Arrow indicator */}
<ArrowRight className="mt-0.5 h-4 w-4 flex-shrink-0 text-slate-300 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-primary-500 dark:text-slate-600 dark:group-hover:text-primary-400" />
</div>
<p className="text-sm text-slate-500 line-clamp-2 dark:text-slate-400 mt-1">
<p className="text-xs leading-relaxed text-slate-500 line-clamp-2 dark:text-slate-400">
{description}
</p>
</div>

View File

@@ -1,6 +1,7 @@
import { useDeferredValue } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { Link } from 'react-router-dom';
import SEOHead from '@/components/seo/SEOHead';
import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo';
import {
@@ -38,6 +39,15 @@ import {
Wrench,
Presentation,
Barcode,
ShieldCheck,
Zap,
Globe,
UploadCloud,
MousePointerClick,
Download,
ArrowRight,
Star,
CheckCircle2,
} from 'lucide-react';
import ToolCard from '@/components/shared/ToolCard';
import HeroUploadZone from '@/components/shared/HeroUploadZone';
@@ -78,6 +88,39 @@ function manifestToToolInfo(tools: readonly ToolEntry[]): ToolInfo[] {
const pdfTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('pdf'));
const otherTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('other'));
const HOW_IT_WORKS = [
{
step: '01',
icon: UploadCloud,
titleKey: 'home.howStep1Title',
titleDefault: 'Upload your file',
descKey: 'home.howStep1Desc',
descDefault: 'Drag & drop or click to select. PDF, Word, images and more — up to 200 MB.',
color: 'bg-blue-600',
glow: 'shadow-blue-200 dark:shadow-blue-900/40',
},
{
step: '02',
icon: MousePointerClick,
titleKey: 'home.howStep2Title',
titleDefault: 'Choose a tool',
descKey: 'home.howStep2Desc',
descDefault: 'We detect your file type and suggest the best tools automatically.',
color: 'bg-violet-600',
glow: 'shadow-violet-200 dark:shadow-violet-900/40',
},
{
step: '03',
icon: Download,
titleKey: 'home.howStep3Title',
titleDefault: 'Download instantly',
descKey: 'home.howStep3Desc',
descDefault: 'Your file is ready in seconds. No account needed — files are auto-deleted.',
color: 'bg-emerald-600',
glow: 'shadow-emerald-200 dark:shadow-emerald-900/40',
},
];
export default function HomePage() {
const { t } = useTranslation();
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
@@ -122,41 +165,103 @@ export default function HomePage() {
]}
/>
{/* Hero Section */}
<section className="py-16 sm:py-24 bg-gradient-to-b from-slate-50 via-white to-white dark:from-slate-900 dark:via-slate-950 dark:to-slate-950 px-4 mb-10 rounded-b-[3rem]">
<div className="max-w-4xl mx-auto text-center">
{/* Badge */}
<div className="inline-flex items-center gap-2 rounded-full bg-primary-50 border border-primary-100 px-4 py-1.5 mb-6 dark:bg-primary-900/30 dark:border-primary-800">
{/* ── Hero Section ──────────────────────────────────────────── */}
<section className="hero-gradient-bg relative overflow-hidden py-16 sm:py-24 px-4 mb-10 rounded-b-[3rem]">
{/* Decorative blobs */}
<div className="pointer-events-none absolute -top-32 left-1/2 h-[600px] w-[600px] -translate-x-1/2 rounded-full bg-primary-400/10 blur-3xl dark:bg-primary-600/10" />
<div className="pointer-events-none absolute top-0 right-0 h-80 w-80 rounded-full bg-accent-400/8 blur-3xl dark:bg-accent-600/8" />
<div className="relative max-w-4xl mx-auto text-center">
{/* Animated badge */}
<div className="inline-flex items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-4 py-1.5 mb-6 dark:border-primary-800 dark:bg-primary-900/30">
<span className="h-2 w-2 rounded-full bg-primary-500 animate-pulse" />
<span className="text-xs font-semibold text-primary-700 dark:text-primary-300 uppercase tracking-wide">
{t('home.heroBadge', 'Free Online PDF Tools')}
<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]">
{t('home.hero')}
</h1>
<p className="mx-auto max-w-2xl text-lg text-slate-500 dark:text-slate-400 mb-10 leading-relaxed">
<p className="mx-auto max-w-2xl text-lg text-slate-500 dark:text-slate-400 mb-4 leading-relaxed">
{t('home.heroSub')}
</p>
{/* Trust strip */}
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 mb-10">
{[
{ icon: ShieldCheck, text: t('home.trustNoSignup', 'No sign-up needed') },
{ icon: Zap, text: t('home.trustFast', 'Results in seconds') },
{ icon: Lock, text: t('home.trustSecure', 'Files auto-deleted') },
{ icon: Globe, text: t('home.trust30Tools', '30+ free tools') },
].map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-1.5 text-sm text-slate-500 dark:text-slate-400">
<Icon className="h-4 w-4 text-primary-500 flex-shrink-0" />
<span>{text}</span>
</div>
))}
</div>
{/* Smart Upload Zone */}
<HeroUploadZone />
</div>
</section>
{/* Ad Slot */}
{/* ── Ad Slot ───────────────────────────────────────────────── */}
<AdSlot slot="home-top" format="horizontal" className="mb-8" />
{/* ── Social Proof Strip ────────────────────────────────────── */}
<SocialProofStrip className="mb-10" />
<section className="mb-10 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
{/* ── 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">
{HOW_IT_WORKS.map(({ step, icon: Icon, titleKey, titleDefault, descKey, descDefault, color, glow }, idx) => (
<div key={step} className="relative">
{/* Connector line (between steps, hidden on mobile) */}
{idx < HOW_IT_WORKS.length - 1 && (
<div className="step-connector" />
)}
<div className="flex flex-col items-center text-center rounded-2xl bg-white p-7 shadow-sm ring-1 ring-slate-200/80 dark:bg-slate-800/70 dark:ring-slate-700/60">
{/* Numbered icon */}
<div className={`relative mb-5 flex h-16 w-16 items-center justify-center rounded-2xl ${color} shadow-lg ${glow} text-white`}>
<Icon className="h-8 w-8" />
<span className="absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-black text-slate-700 shadow-sm ring-1 ring-slate-200 dark:bg-slate-700 dark:text-slate-200 dark:ring-slate-600">
{parseInt(step, 10)}
</span>
</div>
<h3 className="mb-2 text-base font-bold text-slate-900 dark:text-white">
{t(titleKey, titleDefault)}
</h3>
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
{t(descKey, descDefault)}
</p>
</div>
</div>
))}
</div>
</section>
{/* ── Search & Tools ────────────────────────────────────────── */}
<section className="mb-8 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('common.search')}
</h2>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
{t('home.searchToolsPlaceholder')}
</p>
</div>
@@ -184,41 +289,17 @@ export default function HomePage() {
</div>
</section>
<section className="mb-12 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="max-w-2xl">
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
{t('common.developers')}
</p>
<h2 className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
{t('pages.developers.ctaTitle')}
</h2>
<p className="mt-2 text-slate-600 dark:text-slate-400">
{t('pages.developers.ctaSubtitle')}
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<a
href="/developers"
className="inline-flex items-center justify-center rounded-xl bg-primary-600 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-primary-700"
>
{t('pages.developers.openDocs')}
</a>
<a
href="/account"
className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
>
{t('pages.developers.getApiKey')}
</a>
</div>
{/* ── PDF Tools Grid ────────────────────────────────────────── */}
<section className="mb-12">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-200">
{t('home.pdfTools')}
</h2>
<Link to="/tools" className="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
{t('common.allTools')}
<ArrowRight className="h-3.5 w-3.5" />
</Link>
</div>
</section>
{/* Tools Grid */}
<section>
<h2 className="mb-6 text-center text-xl font-semibold text-slate-800 dark:text-slate-200">
{t('home.pdfTools')}
</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-10">
{filteredPdfTools.map((tool) => (
<ToolCard
@@ -232,7 +313,7 @@ export default function HomePage() {
))}
</div>
<h2 className="mb-6 text-center text-xl font-semibold text-slate-800 dark:text-slate-200">
<h2 className="mb-6 text-xl font-bold text-slate-800 dark:text-slate-200">
{t('home.otherTools', 'Other Tools')}
</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mb-12">
@@ -257,49 +338,149 @@ export default function HomePage() {
)}
</section>
{/* Features / Why Choose Us */}
<section className="py-16 bg-slate-50 dark:bg-slate-900 rounded-3xl mb-12 px-6 sm:px-12 text-center">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white mb-10">
{t('home.featuresTitle', 'A smarter way to convert and edit online')}
</h2>
<div className="grid gap-8 sm:grid-cols-3 text-center">
<div className="flex flex-col items-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 mb-6">
<Layers className="h-8 w-8" />
{/* ── 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>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
{t('home.feature1Title', 'One complete workspace')}
</h3>
<p className="text-slate-500 dark:text-slate-400">
{t('home.feature1Desc', 'Edit, convert, compress, merge, split without switching tabs.')}
))}
</div>
</section>
{/* ── Developer API Banner ──────────────────────────────────── */}
<section className="mb-10 rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="max-w-2xl">
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-primary-600 dark:text-primary-400">
{t('common.developers')}
</p>
<h2 className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
{t('pages.developers.ctaTitle')}
</h2>
<p className="mt-2 text-slate-500 dark:text-slate-400">
{t('pages.developers.ctaSubtitle')}
</p>
</div>
<div className="flex flex-col items-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400 mb-6">
<span className="text-2xl font-bold inline-block">100%</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
{t('home.feature2Title', 'Accuracy you can trust')}
</h3>
<p className="text-slate-500 dark:text-slate-400">
{t('home.feature2Desc', 'Get pixel-perfect, editable files in seconds with zero quality loss.')}
</p>
</div>
<div className="flex flex-col items-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400 mb-6">
<Lock className="h-8 w-8" />
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
{t('home.feature3Title', 'Built-in security')}
</h3>
<p className="text-slate-500 dark:text-slate-400">
{t('home.feature3Desc', 'Access files securely, protected by automatic encryption.')}
</p>
<div className="flex flex-col gap-3 sm:flex-row">
<Link
to="/developers"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-primary-600 px-5 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:bg-primary-700 hover:-translate-y-px"
>
{t('pages.developers.openDocs')}
<ArrowRight className="h-4 w-4" />
</Link>
<Link
to="/account"
className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-5 py-3 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
>
{t('pages.developers.getApiKey')}
</Link>
</div>
</div>
</section>
{/* Ad Slot - Bottom */}
{/* ── 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

@@ -114,17 +114,84 @@
animation: fadeSlideIn 0.15s ease-out;
}
/* Hero upload zone — larger variant for the homepage */
/* ──────────────────────────────────────────────────────────────────────────
Hero Upload Zone — premium glassmorphism card for the homepage
────────────────────────────────────────────────────────────────────────── */
.hero-upload-zone {
@apply flex flex-col items-center justify-center rounded-3xl border-2 border-dashed border-slate-300 bg-gradient-to-b from-slate-50 to-white p-10 text-center transition-all duration-300 ease-in-out cursor-pointer sm:p-14 dark:border-slate-600 dark:from-slate-800/60 dark:to-slate-800/30;
@apply relative flex flex-col items-center justify-center rounded-3xl border border-slate-200/80 bg-white/80 backdrop-blur-sm p-10 text-center transition-all duration-300 ease-in-out cursor-pointer sm:p-14 shadow-sm dark:border-slate-700/60 dark:bg-slate-800/60 dark:backdrop-blur-sm;
background-image: radial-gradient(ellipse at top, rgba(219, 234, 254, 0.3) 0%, transparent 70%);
}
.dark .hero-upload-zone {
background-image: radial-gradient(ellipse at top, rgba(30, 58, 138, 0.15) 0%, transparent 70%);
}
.hero-upload-zone::before {
content: '';
@apply absolute inset-0 rounded-3xl transition-opacity duration-300 opacity-0;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.06) 0%, rgba(168, 85, 247, 0.04) 100%);
}
.hero-upload-zone:hover::before {
@apply opacity-100;
}
.hero-upload-zone:hover {
@apply border-primary-400 bg-gradient-to-b from-primary-50 to-white shadow-xl -translate-y-0.5 dark:border-primary-500 dark:from-primary-900/20 dark:to-slate-800/30;
@apply border-primary-300 shadow-lg shadow-primary-100/50 -translate-y-1 dark:border-primary-600/60 dark:shadow-primary-900/30;
}
.hero-upload-zone.drag-active {
@apply border-primary-500 bg-gradient-to-b from-primary-100 to-primary-50/80 ring-2 ring-primary-300 shadow-2xl scale-[1.02] dark:border-primary-400 dark:from-primary-900/30 dark:to-primary-900/10 dark:ring-primary-600;
@apply border-primary-500 shadow-2xl shadow-primary-200/60 scale-[1.02] dark:border-primary-400 dark:shadow-primary-900/40;
background-image: radial-gradient(ellipse at top, rgba(191, 219, 254, 0.5) 0%, rgba(219, 234, 254, 0.2) 100%);
}
/* ──────────────────────────────────────────────────────────────────────────
Glassmorphism card utility
────────────────────────────────────────────────────────────────────────── */
.glass-card {
@apply bg-white/70 backdrop-blur-md border border-white/50 shadow-sm dark:bg-slate-800/60 dark:border-slate-700/50;
}
/* ──────────────────────────────────────────────────────────────────────────
Gradient hero mesh background
────────────────────────────────────────────────────────────────────────── */
.hero-gradient-bg {
background:
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(59, 130, 246, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 80% 20%, rgba(168, 85, 247, 0.06) 0%, transparent 60%),
linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
}
.dark .hero-gradient-bg {
background:
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(30, 58, 138, 0.3) 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 80% 20%, rgba(88, 28, 135, 0.15) 0%, transparent 60%),
linear-gradient(180deg, #0f172a 0%, #0f172a 100%);
}
/* ──────────────────────────────────────────────────────────────────────────
Shimmer loading effect
────────────────────────────────────────────────────────────────────────── */
@keyframes shimmer-sweep {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
.shimmer-text {
background: linear-gradient(90deg, #1e40af 30%, #7c3aed 50%, #1e40af 70%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer-sweep 4s linear infinite;
}
/* ──────────────────────────────────────────────────────────────────────────
How it Works — connector line between steps
────────────────────────────────────────────────────────────────────────── */
.step-connector {
@apply absolute top-8 left-[calc(50%+2.5rem)] hidden h-px w-[calc(100%-5rem)] sm:block;
background: linear-gradient(90deg, rgba(59, 130, 246, 0.4) 0%, rgba(59, 130, 246, 0.1) 100%);
}
/* Modal animations */