feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -567,12 +567,12 @@ export function FileUploader() {
|
||||
>
|
||||
{isTranslating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
<Loader2 className="me-2 h-5 w-5 animate-spin" />
|
||||
Translating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="mr-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
<Zap className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
Translate Document
|
||||
</>
|
||||
)}
|
||||
@@ -630,7 +630,7 @@ export function FileUploader() {
|
||||
size="lg"
|
||||
className="group px-8"
|
||||
>
|
||||
<Download className="mr-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
<Download className="me-2 h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
Download Translated Document
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import {
|
||||
Globe2,
|
||||
FileText,
|
||||
Zap,
|
||||
Shield,
|
||||
Brain,
|
||||
Server
|
||||
} from "lucide-react"
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Globe2,
|
||||
title: "100+ Languages",
|
||||
description: "Translate between any language pair with high accuracy",
|
||||
color: "text-blue-400",
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Preserve Formatting",
|
||||
description: "All styles, fonts, colors, tables, and charts remain intact",
|
||||
color: "text-green-400",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Lightning Fast",
|
||||
description: "Batch processing translates entire documents in seconds",
|
||||
color: "text-amber-400",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Secure & Private",
|
||||
description: "Your documents are encrypted and never stored permanently",
|
||||
color: "text-purple-400",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "AI-Powered",
|
||||
description: "Advanced neural translation for natural, context-aware results",
|
||||
color: "text-teal-400",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Enterprise Ready",
|
||||
description: "API access, team management, and dedicated support",
|
||||
color: "text-orange-400",
|
||||
},
|
||||
]
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<section className="py-16 px-6">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-3">
|
||||
Everything You Need for Document Translation
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
Professional-grade translation with enterprise features, available to everyone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="flex flex-col items-center text-center p-6 rounded-xl border border-border bg-card/50 hover:bg-card transition-colors"
|
||||
>
|
||||
<div className={`mb-4 ${feature.color}`}>
|
||||
<Icon className="size-8" />
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-foreground mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import Link from "next/link"
|
||||
import { FileSpreadsheet, FileText, Presentation } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="flex flex-col items-center gap-6 px-6 pt-16 pb-8 text-center md:pt-24 md:pb-12">
|
||||
<div className="flex items-center gap-2 rounded-full border border-border bg-card px-4 py-1.5 text-xs font-medium text-muted-foreground shadow-sm">
|
||||
<span className="inline-block size-1.5 rounded-full bg-green-500" />
|
||||
Now with Pro LLM Engine
|
||||
</div>
|
||||
|
||||
<h1 className="max-w-2xl text-balance text-4xl font-bold leading-tight tracking-tight text-foreground md:text-5xl lg:text-6xl">
|
||||
Translate Office Documents. Keep the Format Perfect.
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-pretty text-base leading-relaxed text-muted-foreground md:text-lg">
|
||||
Upload your Excel, Word, or PowerPoint files and get accurate translations with zero formatting loss.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/auth/register">Try Free</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild size="lg">
|
||||
<Link href="/pricing">View Pricing</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 pt-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<FileSpreadsheet className="size-4 text-green-500" />
|
||||
<span>.xlsx</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<FileText className="size-4 text-blue-500" />
|
||||
<span>.docx</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Presentation className="size-4 text-orange-500" />
|
||||
<span>.pptx</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export function HeroWordComparison() {
|
||||
</div>
|
||||
|
||||
<div className="relative p-2 sm:p-4 bg-[#1e1e1e]">
|
||||
<p className="mb-3 text-center text-xs text-slate-400 sm:text-left sm:px-1">
|
||||
<p className="mb-3 text-center text-xs text-slate-400 sm:text-start sm:px-1">
|
||||
{t("landing.heroDoc.caption")}
|
||||
</p>
|
||||
<div className="flex rounded-lg overflow-hidden border border-[#3c3c3c] shadow-inner min-h-[240px] sm:min-h-[300px]">
|
||||
@@ -86,7 +86,7 @@ export function HeroWordComparison() {
|
||||
{t("landing.heroDoc.badgeOriginal")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 p-3 sm:p-4 overflow-hidden bg-white text-left">
|
||||
<div className="flex-1 p-3 sm:p-4 overflow-hidden bg-white text-start">
|
||||
<article
|
||||
className="text-[11px] sm:text-xs leading-relaxed text-[#242424]"
|
||||
style={{
|
||||
@@ -108,7 +108,7 @@ export function HeroWordComparison() {
|
||||
{t("landing.heroDoc.badgeTranslated")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 p-3 sm:p-4 overflow-hidden bg-white text-left">
|
||||
<div className="flex-1 p-3 sm:p-4 overflow-hidden bg-white text-start">
|
||||
<article
|
||||
className="text-[11px] sm:text-xs leading-relaxed text-[#242424]"
|
||||
style={{
|
||||
|
||||
@@ -1,226 +1,355 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LanguageSwitcher } from "@/components/ui/language-switcher";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
import { HeroWordComparison } from "@/components/landing/hero-word-comparison";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { API_BASE } from "@/lib/config";
|
||||
import {
|
||||
Table2,
|
||||
FileText,
|
||||
Table2,
|
||||
Presentation,
|
||||
Bot,
|
||||
Lock,
|
||||
Zap,
|
||||
FileSpreadsheet,
|
||||
Check,
|
||||
PlayCircle,
|
||||
ShieldCheck,
|
||||
Clock,
|
||||
Languages,
|
||||
Zap,
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
BadgeCheck,
|
||||
Eye,
|
||||
LayoutGrid,
|
||||
Bot,
|
||||
Brain,
|
||||
BookOpen,
|
||||
Upload,
|
||||
ArrowLeftRight,
|
||||
} from "lucide-react";
|
||||
|
||||
export function LandingPage() {
|
||||
const { t } = useTranslation();
|
||||
const [isYearly, setIsYearly] = useState(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
const [prices, setPrices] = useState({
|
||||
starter: { monthly: 9, yearly: 86.40 },
|
||||
pro: { monthly: 19, yearly: 182.40 },
|
||||
business: { monthly: 49, yearly: 470.40 },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/api/v1/auth/plans`, { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((json) => {
|
||||
if (!json) return;
|
||||
const d = json.data ?? json;
|
||||
const plans = Array.isArray(d.plans) ? d.plans : [];
|
||||
const map: Record<string, { monthly: number; yearly: number }> = {};
|
||||
for (const p of plans) {
|
||||
if (p.id && typeof p.price_monthly === "number") {
|
||||
map[p.id] = {
|
||||
monthly: p.price_monthly,
|
||||
yearly: typeof p.price_yearly === "number" ? p.price_yearly : p.price_monthly * 12,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (map.starter || map.pro || map.business) {
|
||||
setPrices((prev) => ({
|
||||
starter: map.starter || prev.starter,
|
||||
pro: map.pro || prev.pro,
|
||||
business: map.business || prev.business,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground font-sans">
|
||||
{/* Header */}
|
||||
{/* ── Header ── */}
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/80 backdrop-blur-md">
|
||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center rounded-lg bg-primary/10 p-1.5">
|
||||
<Languages className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<span className="text-foreground text-lg font-bold tracking-tight">
|
||||
Office Translator
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
<a
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
href="#features"
|
||||
>
|
||||
{t("nav.features")}
|
||||
<a className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors" href="#why">
|
||||
{t("landing.nav.whyUs")}
|
||||
</a>
|
||||
<a
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
href="#pricing"
|
||||
>
|
||||
{t("nav.pricing")}
|
||||
<a className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors" href="#formats">
|
||||
{t("landing.nav.formats")}
|
||||
</a>
|
||||
<a
|
||||
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
|
||||
href="#enterprise"
|
||||
>
|
||||
{t("nav.enterprise")}
|
||||
<a className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors" href="#pricing">
|
||||
{t("landing.nav.pricing")}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSwitcher variant="button" />
|
||||
<LanguageSwitcher variant="select" />
|
||||
<ThemeToggle />
|
||||
<a
|
||||
className="hidden sm:inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||
href="/auth/login"
|
||||
>
|
||||
{t("common.login")}
|
||||
{t("landing.nav.login")}
|
||||
</a>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90"
|
||||
>
|
||||
<Link href="/auth/register">{t("common.signup")}</Link>
|
||||
<Button asChild className="bg-primary text-primary-foreground font-bold hover:bg-primary/90">
|
||||
<Link href="/auth/register">{t("landing.nav.startFree")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-20 pb-32 overflow-hidden">
|
||||
{/* ── Hero ── */}
|
||||
<section className="relative pt-20 pb-28 overflow-hidden">
|
||||
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background" />
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
||||
{/* Hero Text */}
|
||||
<div className="flex-1 text-center lg:text-left space-y-8">
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-foreground sm:text-5xl lg:text-6xl xl:text-7xl">
|
||||
{t("hero.title")}
|
||||
<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-emerald-400">
|
||||
{t("hero.titleHighlight")}
|
||||
</span>
|
||||
</h1>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 text-center">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/5 px-4 py-1.5 text-sm font-medium text-primary mb-8">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{t("landing.hero.badge")}
|
||||
</div>
|
||||
|
||||
<p className="mx-auto lg:mx-0 max-w-2xl text-lg text-muted-foreground">
|
||||
{t("hero.subtitle")}
|
||||
</p>
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-foreground sm:text-5xl lg:text-6xl xl:text-7xl mx-auto max-w-5xl">
|
||||
{t("landing.hero.title1")}
|
||||
<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-emerald-400">
|
||||
{t("landing.hero.title2")}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90"
|
||||
>
|
||||
<Link href="/auth/register">{t("hero.cta")}</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="gap-2"
|
||||
>
|
||||
<PlayCircle className="h-5 w-5" />
|
||||
{t("hero.demoCta")}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-8 mx-auto max-w-2xl text-lg text-muted-foreground">
|
||||
{t("landing.hero.subtitle")}
|
||||
</p>
|
||||
|
||||
{/* Trust Badges */}
|
||||
<div className="pt-8 flex flex-wrap justify-center lg:justify-start gap-6">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ShieldCheck className="h-5 w-5 text-primary" />
|
||||
<span>{t("hero.badge1")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-5 w-5 text-primary" />
|
||||
<span>{t("hero.badge2")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90 text-base px-8"
|
||||
>
|
||||
<Link href="/auth/register">
|
||||
{t("landing.hero.cta")}
|
||||
<ArrowRight className="ms-2 h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="text-base px-8" asChild>
|
||||
<Link href="#pricing">{t("landing.hero.seePlans")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trust Badges */}
|
||||
<div className="mt-12 flex flex-wrap justify-center gap-8 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-primary" />
|
||||
<span>{t("landing.trust.filesDeleted")}</span>
|
||||
</div>
|
||||
|
||||
{/* Hero Visual — Word EN | FR + illustrations */}
|
||||
<div className="flex-1 w-full max-w-lg lg:max-w-none relative">
|
||||
<HeroWordComparison />
|
||||
<div className="flex items-center gap-2">
|
||||
<BadgeCheck className="h-5 w-5 text-primary" />
|
||||
<span>{t("landing.trust.noBait")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5 text-primary" />
|
||||
<span>{t("landing.trust.preview")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Format pills */}
|
||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||
{[".DOCX", ".XLSX", ".PPTX", ".PDF"].map((fmt) => (
|
||||
<span
|
||||
key={fmt}
|
||||
className="inline-flex items-center rounded-full border border-border bg-card px-4 py-2 text-sm font-mono font-semibold text-foreground"
|
||||
>
|
||||
{fmt}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-24 bg-background relative" id="features">
|
||||
{/* ── How it works ── */}
|
||||
<section className="py-24 bg-background">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl mb-4">
|
||||
{t("features.title")}
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
{t("landing.howItWorks.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("features.subtitle")}
|
||||
{t("landing.howItWorks.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{/* Feature 1 - Excel */}
|
||||
<div className="w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Table2 className="h-6 w-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
||||
{[
|
||||
{
|
||||
step: "1",
|
||||
icon: Upload,
|
||||
title: t("landing.howItWorks.step1.title"),
|
||||
desc: t("landing.howItWorks.step1.desc"),
|
||||
color: "text-blue-500",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
icon: ArrowLeftRight,
|
||||
title: t("landing.howItWorks.step2.title"),
|
||||
desc: t("landing.howItWorks.step2.desc"),
|
||||
color: "text-primary",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
icon: Check,
|
||||
title: t("landing.howItWorks.step3.title"),
|
||||
desc: t("landing.howItWorks.step3.desc"),
|
||||
color: "text-emerald-500",
|
||||
},
|
||||
].map((s, i) => (
|
||||
<div key={i} className="flex flex-col items-center text-center">
|
||||
<div className={`flex size-16 items-center justify-center rounded-2xl bg-muted/50 mb-4 ${s.color}`}>
|
||||
<s.icon className="size-7" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">{s.title}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-xs">{s.desc}</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{t("features.excel.title")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{t("features.excel.description")}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature 2 - Word */}
|
||||
<div className="w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<FileText className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{t("features.word.title")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{t("features.word.description")}</p>
|
||||
{/* ── AI Translation Engine ── */}
|
||||
<section className="py-24 bg-muted/30" id="ai-engine">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/5 px-4 py-1.5 text-sm font-medium text-primary mb-6">
|
||||
<Brain className="h-4 w-4" />
|
||||
{t("landing.ai.badge")}
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
{t("landing.ai.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("landing.ai.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 - PowerPoint */}
|
||||
<div className="w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Presentation className="h-6 w-6" />
|
||||
{/* Three pillars */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mb-16">
|
||||
{[
|
||||
{ icon: Zap, title: t("landing.ai.context.title"), desc: t("landing.ai.context.desc"), color: "text-blue-500" },
|
||||
{ icon: BookOpen, title: t("landing.ai.glossary.title"), desc: t("landing.ai.glossary.desc"), color: "text-primary" },
|
||||
{ icon: Eye, title: t("landing.ai.vision.title"), desc: t("landing.ai.vision.desc"), color: "text-emerald-500" },
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className={`mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-muted/50 ${feature.color}`}>
|
||||
<feature.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
|
||||
<p className="text-muted-foreground text-sm">{feature.desc}</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{t("features.powerpoint.title")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{t("features.powerpoint.description")}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Feature 4 - AI */}
|
||||
<div className="w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Bot className="h-6 w-6" />
|
||||
{/* Comparison */}
|
||||
<div className="max-w-3xl mx-auto rounded-2xl border border-border bg-card p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{t("landing.ai.comparison.source")}</p>
|
||||
<p className="text-sm text-foreground font-medium italic">“{t("landing.ai.comparison.sourceText")}”</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{t("features.ai.title")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{t("features.ai.description")}</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 5 - Speed */}
|
||||
<div className="w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)] group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors">
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Zap className="h-6 w-6" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{t("landing.ai.comparison.google")}</p>
|
||||
<p className="text-sm text-destructive/80">“{t("landing.ai.comparison.googleText")}”</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-primary">{t("landing.ai.comparison.ai")}</p>
|
||||
<p className="text-sm text-emerald-600 font-medium">“{t("landing.ai.comparison.aiText")}”</p>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||
{t("features.speed.title")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{t("features.speed.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<section className="py-24 bg-muted/30" id="pricing">
|
||||
{/* ── Why Us — Format Preservation ── */}
|
||||
<section className="py-24 bg-muted/30" id="why">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
{t("landing.why.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("landing.why.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
title: t("landing.why.smartart.title"),
|
||||
desc: t("landing.why.smartart.desc"),
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: t("landing.why.toc.title"),
|
||||
desc: t("landing.why.toc.desc"),
|
||||
},
|
||||
{
|
||||
icon: Table2,
|
||||
title: t("landing.why.charts.title"),
|
||||
desc: t("landing.why.charts.desc"),
|
||||
},
|
||||
{
|
||||
icon: Presentation,
|
||||
title: t("landing.why.shapes.title"),
|
||||
desc: t("landing.why.shapes.desc"),
|
||||
},
|
||||
{
|
||||
icon: FileSpreadsheet,
|
||||
title: t("landing.why.headers.title"),
|
||||
desc: t("landing.why.headers.desc"),
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: t("landing.why.languages.title"),
|
||||
desc: t("landing.why.languages.desc"),
|
||||
},
|
||||
].map((feature, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group relative rounded-2xl border border-border bg-card p-8 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<feature.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
|
||||
<p className="text-muted-foreground">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Honest Pricing Comparison ── */}
|
||||
<section className="py-24 bg-background" id="pricing">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl mb-4">
|
||||
{t("pricing.title")}
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
{t("landing.pricing.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground">{t("pricing.subtitle")}</p>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("landing.pricing.subtitle")}
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center mt-8">
|
||||
<div className="inline-flex items-center gap-3 bg-card border border-border rounded-full p-1.5">
|
||||
@@ -228,21 +357,24 @@ export function LandingPage() {
|
||||
onClick={() => setIsYearly(false)}
|
||||
className={cn(
|
||||
"px-5 py-2 rounded-full text-sm font-medium transition-all",
|
||||
!isYearly ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground hover:text-foreground"
|
||||
!isYearly ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{t("common.monthly")}
|
||||
{t("landing.pricing.monthly")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsYearly(true)}
|
||||
className={cn(
|
||||
"px-5 py-2 rounded-full text-sm font-medium transition-all flex items-center gap-2",
|
||||
isYearly ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground hover:text-foreground"
|
||||
isYearly ? "bg-primary text-primary-foreground shadow" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{t("common.yearly")}
|
||||
<span className={cn("text-xs px-1.5 py-0.5 rounded-full font-bold", isYearly ? "bg-background text-primary" : "bg-emerald-500/20 text-emerald-600 dark:text-emerald-400")}>
|
||||
{t("common.save20")}
|
||||
{t("landing.pricing.yearly")}
|
||||
<span className={cn(
|
||||
"text-xs px-1.5 py-0.5 rounded-full font-bold",
|
||||
isYearly ? "bg-background text-primary" : "bg-emerald-500/20 text-emerald-600"
|
||||
)}>
|
||||
-20%
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -250,140 +382,218 @@ export function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||
{/* Starter Plan */}
|
||||
{/* Starter */}
|
||||
<div className="rounded-2xl border border-border bg-card p-8 flex flex-col">
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
{t("pricing.starter.name")}
|
||||
</h3>
|
||||
<p className="mt-4 text-muted-foreground text-sm">
|
||||
{t("pricing.starter.description")}
|
||||
<h3 className="text-xl font-semibold">{t("landing.pricing.starter.title")}</h3>
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
{t("landing.pricing.starter.desc")}
|
||||
</p>
|
||||
<div className="mt-6 mb-2">
|
||||
<span className="text-4xl font-bold text-foreground">
|
||||
{isYearly ? t("pricing.starter.priceYearly") : t("pricing.starter.priceMonthly")}
|
||||
<span className="text-4xl font-bold">
|
||||
€{isYearly ? prices.starter.yearly / 12 : prices.starter.monthly}
|
||||
</span>
|
||||
<span className="text-muted-foreground">/{t("common.month")}</span>
|
||||
<span className="text-muted-foreground">{t("landing.pricing.perMonth")}</span>
|
||||
</div>
|
||||
<div className="h-6 mb-4">
|
||||
{isYearly && (
|
||||
<span className="text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
||||
{t("pricing.starter.billedYearly")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-4 mb-8 flex-1">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<li key={i} className="flex items-center text-muted-foreground text-sm">
|
||||
<Check className="h-4 w-4 text-primary mr-2 shrink-0" />
|
||||
{t(`pricing.starter.features.${i}`)}
|
||||
{isYearly && (
|
||||
<span className="text-sm text-emerald-600 mb-4">€{prices.starter.yearly} {t("landing.pricing.billedYearly")}</span>
|
||||
)}
|
||||
<ul className="space-y-3 mb-8 flex-1 mt-4">
|
||||
{[
|
||||
t("landing.pricing.starter.f1"),
|
||||
t("landing.pricing.starter.f2"),
|
||||
t("landing.pricing.starter.f3"),
|
||||
t("landing.pricing.starter.f4"),
|
||||
].map((f, i) => (
|
||||
<li key={i} className="flex items-center text-sm text-muted-foreground">
|
||||
<Check className="h-4 w-4 text-primary me-2 shrink-0" />{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/pricing?plan=starter">{t("common.signup")}</Link>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/pricing?plan=starter">{t("landing.pricing.starter.cta")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Pro Plan */}
|
||||
<div className="relative rounded-2xl border border-primary bg-card p-8 flex flex-col shadow-lg shadow-primary/10 scale-105 z-10">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 rounded-full bg-warning px-3 py-1 text-xs font-bold text-warning-foreground uppercase tracking-wide">
|
||||
{t("common.popular")}
|
||||
{/* Pro — Popular */}
|
||||
<div className="relative rounded-2xl border-2 border-primary bg-card p-8 flex flex-col shadow-lg shadow-primary/10 scale-105 z-10">
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 rounded-full bg-primary px-4 py-1 text-xs font-bold text-primary-foreground uppercase tracking-wide">
|
||||
{t("landing.pricing.pro.badge")}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
{t("pricing.pro.name")}
|
||||
</h3>
|
||||
<p className="mt-4 text-muted-foreground text-sm">
|
||||
{t("pricing.pro.description")}
|
||||
<h3 className="text-xl font-semibold">{t("landing.pricing.pro.title")}</h3>
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
{t("landing.pricing.pro.desc")}
|
||||
</p>
|
||||
<div className="mt-6 mb-2">
|
||||
<span className="text-4xl font-bold text-foreground">
|
||||
{isYearly ? t("pricing.pro.priceYearly") : t("pricing.pro.priceMonthly")}
|
||||
<span className="text-4xl font-bold">
|
||||
€{isYearly ? prices.pro.yearly / 12 : prices.pro.monthly}
|
||||
</span>
|
||||
<span className="text-muted-foreground">/{t("common.month")}</span>
|
||||
<span className="text-muted-foreground">{t("landing.pricing.perMonth")}</span>
|
||||
</div>
|
||||
<div className="h-6 mb-4">
|
||||
{isYearly && (
|
||||
<span className="text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
||||
{t("pricing.pro.billedYearly")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-4 mb-8 flex-1">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<li key={i} className="flex items-center text-foreground text-sm">
|
||||
<Check className="h-4 w-4 text-primary mr-2 shrink-0" />
|
||||
{t(`pricing.pro.features.${i}`)}
|
||||
{isYearly && (
|
||||
<span className="text-sm text-emerald-600 mb-4">€{prices.pro.yearly} {t("landing.pricing.billedYearly")}</span>
|
||||
)}
|
||||
<ul className="space-y-3 mb-8 flex-1 mt-4">
|
||||
{[
|
||||
t("landing.pricing.pro.f1"),
|
||||
t("landing.pricing.pro.f2"),
|
||||
t("landing.pricing.pro.f3"),
|
||||
t("landing.pricing.pro.f4"),
|
||||
t("landing.pricing.pro.f5"),
|
||||
t("landing.pricing.pro.f6"),
|
||||
].map((f, i) => (
|
||||
<li key={i} className="flex items-center text-sm text-foreground font-medium">
|
||||
<Check className="h-4 w-4 text-primary me-2 shrink-0" />{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90"
|
||||
>
|
||||
<Link href="/pricing?plan=pro">{t("common.tryPro")}</Link>
|
||||
<Button asChild className="w-full bg-primary text-primary-foreground font-bold hover:bg-primary/90">
|
||||
<Link href="/pricing?plan=pro">{t("landing.pricing.pro.cta")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Business Plan */}
|
||||
{/* Business */}
|
||||
<div className="rounded-2xl border border-border bg-card p-8 flex flex-col">
|
||||
<h3 className="text-xl font-semibold text-foreground">
|
||||
{t("pricing.business.name")}
|
||||
</h3>
|
||||
<p className="mt-4 text-muted-foreground text-sm">
|
||||
{t("pricing.business.description")}
|
||||
<h3 className="text-xl font-semibold">{t("landing.pricing.business.title")}</h3>
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
{t("landing.pricing.business.desc")}
|
||||
</p>
|
||||
<div className="mt-6 mb-2">
|
||||
<span className="text-4xl font-bold text-foreground">
|
||||
{isYearly ? t("pricing.business.priceYearly") : t("pricing.business.priceMonthly")}
|
||||
<span className="text-4xl font-bold">
|
||||
€{isYearly ? prices.business.yearly / 12 : prices.business.monthly}
|
||||
</span>
|
||||
<span className="text-muted-foreground">/{t("common.month")}</span>
|
||||
<span className="text-muted-foreground">{t("landing.pricing.perMonth")}</span>
|
||||
</div>
|
||||
<div className="h-6 mb-4">
|
||||
{isYearly && (
|
||||
<span className="text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
||||
{t("pricing.business.billedYearly")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ul className="space-y-4 mb-8 flex-1">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<li key={i} className="flex items-center text-muted-foreground text-sm">
|
||||
<Check className="h-4 w-4 text-primary mr-2 shrink-0" />
|
||||
{t(`pricing.business.features.${i}`)}
|
||||
{isYearly && (
|
||||
<span className="text-sm text-emerald-600 mb-4">€{prices.business.yearly} {t("landing.pricing.billedYearly")}</span>
|
||||
)}
|
||||
<ul className="space-y-3 mb-8 flex-1 mt-4">
|
||||
{[
|
||||
t("landing.pricing.business.f1"),
|
||||
t("landing.pricing.business.f2"),
|
||||
t("landing.pricing.business.f3"),
|
||||
t("landing.pricing.business.f4"),
|
||||
t("landing.pricing.business.f5"),
|
||||
t("landing.pricing.business.f6"),
|
||||
].map((f, i) => (
|
||||
<li key={i} className="flex items-center text-sm text-muted-foreground">
|
||||
<Check className="h-4 w-4 text-primary me-2 shrink-0" />{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/pricing?plan=business">{t("common.tryPro")}</Link>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/pricing?plan=business">{t("landing.pricing.business.cta")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Honest pricing callout */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/5 px-5 py-2.5 text-sm text-emerald-700 dark:text-emerald-400">
|
||||
<BadgeCheck className="h-5 w-5" />
|
||||
<span className="font-medium">{t("landing.pricing.honest")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
{/* ── Format Support ── */}
|
||||
<section className="py-24 bg-muted/30" id="formats">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
{t("landing.formats.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
{t("landing.formats.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{[
|
||||
{
|
||||
format: t("landing.formats.word"),
|
||||
items: [
|
||||
t("landing.formats.word.f1"),
|
||||
t("landing.formats.word.f2"),
|
||||
t("landing.formats.word.f3"),
|
||||
t("landing.formats.word.f4"),
|
||||
t("landing.formats.word.f5"),
|
||||
t("landing.formats.word.f6"),
|
||||
t("landing.formats.word.f7"),
|
||||
],
|
||||
},
|
||||
{
|
||||
format: t("landing.formats.excel"),
|
||||
items: [
|
||||
t("landing.formats.excel.f1"),
|
||||
t("landing.formats.excel.f2"),
|
||||
t("landing.formats.excel.f3"),
|
||||
t("landing.formats.excel.f4"),
|
||||
t("landing.formats.excel.f5"),
|
||||
],
|
||||
},
|
||||
{
|
||||
format: t("landing.formats.powerpoint"),
|
||||
items: [
|
||||
t("landing.formats.powerpoint.f1"),
|
||||
t("landing.formats.powerpoint.f2"),
|
||||
t("landing.formats.powerpoint.f3"),
|
||||
t("landing.formats.powerpoint.f4"),
|
||||
t("landing.formats.powerpoint.f5"),
|
||||
],
|
||||
},
|
||||
{
|
||||
format: t("landing.formats.pdf"),
|
||||
items: [
|
||||
t("landing.formats.pdf.f1"),
|
||||
t("landing.formats.pdf.f2"),
|
||||
t("landing.formats.pdf.f3"),
|
||||
t("landing.formats.pdf.f4"),
|
||||
t("landing.formats.pdf.f5"),
|
||||
],
|
||||
},
|
||||
].map((fmt, i) => (
|
||||
<div key={i} className="rounded-2xl border border-border bg-card p-8">
|
||||
<h3 className="text-xl font-bold mb-4">{fmt.format}</h3>
|
||||
<ul className="space-y-2">
|
||||
{fmt.items.map((item, j) => (
|
||||
<li key={j} className="flex items-center text-sm text-muted-foreground">
|
||||
<Check className="h-4 w-4 text-primary me-2 shrink-0" />{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── CTA ── */}
|
||||
<section className="relative py-20 px-4 bg-background">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary/5 to-transparent pointer-events-none" />
|
||||
<div className="mx-auto max-w-4xl rounded-3xl bg-card border border-border p-12 text-center shadow-2xl overflow-hidden relative">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary to-transparent opacity-50" />
|
||||
<h2 className="text-3xl font-bold text-foreground mb-6">
|
||||
{t("cta.title")}
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
{t("landing.cta.title")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
{t("cta.subtitle")}
|
||||
{t("landing.cta.subtitle")}
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90"
|
||||
className="bg-primary text-primary-foreground font-bold hover:bg-primary/90 text-base px-8"
|
||||
>
|
||||
<Link href="/auth/register">{t("cta.button")}</Link>
|
||||
<Link href="/auth/register">
|
||||
{t("landing.cta.button")}
|
||||
<ArrowRight className="ms-2 h-5 w-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
{/* ── Footer ── */}
|
||||
<footer className="border-t border-border bg-background py-12">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
@@ -396,17 +606,13 @@ export function LandingPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-6 text-sm text-muted-foreground">
|
||||
<a className="hover:text-foreground transition-colors" href="#">
|
||||
{t("footer.privacy")}
|
||||
</a>
|
||||
<a className="hover:text-foreground transition-colors" href="#">
|
||||
{t("footer.terms")}
|
||||
</a>
|
||||
<a className="hover:text-foreground transition-colors" href="#">
|
||||
{t("footer.contact")}
|
||||
</a>
|
||||
<a className="hover:text-foreground transition-colors" href="#">{t("landing.footer.privacy")}</a>
|
||||
<a className="hover:text-foreground transition-colors" href="#">{t("landing.footer.terms")}</a>
|
||||
<a className="hover:text-foreground transition-colors" href="#">{t("landing.footer.contact")}</a>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
© {new Date().getFullYear()} Office Translator
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">{t("footer.copyright")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useEffect, useCallback, memo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Upload,
|
||||
LayoutDashboard,
|
||||
LogIn,
|
||||
Crown,
|
||||
LogOut,
|
||||
BookOpen,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
plan: string;
|
||||
}
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
name: "Translate",
|
||||
href: "/",
|
||||
icon: Upload,
|
||||
description: "Translate documents",
|
||||
},
|
||||
{
|
||||
name: "Context",
|
||||
href: "/settings/context",
|
||||
icon: BookOpen,
|
||||
description: "Configure AI instructions & glossary",
|
||||
},
|
||||
];
|
||||
|
||||
const planColors: Record<string, string> = {
|
||||
free: "bg-zinc-600",
|
||||
starter: "bg-blue-500",
|
||||
pro: "bg-teal-500",
|
||||
business: "bg-purple-500",
|
||||
enterprise: "bg-amber-500",
|
||||
};
|
||||
|
||||
// Memoized NavItem for performance
|
||||
const NavItem = memo(function NavItem({
|
||||
item,
|
||||
isActive
|
||||
}: {
|
||||
item: typeof navigation[0];
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-teal-500/10 text-teal-400"
|
||||
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{item.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const storedUser = localStorage.getItem("user");
|
||||
if (storedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
} catch {
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
// Listen for storage changes
|
||||
const handleStorage = (e: StorageEvent) => {
|
||||
if (e.key === "user") {
|
||||
setUser(e.newValue ? JSON.parse(e.newValue) : null);
|
||||
}
|
||||
};
|
||||
window.addEventListener("storage", handleStorage);
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
}, []);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
setUser(null);
|
||||
window.location.href = "/";
|
||||
}, []);
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return (
|
||||
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
||||
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-teal-500 text-white font-bold">文A</div>
|
||||
<span className="text-lg font-semibold text-white">Office Translator</span>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
||||
{/* Logo */}
|
||||
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-teal-500 text-white font-bold">
|
||||
文A
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-white">Office Translator</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-col gap-1 p-4">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Tooltip key={item.name}>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-teal-500/10 text-teal-400"
|
||||
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{item.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* User Section */}
|
||||
{user && (
|
||||
<div className="mt-4 pt-4 border-t border-zinc-800">
|
||||
<p className="px-3 mb-2 text-xs font-medium text-zinc-600 uppercase tracking-wider">Account</p>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
pathname === "/dashboard"
|
||||
? "bg-teal-500/10 text-teal-400"
|
||||
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<LayoutDashboard className="h-5 w-5" />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>View your usage and settings</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/settings/subscription"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
pathname === "/settings/subscription"
|
||||
? "bg-violet-500/10 text-violet-400"
|
||||
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
||||
)}
|
||||
>
|
||||
<Crown className="h-5 w-5" />
|
||||
<span>Mon abonnement</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Gérer votre forfait et votre usage</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{user.plan === "free" && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
pathname === "/pricing"
|
||||
? "bg-amber-500/10 text-amber-400"
|
||||
: "text-amber-400/70 hover:bg-zinc-800 hover:text-amber-400"
|
||||
)}
|
||||
>
|
||||
<Zap className="h-5 w-5" />
|
||||
<span>Passer Pro</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Voir tous les forfaits disponibles</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* User section at bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 border-t border-zinc-800 p-4">
|
||||
{user ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/dashboard" className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-teal-500 to-teal-600 text-white text-sm font-medium shrink-0">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-medium text-white truncate">{user.name}</span>
|
||||
<Badge className={cn("text-xs mt-0.5 w-fit", planColors[user.plan] || "bg-zinc-600")}>
|
||||
{{ free: "Gratuit", starter: "Starter", pro: "Pro", business: "Business", enterprise: "Entreprise" }[user.plan] ?? user.plan}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Link href="/auth/login" className="block">
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-white text-sm font-medium transition-colors">
|
||||
<LogIn className="h-4 w-4" />
|
||||
Sign In
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/auth/register" className="block">
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-teal-500 hover:bg-teal-600 text-white text-sm font-medium transition-colors">
|
||||
Get Started Free
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
|
||||
{/* Icon */}
|
||||
{icon && (
|
||||
<span className="mr-1 flex-shrink-0">
|
||||
<span className="me-1 flex-shrink-0">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
@@ -126,7 +126,7 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
<span className="flex items-center gap-1">
|
||||
{children}
|
||||
{displayCount && (
|
||||
<span className="ml-1 bg-white/20 px-1.5 py-0.5 rounded text-xs font-bold">
|
||||
<span className="ms-1 bg-white/20 px-1.5 py-0.5 rounded text-xs font-bold">
|
||||
{displayCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -137,7 +137,7 @@ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemove}
|
||||
className="ml-1 flex-shrink-0 rounded-full bg-white/20 hover:bg-white/30 transition-colors p-0.5"
|
||||
className="ms-1 flex-shrink-0 rounded-full bg-white/20 hover:bg-white/30 transition-colors p-0.5"
|
||||
aria-label="Remove badge"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -118,7 +118,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-2 h-4 w-4"
|
||||
className="animate-spin -ms-1 me-2 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -84,7 +84,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ function DropdownMenuItem({
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -155,7 +155,7 @@ function DropdownMenuLabel({
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:ps-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -80,8 +80,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
"outline-none focus:outline-none",
|
||||
"read-only:cursor-default read-only:bg-surface/50",
|
||||
variants[variant],
|
||||
leftIcon && "pl-10",
|
||||
rightIcon && "pr-10",
|
||||
leftIcon && "ps-10",
|
||||
rightIcon && "pe-10",
|
||||
(leftIcon && rightIcon) && "px-10",
|
||||
error && "text-destructive",
|
||||
className
|
||||
@@ -264,7 +264,7 @@ export const SearchInput = React.forwardRef<
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
"pl-10 pr-10",
|
||||
"ps-10 pe-10",
|
||||
focused && "shadow-lg shadow-primary/10",
|
||||
className
|
||||
)}
|
||||
@@ -331,7 +331,7 @@ export const FileInput = React.forwardRef<
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
className={cn(
|
||||
"file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 cursor-pointer",
|
||||
"file:me-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 cursor-pointer",
|
||||
dragActive && "border-primary bg-primary/5",
|
||||
className
|
||||
)}
|
||||
@@ -344,7 +344,7 @@ export const FileInput = React.forwardRef<
|
||||
/>
|
||||
|
||||
{fileName && (
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pe-3">
|
||||
<span className="text-xs text-muted-foreground truncate max-w-32">
|
||||
{fileName}
|
||||
</span>
|
||||
|
||||
@@ -14,6 +14,17 @@ import { useI18n, type Locale } from "@/lib/i18n";
|
||||
const languages: { value: Locale; label: string; flag: string }[] = [
|
||||
{ value: "en", label: "English", flag: "🇬🇧" },
|
||||
{ value: "fr", label: "Français", flag: "🇫🇷" },
|
||||
{ value: "es", label: "Español", flag: "🇪🇸" },
|
||||
{ value: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ value: "pt", label: "Português", flag: "🇧🇷" },
|
||||
{ value: "it", label: "Italiano", flag: "🇮🇹" },
|
||||
{ value: "nl", label: "Nederlands", flag: "🇳🇱" },
|
||||
{ value: "ru", label: "Русский", flag: "🇷🇺" },
|
||||
{ value: "ja", label: "日本語", flag: "🇯🇵" },
|
||||
{ value: "ko", label: "한국어", flag: "🇰🇷" },
|
||||
{ value: "zh", label: "中文", flag: "🇨🇳" },
|
||||
{ value: "ar", label: "العربية", flag: "🇸🇦" },
|
||||
{ value: "fa", label: "فارسی", flag: "🇮🇷" },
|
||||
];
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
@@ -43,7 +54,7 @@ export function LanguageSwitcher({ variant = "select" }: LanguageSwitcherProps)
|
||||
|
||||
return (
|
||||
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
|
||||
<SelectTrigger className="w-[130px] h-9 bg-transparent border-border-dark hover:bg-surface-dark">
|
||||
<SelectTrigger className="w-[150px] h-9">
|
||||
<SelectValue>
|
||||
<span className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
@@ -52,12 +63,11 @@ export function LanguageSwitcher({ variant = "select" }: LanguageSwitcherProps)
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-surface-dark border-border-dark">
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem
|
||||
key={lang.value}
|
||||
value={lang.value}
|
||||
className="hover:bg-primary/10 focus:bg-primary/10"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{lang.flag}</span>
|
||||
|
||||
@@ -153,7 +153,7 @@ export function ModelCombobox({
|
||||
<span className="text-muted-foreground">ctx: {(model.context_length / 1000).toFixed(0)}k</span>
|
||||
)}
|
||||
</div>
|
||||
{value === model.id && <Check className="size-3.5 text-primary flex-shrink-0 ml-2" />}
|
||||
{value === model.id && <Check className="size-3.5 text-primary flex-shrink-0 ms-2" />}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucid
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const notificationVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pr-8 shadow-lg transition-all duration-300 ease-out",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pe-8 shadow-lg transition-all duration-300 ease-out",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -109,7 +109,7 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pe-8 ps-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -74,7 +74,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"h-10 px-2 text-start align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -89,7 +89,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:animate-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:animate-out",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-6 pe-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:animate-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Reference in New Issue
Block a user