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:
2026-05-10 11:43:28 +02:00
parent 16ac7ca2b9
commit ce8e150a61
110 changed files with 6935 additions and 4301 deletions

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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={{

View File

@@ -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">&ldquo;{t("landing.ai.comparison.sourceText")}&rdquo;</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">&ldquo;{t("landing.ai.comparison.googleText")}&rdquo;</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">&ldquo;{t("landing.ai.comparison.aiText")}&rdquo;</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>

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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}
/>
)

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
))
)}

View File

@@ -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: {

View File

@@ -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}

View File

@@ -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}

View File

@@ -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: {