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

@@ -1,615 +1,102 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import {
FileText,
CreditCard,
Settings,
LogOut,
ChevronRight,
Zap,
TrendingUp,
Clock,
Check,
ExternalLink,
Crown,
Users,
BarChart3,
Shield,
Globe2,
FileSpreadsheet,
Presentation,
AlertTriangle,
Download,
Eye,
RefreshCw,
Calendar,
Activity,
Target,
Award,
ArrowUpRight,
ArrowDownRight,
Upload,
LogIn,
UserPlus
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardStats, CardFeature } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface User {
id: string;
email: string;
name: string;
plan: string;
subscription_status: string;
docs_translated_this_month: number;
pages_translated_this_month: number;
extra_credits: number;
plan_limits: {
docs_per_month: number;
max_pages_per_doc: number;
features: string[];
providers: string[];
};
}
interface UsageStats {
docs_used: number;
docs_limit: number;
docs_remaining: number;
pages_used: number;
extra_credits: number;
max_pages_per_doc: number;
allowed_providers: string[];
}
interface ActivityItem {
id: string;
type: "translation" | "upload" | "download" | "login" | "signup";
title: string;
description: string;
timestamp: string;
status: "success" | "pending" | "error";
amount?: number;
}
import Link from 'next/link';
import { FileText, Key, BookText, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useUser } from './useUser';
export default function DashboardPage() {
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [usage, setUsage] = useState<UsageStats | null>(null);
const [loading, setLoading] = useState(true);
const [recentActivity, setRecentActivity] = useState<ActivityItem[]>([]);
const [timeRange, setTimeRange] = useState<"7d" | "30d" | "24h">("30d");
const [selectedMetric, setSelectedMetric] = useState<"documents" | "pages" | "users" | "revenue">("documents");
const { data: user, isLoading } = useUser();
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
router.push("/auth/login?redirect=/dashboard");
return;
}
const fetchData = async () => {
try {
const [userRes, usageRes] = await Promise.all([
fetch("http://localhost:8000/api/auth/me", {
headers: { Authorization: `Bearer ${token}` },
}),
fetch("http://localhost:8000/api/auth/usage", {
headers: { Authorization: `Bearer ${token}` },
}),
]);
if (!userRes.ok) {
throw new Error("Session expired");
}
const userData = await userRes.json();
const usageData = await usageRes.json();
setUser(userData);
setUsage(usageData);
// Mock recent activity
setRecentActivity([
{
id: "1",
type: "translation",
title: "Document translated",
description: "Q4 Financial Report.xlsx",
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
status: "success",
amount: 15
},
{
id: "2",
type: "upload",
title: "Document uploaded",
description: "Marketing_Presentation.pptx",
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
status: "success"
},
{
id: "3",
type: "download",
title: "Document downloaded",
description: "Translated_Q4_Report.xlsx",
timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
status: "success"
},
{
id: "4",
type: "login",
title: "User login",
description: "Login from new device",
timestamp: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(),
status: "success"
}
]);
} catch (error) {
console.error("Dashboard data fetch error:", error);
localStorage.removeItem("token");
localStorage.removeItem("user");
router.push("/auth/login?redirect=/dashboard");
} finally {
setLoading(false);
}
};
fetchData();
}, [router]);
const handleLogout = () => {
localStorage.removeItem("token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
router.push("/");
};
const handleUpgrade = () => {
router.push("/pricing");
};
const handleManageBilling = async () => {
const token = localStorage.getItem("token");
try {
const res = await fetch("http://localhost:8000/api/auth/billing-portal", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
if (data.url) {
window.open(data.url, "_blank");
}
} catch (error) {
console.error("Failed to open billing portal:", error);
}
};
if (loading) {
if (isLoading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-border-subtle border-t-primary"></div>
<p className="text-lg font-medium text-foreground">Loading your dashboard...</p>
</div>
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground"></div>
</div>
);
}
if (!user || !usage) {
return null;
}
const docsPercentage = usage.docs_limit > 0
? Math.min(100, (usage.docs_used / usage.docs_limit) * 100)
: 0;
const planColors: Record<string, string> = {
free: "bg-zinc-600",
starter: "bg-blue-500",
pro: "bg-teal-500",
business: "bg-purple-500",
enterprise: "bg-amber-500",
};
const getActivityIcon = (type: ActivityItem["type"]) => {
switch (type) {
case "translation": return <FileText className="h-4 w-4" />;
case "upload": return <Upload className="h-4 w-4" />;
case "download": return <Download className="h-4 w-4" />;
case "login": return <LogIn className="h-4 w-4" />;
case "signup": return <UserPlus className="h-4 w-4" />;
default: return <Activity className="h-4 w-4" />;
}
};
const getStatusColor = (status: ActivityItem["status"]) => {
switch (status) {
case "success": return "text-success";
case "pending": return "text-warning";
case "error": return "text-destructive";
default: return "text-text-tertiary";
}
};
const formatTimeAgo = (timestamp: string) => {
const now = new Date();
const past = new Date(timestamp);
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return `${Math.floor(diffInSeconds / 86400)}d ago`;
};
const firstName = user?.name?.split(' ')[0] || 'User';
return (
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-background">
{/* Header */}
<header className="sticky top-0 z-50 glass border-b border-border/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Link href="/" className="flex items-center gap-3 group">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-accent text-white font-bold shadow-lg group-hover:shadow-xl group-hover:shadow-primary/25 transition-all duration-300 group-hover:-translate-y-0.5">
A
</div>
<span className="text-lg font-semibold text-white group-hover:text-primary transition-colors duration-300">
Translate Co.
</span>
</Link>
<div className="mx-auto max-w-5xl px-4 py-6 lg:px-8 lg:py-8">
{/* Page heading */}
<div className="mb-6">
<h2 className="text-xl font-semibold tracking-tight text-foreground">
Welcome back, {firstName}!
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Monitor your usage, manage API keys, and configure translation preferences.
</p>
</div>
<div className="flex items-center gap-4">
<Link href="/">
<Button variant="glass" size="sm" className="group">
<FileText className="h-4 w-4 mr-2 transition-transform duration-200 group-hover:scale-110" />
Translate
<ChevronRight className="h-4 w-4 ml-1 transition-transform duration-200 group-hover:translate-x-1" />
</Button>
</Link>
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-primary to-accent text-white text-sm font-bold flex items-center justify-center">
{user.name.charAt(0).toUpperCase()}
</div>
<div className="text-right">
<p className="text-sm font-medium text-foreground">{user.name}</p>
<Badge
variant="outline"
className={cn("ml-2", planColors[user.plan])}
>
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
</Badge>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleLogout}
className="text-text-tertiary hover:text-destructive transition-colors duration-200"
>
<LogOut className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
Welcome back, <span className="text-primary">{user.name.split(" ")[0]}</span>!
</h1>
<p className="text-lg text-text-secondary">
Here's an overview of your translation usage
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{/* Current Plan */}
<CardStats
title="Current Plan"
value={user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
change={undefined}
icon={<Crown className="h-5 w-5 text-amber-400" />}
/>
{/* Documents Used */}
<CardStats
title="Documents This Month"
value={`${usage.docs_used} / ${usage.docs_limit === -1 ? "∞" : usage.docs_limit}`}
change={{
value: 15,
type: "increase",
period: "this month"
}}
icon={<FileText className="h-5 w-5 text-primary" />}
/>
{/* Pages Translated */}
<CardStats
title="Pages Translated"
value={usage.pages_used}
icon={<TrendingUp className="h-5 w-5 text-teal-400" />}
/>
{/* Extra Credits */}
<CardStats
title="Extra Credits"
value={usage.extra_credits}
icon={<Zap className="h-5 w-5 text-amber-400" />}
/>
</div>
{/* Quick Actions & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Available Features */}
<Card variant="elevated" className="animate-fade-in-up animation-delay-200">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Shield className="h-5 w-5 text-primary" />
Your Plan Features
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<ul className="space-y-3">
{user.plan_limits.features.map((feature, idx) => (
<li key={idx} className="flex items-start gap-3">
<Check className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
<span className="text-sm text-text-secondary">{feature}</span>
</li>
))}
</ul>
</CardContent>
</Card>
{/* Quick Actions */}
<Card variant="elevated" className="animate-fade-in-up animation-delay-400">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Settings className="h-5 w-5 text-primary" />
Quick Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Link href="/">
<button className="w-full flex items-center justify-between p-4 rounded-lg bg-surface hover:bg-surface-hover transition-colors group">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-teal-400" />
<span className="text-white">Translate a Document</span>
</div>
<ChevronRight className="h-4 w-4 text-text-tertiary group-hover:text-primary transition-colors duration-200" />
</button>
</Link>
<Link href="/settings/services">
<button className="w-full flex items-center justify-between p-4 rounded-lg bg-surface hover:bg-surface-hover transition-colors group">
<div className="flex items-center gap-3">
<Settings className="h-5 w-5 text-blue-400" />
<span className="text-white">Configure Providers</span>
</div>
<ChevronRight className="h-4 w-4 text-text-tertiary group-hover:text-primary transition-colors duration-200" />
</button>
</Link>
{user.plan !== "free" && (
<button
onClick={handleUpgrade}
className="w-full flex items-center justify-between p-4 rounded-lg bg-gradient-to-r from-amber-500 to-orange-600 text-white hover:from-amber-600 hover:to-orange-700 transition-all duration-300 group"
>
<div className="flex items-center gap-3">
<Crown className="h-5 w-5" />
<span>Upgrade Plan</span>
</div>
<ArrowUpRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-1" />
</button>
)}
{user.plan !== "free" && (
<button
onClick={handleManageBilling}
className="w-full flex items-center justify-between p-4 rounded-lg bg-surface hover:bg-surface-hover transition-colors group"
>
<div className="flex items-center gap-3">
<CreditCard className="h-5 w-5 text-purple-400" />
<span>Manage Billing</span>
</div>
<ExternalLink className="h-4 w-4 text-text-tertiary group-hover:text-primary transition-colors duration-200" />
</button>
)}
<Link href="/pricing">
<button className="w-full flex items-center justify-between p-4 rounded-lg bg-surface hover:bg-surface-hover transition-colors group">
<div className="flex items-center gap-3">
<Crown className="h-5 w-5 text-amber-400" />
<span>View Plans & Pricing</span>
</div>
<ChevronRight className="h-4 w-4 text-text-tertiary group-hover:text-primary transition-colors duration-200" />
</button>
</Link>
</CardContent>
</Card>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Usage Chart */}
<Card variant="elevated" className="animate-fade-in-up animation-delay-600">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<BarChart3 className="h-5 w-5 text-primary" />
Usage Overview
</CardTitle>
<div className="flex items-center gap-2 ml-auto">
<Button
variant="ghost"
size="sm"
onClick={() => setTimeRange(timeRange === "7d" ? "30d" : "7d")}
className={cn("text-xs", timeRange === "7d" && "text-primary")}
>
7D
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setTimeRange(timeRange === "30d" ? "24h" : "30d")}
className={cn("text-xs", timeRange === "30d" && "text-primary")}
>
30D
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setTimeRange("24h")}
className={cn("text-xs", timeRange === "24h" && "text-primary")}
>
24H
</Button>
</div>
{/* Quick actions */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Link href="/dashboard/translate">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Translate Document</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="h-64 flex items-center justify-center">
{/* Mock Chart */}
<div className="relative w-full h-full flex items-center justify-center">
<svg className="w-full h-full" viewBox="0 0 100 100">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#8b5cf6" />
</linearGradient>
</defs>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="text-border"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="url(#gradient)"
strokeWidth="2"
className="opacity-80"
style={{
strokeDasharray: `${2 * Math.PI * 40}`,
strokeDashoffset: `${2 * Math.PI * 40 * 0.25}`,
animation: "progress 2s ease-in-out infinite"
}}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="text-6xl font-bold text-text-tertiary">85%</div>
<div className="text-sm text-text-tertiary">Usage</div>
</div>
</div>
</div>
<CardDescription>
Upload and translate Excel, Word, or PowerPoint files
</CardDescription>
</CardContent>
</Card>
</Link>
{/* Recent Activity */}
<Card variant="elevated" className="animate-fade-in-up animation-delay-800">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Activity className="h-5 w-5 text-primary" />
Recent Activity
<Button
variant="ghost"
size="icon"
onClick={() => setRecentActivity([])}
className="ml-auto text-text-tertiary hover:text-primary transition-colors duration-200"
>
<RefreshCw className="h-4 w-4" />
</Button>
</CardTitle>
<Link href="/dashboard/api-keys">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">API Keys</CardTitle>
<Key className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentActivity.slice(0, 5).map((activity) => (
<div key={activity.id} className="flex items-start gap-4 p-3 rounded-lg bg-surface/50 hover:bg-surface transition-colors">
<div className="flex-shrink-0 mt-1">
<div className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center",
activity.status === "success" && "bg-success/20 text-success",
activity.status === "pending" && "bg-warning/20 text-warning",
activity.status === "error" && "bg-destructive/20 text-destructive"
)}>
{getActivityIcon(activity.type)}
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground mb-1">{activity.title}</p>
<p className="text-xs text-text-tertiary">{activity.description}</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-xs text-text-tertiary">{formatTimeAgo(activity.timestamp)}</span>
{activity.amount && (
<Badge variant="outline" size="sm">
{activity.amount}
</Badge>
)}
</div>
</div>
</div>
))}
</div>
<CardDescription>
Manage your API keys for automation
</CardDescription>
</CardContent>
</Card>
</div>
</Link>
{/* Available Providers */}
<Card variant="elevated" className="animate-fade-in-up animation-delay-1000">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Globe2 className="h-5 w-5 text-primary" />
Available Translation Providers
</CardTitle>
</CardHeader>
<CardContent>
{usage && (
<div className="flex flex-wrap gap-3">
{["ollama", "google", "deepl", "openai", "libre", "azure"].map((provider) => {
const isAvailable = usage.allowed_providers.includes(provider);
return (
<Badge
key={provider}
variant="outline"
className={cn(
"capitalize",
isAvailable
? "border-success/50 text-success bg-success/10"
: "border-border text-text-tertiary"
)}
>
{isAvailable && <Check className="h-3 w-3 mr-1" />}
{provider}
</Badge>
);
})}
</div>
)}
{user && user.plan === "free" && (
<p className="text-sm text-text-tertiary mt-4">
<Link href="/pricing" className="text-primary hover:text-primary/80">
Upgrade your plan
</Link>
{" "}
to access more translation providers including Google, DeepL, and OpenAI.
{user?.tier === 'pro' && (
<Link href="/dashboard/glossaries">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Glossaries</CardTitle>
<BookText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription>
Create custom terminology for translations
</CardDescription>
</CardContent>
</Card>
</Link>
)}
</div>
{/* Plan info */}
{user?.tier === 'free' && (
<Card className="mt-6 border-primary/50 bg-primary/5">
<CardContent className="flex items-center justify-between pt-6">
<div>
<p className="text-sm font-medium text-foreground">Upgrade to Pro</p>
<p className="text-xs text-muted-foreground">
Get unlimited translations, API access, and custom glossaries
</p>
)}
</div>
<Link href="/pricing">
<Button variant="premium" size="sm" className="gap-1">
View Plans
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</CardContent>
</Card>
</main>
)}
</div>
);
}