Production-ready improvements: security hardening, Redis sessions, retry logic, updated pricing
Changes: - Removed hardcoded admin credentials (now requires env vars) - Added Redis session storage with in-memory fallback - Improved CORS configuration with warnings for development mode - Added retry_with_backoff decorator for translation API calls - Updated pricing: Starter=, Pro=, Business= - Stripe price IDs now loaded from environment variables - Added redis to requirements.txt - Updated .env.example with all new configuration options - Created COMPREHENSIVE_REVIEW_AND_PLAN.md with deployment roadmap - Frontend: Updated pricing page, new UI components
This commit is contained in:
@@ -15,10 +15,30 @@ import {
|
||||
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 {
|
||||
@@ -48,11 +68,24 @@ interface UsageStats {
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -61,37 +94,75 @@ export default function DashboardPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchUserData(token);
|
||||
}, [router]);
|
||||
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}` },
|
||||
}),
|
||||
]);
|
||||
|
||||
const fetchUserData = async (token: string) => {
|
||||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const userData = await userRes.json();
|
||||
const usageData = await usageRes.json();
|
||||
|
||||
setUser(userData);
|
||||
setUsage(usageData);
|
||||
} catch (error) {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
router.push("/auth/login?redirect=/dashboard");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [router]);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem("token");
|
||||
@@ -115,14 +186,17 @@ export default function DashboardPage() {
|
||||
window.open(data.url, "_blank");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to open billing portal");
|
||||
console.error("Failed to open billing portal:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#262626] flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-teal-500" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -136,45 +210,91 @@ export default function DashboardPage() {
|
||||
: 0;
|
||||
|
||||
const planColors: Record<string, string> = {
|
||||
free: "bg-zinc-500",
|
||||
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`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#1a1a1a] to-[#262626]">
|
||||
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-background">
|
||||
{/* Header */}
|
||||
<header className="border-b border-zinc-800 bg-[#1a1a1a]/80 backdrop-blur-sm sticky top-0 z-50">
|
||||
<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">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-teal-500 text-white font-bold">
|
||||
<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">Translate Co.</span>
|
||||
<span className="text-lg font-semibold text-white group-hover:text-primary transition-colors duration-300">
|
||||
Translate Co.
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<Button variant="outline" size="sm" className="border-zinc-700 text-zinc-300 hover:bg-zinc-800">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
<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-teal-600 flex items-center justify-center text-white text-sm font-medium">
|
||||
<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>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</button>
|
||||
<div 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>
|
||||
@@ -183,180 +303,312 @@ export default function DashboardPage() {
|
||||
<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-3xl font-bold text-white">Welcome back, {user.name.split(" ")[0]}!</h1>
|
||||
<p className="text-zinc-400 mt-1">Here's an overview of your translation usage</p>
|
||||
<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 */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-zinc-400">Current Plan</span>
|
||||
<Badge className={cn("text-white", planColors[user.plan])}>
|
||||
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Crown className="h-5 w-5 text-amber-400" />
|
||||
<span className="text-2xl font-bold text-white capitalize">{user.plan}</span>
|
||||
</div>
|
||||
{user.plan !== "enterprise" && (
|
||||
<Button
|
||||
onClick={handleUpgrade}
|
||||
size="sm"
|
||||
className="mt-4 w-full bg-teal-500 hover:bg-teal-600 text-white"
|
||||
>
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-zinc-400">Documents This Month</span>
|
||||
<FileText className="h-4 w-4 text-zinc-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white mb-2">
|
||||
{usage.docs_used} / {usage.docs_limit === -1 ? "∞" : usage.docs_limit}
|
||||
</div>
|
||||
<Progress value={docsPercentage} className="h-2 bg-zinc-800" />
|
||||
<p className="text-xs text-zinc-500 mt-2">
|
||||
{usage.docs_remaining === -1
|
||||
? "Unlimited"
|
||||
: `${usage.docs_remaining} remaining`}
|
||||
</p>
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-zinc-400">Pages Translated</span>
|
||||
<TrendingUp className="h-4 w-4 text-teal-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{usage.pages_used}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 mt-2">
|
||||
Max {usage.max_pages_per_doc === -1 ? "unlimited" : usage.max_pages_per_doc} pages/doc
|
||||
</p>
|
||||
</div>
|
||||
<CardStats
|
||||
title="Pages Translated"
|
||||
value={usage.pages_used}
|
||||
icon={<TrendingUp className="h-5 w-5 text-teal-400" />}
|
||||
/>
|
||||
|
||||
{/* Extra Credits */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm text-zinc-400">Extra Credits</span>
|
||||
<Zap className="h-4 w-4 text-amber-400" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{usage.extra_credits}
|
||||
</div>
|
||||
<Link href="/pricing#credits">
|
||||
<Button variant="outline" size="sm" className="mt-4 w-full border-zinc-700 text-zinc-300 hover:bg-zinc-800">
|
||||
Buy Credits
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<CardStats
|
||||
title="Extra Credits"
|
||||
value={usage.extra_credits}
|
||||
icon={<Zap className="h-5 w-5 text-amber-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Features & Actions */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Quick Actions & Recent Activity */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Available Features */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Your Plan Features</h2>
|
||||
<ul className="space-y-3">
|
||||
{user.plan_limits.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<Check className="h-5 w-5 text-teal-400 shrink-0 mt-0.5" />
|
||||
<span className="text-zinc-300">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Quick Actions</h2>
|
||||
<div className="space-y-2">
|
||||
<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-3 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors">
|
||||
<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-zinc-500" />
|
||||
<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-3 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors">
|
||||
<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-zinc-500" />
|
||||
<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-3 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors"
|
||||
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 className="text-white">Manage Billing</span>
|
||||
<span>Manage Billing</span>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-zinc-500" />
|
||||
<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-3 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors">
|
||||
<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 className="text-white">View Plans & Pricing</span>
|
||||
<span>View Plans & Pricing</span>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-zinc-500" />
|
||||
<ChevronRight className="h-4 w-4 text-text-tertiary group-hover:text-primary transition-colors duration-200" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Available Providers */}
|
||||
<div className="mt-6 rounded-xl border border-zinc-800 bg-zinc-900/50 p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Available Translation Providers</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{["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-teal-500/50 text-teal-400 bg-teal-500/10"
|
||||
: "border-zinc-700 text-zinc-500"
|
||||
)}
|
||||
>
|
||||
{isAvailable && <Check className="h-3 w-3 mr-1" />}
|
||||
{provider}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{user.plan === "free" && (
|
||||
<p className="text-sm text-zinc-500 mt-4">
|
||||
<Link href="/pricing" className="text-teal-400 hover:text-teal-300">
|
||||
Upgrade your plan
|
||||
</Link>{" "}
|
||||
to access more translation providers including Google, DeepL, and OpenAI.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<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.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user