Backend: - User authentication with JWT tokens (auth_service.py) - Subscription plans: Free, Starter (), Pro (), Business (), Enterprise - Stripe integration for payments (payment_service.py) - Usage tracking and quotas - Credit packages for pay-per-use - Plan-based provider restrictions Frontend: - Landing page with hero, features, pricing preview (landing-sections.tsx) - Pricing page with all plans and credit packages (/pricing) - User dashboard with usage stats (/dashboard) - Login/Register pages with validation (/auth/login, /auth/register) - Ollama self-hosting setup guide (/ollama-setup) - Updated sidebar with user section and plan badge Monetization strategy: - Freemium: 3 docs/day, Ollama only - Starter: 50 docs/month, Google Translate - Pro: 200 docs/month, all providers, API access - Business: 1000 docs/month, team management - Enterprise: Custom pricing, SLA Self-hosted option: - Free unlimited usage with own Ollama server - Complete privacy (data never leaves machine) - Step-by-step setup guide included
263 lines
8.5 KiB
TypeScript
263 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useState, useEffect } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Settings,
|
|
Cloud,
|
|
BookText,
|
|
Upload,
|
|
Shield,
|
|
CreditCard,
|
|
LayoutDashboard,
|
|
LogIn,
|
|
Crown,
|
|
LogOut,
|
|
} from "lucide-react";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
plan: string;
|
|
}
|
|
|
|
const navigation = [
|
|
{
|
|
name: "Translate",
|
|
href: "/",
|
|
icon: Upload,
|
|
description: "Translate documents",
|
|
},
|
|
{
|
|
name: "General Settings",
|
|
href: "/settings",
|
|
icon: Settings,
|
|
description: "Configure general settings",
|
|
},
|
|
{
|
|
name: "Translation Services",
|
|
href: "/settings/services",
|
|
icon: Cloud,
|
|
description: "Configure translation providers",
|
|
},
|
|
{
|
|
name: "Context & Glossary",
|
|
href: "/settings/context",
|
|
icon: BookText,
|
|
description: "System prompts and glossary",
|
|
},
|
|
];
|
|
|
|
const adminNavigation = [
|
|
{
|
|
name: "Admin Dashboard",
|
|
href: "/admin",
|
|
icon: Shield,
|
|
description: "System monitoring (login required)",
|
|
},
|
|
];
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Check for user in localStorage
|
|
const storedUser = localStorage.getItem("user");
|
|
if (storedUser) {
|
|
try {
|
|
setUser(JSON.parse(storedUser));
|
|
} catch {
|
|
setUser(null);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem("token");
|
|
localStorage.removeItem("refresh_token");
|
|
localStorage.removeItem("user");
|
|
setUser(null);
|
|
window.location.href = "/";
|
|
};
|
|
|
|
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",
|
|
};
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-[#1a1a1a]">
|
|
{/* Logo */}
|
|
<div className="flex h-16 items-center gap-3 border-b border-zinc-800 px-6">
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-teal-500 text-white font-bold">
|
|
文A
|
|
</div>
|
|
<span className="text-lg font-semibold text-white">Translate Co.</span>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex flex-col gap-1 p-4">
|
|
{navigation.map((item) => {
|
|
const isActive = pathname === item.href;
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<Tooltip key={item.name}>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href={item.href}
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
isActive
|
|
? "bg-teal-500/10 text-teal-400"
|
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
|
)}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
<span>{item.name}</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
<p>{item.description}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
|
|
{/* User Section */}
|
|
{user && (
|
|
<div className="mt-4 pt-4 border-t border-zinc-800">
|
|
<p className="px-3 mb-2 text-xs font-medium text-zinc-600 uppercase tracking-wider">Account</p>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href="/dashboard"
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
pathname === "/dashboard"
|
|
? "bg-teal-500/10 text-teal-400"
|
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
|
)}
|
|
>
|
|
<LayoutDashboard className="h-5 w-5" />
|
|
<span>Dashboard</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
<p>View your usage and settings</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href="/pricing"
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
pathname === "/pricing"
|
|
? "bg-amber-500/10 text-amber-400"
|
|
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
|
|
)}
|
|
>
|
|
<Crown className="h-5 w-5" />
|
|
<span>Upgrade Plan</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
<p>View plans and pricing</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
|
|
{/* Admin Section */}
|
|
<div className="mt-4 pt-4 border-t border-zinc-800">
|
|
<p className="px-3 mb-2 text-xs font-medium text-zinc-600 uppercase tracking-wider">Admin</p>
|
|
{adminNavigation.map((item) => {
|
|
const isActive = pathname === item.href;
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<Tooltip key={item.name}>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href={item.href}
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
isActive
|
|
? "bg-blue-500/10 text-blue-400"
|
|
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
|
|
)}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
<span>{item.name}</span>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
<p>{item.description}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* User section at bottom */}
|
|
<div className="absolute bottom-0 left-0 right-0 border-t border-zinc-800 p-4">
|
|
{user ? (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-teal-600 text-white text-sm font-medium">
|
|
{user.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-medium text-white">{user.name}</span>
|
|
<Badge className={cn("text-xs mt-0.5", planColors[user.plan] || "bg-zinc-600")}>
|
|
{user.plan.charAt(0).toUpperCase() + user.plan.slice(1)}
|
|
</Badge>
|
|
</div>
|
|
</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>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<Link href="/auth/login" className="block">
|
|
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-white text-sm font-medium transition-colors">
|
|
<LogIn className="h-4 w-4" />
|
|
Sign In
|
|
</button>
|
|
</Link>
|
|
<Link href="/auth/register" className="block">
|
|
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-teal-500 hover:bg-teal-600 text-white text-sm font-medium transition-colors">
|
|
Get Started Free
|
|
</button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</TooltipProvider>
|
|
);
|
|
}
|