feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
86
frontend/src/components/landing/features-section.tsx
Normal file
86
frontend/src/components/landing/features-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
46
frontend/src/components/landing/hero-section.tsx
Normal file
46
frontend/src/components/landing/hero-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
420
frontend/src/components/landing/landing-page.tsx
Normal file
420
frontend/src/components/landing/landing-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
frontend/src/components/layout/site-footer.tsx
Normal file
22
frontend/src/components/layout/site-footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
frontend/src/components/layout/site-header.tsx
Normal file
39
frontend/src/components/layout/site-header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
53
frontend/src/components/ui/avatar.tsx
Normal file
53
frontend/src/components/ui/avatar.tsx
Normal 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 }
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
71
frontend/src/components/ui/language-switcher.tsx
Normal file
71
frontend/src/components/ui/language-switcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
121
frontend/src/components/ui/table.tsx
Normal file
121
frontend/src/components/ui/table.tsx
Normal 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,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user