Hide admin section in sidebar, optimize translation service with parallel processing, improve UX

This commit is contained in:
Sepehr 2025-11-30 21:33:44 +01:00
parent fcabe882cd
commit d2b820c6f1
2 changed files with 149 additions and 74 deletions

View File

@ -2,19 +2,18 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback, memo } from "react";
import { cn } from "@/lib/utils";
import {
Settings,
Cloud,
BookText,
Upload,
Shield,
CreditCard,
LayoutDashboard,
LogIn,
Crown,
LogOut,
Server,
} from "lucide-react";
import {
Tooltip,
@ -38,12 +37,6 @@ const navigation = [
icon: Upload,
description: "Translate documents",
},
{
name: "General Settings",
href: "/settings",
icon: Settings,
description: "Configure general settings",
},
{
name: "Translation Services",
href: "/settings/services",
@ -56,23 +49,61 @@ const navigation = [
icon: BookText,
description: "System prompts and glossary",
},
];
const adminNavigation = [
{
name: "Admin Dashboard",
href: "/admin",
icon: Shield,
description: "System monitoring (login required)",
name: "General Settings",
href: "/settings",
icon: Settings,
description: "Configure general settings",
},
];
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",
};
// Memoized NavItem for performance
const NavItem = memo(function NavItem({
item,
isActive
}: {
item: typeof navigation[0];
isActive: boolean;
}) {
const Icon = item.icon;
return (
<Tooltip>
<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>
);
});
export function Sidebar() {
const pathname = usePathname();
const [user, setUser] = useState<User | null>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Check for user in localStorage
setMounted(true);
const storedUser = localStorage.getItem("user");
if (storedUser) {
try {
@ -81,26 +112,38 @@ export function Sidebar() {
setUser(null);
}
}
// Listen for storage changes
const handleStorage = (e: StorageEvent) => {
if (e.key === "user") {
setUser(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, []);
const handleLogout = () => {
const handleLogout = useCallback(() => {
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",
};
// Prevent hydration mismatch
if (!mounted) {
return (
<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>
</div>
</aside>
);
}
return (
<TooltipProvider>
<TooltipProvider delayDuration={300}>
<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">
@ -164,6 +207,7 @@ export function Sidebar() {
</TooltipContent>
</Tooltip>
{user.plan === "free" && (
<Tooltip>
<TooltipTrigger asChild>
<Link
@ -172,7 +216,7 @@ export function Sidebar() {
"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"
: "text-amber-400/70 hover:bg-zinc-800 hover:text-amber-400"
)}
>
<Crown className="h-5 w-5" />
@ -180,41 +224,34 @@ export function Sidebar() {
</Link>
</TooltipTrigger>
<TooltipContent side="right">
<p>View plans and pricing</p>
<p>Get more translations and features</p>
</TooltipContent>
</Tooltip>
)}
</div>
)}
{/* Admin Section */}
{/* Self-Host Option */}
<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}>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={item.href}
href="/ollama-setup"
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"
pathname === "/ollama-setup"
? "bg-orange-500/10 text-orange-400"
: "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
)}
>
<Icon className="h-5 w-5" />
<span>{item.name}</span>
<Server className="h-5 w-5" />
<span>Self-Host (Free)</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">
<p>{item.description}</p>
<p>Run your own Ollama for unlimited free translations</p>
</TooltipContent>
</Tooltip>
);
})}
</div>
</nav>
@ -222,17 +259,17 @@ export function Sidebar() {
<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">
<Link href="/dashboard" className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-teal-500 to-teal-600 text-white text-sm font-medium shrink-0">
{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")}>
<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)}
</Badge>
</div>
</div>
</Link>
<button
onClick={handleLogout}
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"

View File

@ -1,14 +1,22 @@
"""
Translation Service Abstraction
Provides a unified interface for different translation providers
Optimized for high performance with parallel processing
"""
from abc import ABC, abstractmethod
from typing import Optional, List, Dict
from typing import Optional, List, Dict, Tuple
import requests
from deep_translator import GoogleTranslator, DeeplTranslator, LibreTranslator
from config import config
import concurrent.futures
import threading
import asyncio
from functools import lru_cache
import time
# Global thread pool for parallel translations
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=8)
class TranslationProvider(ABC):
@ -23,6 +31,36 @@ class TranslationProvider(ABC):
"""Translate multiple texts at once - default implementation"""
return [self.translate(text, target_language, source_language) for text in texts]
def translate_batch_parallel(self, texts: List[str], target_language: str, source_language: str = 'auto', max_workers: int = 4) -> List[str]:
"""Parallel batch translation using thread pool"""
if not texts:
return []
results = [''] * len(texts)
non_empty = [(i, t) for i, t in enumerate(texts) if t and t.strip()]
if not non_empty:
return [t if t else '' for t in texts]
def translate_one(item: Tuple[int, str]) -> Tuple[int, str]:
idx, text = item
try:
return (idx, self.translate(text, target_language, source_language))
except Exception as e:
print(f"Translation error at index {idx}: {e}")
return (idx, text)
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
for idx, translated in executor.map(translate_one, non_empty):
results[idx] = translated
# Fill empty positions
for i, text in enumerate(texts):
if not text or not text.strip():
results[i] = text if text else ''
return results
class GoogleTranslationProvider(TranslationProvider):
"""Google Translate implementation with batch support"""