feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle

Made-with: Cursor
This commit is contained in:
Sepehr Ramezani
2026-03-07 11:42:58 +01:00
parent 3d37ce4582
commit 473b3e26c7
181 changed files with 30617 additions and 7170 deletions

View File

@@ -20,7 +20,8 @@ import {
Eye,
Trash2,
Copy,
ExternalLink
ExternalLink,
ChevronRight
} from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -183,23 +184,25 @@ const FilePreview = ({ file, onRemove }: FilePreviewProps) => {
};
export function FileUploader() {
const { settings, isTranslating, progress, setTranslating, setProgress } = useTranslationStore();
const { settings } = useTranslationStore();
const webllm = useWebLLM();
const [file, setFile] = useState<File | null>(null);
const [targetLanguage, setTargetLanguage] = useState(settings.defaultTargetLanguage);
const [provider, setProvider] = useState<ProviderType>(settings.defaultProvider);
const [provider, setProvider] = useState<ProviderType>(settings.defaultProvider as ProviderType);
const [translateImages, setTranslateImages] = useState(settings.translateImages);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [translationStatus, setTranslationStatus] = useState<string>("");
const [showAdvanced, setShowAdvanced] = useState(false);
const [isTranslating, setTranslating] = useState(false);
const [progress, setProgress] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
// Sync with store settings when they change
useEffect(() => {
setTargetLanguage(settings.defaultTargetLanguage);
setProvider(settings.defaultProvider);
setProvider(settings.defaultProvider as ProviderType);
setTranslateImages(settings.translateImages);
}, [settings.defaultTargetLanguage, settings.defaultProvider, settings.translateImages]);
@@ -227,17 +230,6 @@ export function FileUploader() {
const handleTranslate = async () => {
if (!file) return;
// Validate provider-specific requirements
if (provider === "openai" && !settings.openaiApiKey) {
setError("OpenAI API key not configured. Go to Settings > Translation Services to add your API key.");
return;
}
if (provider === "deepl" && !settings.deeplApiKey) {
setError("DeepL API key not configured. Go to Settings > Translation Services to add your API key.");
return;
}
// WebLLM specific validation
if (provider === "webllm") {
if (!webllm.isWebGPUSupported()) {

View File

@@ -1,594 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import {
ArrowRight,
Check,
FileText,
Globe2,
Zap,
Shield,
Server,
Sparkles,
FileSpreadsheet,
Presentation,
Star,
TrendingUp,
Users,
Clock,
Award,
ChevronRight,
Play,
BarChart3,
Brain,
Lock,
Zap as ZapIcon
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFeature } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface User {
name: string;
plan: string;
}
export function LandingHero() {
const [user, setUser] = useState<User | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const storedUser = localStorage.getItem("user");
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch {
setUser(null);
}
}
// Trigger animation after mount
setTimeout(() => setIsLoaded(true), 100);
}, []);
return (
<div className="relative overflow-hidden">
{/* Enhanced Background with animated gradient */}
<div className="absolute inset-0">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5" />
<div className="absolute inset-0 bg-[url('/grid.svg')] opacity-5" />
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/90 to-background/50" />
</div>
{/* Animated floating elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className={cn(
"absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl animate-pulse",
isLoaded && "animate-float"
)} />
<div className={cn(
"absolute top-40 right-20 w-32 h-32 bg-accent/10 rounded-full blur-2xl animate-pulse animation-delay-2000",
isLoaded && "animate-float-delayed"
)} />
<div className={cn(
"absolute bottom-20 left-1/4 w-16 h-16 bg-success/10 rounded-full blur-lg animate-pulse animation-delay-4000",
isLoaded && "animate-float-slow"
)} />
</div>
{/* Hero content */}
<div className={cn(
"relative px-4 py-24 sm:py-32 transition-all duration-1000 ease-out",
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
)}>
<div className="text-center max-w-5xl mx-auto">
{/* Premium Badge */}
<Badge
variant="premium"
size="lg"
className="mb-8 animate-slide-up animation-delay-200"
>
<Sparkles className="w-4 h-4 mr-2" />
AI-Powered Document Translation
</Badge>
{/* Enhanced Headline */}
<h1 className="text-display text-4xl sm:text-6xl lg:text-7xl font-bold text-white mb-6 leading-tight">
<span className={cn(
"block bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent bg-size-200 animate-gradient",
isLoaded && "animate-gradient-shift"
)}>
Translate Documents
</span>
<span className="block text-3xl sm:text-4xl lg:text-5xl mt-2">
<span className="relative">
Instantly
<span className="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-primary to-accent rounded-full animate-underline-expand" />
</span>
</span>
</h1>
{/* Enhanced Description */}
<p className={cn(
"text-xl text-text-secondary mb-12 max-w-3xl mx-auto leading-relaxed",
isLoaded && "animate-slide-up animation-delay-400"
)}>
Upload Word, Excel, and PowerPoint files. Get perfect translations while preserving
all formatting, styles, and layouts. Powered by advanced AI technology.
</p>
{/* Enhanced CTA Buttons */}
<div className={cn(
"flex flex-col sm:flex-row gap-6 justify-center mb-16",
isLoaded && "animate-slide-up animation-delay-600"
)}>
{user ? (
<Link href="#upload">
<Button size="lg" variant="premium" className="group px-8 py-4 text-lg">
Start Translating
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
) : (
<>
<Link href="/auth/register">
<Button size="lg" variant="premium" className="group px-8 py-4 text-lg">
Get Started Free
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
<Link href="/pricing">
<Button
size="lg"
variant="glass"
className="px-8 py-4 text-lg border-2 hover:border-primary/50"
>
View Pricing
</Button>
</Link>
</>
)}
</div>
{/* Enhanced Supported formats */}
<div className={cn(
"flex flex-wrap justify-center gap-4 mb-16",
isLoaded && "animate-slide-up animation-delay-800"
)}>
{[
{ icon: FileText, name: "Word", ext: ".docx", color: "text-blue-400" },
{ icon: FileSpreadsheet, name: "Excel", ext: ".xlsx", color: "text-green-400" },
{ icon: Presentation, name: "PowerPoint", ext: ".pptx", color: "text-orange-400" },
].map((format, idx) => (
<Card
key={format.name}
variant="glass"
className="group px-6 py-4 hover:scale-105 transition-all duration-300"
>
<div className="flex items-center gap-3">
<div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300",
format.color
)}>
<format.icon className="w-6 h-6" />
</div>
<div className="text-left">
<div className="font-semibold text-white">{format.name}</div>
<div className="text-sm text-text-tertiary">{format.ext}</div>
</div>
</div>
</Card>
))}
</div>
{/* Trust Indicators */}
<div className={cn(
"flex flex-wrap justify-center gap-8 text-sm text-text-tertiary",
isLoaded && "animate-slide-up animation-delay-1000"
)}>
{[
{ icon: Users, text: "10,000+ Users" },
{ icon: Star, text: "4.9/5 Rating" },
{ icon: Shield, text: "Bank-level Security" },
{ icon: ZapIcon, text: "Lightning Fast" },
].map((indicator, idx) => (
<div key={indicator.text} className="flex items-center gap-2">
<indicator.icon className="w-4 h-4 text-primary" />
<span>{indicator.text}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
export function FeaturesSection() {
const [ref, setRef] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setRef(true);
}
},
{ threshold: 0.1 }
);
const element = document.getElementById('features-section');
if (element) {
observer.observe(element);
}
return () => observer.disconnect();
}, []);
const features = [
{
icon: Globe2,
title: "100+ Languages",
description: "Translate between any language pair with high accuracy using advanced AI models",
color: "text-blue-400",
stats: "100+",
},
{
icon: FileText,
title: "Preserve Formatting",
description: "All styles, fonts, colors, tables, and charts remain intact",
color: "text-green-400",
stats: "100%",
},
{
icon: Zap,
title: "Lightning Fast",
description: "Batch processing translates entire documents in seconds",
color: "text-amber-400",
stats: "2s",
},
{
icon: Shield,
title: "Secure & Private",
description: "Your documents are encrypted and never stored permanently",
color: "text-purple-400",
stats: "AES-256",
},
{
icon: Brain,
title: "AI-Powered",
description: "Advanced neural translation for natural, context-aware results",
color: "text-teal-400",
stats: "GPT-4",
},
{
icon: Server,
title: "Enterprise Ready",
description: "API access, team management, and dedicated support for businesses",
color: "text-orange-400",
stats: "99.9%",
},
];
return (
<div id="features-section" className="py-24 px-4 relative">
{/* Background decoration */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-surface/50 pointer-events-none" />
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<Badge variant="glass" className="mb-4">
Features
</Badge>
<h2 className="text-3xl font-bold text-white mb-4">
Everything You Need for Document Translation
</h2>
<p className="text-xl text-text-secondary max-w-3xl 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-8">
{features.map((feature, idx) => {
const Icon = feature.icon;
return (
<CardFeature
key={feature.title}
icon={<Icon className="w-6 h-6" />}
title={feature.title}
description={feature.description}
color="primary"
className={cn(
"group",
ref && "animate-fade-in-up",
`animation-delay-${idx * 100}`
)}
/>
);
})}
</div>
{/* Enhanced Stats Row */}
<div className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8">
{[
{ value: "10M+", label: "Documents Translated", icon: FileText },
{ value: "150+", label: "Countries", icon: Globe2 },
{ value: "99.9%", label: "Uptime", icon: Shield },
{ value: "24/7", label: "Support", icon: Clock },
].map((stat, idx) => (
<div
key={stat.label}
className={cn(
"text-center p-6 rounded-xl surface-elevated border border-border-subtle",
ref && "animate-fade-in-up",
`animation-delay-${idx * 100 + 600}`
)}
>
<stat.icon className="w-8 h-8 mx-auto mb-3 text-primary" />
<div className="text-2xl font-bold text-white mb-1">{stat.value}</div>
<div className="text-sm text-text-secondary">{stat.label}</div>
</div>
))}
</div>
</div>
</div>
);
}
export function PricingPreview() {
const [ref, setRef] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setRef(true);
}
},
{ threshold: 0.1 }
);
const element = document.getElementById('pricing-preview');
if (element) {
observer.observe(element);
}
return () => observer.disconnect();
}, []);
const plans = [
{
name: "Free",
price: "$0",
description: "Perfect for trying out",
features: ["5 documents/day", "10 pages/doc", "Basic support"],
cta: "Get Started",
href: "/auth/register",
popular: false,
},
{
name: "Pro",
price: "$29",
period: "/month",
description: "For professionals",
features: ["200 documents/month", "Unlimited pages", "Priority support", "API access"],
cta: "Start Free Trial",
href: "/pricing",
popular: true,
},
{
name: "Business",
price: "$79",
period: "/month",
description: "For teams",
features: ["1000 documents/month", "Team management", "Dedicated support", "SLA"],
cta: "Contact Sales",
href: "/pricing",
popular: false,
},
];
return (
<div id="pricing-preview" className="py-24 px-4 relative">
{/* Background decoration */}
<div className="absolute inset-0 bg-gradient-to-b from-surface/50 to-transparent pointer-events-none" />
<div className="max-w-5xl mx-auto">
<div className="text-center mb-16">
<Badge variant="glass" className="mb-4">
Pricing
</Badge>
<h2 className="text-3xl font-bold text-white mb-4">
Simple, Transparent Pricing
</h2>
<p className="text-xl text-text-secondary max-w-3xl mx-auto">
Start free, upgrade when you need more.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{plans.map((plan, idx) => (
<Card
key={plan.name}
variant={plan.popular ? "gradient" : "elevated"}
className={cn(
"relative overflow-hidden group",
ref && "animate-fade-in-up",
`animation-delay-${idx * 100}`
)}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="premium" className="animate-pulse">
Most Popular
</Badge>
</div>
)}
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl mb-2">{plan.name}</CardTitle>
<CardDescription>{plan.description}</CardDescription>
<div className="my-6">
<span className="text-4xl font-bold text-white">
{plan.price}
</span>
{plan.period && (
<span className="text-lg text-text-secondary ml-1">
{plan.period}
</span>
)}
</div>
</CardHeader>
<CardContent className="pt-0">
<ul className="space-y-3 mb-6">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-text-secondary">
<Check className="h-4 w-4 text-success flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
<Link href={plan.href}>
<Button
variant={plan.popular ? "default" : "outline"}
className="w-full group"
size="lg"
>
{plan.cta}
<ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</CardContent>
{/* Hover effect for popular plan */}
{plan.popular && (
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
)}
</Card>
))}
</div>
<div className="text-center mt-12">
<Link href="/pricing" className="group">
<Button variant="ghost" className="text-primary hover:text-primary/80">
View all plans and features
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</div>
</div>
</div>
);
}
export function SelfHostCTA() {
return null; // Removed for commercial version
}
// Custom animations
const style = document.createElement('style');
style.textContent = `
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-20px) rotate(120deg); }
66% { transform: translateY(-10px) rotate(240deg); }
}
@keyframes float-delayed {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-30px) rotate(90deg); }
66% { transform: translateY(-15px) rotate(180deg); }
}
@keyframes float-slow {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-15px) translateX(10px); }
50% { transform: translateY(-25px) translateX(-10px); }
75% { transform: translateY(-10px) translateX(5px); }
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes underline-expand {
0% { width: 0%; left: 50%; }
100% { width: 100%; left: 0%; }
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-float-delayed {
animation: float-delayed 8s ease-in-out infinite;
}
.animate-float-slow {
animation: float-slow 10s ease-in-out infinite;
}
.animate-gradient {
background-size: 200% 200%;
animation: gradient-shift 3s ease-in-out infinite;
}
.animate-gradient-shift {
animation: gradient-shift 4s ease-in-out infinite;
}
.animate-underline-expand {
animation: underline-expand 1s ease-out forwards;
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out forwards;
}
.animation-delay-200 { animation-delay: 200ms; }
.animation-delay-400 { animation-delay: 400ms; }
.animation-delay-600 { animation-delay: 600ms; }
.animation-delay-800 { animation-delay: 800ms; }
.animation-delay-1000 { animation-delay: 1000ms; }
.animation-delay-2000 { animation-delay: 2000ms; }
.animation-delay-4000 { animation-delay: 4000ms; }
.bg-size-200 {
background-size: 200% 200%;
}
`;
if (typeof document !== 'undefined') {
document.head.appendChild(style);
}

View File

@@ -0,0 +1,86 @@
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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,420 @@
"use client";
import Link from "next/link";
import { useTranslation } from "@/lib/i18n";
import { Button } from "@/components/ui/button";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
import {
Table2,
FileText,
Presentation,
Bot,
Lock,
Zap,
Check,
PlayCircle,
ShieldCheck,
Clock,
Languages,
} from "lucide-react";
export function LandingPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-[#0A0A0B] text-white font-sans">
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b border-[#27272A] bg-[#0A0A0B]/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">
<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-white text-lg font-bold tracking-tight">
Office Translator
</span>
</div>
<nav className="hidden md:flex items-center gap-8">
<a
className="text-slate-300 hover:text-white text-sm font-medium transition-colors"
href="#features"
>
{t("nav.features")}
</a>
<a
className="text-slate-300 hover:text-white text-sm font-medium transition-colors"
href="#pricing"
>
{t("nav.pricing")}
</a>
<a
className="text-slate-300 hover:text-white text-sm font-medium transition-colors"
href="#enterprise"
>
{t("nav.enterprise")}
</a>
</nav>
<div className="flex items-center gap-3">
<LanguageSwitcher variant="button" />
<a
className="hidden sm:inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-slate-300 transition-colors hover:text-white"
href="/auth/login"
>
{t("common.login")}
</a>
<Button
asChild
className="bg-primary text-[#0A0A0B] font-bold hover:bg-primary/90"
>
<Link href="/auth/register">{t("common.signup")}</Link>
</Button>
</div>
</div>
</header>
<main>
{/* Hero Section */}
<section className="relative pt-20 pb-32 overflow-hidden">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-[#0A0A0B] to-[#0A0A0B]" />
<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-white 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>
<p className="mx-auto lg:mx-0 max-w-2xl text-lg text-slate-400">
{t("hero.subtitle")}
</p>
<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-[#0A0A0B] font-bold hover:bg-primary/90"
>
<Link href="/auth/register">{t("hero.cta")}</Link>
</Button>
<Button
variant="outline"
size="lg"
className="border-[#27272A] bg-[#141416] text-white hover:bg-[#27272A] gap-2"
>
<PlayCircle className="h-5 w-5" />
{t("hero.demoCta")}
</Button>
</div>
{/* 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-slate-400">
<ShieldCheck className="h-5 w-5 text-primary" />
<span>{t("hero.badge1")}</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-400">
<Clock className="h-5 w-5 text-primary" />
<span>{t("hero.badge2")}</span>
</div>
</div>
</div>
{/* Hero Visual */}
<div className="flex-1 w-full max-w-lg lg:max-w-none relative">
<div className="relative rounded-xl bg-[#141416] border border-[#27272A] p-2 shadow-2xl">
<div className="absolute -inset-0.5 bg-gradient-to-br from-primary/30 to-transparent opacity-30 blur-lg rounded-xl" />
<div className="relative aspect-[16/10] overflow-hidden rounded-lg bg-[#0A0A0B]">
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-full h-full flex">
<div className="w-1/2 border-r border-[#27272A] relative p-4">
<div className="absolute top-2 left-2 bg-black/50 text-white text-xs px-2 py-1 rounded">
{t("landing.originalLabel")} (EN)
</div>
<div className="mt-8 space-y-2">
<div className="h-3 bg-slate-700 rounded w-3/4" />
<div className="h-3 bg-slate-700 rounded w-1/2" />
<div className="h-3 bg-slate-700 rounded w-5/6" />
</div>
</div>
<div className="w-1/2 relative p-4">
<div className="absolute top-2 left-2 bg-primary text-[#0A0A0B] font-bold text-xs px-2 py-1 rounded">
{t("landing.translatedLabel")} (FR)
</div>
<div className="mt-8 space-y-2">
<div className="h-3 bg-primary/30 rounded w-3/4" />
<div className="h-3 bg-primary/30 rounded w-1/2" />
<div className="h-3 bg-primary/30 rounded w-5/6" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Features Grid */}
<section className="py-24 bg-[#0A0A0B] relative" id="features">
<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-white sm:text-4xl mb-4">
{t("features.title")}
</h2>
<p className="text-lg text-slate-400 max-w-2xl mx-auto">
{t("features.subtitle")}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Feature 1 - Excel */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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>
<h3 className="text-xl font-bold text-white mb-2">
{t("features.excel.title")}
</h3>
<p className="text-slate-400">{t("features.excel.description")}</p>
</div>
{/* Feature 2 - Word */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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-white mb-2">
{t("features.word.title")}
</h3>
<p className="text-slate-400">{t("features.word.description")}</p>
</div>
{/* Feature 3 - PowerPoint */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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" />
</div>
<h3 className="text-xl font-bold text-white mb-2">
{t("features.powerpoint.title")}
</h3>
<p className="text-slate-400">
{t("features.powerpoint.description")}
</p>
</div>
{/* Feature 4 - AI */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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" />
</div>
<h3 className="text-xl font-bold text-white mb-2">
{t("features.ai.title")}
</h3>
<p className="text-slate-400">{t("features.ai.description")}</p>
</div>
{/* Feature 5 - Privacy */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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">
<Lock className="h-6 w-6" />
</div>
<h3 className="text-xl font-bold text-white mb-2">
{t("features.privacy.title")}
</h3>
<p className="text-slate-400">{t("features.privacy.description")}</p>
</div>
{/* Feature 6 - Speed */}
<div className="group relative rounded-2xl border border-[#27272A] bg-[#141416] 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>
<h3 className="text-xl font-bold text-white mb-2">
{t("features.speed.title")}
</h3>
<p className="text-slate-400">{t("features.speed.description")}</p>
</div>
</div>
</div>
</section>
{/* Pricing Section */}
<section className="py-24 bg-[#050505]" id="pricing">
<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-white sm:text-4xl mb-4">
{t("pricing.title")}
</h2>
<p className="text-lg text-slate-400">{t("pricing.subtitle")}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{/* Free Plan */}
<div className="rounded-2xl border border-[#27272A] bg-[#141416] p-8 flex flex-col">
<h3 className="text-xl font-semibold text-white">
{t("pricing.free.name")}
</h3>
<p className="mt-4 text-slate-400 text-sm">
{t("pricing.free.description")}
</p>
<div className="my-6">
<span className="text-4xl font-bold text-white">
{t("pricing.free.price")}
</span>
<span className="text-slate-500">/{t("common.month")}</span>
</div>
<ul className="space-y-4 mb-8 flex-1">
{["5 documents / month", "Word & Excel only", "Community support"].map(
(feature, i) => (
<li key={i} className="flex items-center text-slate-300 text-sm">
<Check className="h-4 w-4 text-primary mr-2" />
{feature}
</li>
)
)}
</ul>
<Button
asChild
variant="outline"
className="border-[#27272A] text-white hover:bg-[#27272A]"
>
<Link href="/auth/register">{t("common.signup")}</Link>
</Button>
</div>
{/* Pro Plan */}
<div className="relative rounded-2xl border border-primary bg-[#141416] 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-[#F59E0B] px-3 py-1 text-xs font-bold text-black uppercase tracking-wide">
{t("common.popular")}
</div>
<h3 className="text-xl font-semibold text-white">
{t("pricing.pro.name")}
</h3>
<p className="mt-4 text-slate-400 text-sm">
{t("pricing.pro.description")}
</p>
<div className="my-6">
<span className="text-4xl font-bold text-white">
{t("pricing.pro.price")}
</span>
<span className="text-slate-500">/{t("common.month")}</span>
</div>
<ul className="space-y-4 mb-8 flex-1">
{[
"50 documents / month",
"All formats (PPTX included)",
"Advanced AI models (GPT-4)",
"Priority support",
].map((feature, i) => (
<li key={i} className="flex items-center text-white text-sm">
<Check className="h-4 w-4 text-primary mr-2" />
{feature}
</li>
))}
</ul>
<Button
asChild
className="bg-primary text-[#0A0A0B] font-bold hover:bg-primary/90"
>
<Link href="/auth/register">{t("common.tryPro")}</Link>
</Button>
</div>
{/* Enterprise Plan */}
<div className="rounded-2xl border border-[#27272A] bg-[#141416] p-8 flex flex-col">
<h3 className="text-xl font-semibold text-white">
{t("pricing.enterprise.name")}
</h3>
<p className="mt-4 text-slate-400 text-sm">
{t("pricing.enterprise.description")}
</p>
<div className="my-6">
<span className="text-4xl font-bold text-white">
{t("common.onRequest")}
</span>
</div>
<ul className="space-y-4 mb-8 flex-1">
{[
"Unlimited documents",
"API Access",
"SSO & Advanced Security",
"Dedicated Account Manager",
].map((feature, i) => (
<li key={i} className="flex items-center text-slate-300 text-sm">
<Check className="h-4 w-4 text-primary mr-2" />
{feature}
</li>
))}
</ul>
<Button
asChild
variant="outline"
className="border-[#27272A] text-white hover:bg-[#27272A]"
>
<Link href="#contact">{t("common.contactSales")}</Link>
</Button>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative py-20 px-4">
<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-[#141416] border border-[#27272A] 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-white mb-6">
{t("cta.title")}
</h2>
<p className="text-slate-400 mb-8 max-w-2xl mx-auto">
{t("cta.subtitle")}
</p>
<Button
asChild
size="lg"
className="bg-primary text-[#0A0A0B] font-bold hover:bg-primary/90"
>
<Link href="/auth/register">{t("cta.button")}</Link>
</Button>
</div>
</section>
</main>
{/* Footer */}
<footer className="border-t border-[#27272A] bg-[#0A0A0B] 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">
<div className="flex items-center gap-2">
<div className="flex items-center justify-center rounded-lg bg-primary/10 p-1">
<Languages className="h-4 w-4 text-primary" />
</div>
<span className="text-white font-bold text-lg">
Office Translator
</span>
</div>
<div className="flex gap-6 text-sm text-slate-400">
<a className="hover:text-white transition-colors" href="#">
{t("footer.privacy")}
</a>
<a className="hover:text-white transition-colors" href="#">
{t("footer.terms")}
</a>
<a className="hover:text-white transition-colors" href="#">
{t("footer.contact")}
</a>
</div>
<div className="text-slate-500 text-sm">{t("footer.copyright")}</div>
</div>
</div>
</footer>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import Link from "next/link"
export function SiteFooter() {
return (
<footer className="border-t border-border py-6 text-center text-xs text-muted-foreground">
<div className="mx-auto max-w-5xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<span>© 2026 Office Translator. All rights reserved.</span>
<div className="flex items-center gap-6">
<Link href="/pricing" className="hover:text-foreground transition-colors">
Pricing
</Link>
<Link href="/pricing#terms" className="hover:text-foreground transition-colors">
Terms
</Link>
<Link href="/pricing#privacy" className="hover:text-foreground transition-colors">
Privacy
</Link>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,39 @@
import Link from "next/link"
import { Languages } from "lucide-react"
import { Button } from "@/components/ui/button"
export function SiteHeader() {
return (
<header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-lg">
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
<Link href="/" className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-lg bg-primary">
<Languages className="size-4 text-primary-foreground" />
</div>
<span className="text-base font-semibold tracking-tight text-foreground">
Office Translator
</span>
</Link>
<nav className="hidden items-center gap-1 md:flex">
<Button variant="ghost" size="sm" asChild>
<Link href="/pricing">Pricing</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/pricing#api">API Access</Link>
</Button>
<div className="mx-2 h-4 w-px bg-border" />
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard">Login</Link>
</Button>
</nav>
<div className="md:hidden">
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard">Login</Link>
</Button>
</div>
</div>
</header>
)
}

View File

@@ -11,6 +11,7 @@ import {
Crown,
LogOut,
BookOpen,
Zap,
} from "lucide-react";
import {
Tooltip,
@@ -121,7 +122,7 @@ export function Sidebar() {
<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">Translate Co.</span>
<span className="text-lg font-semibold text-white">Office Translator</span>
</div>
</aside>
);
@@ -135,7 +136,7 @@ export function Sidebar() {
<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">Translate Co.</span>
<span className="text-lg font-semibold text-white">Office Translator</span>
</div>
{/* Navigation */}
@@ -192,6 +193,25 @@ export function Sidebar() {
</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>
@@ -204,12 +224,12 @@ export function Sidebar() {
: "text-amber-400/70 hover:bg-zinc-800 hover:text-amber-400"
)}
>
<Crown className="h-5 w-5" />
<span>Upgrade Plan</span>
<Zap className="h-5 w-5" />
<span>Passer Pro</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">
<p>Get more translations and features</p>
<p>Voir tous les forfaits disponibles</p>
</TooltipContent>
</Tooltip>
)}
@@ -228,7 +248,7 @@ export function Sidebar() {
<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")}>
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
{{ free: "Gratuit", starter: "Starter", pro: "Pro", business: "Business", enterprise: "Entreprise" }[user.plan] ?? user.plan}
</Badge>
</div>
</Link>

View File

@@ -0,0 +1,53 @@
'use client'
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from '@/lib/utils'
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full', className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
@@ -292,4 +294,4 @@ export const ProgressBadge = React.forwardRef<
})
ProgressBadge.displayName = "ProgressBadge"
export { Badge, badgeVariants, StatusBadge, CounterBadge, ProgressBadge }
export { Badge, badgeVariants }

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
@@ -47,11 +49,10 @@ interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading = false, ripple = true, children, disabled, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const [ripples, setRipples] = React.useState<Array<{ id: number; x: number; y: number; size: number }>>([])
const createRipple = (event: React.MouseEvent<HTMLButtonElement>) => {
if (!ripple || disabled || loading) return
if (!ripple || disabled || loading || asChild) return
const button = event.currentTarget
const rect = button.getBoundingClientRect()
@@ -68,14 +69,27 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
setRipples(prev => [...prev, newRipple])
// Remove ripple after animation
setTimeout(() => {
setRipples(prev => prev.filter(r => r.id !== newRipple.id))
}, 600)
}
// When asChild is true, just render the Slot with merged props (no extra children)
if (asChild) {
const Comp = Slot
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{children}
</Comp>
)
}
return (
<Comp
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || loading}
@@ -134,7 +148,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{variant === 'premium' && (
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-out pointer-events-none" />
)}
</Comp>
</button>
)
}
)

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"

View File

@@ -0,0 +1,71 @@
"use client";
import { Globe } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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: "🇫🇷" },
];
interface LanguageSwitcherProps {
variant?: "select" | "button";
}
export function LanguageSwitcher({ variant = "select" }: LanguageSwitcherProps) {
const { locale, setLocale } = useI18n();
if (variant === "button") {
const currentIndex = languages.findIndex((l) => l.value === locale);
const nextIndex = (currentIndex + 1) % languages.length;
const nextLang = languages[nextIndex];
return (
<Button
variant="ghost"
size="sm"
onClick={() => setLocale(nextLang.value)}
className="gap-1.5 text-muted-foreground hover:text-foreground"
>
<Globe className="h-4 w-4" />
<span className="text-sm">{nextLang.flag}</span>
</Button>
);
}
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">
<SelectValue>
<span className="flex items-center gap-2">
<Globe className="h-4 w-4" />
{languages.find((l) => l.value === locale)?.flag}{" "}
{languages.find((l) => l.value === locale)?.label}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-surface-dark border-border-dark">
{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>
<span>{lang.label}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucide-react"
@@ -189,23 +191,25 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
Notification.displayName = "Notification"
// Notification Context
type NotificationItem = {
id: string
title?: string
description?: string
variant?: VariantProps<typeof notificationVariants>["variant"]
duration?: number
action?: React.ReactNode
icon?: React.ReactNode
closable?: boolean
autoClose?: boolean
}
interface NotificationContextType {
notifications: Array<{
id: string
title?: string
description?: string
variant?: VariantProps<typeof notificationVariants>["variant"]
duration?: number
action?: React.ReactNode
icon?: React.ReactNode
closable?: boolean
autoClose?: boolean
}>
notify: (notification: Omit<NotificationContextType["notifications"][0], "id">) => void
success: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
error: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
warning: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
info: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
notifications: NotificationItem[]
notify: (notification: Omit<NotificationItem, "id">) => void
success: (notification: Omit<NotificationItem, "id" | "variant">) => void
error: (notification: Omit<NotificationItem, "id" | "variant">) => void
warning: (notification: Omit<NotificationItem, "id" | "variant">) => void
info: (notification: Omit<NotificationItem, "id" | "variant">) => void
dismiss: (id: string) => void
dismissAll: () => void
}
@@ -213,10 +217,10 @@ interface NotificationContextType {
const NotificationContext = React.createContext<NotificationContextType | undefined>(undefined)
export function NotificationProvider({ children }: { children: React.ReactNode }) {
const [notifications, setNotifications] = React.useState<NotificationContextType["notifications"]>([])
const [notifications, setNotifications] = React.useState<NotificationItem[]>([])
const notify = React.useCallback(
(notification: Omit<NotificationContextType["notifications"][0], "id">) => {
(notification: Omit<NotificationItem, "id">) => {
const id = Math.random().toString(36).substr(2, 9)
setNotifications(prev => [...prev, { ...notification, id }])
},
@@ -224,25 +228,25 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
)
const success = React.useCallback(
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
(notification: Omit<NotificationItem, "id" | "variant">) =>
notify({ ...notification, variant: "success" }),
[notify]
)
const error = React.useCallback(
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
(notification: Omit<NotificationItem, "id" | "variant">) =>
notify({ ...notification, variant: "destructive" }),
[notify]
)
const warning = React.useCallback(
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
(notification: Omit<NotificationItem, "id" | "variant">) =>
notify({ ...notification, variant: "warning" }),
[notify]
)
const info = React.useCallback(
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
(notification: Omit<NotificationItem, "id" | "variant">) =>
notify({ ...notification, variant: "info" }),
[notify]
)

View File

@@ -8,8 +8,9 @@ import { cn } from "@/lib/utils"
function Progress({
className,
value,
animate = true,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
}: React.ComponentProps<typeof ProgressPrimitive.Root> & { animate?: boolean }) {
return (
<ProgressPrimitive.Root
data-slot="progress"
@@ -21,7 +22,7 @@ function Progress({
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
className={cn("bg-primary h-full w-full flex-1", animate && "transition-all duration-500")}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View File

@@ -0,0 +1,121 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom-0", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("border-b border-border", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b border-border/50 transition-colors hover:bg-secondary/50 data-[state=selected]:bg-secondary",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<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]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
@@ -116,7 +118,7 @@ const Toast = React.forwardRef<
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive"
)}
alt={typeof action === 'string' ? action : undefined}
altText={typeof action === 'string' ? action : 'Action'}
>
{action}
</ToastPrimitives.Action>
@@ -146,6 +148,21 @@ const ToastAction = React.forwardRef<
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
{...props}
/>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
@@ -185,7 +202,7 @@ export function useToast() {
description?: string
variant?: VariantProps<typeof toastVariants>["variant"]
duration?: number
action?: ToastActionElement
action?: React.ReactNode
icon?: React.ReactNode
}>>([])
@@ -267,7 +284,7 @@ export const ToastContainer = ({ children }: { children: React.ReactNode }) => {
// Individual Toast Component for use in ToastContainer
export const ToastItem = React.forwardRef<
HTMLDivElement,
HTMLLIElement,
{
toast: {
id: string
@@ -275,15 +292,15 @@ export const ToastItem = React.forwardRef<
description?: string
variant?: VariantProps<typeof toastVariants>["variant"]
duration?: number
action?: ToastActionElement
action?: React.ReactNode
icon?: React.ReactNode
}
onDismiss: (id: string) => void
}
>(({ toast, onDismiss, ...props }, ref) => {
return (
<li ref={ref}>
<Toast
ref={ref}
variant={toast.variant}
title={toast.title}
description={toast.description}
@@ -297,6 +314,7 @@ export const ToastItem = React.forwardRef<
}}
{...props}
/>
</li>
)
})
ToastItem.displayName = "ToastItem"