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:
@@ -3,10 +3,13 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2 } from "lucide-react";
|
||||
import { Eye, EyeOff, Mail, Lock, ArrowRight, Loader2, Shield, CheckCircle, AlertTriangle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function LoginForm() {
|
||||
const router = useRouter();
|
||||
@@ -18,6 +21,54 @@ function LoginForm() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [isValidating, setIsValidating] = useState({
|
||||
email: false,
|
||||
password: false,
|
||||
});
|
||||
const [isFocused, setIsFocused] = useState({
|
||||
email: false,
|
||||
password: false,
|
||||
});
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const validatePassword = (password: string) => {
|
||||
return password.length >= 8;
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmail(value);
|
||||
setIsValidating(prev => ({ ...prev, email: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setPassword(value);
|
||||
setIsValidating(prev => ({ ...prev, password: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handleEmailBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, email: false }));
|
||||
setIsFocused(prev => ({ ...prev, email: false }));
|
||||
};
|
||||
|
||||
const handlePasswordBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, password: false }));
|
||||
setIsFocused(prev => ({ ...prev, password: false }));
|
||||
};
|
||||
|
||||
const handleEmailFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, email: true }));
|
||||
};
|
||||
|
||||
const handlePasswordFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, password: true }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -42,114 +93,252 @@ function LoginForm() {
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
|
||||
// Redirect
|
||||
router.push(redirect);
|
||||
// Show success animation
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push(redirect);
|
||||
}, 1000);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Login failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getEmailValidationState = () => {
|
||||
if (!isValidating.email) return "";
|
||||
if (email.length === 0) return "";
|
||||
return validateEmail(email) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
const getPasswordValidationState = () => {
|
||||
if (!isValidating.password) return "";
|
||||
if (password.length === 0) return "";
|
||||
return validatePassword(password) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Card */}
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/50 p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome back</h1>
|
||||
<p className="text-zinc-400">Sign in to continue translating</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-zinc-300">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="pl-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
{/* Enhanced Login Card */}
|
||||
<Card
|
||||
variant="elevated"
|
||||
className="w-full max-w-md mx-auto overflow-hidden animate-fade-in"
|
||||
>
|
||||
<CardHeader className="text-center pb-6">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="inline-flex items-center gap-3 mb-6 group">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-accent text-white font-bold text-xl shadow-lg group-hover:shadow-xl group-hover:shadow-primary/25 transition-all duration-300 group-hover:-translate-y-0.5">
|
||||
文A
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-semibold text-white group-hover:text-primary transition-colors duration-300">
|
||||
Translate Co.
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-zinc-300">Password</Label>
|
||||
<Link href="/auth/forgot-password" className="text-sm text-teal-400 hover:text-teal-300">
|
||||
Forgot password?
|
||||
</Link>
|
||||
<CardTitle className="text-2xl font-bold text-white mb-2">
|
||||
Welcome back
|
||||
</CardTitle>
|
||||
<CardDescription className="text-text-secondary">
|
||||
Sign in to continue translating
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Success Message */}
|
||||
{showSuccess && (
|
||||
<div className="rounded-lg bg-success/10 border border-success/30 p-4 animate-slide-up">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-success" />
|
||||
<span className="text-success font-medium">Login successful! Redirecting...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="pl-10 pr-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-4 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-destructive font-medium">Authentication Error</p>
|
||||
<p className="text-destructive/80 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-teal-500 hover:bg-teal-600 text-white"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Sign In
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Email Field */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="email" className="text-text-secondary font-medium">
|
||||
Email Address
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onBlur={handleEmailBlur}
|
||||
onFocus={handleEmailFocus}
|
||||
required
|
||||
className={cn(
|
||||
"pl-12 h-12 text-lg",
|
||||
getEmailValidationState() === "valid" && "border-success focus:border-success",
|
||||
getEmailValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.email && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<Mail className="h-5 w-5" />}
|
||||
/>
|
||||
|
||||
{/* Validation Indicator */}
|
||||
{isValidating.email && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{getEmailValidationState() === "valid" && (
|
||||
<CheckCircle className="h-5 w-5 text-success animate-fade-in" />
|
||||
)}
|
||||
{getEmailValidationState() === "invalid" && (
|
||||
<AlertTriangle className="h-5 w-5 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-zinc-400">
|
||||
Don't have an account?{" "}
|
||||
{/* Password Field */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="password" className="text-text-secondary font-medium">
|
||||
Password
|
||||
</Label>
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors duration-200"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••••"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onBlur={handlePasswordBlur}
|
||||
onFocus={handlePasswordFocus}
|
||||
required
|
||||
className={cn(
|
||||
"pl-12 pr-12 h-12 text-lg",
|
||||
getPasswordValidationState() === "valid" && "border-success focus:border-success",
|
||||
getPasswordValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.password && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<Lock className="h-5 w-5" />}
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors duration-200"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Validation Indicator */}
|
||||
{isValidating.password && (
|
||||
<div className="absolute right-12 top-1/2 -translate-y-1/2">
|
||||
{getPasswordValidationState() === "valid" && (
|
||||
<CheckCircle className="h-5 w-5 text-success animate-fade-in" />
|
||||
)}
|
||||
{getPasswordValidationState() === "invalid" && (
|
||||
<AlertTriangle className="h-5 w-5 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Strength Indicator */}
|
||||
{isValidating.password && password.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex justify-between text-xs text-text-tertiary">
|
||||
<span>Password strength</span>
|
||||
<span className={cn(
|
||||
password.length < 8 && "text-destructive",
|
||||
password.length >= 8 && password.length < 12 && "text-warning",
|
||||
password.length >= 12 && "text-success"
|
||||
)}>
|
||||
{password.length < 8 && "Weak"}
|
||||
{password.length >= 8 && password.length < 12 && "Fair"}
|
||||
{password.length >= 12 && "Strong"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-border-subtle rounded-full h-1 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all duration-300 ease-out",
|
||||
password.length < 8 && "bg-destructive w-1/3",
|
||||
password.length >= 8 && password.length < 12 && "bg-warning w-2/3",
|
||||
password.length >= 12 && "bg-success w-full"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !email || !password}
|
||||
variant="premium"
|
||||
size="lg"
|
||||
className="w-full h-12 text-lg group"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Sign In
|
||||
<ArrowRight className="ml-2 h-5 w-5 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Enhanced Footer */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-text-tertiary mb-6">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href={`/auth/register${redirect !== "/" ? `?redirect=${redirect}` : ""}`}
|
||||
className="text-teal-400 hover:text-teal-300"
|
||||
className="text-primary hover:text-primary/80 font-medium transition-colors duration-200"
|
||||
>
|
||||
Sign up for free
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features reminder */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-zinc-500 mb-3">Start with our free plan:</p>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{["5 docs/day", "10 pages/doc", "Free forever"].map((feature) => (
|
||||
<span
|
||||
key={feature}
|
||||
className="px-3 py-1 rounded-full bg-zinc-800 text-zinc-400 text-xs"
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<div className="flex flex-wrap justify-center gap-6 text-xs text-text-tertiary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-success" />
|
||||
<span>Secure login</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-primary" />
|
||||
<span>SSL encrypted</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -158,32 +347,40 @@ function LoginForm() {
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/50 p-8">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-teal-500" />
|
||||
</div>
|
||||
</div>
|
||||
<Card variant="elevated" className="w-full max-w-md mx-auto">
|
||||
<CardContent className="flex items-center justify-center py-16">
|
||||
<div className="text-center space-y-4">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary mx-auto" />
|
||||
<p className="text-lg font-medium text-foreground">Loading...</p>
|
||||
<div className="w-16 h-1 bg-border-subtle rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary animate-loading-shimmer" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#1a1a1a] to-[#262626] flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-teal-500 text-white font-bold text-xl">
|
||||
文A
|
||||
</div>
|
||||
<span className="text-2xl font-semibold text-white">Translate Co.</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
<div className="min-h-screen bg-gradient-to-br from-surface via-surface-elevated to-background flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Effects */}
|
||||
<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 Background Elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-20 left-10 w-32 h-32 bg-primary/5 rounded-full blur-3xl animate-float" />
|
||||
<div className="absolute bottom-20 right-20 w-24 h-24 bg-accent/5 rounded-full blur-2xl animate-float-delayed" />
|
||||
<div className="absolute top-1/2 right-1/4 w-16 h-16 bg-success/5 rounded-full blur-xl animate-float-slow" />
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,26 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Eye, EyeOff, Mail, Lock, User, ArrowRight, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
Mail,
|
||||
Lock,
|
||||
User,
|
||||
ArrowRight,
|
||||
Loader2,
|
||||
Shield,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
UserPlus,
|
||||
Info
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function RegisterForm() {
|
||||
const router = useRouter();
|
||||
@@ -18,22 +34,180 @@ function RegisterForm() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const [isValidating, setIsValidating] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
password: false,
|
||||
confirmPassword: false,
|
||||
});
|
||||
|
||||
const [isFocused, setIsFocused] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
password: false,
|
||||
confirmPassword: false,
|
||||
});
|
||||
|
||||
const validateName = (name: string) => {
|
||||
return name.trim().length >= 2;
|
||||
};
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const validatePassword = (password: string) => {
|
||||
return password.length >= 8;
|
||||
};
|
||||
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
return password === confirmPassword && password.length > 0;
|
||||
};
|
||||
|
||||
// Real-time validation
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setName(value);
|
||||
setIsValidating(prev => ({ ...prev, name: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmail(value);
|
||||
setIsValidating(prev => ({ ...prev, email: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setPassword(value);
|
||||
setIsValidating(prev => ({ ...prev, password: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setConfirmPassword(value);
|
||||
setIsValidating(prev => ({ ...prev, confirmPassword: value.length > 0 }));
|
||||
};
|
||||
|
||||
const handleNameBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, name: false }));
|
||||
setIsFocused(prev => ({ ...prev, name: false }));
|
||||
};
|
||||
|
||||
const handleEmailBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, email: false }));
|
||||
setIsFocused(prev => ({ ...prev, email: false }));
|
||||
};
|
||||
|
||||
const handlePasswordBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, password: false }));
|
||||
setIsFocused(prev => ({ ...prev, password: false }));
|
||||
};
|
||||
|
||||
const handleConfirmPasswordBlur = () => {
|
||||
setIsValidating(prev => ({ ...prev, confirmPassword: false }));
|
||||
setIsFocused(prev => ({ ...prev, confirmPassword: false }));
|
||||
};
|
||||
|
||||
const handleNameFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, name: true }));
|
||||
};
|
||||
|
||||
const handleEmailFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, email: true }));
|
||||
};
|
||||
|
||||
const handlePasswordFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, password: true }));
|
||||
};
|
||||
|
||||
const handleConfirmPasswordFocus = () => {
|
||||
setIsFocused(prev => ({ ...prev, confirmPassword: true }));
|
||||
};
|
||||
|
||||
const getNameValidationState = () => {
|
||||
if (!isValidating.name) return "";
|
||||
if (name.length === 0) return "";
|
||||
return validateName(name) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
const getEmailValidationState = () => {
|
||||
if (!isValidating.email) return "";
|
||||
if (email.length === 0) return "";
|
||||
return validateEmail(email) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
const getPasswordValidationState = () => {
|
||||
if (!isValidating.password) return "";
|
||||
if (password.length === 0) return "";
|
||||
return validatePassword(password) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
const getConfirmPasswordValidationState = () => {
|
||||
if (!isValidating.confirmPassword) return "";
|
||||
if (confirmPassword.length === 0) return "";
|
||||
return validateConfirmPassword(password, confirmPassword) ? "valid" : "invalid";
|
||||
};
|
||||
|
||||
const getPasswordStrength = () => {
|
||||
if (password.length === 0) return { strength: 0, text: "", color: "" };
|
||||
|
||||
let strength = 0;
|
||||
let text = "";
|
||||
let color = "";
|
||||
|
||||
if (password.length >= 8) strength++;
|
||||
if (password.length >= 12) strength++;
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
if (/[0-9]/.test(password)) strength++;
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
||||
|
||||
if (strength <= 2) {
|
||||
text = "Weak";
|
||||
color = "text-destructive";
|
||||
} else if (strength <= 3) {
|
||||
text = "Fair";
|
||||
color = "text-warning";
|
||||
} else {
|
||||
text = "Strong";
|
||||
color = "text-success";
|
||||
}
|
||||
|
||||
return { strength, text, color };
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
// Validate all fields
|
||||
if (!validateName(name)) {
|
||||
setError("Name must be at least 2 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
if (!validateEmail(email)) {
|
||||
setError("Please enter a valid email address");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePassword(password)) {
|
||||
setError("Password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateConfirmPassword(password, confirmPassword)) {
|
||||
setError("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -55,168 +229,373 @@ function RegisterForm() {
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
|
||||
// Redirect
|
||||
router.push(redirect);
|
||||
// Show success animation
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push(redirect);
|
||||
}, 1500);
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Registration failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const passwordStrength = getPasswordStrength();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Card */}
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/50 p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Create an account</h1>
|
||||
<p className="text-zinc-400">Start translating documents for free</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
{/* Enhanced Registration Card */}
|
||||
<Card
|
||||
variant="elevated"
|
||||
className={cn(
|
||||
"w-full max-w-md mx-auto overflow-hidden animate-fade-in",
|
||||
showSuccess && "scale-95 opacity-0"
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-zinc-300">Full Name</Label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
className="pl-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
>
|
||||
<CardHeader className="text-center pb-6">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="inline-flex items-center gap-3 mb-6 group">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-accent text-white font-bold text-xl shadow-lg group-hover:shadow-xl group-hover:shadow-primary/25 transition-all duration-300 group-hover:-translate-y-0.5">
|
||||
文A
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-semibold text-white group-hover:text-primary transition-colors duration-300">
|
||||
Translate Co.
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-zinc-300">Email</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="pl-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
<CardTitle className="text-2xl font-bold text-white mb-2">
|
||||
Create an account
|
||||
</CardTitle>
|
||||
<CardDescription className="text-text-secondary">
|
||||
Start translating documents for free
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Success Message */}
|
||||
{showSuccess && (
|
||||
<div className="rounded-lg bg-success/10 border border-success/30 p-6 mb-6 animate-slide-up">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="h-8 w-8 text-success animate-pulse" />
|
||||
<div>
|
||||
<p className="text-lg font-medium text-success mb-1">Registration Successful!</p>
|
||||
<p className="text-sm text-success/80">Redirecting to your dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-zinc-300">Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="pl-10 pr-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-4 mb-6 animate-slide-up">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-destructive mb-1">Registration Error</p>
|
||||
<p className="text-sm text-destructive/80">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="flex items-center justify-center mb-8">
|
||||
{[1, 2, 3].map((stepNumber) => (
|
||||
<div
|
||||
key={stepNumber}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300",
|
||||
step === stepNumber
|
||||
? "bg-primary text-white scale-110"
|
||||
: "bg-surface text-text-tertiary"
|
||||
)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm font-medium">{stepNumber}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="h-0.5 bg-border-subtle flex-1 mx-2" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword" className="text-zinc-300">Confirm Password</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" />
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
className="pl-10 bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Name Field */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="name" className="text-text-secondary font-medium">
|
||||
Full Name
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
onBlur={handleNameBlur}
|
||||
onFocus={handleNameFocus}
|
||||
required
|
||||
className={cn(
|
||||
"pl-12 h-12 text-lg",
|
||||
getNameValidationState() === "valid" && "border-success focus:border-success",
|
||||
getNameValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.name && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<User className="h-5 w-5" />}
|
||||
/>
|
||||
|
||||
{/* Validation Indicator */}
|
||||
{isValidating.name && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{getNameValidationState() === "valid" && (
|
||||
<CheckCircle className="h-5 w-5 text-success animate-fade-in" />
|
||||
)}
|
||||
{getNameValidationState() === "invalid" && (
|
||||
<AlertTriangle className="h-5 w-5 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="email" className="text-text-secondary font-medium">
|
||||
Email Address
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
onBlur={handleEmailBlur}
|
||||
onFocus={handleEmailFocus}
|
||||
required
|
||||
className={cn(
|
||||
"pl-12 h-12 text-lg",
|
||||
getEmailValidationState() === "valid" && "border-success focus:border-success",
|
||||
getEmailValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.email && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<Mail className="h-5 w-5" />}
|
||||
/>
|
||||
|
||||
{/* Validation Indicator */}
|
||||
{isValidating.email && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{getEmailValidationState() === "valid" && (
|
||||
<CheckCircle className="h-5 w-5 text-success animate-fade-in" />
|
||||
)}
|
||||
{getEmailValidationState() === "invalid" && (
|
||||
<AlertTriangle className="h-5 w-5 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="password" className="text-text-secondary font-medium">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="•••••••••••"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
onBlur={handlePasswordBlur}
|
||||
onFocus={handlePasswordFocus}
|
||||
required
|
||||
minLength={8}
|
||||
className={cn(
|
||||
"pl-12 pr-12 h-12 text-lg",
|
||||
getPasswordValidationState() === "valid" && "border-success focus:border-success",
|
||||
getPasswordValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.password && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<Lock className="h-5 w-5" />}
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors duration-200"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Password Strength Indicator */}
|
||||
{password.length > 0 && (
|
||||
<div className="absolute right-12 top-1/2 -translate-y-1/2">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex space-x-1">
|
||||
{[1, 2, 3, 4].map((level) => (
|
||||
<div
|
||||
key={level}
|
||||
className={cn(
|
||||
"w-1 h-1 rounded-full",
|
||||
level <= passwordStrength.strength ? "bg-success" : "bg-border"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className={cn("text-xs", passwordStrength.color)}>
|
||||
{passwordStrength.text}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="confirmPassword" className="text-text-secondary font-medium">
|
||||
Confirm Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="•••••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
onBlur={handleConfirmPasswordBlur}
|
||||
onFocus={handleConfirmPasswordFocus}
|
||||
required
|
||||
className={cn(
|
||||
"pl-12 pr-12 h-12 text-lg",
|
||||
getConfirmPasswordValidationState() === "valid" && "border-success focus:border-success",
|
||||
getConfirmPasswordValidationState() === "invalid" && "border-destructive focus:border-destructive",
|
||||
isFocused.confirmPassword && "ring-2 ring-primary/20"
|
||||
)}
|
||||
leftIcon={<Lock className="h-5 w-5" />}
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors duration-200"
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeOff className="h-5 w-5" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Validation Indicator */}
|
||||
{isValidating.confirmPassword && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{getConfirmPasswordValidationState() === "valid" && (
|
||||
<CheckCircle className="h-5 w-5 text-success animate-fade-in" />
|
||||
)}
|
||||
{getConfirmPasswordValidationState() === "invalid" && (
|
||||
<AlertTriangle className="h-5 w-5 text-destructive animate-fade-in" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !name || !email || !password || !confirmPassword}
|
||||
variant="premium"
|
||||
size="lg"
|
||||
className="w-full h-12 text-lg group"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Creating Account...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus className="mr-2 h-5 w-5 transition-transform duration-200 group-hover:scale-110" />
|
||||
Create Account
|
||||
<ArrowRight className="ml-2 h-5 w-5 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Sign In Link */}
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-text-tertiary mb-4">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href={`/auth/login${redirect !== "/" ? `?redirect=${redirect}` : ""}`}
|
||||
className="text-primary hover:text-primary/80 font-medium transition-colors duration-200"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-teal-500 hover:bg-teal-600 text-white"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Create Account
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-zinc-400">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href={`/auth/login${redirect !== "/" ? `?redirect=${redirect}` : ""}`}
|
||||
className="text-teal-400 hover:text-teal-300"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-zinc-500">
|
||||
By creating an account, you agree to our{" "}
|
||||
<Link href="/terms" className="text-zinc-400 hover:text-zinc-300">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/privacy" className="text-zinc-400 hover:text-zinc-300">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Terms and Privacy */}
|
||||
<div className="text-center text-xs text-text-tertiary space-y-2">
|
||||
<p>
|
||||
By creating an account, you agree to our{" "}
|
||||
<Link href="/terms" className="text-primary hover:text-primary/80 transition-colors duration-200">
|
||||
Terms of Service
|
||||
</Link>
|
||||
{" "} and{" "}
|
||||
<Link href="/privacy" className="text-primary hover:text-primary/80 transition-colors duration-200">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-900/50 p-8">
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-teal-500" />
|
||||
</div>
|
||||
</div>
|
||||
<Card variant="elevated" className="w-full max-w-md mx-auto">
|
||||
<CardContent className="flex items-center justify-center py-16">
|
||||
<div className="text-center space-y-4">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary mx-auto" />
|
||||
<p className="text-lg font-medium text-foreground">Creating your account...</p>
|
||||
<div className="w-16 h-1 bg-border-subtle rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary animate-loading-shimmer" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#1a1a1a] to-[#262626] flex items-center justify-center p-4">
|
||||
<div className="min-h-screen bg-gradient-to-br from-surface via-surface-elevated to-background flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* Background Effects */}
|
||||
<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>
|
||||
|
||||
{/* Floating Elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl animate-pulse" />
|
||||
<div className="absolute top-40 right-20 w-32 h-32 bg-accent/10 rounded-full blur-2xl animate-pulse animation-delay-2000" />
|
||||
<div className="absolute bottom-20 left-1/4 w-16 h-16 bg-success/10 rounded-full blur-lg animate-pulse animation-delay-4000" />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-teal-500 text-white font-bold text-xl">
|
||||
文A
|
||||
</div>
|
||||
<span className="text-2xl font-semibold text-white">Translate Co.</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, Zap, Building2, Crown, Sparkles } from "lucide-react";
|
||||
import { Check, Zap, Building2, Crown, Sparkles, ArrowRight, Star, Shield, Rocket, Users, Headphones, Lock, Globe, Clock, ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Plan {
|
||||
@@ -16,6 +17,8 @@ interface Plan {
|
||||
max_pages_per_doc: number;
|
||||
providers: string[];
|
||||
popular?: boolean;
|
||||
description?: string;
|
||||
highlight?: string;
|
||||
}
|
||||
|
||||
interface CreditPackage {
|
||||
@@ -25,6 +28,12 @@ interface CreditPackage {
|
||||
popular?: boolean;
|
||||
}
|
||||
|
||||
interface FAQ {
|
||||
question: string;
|
||||
answer: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
const planIcons: Record<string, any> = {
|
||||
free: Sparkles,
|
||||
starter: Zap,
|
||||
@@ -33,11 +42,20 @@ const planIcons: Record<string, any> = {
|
||||
enterprise: Building2,
|
||||
};
|
||||
|
||||
const planGradients: Record<string, string> = {
|
||||
free: "from-zinc-600 to-zinc-700",
|
||||
starter: "from-blue-600 to-blue-700",
|
||||
pro: "from-teal-600 to-teal-700",
|
||||
business: "from-purple-600 to-purple-700",
|
||||
enterprise: "from-amber-600 to-amber-700",
|
||||
};
|
||||
|
||||
export default function PricingPage() {
|
||||
const [isYearly, setIsYearly] = useState(false);
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [creditPackages, setCreditPackages] = useState<CreditPackage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedFAQ, setExpandedFAQ] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
@@ -57,11 +75,13 @@ export default function PricingPage() {
|
||||
name: "Free",
|
||||
price_monthly: 0,
|
||||
price_yearly: 0,
|
||||
description: "Perfect for trying out our service",
|
||||
features: [
|
||||
"3 documents per day",
|
||||
"Up to 10 pages per document",
|
||||
"Ollama (self-hosted) only",
|
||||
"Basic support via community",
|
||||
"Secure document processing",
|
||||
],
|
||||
docs_per_month: 3,
|
||||
max_pages_per_doc: 10,
|
||||
@@ -70,14 +90,16 @@ export default function PricingPage() {
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
price_monthly: 9,
|
||||
price_yearly: 90,
|
||||
price_monthly: 12,
|
||||
price_yearly: 120,
|
||||
description: "For individuals and small projects",
|
||||
features: [
|
||||
"50 documents per month",
|
||||
"Up to 50 pages per document",
|
||||
"Google Translate included",
|
||||
"LibreTranslate included",
|
||||
"Email support",
|
||||
"Document history (30 days)",
|
||||
],
|
||||
docs_per_month: 50,
|
||||
max_pages_per_doc: 50,
|
||||
@@ -86,8 +108,10 @@ export default function PricingPage() {
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
price_monthly: 29,
|
||||
price_yearly: 290,
|
||||
price_monthly: 39,
|
||||
price_yearly: 390,
|
||||
description: "For professionals and growing teams",
|
||||
highlight: "Most Popular",
|
||||
features: [
|
||||
"200 documents per month",
|
||||
"Up to 200 pages per document",
|
||||
@@ -95,17 +119,20 @@ export default function PricingPage() {
|
||||
"DeepL & OpenAI included",
|
||||
"API access (1000 calls/month)",
|
||||
"Priority email support",
|
||||
"Document history (90 days)",
|
||||
"Custom formatting options",
|
||||
],
|
||||
docs_per_month: 200,
|
||||
max_pages_per_doc: 200,
|
||||
providers: ["ollama", "google", "deepl", "openai", "libre"],
|
||||
providers: ["ollama", "google", "deepl", "openai", "libre", "openrouter"],
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "business",
|
||||
name: "Business",
|
||||
price_monthly: 79,
|
||||
price_yearly: 790,
|
||||
price_monthly: 99,
|
||||
price_yearly: 990,
|
||||
description: "For teams and organizations",
|
||||
features: [
|
||||
"1000 documents per month",
|
||||
"Up to 500 pages per document",
|
||||
@@ -115,16 +142,20 @@ export default function PricingPage() {
|
||||
"Priority processing queue",
|
||||
"Dedicated support",
|
||||
"Team management (up to 5 users)",
|
||||
"Document history (1 year)",
|
||||
"Advanced analytics",
|
||||
],
|
||||
docs_per_month: 1000,
|
||||
max_pages_per_doc: 500,
|
||||
providers: ["ollama", "google", "deepl", "openai", "libre", "azure"],
|
||||
providers: ["ollama", "google", "deepl", "openai", "libre", "openrouter", "azure"],
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
name: "Enterprise",
|
||||
price_monthly: -1,
|
||||
price_yearly: -1,
|
||||
description: "Custom solutions for large organizations",
|
||||
highlight: "Custom",
|
||||
features: [
|
||||
"Unlimited documents",
|
||||
"Unlimited pages",
|
||||
@@ -134,6 +165,8 @@ export default function PricingPage() {
|
||||
"24/7 dedicated support",
|
||||
"Custom AI models",
|
||||
"White-label option",
|
||||
"Unlimited users",
|
||||
"Advanced security features",
|
||||
],
|
||||
docs_per_month: -1,
|
||||
max_pages_per_doc: -1,
|
||||
@@ -187,235 +220,381 @@ export default function PricingPage() {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#1a1a1a] to-[#262626] py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16">
|
||||
<Badge className="mb-4 bg-teal-500/20 text-teal-400 border-teal-500/30">
|
||||
Pricing
|
||||
</Badge>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
Simple, Transparent Pricing
|
||||
</h1>
|
||||
<p className="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||||
Choose the perfect plan for your translation needs. Start free and scale as you grow.
|
||||
</p>
|
||||
const faqs: FAQ[] = [
|
||||
{
|
||||
question: "Can I use my own Ollama instance?",
|
||||
answer: "Yes! The Free plan lets you connect your own Ollama server for unlimited translations. You just need to configure your Ollama endpoint in settings.",
|
||||
category: "Technical"
|
||||
},
|
||||
{
|
||||
question: "What happens if I exceed my monthly limit?",
|
||||
answer: "You can either wait for the next month, upgrade to a higher plan, or purchase credit packages for additional pages. Credits never expire.",
|
||||
category: "Billing"
|
||||
},
|
||||
{
|
||||
question: "Can I cancel my subscription anytime?",
|
||||
answer: "Yes, you can cancel anytime. You'll continue to have access until the end of your billing period. No questions asked.",
|
||||
category: "Billing"
|
||||
},
|
||||
{
|
||||
question: "Do credits expire?",
|
||||
answer: "No, purchased credits never expire and can be used anytime. They remain in your account until you use them.",
|
||||
category: "Credits"
|
||||
},
|
||||
{
|
||||
question: "What file formats are supported?",
|
||||
answer: "We support Microsoft Office documents: Word (.docx), Excel (.xlsx), and PowerPoint (.pptx). The formatting is fully preserved during translation.",
|
||||
category: "Technical"
|
||||
},
|
||||
{
|
||||
question: "How secure are my documents?",
|
||||
answer: "All documents are encrypted in transit and at rest. We use industry-standard security practices and never share your data with third parties.",
|
||||
category: "Security"
|
||||
},
|
||||
{
|
||||
question: "Can I change plans anytime?",
|
||||
answer: "Yes, you can upgrade or downgrade your plan at any time. When upgrading, you'll be charged the prorated difference immediately.",
|
||||
category: "Billing"
|
||||
},
|
||||
{
|
||||
question: "Do you offer refunds?",
|
||||
answer: "We offer a 30-day money-back guarantee for all paid plans. If you're not satisfied, contact our support team for a full refund.",
|
||||
category: "Billing"
|
||||
}
|
||||
];
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div className="mt-8 flex items-center justify-center gap-4">
|
||||
<span className={cn("text-sm", !isYearly ? "text-white" : "text-zinc-500")}>
|
||||
Monthly
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsYearly(!isYearly)}
|
||||
className={cn(
|
||||
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
|
||||
isYearly ? "bg-teal-500" : "bg-zinc-700"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-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 pricing plans...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-background">
|
||||
{/* Header */}
|
||||
<header className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-accent/20"></div>
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-16 pb-20">
|
||||
<div className="text-center">
|
||||
<Badge variant="outline" className="mb-6 border-primary/30 text-primary bg-primary/10 backdrop-blur-sm">
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Transparent Pricing
|
||||
</Badge>
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6">
|
||||
Choose Your
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent ml-3">
|
||||
Perfect Plan
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl text-text-secondary max-w-3xl mx-auto mb-8">
|
||||
Start with our free plan and scale as your translation needs grow.
|
||||
No hidden fees, no surprises. Just powerful translation tools.
|
||||
</p>
|
||||
|
||||
{/* Billing Toggle */}
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
<span className={cn(
|
||||
"text-sm font-medium transition-colors duration-200",
|
||||
!isYearly ? "text-white" : "text-text-tertiary"
|
||||
)}>
|
||||
Monthly Billing
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsYearly(!isYearly)}
|
||||
className={cn(
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
|
||||
isYearly ? "translate-x-6" : "translate-x-1"
|
||||
"relative inline-flex h-8 w-14 items-center rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary/50",
|
||||
isYearly ? "bg-primary" : "bg-surface-hover"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<span className={cn("text-sm", isYearly ? "text-white" : "text-zinc-500")}>
|
||||
Yearly
|
||||
<Badge className="ml-2 bg-green-500/20 text-green-400 border-green-500/30 text-xs">
|
||||
Save 17%
|
||||
</Badge>
|
||||
</span>
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block h-6 w-6 transform rounded-full bg-white transition-transform duration-300 shadow-lg",
|
||||
isYearly ? "translate-x-7" : "translate-x-1"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<span className={cn(
|
||||
"text-sm font-medium transition-colors duration-200",
|
||||
isYearly ? "text-white" : "text-text-tertiary"
|
||||
)}>
|
||||
Yearly Billing
|
||||
<Badge className="ml-2 bg-success/20 text-success border-success/30 text-xs">
|
||||
Save 17%
|
||||
</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
{/* Plans Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
|
||||
{plans.slice(0, 4).map((plan) => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-20">
|
||||
{plans.slice(0, 4).map((plan, index) => {
|
||||
const Icon = planIcons[plan.id] || Sparkles;
|
||||
const price = isYearly ? plan.price_yearly : plan.price_monthly;
|
||||
const isEnterprise = plan.id === "enterprise";
|
||||
const isPro = plan.popular;
|
||||
const isPopular = plan.popular;
|
||||
|
||||
return (
|
||||
<div
|
||||
<Card
|
||||
key={plan.id}
|
||||
variant={isPopular ? "gradient" : "elevated"}
|
||||
className={cn(
|
||||
"relative rounded-2xl border p-6 flex flex-col",
|
||||
isPro
|
||||
? "border-teal-500 bg-gradient-to-b from-teal-500/10 to-transparent"
|
||||
: "border-zinc-800 bg-zinc-900/50"
|
||||
"relative overflow-hidden group animate-fade-in-up",
|
||||
isPopular && "scale-105 shadow-2xl shadow-primary/20",
|
||||
`animation-delay-${index * 100}`
|
||||
)}
|
||||
>
|
||||
{isPro && (
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-teal-500 text-white">
|
||||
Most Popular
|
||||
</Badge>
|
||||
{isPopular && (
|
||||
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-br from-primary to-accent opacity-20 rounded-full -mr-10 -mt-10"></div>
|
||||
)}
|
||||
|
||||
<CardHeader className="relative">
|
||||
{isPopular && (
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-gradient-to-r from-primary to-accent text-white border-0">
|
||||
{plan.highlight}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className={cn(
|
||||
"p-3 rounded-xl",
|
||||
isPopular
|
||||
? "bg-gradient-to-br from-primary/20 to-accent/20"
|
||||
: "bg-surface"
|
||||
)}>
|
||||
<Icon className={cn(
|
||||
"h-6 w-6",
|
||||
isPopular ? "text-primary" : "text-text-secondary"
|
||||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">{plan.name}</h3>
|
||||
<p className="text-sm text-text-tertiary">{plan.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div
|
||||
<div className="mb-2">
|
||||
{isEnterprise || price < 0 ? (
|
||||
<div className="text-3xl font-bold text-white">Custom</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold text-white">
|
||||
${isYearly ? Math.round(price / 12) : price}
|
||||
</span>
|
||||
<span className="text-text-tertiary">/month</span>
|
||||
</div>
|
||||
{isYearly && price > 0 && (
|
||||
<div className="text-sm text-text-tertiary">
|
||||
${price} billed yearly (save 17%)
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<ul className="space-y-3">
|
||||
{plan.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>
|
||||
|
||||
<Button
|
||||
onClick={() => handleSubscribe(plan.id)}
|
||||
variant={isPopular ? "default" : "outline"}
|
||||
size="lg"
|
||||
className={cn(
|
||||
"p-2 rounded-lg",
|
||||
isPro ? "bg-teal-500/20" : "bg-zinc-800"
|
||||
"w-full group",
|
||||
isPopular && "bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-5 w-5", isPro ? "text-teal-400" : "text-zinc-400")} />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white">{plan.name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
{isEnterprise || price < 0 ? (
|
||||
<div className="text-3xl font-bold text-white">Custom</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-4xl font-bold text-white">
|
||||
${isYearly ? Math.round(price / 12) : price}
|
||||
</span>
|
||||
<span className="text-zinc-500">/month</span>
|
||||
{isYearly && price > 0 && (
|
||||
<div className="text-sm text-zinc-500">
|
||||
${price} billed yearly
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 mb-6 flex-1">
|
||||
{plan.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-sm text-zinc-300">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
onClick={() => handleSubscribe(plan.id)}
|
||||
className={cn(
|
||||
"w-full",
|
||||
isPro
|
||||
? "bg-teal-500 hover:bg-teal-600 text-white"
|
||||
: "bg-zinc-800 hover:bg-zinc-700 text-white"
|
||||
)}
|
||||
>
|
||||
{plan.id === "free"
|
||||
? "Get Started"
|
||||
: isEnterprise
|
||||
? "Contact Sales"
|
||||
: "Subscribe"}
|
||||
</Button>
|
||||
</div>
|
||||
{plan.id === "free" ? (
|
||||
<>
|
||||
Get Started
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</>
|
||||
) : isEnterprise ? (
|
||||
<>
|
||||
Contact Sales
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Subscribe Now
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Enterprise Section */}
|
||||
{plans.find((p) => p.id === "enterprise") && (
|
||||
<div className="rounded-2xl border border-zinc-800 bg-gradient-to-r from-purple-500/10 to-teal-500/10 p-8 mb-16">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-white mb-2">
|
||||
Need Enterprise Features?
|
||||
<Card variant="gradient" className="mb-20 animate-fade-in-up animation-delay-400">
|
||||
<CardContent className="p-8 md:p-12">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||
<div className="flex-1">
|
||||
<Badge variant="outline" className="mb-4 border-amber-500/30 text-amber-400 bg-amber-500/10">
|
||||
<Crown className="h-3 w-3 mr-1" />
|
||||
Enterprise
|
||||
</Badge>
|
||||
<h3 className="text-3xl font-bold text-white mb-4">
|
||||
Need a Custom Solution?
|
||||
</h3>
|
||||
<p className="text-zinc-400 max-w-xl">
|
||||
Get unlimited translations, custom integrations, on-premise deployment,
|
||||
dedicated support, and SLA guarantees. Perfect for large organizations.
|
||||
<p className="text-text-secondary text-lg mb-6 max-w-2xl">
|
||||
Get unlimited translations, custom integrations, on-premise deployment,
|
||||
dedicated support, and SLA guarantees. Perfect for large organizations
|
||||
with specific requirements.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
{[
|
||||
{ icon: Shield, text: "Advanced Security & Compliance" },
|
||||
{ icon: Users, text: "Unlimited Users & Teams" },
|
||||
{ icon: Headphones, text: "24/7 Dedicated Support" },
|
||||
{ icon: Lock, text: "On-Premise Deployment Options" }
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-surface">
|
||||
<item.icon className="h-5 w-5 text-amber-400" />
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary">{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white group"
|
||||
>
|
||||
<Building2 className="h-5 w-5 mr-2" />
|
||||
Contact Sales Team
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</Button>
|
||||
<Button variant="glass" size="lg">
|
||||
Schedule a Demo
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-purple-500 to-teal-500 hover:from-purple-600 hover:to-teal-600 text-white whitespace-nowrap"
|
||||
>
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Credit Packages */}
|
||||
<div className="mb-16">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Need Extra Pages?</h2>
|
||||
<p className="text-zinc-400">
|
||||
Buy credit packages to translate more pages. Credits never expire.
|
||||
<div className="mb-20">
|
||||
<div className="text-center mb-12">
|
||||
<Badge variant="outline" className="mb-4 border-primary/30 text-primary bg-primary/10">
|
||||
<Zap className="h-3 w-3 mr-1" />
|
||||
Extra Credits
|
||||
</Badge>
|
||||
<h2 className="text-3xl font-bold text-white mb-4">
|
||||
Need More Pages?
|
||||
</h2>
|
||||
<p className="text-text-secondary text-lg max-w-2xl mx-auto">
|
||||
Purchase credit packages to translate additional pages.
|
||||
Credits never expire and can be used across all documents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{creditPackages.map((pkg, idx) => (
|
||||
<div
|
||||
<Card
|
||||
key={idx}
|
||||
variant={pkg.popular ? "gradient" : "elevated"}
|
||||
className={cn(
|
||||
"rounded-xl border p-4 text-center",
|
||||
pkg.popular
|
||||
? "border-teal-500 bg-teal-500/10"
|
||||
: "border-zinc-800 bg-zinc-900/50"
|
||||
"text-center group hover:scale-105 transition-all duration-300 animate-fade-in-up",
|
||||
`animation-delay-${idx * 100}`
|
||||
)}
|
||||
>
|
||||
{pkg.popular && (
|
||||
<Badge className="mb-2 bg-teal-500/20 text-teal-400 border-teal-500/30 text-xs">
|
||||
Best Value
|
||||
</Badge>
|
||||
)}
|
||||
<div className="text-2xl font-bold text-white">{pkg.credits}</div>
|
||||
<div className="text-sm text-zinc-500 mb-2">pages</div>
|
||||
<div className="text-xl font-semibold text-white">${pkg.price}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
${pkg.price_per_credit.toFixed(2)}/page
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-3 w-full border-zinc-700 hover:bg-zinc-800"
|
||||
>
|
||||
Buy
|
||||
</Button>
|
||||
</div>
|
||||
<CardContent className="p-6">
|
||||
{pkg.popular && (
|
||||
<Badge className="mb-3 bg-gradient-to-r from-primary to-accent text-white border-0">
|
||||
Best Value
|
||||
</Badge>
|
||||
)}
|
||||
<div className="text-3xl font-bold text-white mb-1">{pkg.credits}</div>
|
||||
<div className="text-sm text-text-tertiary mb-4">pages</div>
|
||||
<div className="text-2xl font-semibold text-white mb-1">${pkg.price}</div>
|
||||
<div className="text-xs text-text-tertiary mb-4">
|
||||
${pkg.price_per_credit.toFixed(2)}/page
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={pkg.popular ? "default" : "outline"}
|
||||
className={cn(
|
||||
"w-full group",
|
||||
pkg.popular && "bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
)}
|
||||
>
|
||||
Buy Now
|
||||
<ArrowRight className="h-3 w-3 ml-1 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl font-bold text-white text-center mb-8">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<Badge variant="outline" className="mb-4 border-primary/30 text-primary bg-primary/10">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Questions
|
||||
</Badge>
|
||||
<h2 className="text-3xl font-bold text-white mb-4">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<p className="text-text-secondary text-lg">
|
||||
Everything you need to know about our pricing and plans
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{
|
||||
q: "Can I use my own Ollama instance?",
|
||||
a: "Yes! The Free plan lets you connect your own Ollama server for unlimited translations. You just need to configure your Ollama endpoint in the settings.",
|
||||
},
|
||||
{
|
||||
q: "What happens if I exceed my monthly limit?",
|
||||
a: "You can either wait for the next month, upgrade to a higher plan, or purchase credit packages for additional pages.",
|
||||
},
|
||||
{
|
||||
q: "Can I cancel my subscription anytime?",
|
||||
a: "Yes, you can cancel anytime. You'll continue to have access until the end of your billing period.",
|
||||
},
|
||||
{
|
||||
q: "Do credits expire?",
|
||||
a: "No, purchased credits never expire and can be used anytime.",
|
||||
},
|
||||
{
|
||||
q: "What file formats are supported?",
|
||||
a: "We support Microsoft Office documents: Word (.docx), Excel (.xlsx), and PowerPoint (.pptx). The formatting is fully preserved during translation.",
|
||||
},
|
||||
].map((faq, idx) => (
|
||||
<div key={idx} className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4">
|
||||
<h3 className="font-medium text-white mb-2">{faq.q}</h3>
|
||||
<p className="text-sm text-zinc-400">{faq.a}</p>
|
||||
</div>
|
||||
{faqs.map((faq, idx) => (
|
||||
<Card key={idx} variant="elevated" className="animate-fade-in-up animation-delay-100">
|
||||
<button
|
||||
onClick={() => setExpandedFAQ(expandedFAQ === idx ? null : idx)}
|
||||
className="w-full text-left p-6 focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-surface">
|
||||
<Globe className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-white">{faq.question}</h3>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-5 w-5 text-text-tertiary transition-transform duration-200",
|
||||
expandedFAQ === idx && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
{expandedFAQ === idx && (
|
||||
<div className="px-6 pb-6">
|
||||
<p className="text-text-secondary pl-11">{faq.answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTranslationStore } from "@/lib/store";
|
||||
import { Save, Loader2, Brain, BookOpen, Sparkles, Trash2 } from "lucide-react";
|
||||
import { Save, Loader2, Brain, BookOpen, Sparkles, Trash2, ArrowRight, AlertCircle, CheckCircle, Zap } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ContextGlossaryPage() {
|
||||
const { settings, updateSettings, applyPreset, clearContext } = useTranslationStore();
|
||||
@@ -36,7 +37,7 @@ export default function ContextGlossaryPage() {
|
||||
|
||||
const handleApplyPreset = (preset: 'hvac' | 'it' | 'legal' | 'medical') => {
|
||||
applyPreset(preset);
|
||||
// Need to get the updated values from the store after applying preset
|
||||
// Need to get updated values from store after applying preset
|
||||
setTimeout(() => {
|
||||
setLocalSettings({
|
||||
systemPrompt: useTranslationStore.getState().settings.systemPrompt,
|
||||
@@ -59,180 +60,294 @@ export default function ContextGlossaryPage() {
|
||||
const isWebLLMAvailable = typeof window !== 'undefined' && 'gpu' in navigator;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Context & Glossary</h1>
|
||||
<p className="text-zinc-400 mt-1">
|
||||
Configure translation context and glossary for LLM-based providers.
|
||||
</p>
|
||||
|
||||
{/* LLM Provider Status */}
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${isOllamaConfigured ? 'border-green-500 text-green-400' : 'border-zinc-600 text-zinc-500'}`}
|
||||
>
|
||||
🤖 Ollama {isOllamaConfigured ? '✓' : '○'}
|
||||
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Badge variant="outline" className="mb-4 border-primary/30 text-primary bg-primary/10">
|
||||
<Brain className="h-3 w-3 mr-1" />
|
||||
Context & Glossary
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${isOpenAIConfigured ? 'border-green-500 text-green-400' : 'border-zinc-600 text-zinc-500'}`}
|
||||
>
|
||||
🧠 OpenAI {isOpenAIConfigured ? '✓' : '○'}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${isWebLLMAvailable ? 'border-green-500 text-green-400' : 'border-zinc-600 text-zinc-500'}`}
|
||||
>
|
||||
💻 WebLLM {isWebLLMAvailable ? '✓' : '○'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Banner */}
|
||||
<div className="p-4 rounded-lg bg-teal-500/10 border border-teal-500/30">
|
||||
<p className="text-teal-400 text-sm flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span>
|
||||
<strong>Context & Glossary</strong> settings apply to all LLM providers:
|
||||
<strong> Ollama</strong>, <strong>OpenAI</strong>, and <strong>WebLLM</strong>.
|
||||
Use them to improve translation quality with domain-specific instructions.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6">
|
||||
{/* System Prompt */}
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Brain className="h-5 w-5 text-teal-400" />
|
||||
System Prompt
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Instructions for the LLM to follow during translation.
|
||||
Works with Ollama, OpenAI, and WebLLM.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
id="system-prompt"
|
||||
value={localSettings.systemPrompt}
|
||||
onChange={(e) =>
|
||||
setLocalSettings({ ...localSettings, systemPrompt: e.target.value })
|
||||
}
|
||||
placeholder="Example: You are translating technical HVAC documents. Use precise engineering terminology. Maintain consistency with industry standards..."
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 min-h-[200px] resize-y"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">
|
||||
💡 Tip: Include domain context, tone preferences, or specific terminology rules.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Presets */}
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Quick Presets</CardTitle>
|
||||
<CardDescription>
|
||||
Load pre-configured prompts & glossaries for common domains.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("hvac")}
|
||||
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800 hover:text-teal-400 justify-start"
|
||||
>
|
||||
🔧 HVAC / Engineering
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("it")}
|
||||
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800 hover:text-teal-400 justify-start"
|
||||
>
|
||||
💻 IT / Software
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("legal")}
|
||||
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800 hover:text-teal-400 justify-start"
|
||||
>
|
||||
⚖️ Legal / Contracts
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("medical")}
|
||||
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800 hover:text-teal-400 justify-start"
|
||||
>
|
||||
🏥 Medical / Healthcare
|
||||
</Button>
|
||||
<h1 className="text-4xl font-bold text-white mb-2">
|
||||
Context & Glossary
|
||||
</h1>
|
||||
<p className="text-lg text-text-secondary">
|
||||
Configure translation context and glossary for LLM-based providers
|
||||
</p>
|
||||
|
||||
{/* LLM Provider Status */}
|
||||
<div className="flex flex-wrap gap-3 mt-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-3 py-2",
|
||||
isOllamaConfigured
|
||||
? "border-success/50 text-success bg-success/10"
|
||||
: "border-border-subtle text-text-tertiary bg-surface/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isOllamaConfigured && <CheckCircle className="h-4 w-4" />}
|
||||
<span>🤖 Ollama</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleClear}
|
||||
className="w-full mt-3 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Clear All
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-3 py-2",
|
||||
isOpenAIConfigured
|
||||
? "border-success/50 text-success bg-success/10"
|
||||
: "border-border-subtle text-text-tertiary bg-surface/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isOpenAIConfigured && <CheckCircle className="h-4 w-4" />}
|
||||
<span>🧠 OpenAI</span>
|
||||
</div>
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"px-3 py-2",
|
||||
isWebLLMAvailable
|
||||
? "border-success/50 text-success bg-success/10"
|
||||
: "border-border-subtle text-text-tertiary bg-surface/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isWebLLMAvailable && <CheckCircle className="h-4 w-4" />}
|
||||
<span>💻 WebLLM</span>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="space-y-6">
|
||||
{/* Glossary */}
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5 text-teal-400" />
|
||||
Technical Glossary
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Define specific term translations. Format: source=target (one per line).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
id="glossary"
|
||||
value={localSettings.glossary}
|
||||
onChange={(e) =>
|
||||
setLocalSettings({ ...localSettings, glossary: e.target.value })
|
||||
}
|
||||
placeholder="pression statique=static pressure récupérateur=heat recovery unit ventilo-connecteur=fan coil unit gaine=duct diffuseur=diffuser"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 min-h-[280px] resize-y font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">
|
||||
💡 The glossary is included in the system prompt to guide translations.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
{/* Info Banner */}
|
||||
<Card variant="gradient" className="mb-8 animate-fade-in-up">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 rounded-lg bg-primary/20">
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Context & Glossary Settings
|
||||
</h3>
|
||||
<p className="text-text-secondary leading-relaxed">
|
||||
These settings apply to all LLM providers: <strong>Ollama</strong>, <strong>OpenAI</strong>, and <strong>WebLLM</strong>.
|
||||
Use them to improve translation quality with domain-specific instructions and terminology.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white px-8"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Left Column - System Prompt */}
|
||||
<div className="space-y-6">
|
||||
<Card variant="elevated" className="animate-fade-in-up animation-delay-100">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/20">
|
||||
<Brain className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">System Prompt</CardTitle>
|
||||
<CardDescription>
|
||||
Instructions for LLM to follow during translation
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
id="system-prompt"
|
||||
value={localSettings.systemPrompt}
|
||||
onChange={(e) =>
|
||||
setLocalSettings({ ...localSettings, systemPrompt: e.target.value })
|
||||
}
|
||||
placeholder="Example: You are translating technical HVAC documents. Use precise engineering terminology. Maintain consistency with industry standards..."
|
||||
className="bg-surface border-border-subtle text-white placeholder:text-text-tertiary min-h-[200px] resize-y focus:border-primary focus:ring-primary/20"
|
||||
/>
|
||||
<div className="p-4 rounded-lg bg-primary/10 border border-primary/30">
|
||||
<p className="text-sm text-primary flex items-start gap-2">
|
||||
<Zap className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
<strong>Tip:</strong> Include domain context, tone preferences, or specific terminology rules for better translation accuracy.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<Card variant="elevated" className="animate-fade-in-up animation-delay-200">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-accent/20">
|
||||
<Zap className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">Quick Presets</CardTitle>
|
||||
<CardDescription>
|
||||
Load pre-configured prompts & glossaries for common domains
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("hvac")}
|
||||
className="border-border-subtle text-text-secondary hover:bg-surface-hover hover:text-primary justify-start h-auto p-4 group"
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-lg mb-1">🔧</div>
|
||||
<div className="font-medium">HVAC / Engineering</div>
|
||||
<div className="text-xs text-text-tertiary">Technical terminology</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("it")}
|
||||
className="border-border-subtle text-text-secondary hover:bg-surface-hover hover:text-primary justify-start h-auto p-4 group"
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-lg mb-1">💻</div>
|
||||
<div className="font-medium">IT / Software</div>
|
||||
<div className="text-xs text-text-tertiary">Development terms</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("legal")}
|
||||
className="border-border-subtle text-text-secondary hover:bg-surface-hover hover:text-primary justify-start h-auto p-4 group"
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-lg mb-1">⚖️</div>
|
||||
<div className="font-medium">Legal / Contracts</div>
|
||||
<div className="text-xs text-text-tertiary">Legal terminology</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleApplyPreset("medical")}
|
||||
className="border-border-subtle text-text-secondary hover:bg-surface-hover hover:text-primary justify-start h-auto p-4 group"
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-lg mb-1">🏥</div>
|
||||
<div className="font-medium">Medical / Healthcare</div>
|
||||
<div className="text-xs text-text-tertiary">Medical terms</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleClear}
|
||||
className="w-full text-destructive hover:text-destructive/80 hover:bg-destructive/10 group"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
|
||||
Clear All
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Glossary */}
|
||||
<div className="space-y-6">
|
||||
<Card variant="elevated" className="animate-fade-in-up animation-delay-300">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-success/20">
|
||||
<BookOpen className="h-5 w-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">Technical Glossary</CardTitle>
|
||||
<CardDescription>
|
||||
Define specific term translations. Format: source=target (one per line)
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
id="glossary"
|
||||
value={localSettings.glossary}
|
||||
onChange={(e) =>
|
||||
setLocalSettings({ ...localSettings, glossary: e.target.value })
|
||||
}
|
||||
placeholder="pression statique=static pressure récupérateur=heat recovery unit ventilo-connecteur=fan coil unit gaine=duct diffuseur=diffuser"
|
||||
className="bg-surface border-border-subtle text-white placeholder:text-text-tertiary min-h-[280px] resize-y font-mono text-sm focus:border-success focus:ring-success/20"
|
||||
/>
|
||||
<div className="p-4 rounded-lg bg-success/10 border border-success/30">
|
||||
<p className="text-sm text-success flex items-start gap-2">
|
||||
<Zap className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
<strong>Pro Tip:</strong> The glossary is included in system prompt to guide translations and ensure consistent terminology.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Usage Examples */}
|
||||
<Card variant="glass" className="animate-fade-in-up animation-delay-400">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-accent/20">
|
||||
<AlertCircle className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">Usage Examples</CardTitle>
|
||||
<CardDescription>
|
||||
See how context and glossary improve translations
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="p-4 rounded-lg bg-surface/50 border border-border-subtle">
|
||||
<h4 className="font-medium text-white mb-2">Before (Generic Translation)</h4>
|
||||
<p className="text-sm text-text-tertiary italic">
|
||||
"The pressure in the duct should be maintained."
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-success/10 border border-success/30">
|
||||
<h4 className="font-medium text-white mb-2">After (With Context & Glossary)</h4>
|
||||
<p className="text-sm text-success italic">
|
||||
"La pression statique dans la gaine doit être maintenue."
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-xs text-text-tertiary">
|
||||
<strong>Key improvements:</strong> Technical terms are correctly translated, context is preserved, and industry-standard terminology is used.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end animate-fade-in-up animation-delay-500">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-white group"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTranslationStore } from "@/lib/store";
|
||||
import { languages } from "@/lib/api";
|
||||
import { Save, Loader2, Settings, Globe, Trash2 } from "lucide-react";
|
||||
import { Save, Loader2, Settings, Globe, Trash2, ArrowRight, Shield, Zap, Database } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function GeneralSettingsPage() {
|
||||
const { settings, updateSettings } = useTranslationStore();
|
||||
@@ -58,189 +59,335 @@ export default function GeneralSettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">General Settings</h1>
|
||||
<p className="text-zinc-400 mt-1">
|
||||
Configure general application settings and preferences.
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen bg-gradient-to-b from-surface via-surface-elevated to-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Badge variant="outline" className="mb-4 border-primary/30 text-primary bg-primary/10">
|
||||
<Settings className="h-3 w-3 mr-1" />
|
||||
Settings
|
||||
</Badge>
|
||||
<h1 className="text-4xl font-bold text-white mb-2">
|
||||
General Settings
|
||||
</h1>
|
||||
<p className="text-lg text-text-secondary">
|
||||
Configure general application settings and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="h-6 w-6 text-teal-400" />
|
||||
<div>
|
||||
<CardTitle className="text-white">Application Settings</CardTitle>
|
||||
<CardDescription>
|
||||
General configuration options
|
||||
</CardDescription>
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Card variant="elevated" className="group hover:scale-105 transition-all duration-300 animate-fade-in-up">
|
||||
<Link href="/settings/services" className="block">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="p-3 rounded-xl bg-primary/20 group-hover:bg-primary/30 transition-colors duration-300">
|
||||
<Zap className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-primary transition-colors duration-300">
|
||||
Translation Services
|
||||
</h3>
|
||||
<p className="text-sm text-text-tertiary">Configure providers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-primary">
|
||||
<span className="text-sm font-medium">Manage providers</span>
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" className="group hover:scale-105 transition-all duration-300 animate-fade-in-up animation-delay-100">
|
||||
<Link href="/settings/context" className="block">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="p-3 rounded-xl bg-accent/20 group-hover:bg-accent/30 transition-colors duration-300">
|
||||
<Globe className="h-6 w-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-accent transition-colors duration-300">
|
||||
Context & Glossary
|
||||
</h3>
|
||||
<p className="text-sm text-text-tertiary">Domain-specific settings</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-accent">
|
||||
<span className="text-sm font-medium">Configure context</span>
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" className="group hover:scale-105 transition-all duration-300 animate-fade-in-up animation-delay-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="p-3 rounded-xl bg-success/20 group-hover:bg-success/30 transition-colors duration-300">
|
||||
<Shield className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-success transition-colors duration-300">
|
||||
Privacy & Security
|
||||
</h3>
|
||||
<p className="text-sm text-text-tertiary">Data protection</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-success">
|
||||
<span className="text-sm font-medium">Coming soon</span>
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Application Settings */}
|
||||
<Card variant="elevated" className="mb-8 animate-fade-in-up animation-delay-300">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/20">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">Application Settings</CardTitle>
|
||||
<CardDescription>
|
||||
General configuration options
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="default-language" className="text-text-secondary font-medium">
|
||||
Default Target Language
|
||||
</Label>
|
||||
<Select value={defaultLanguage} onValueChange={setDefaultLanguage}>
|
||||
<SelectTrigger className="bg-surface border-border-subtle text-white focus:border-primary focus:ring-primary/20">
|
||||
<SelectValue placeholder="Select default language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-surface-elevated border-border-subtle max-h-[300px]">
|
||||
{languages.map((lang) => (
|
||||
<SelectItem
|
||||
key={lang.code}
|
||||
value={lang.code}
|
||||
className="text-white hover:bg-surface-hover focus:bg-primary/20 focus:text-primary"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{lang.flag}</span>
|
||||
<span>{lang.name}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
This language will be pre-selected when translating documents
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Supported Formats */}
|
||||
<Card variant="elevated" className="mb-8 animate-fade-in-up animation-delay-400">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-accent/20">
|
||||
<Globe className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">Supported Formats</CardTitle>
|
||||
<CardDescription>
|
||||
Document types that can be translated
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card variant="glass" className="group hover:scale-105 transition-all duration-300">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-4xl mb-3 group-hover:scale-110 transition-transform duration-300">📊</div>
|
||||
<h3 className="font-semibold text-white mb-2">Excel</h3>
|
||||
<p className="text-sm text-text-tertiary mb-4">.xlsx, .xls</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<Badge variant="outline" className="border-success/50 text-success bg-success/10 text-xs">
|
||||
Formulas
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 text-xs">
|
||||
Styles
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-accent/50 text-accent bg-accent/10 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card variant="glass" className="group hover:scale-105 transition-all duration-300">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-4xl mb-3 group-hover:scale-110 transition-transform duration-300">📝</div>
|
||||
<h3 className="font-semibold text-white mb-2">Word</h3>
|
||||
<p className="text-sm text-text-tertiary mb-4">.docx, .doc</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<Badge variant="outline" className="border-success/50 text-success bg-success/10 text-xs">
|
||||
Headers
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 text-xs">
|
||||
Tables
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-accent/50 text-accent bg-accent/10 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card variant="glass" className="group hover:scale-105 transition-all duration-300">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-4xl mb-3 group-hover:scale-110 transition-transform duration-300">📽️</div>
|
||||
<h3 className="font-semibold text-white mb-2">PowerPoint</h3>
|
||||
<p className="text-sm text-text-tertiary mb-4">.pptx, .ppt</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<Badge variant="outline" className="border-success/50 text-success bg-success/10 text-xs">
|
||||
Slides
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-primary/50 text-primary bg-primary/10 text-xs">
|
||||
Notes
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-accent/50 text-accent bg-accent/10 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<Card variant="elevated" className="animate-fade-in-up animation-delay-500">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-success/20">
|
||||
<Database className="h-5 w-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">API Information</CardTitle>
|
||||
<CardDescription>
|
||||
Backend server connection details
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50 hover:bg-surface transition-colors duration-200">
|
||||
<span className="text-text-tertiary">API Endpoint</span>
|
||||
<code className="text-primary text-sm font-mono bg-surface px-2 py-1 rounded">http://localhost:8000</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50 hover:bg-surface transition-colors duration-200">
|
||||
<span className="text-text-tertiary">Health Check</span>
|
||||
<code className="text-primary text-sm font-mono bg-surface px-2 py-1 rounded">/health</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50 hover:bg-surface transition-colors duration-200">
|
||||
<span className="text-text-tertiary">Translate Endpoint</span>
|
||||
<code className="text-primary text-sm font-mono bg-surface px-2 py-1 rounded">/translate</code>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" className="animate-fade-in-up animation-delay-600">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-warning/20">
|
||||
<Shield className="h-5 w-5 text-warning" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-white">System Status</CardTitle>
|
||||
<CardDescription>
|
||||
Application health and performance
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50">
|
||||
<span className="text-text-tertiary">Connection Status</span>
|
||||
<Badge variant="outline" className="border-success/50 text-success bg-success/10">
|
||||
<div className="w-2 h-2 bg-success rounded-full mr-2 animate-pulse"></div>
|
||||
Connected
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50">
|
||||
<span className="text-text-tertiary">Last Sync</span>
|
||||
<span className="text-sm text-text-secondary">Just now</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-surface/50">
|
||||
<span className="text-text-tertiary">Version</span>
|
||||
<span className="text-sm text-text-secondary">v2.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center animate-fade-in-up animation-delay-700">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-language" className="text-zinc-300">
|
||||
Default Target Language
|
||||
</Label>
|
||||
<Select value={defaultLanguage} onValueChange={setDefaultLanguage}>
|
||||
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
|
||||
<SelectValue placeholder="Select default language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-[300px]">
|
||||
{languages.map((lang) => (
|
||||
<SelectItem
|
||||
key={lang.code}
|
||||
value={lang.code}
|
||||
className="text-white hover:bg-zinc-700"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{lang.flag}</span>
|
||||
<span>{lang.name}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-zinc-500">
|
||||
This language will be pre-selected when translating documents
|
||||
<p className="text-sm text-text-tertiary">
|
||||
Need help with settings? Check our documentation.
|
||||
</p>
|
||||
<Button variant="glass" size="sm" className="group">
|
||||
<Settings className="h-4 w-4 mr-2 transition-transform duration-200 group-hover:rotate-90" />
|
||||
View Documentation
|
||||
<ArrowRight className="h-4 w-4 ml-2 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Supported Formats */}
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-6 w-6 text-teal-400" />
|
||||
<div>
|
||||
<CardTitle className="text-white">Supported Formats</CardTitle>
|
||||
<CardDescription>
|
||||
Document types that can be translated
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleClearCache}
|
||||
disabled={isClearing}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-destructive/50 text-destructive hover:bg-destructive/10 hover:border-destructive group"
|
||||
>
|
||||
{isClearing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Clearing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
|
||||
Clear Cache
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 text-white group"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4 transition-transform duration-200 group-hover:scale-110" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-lg border border-zinc-800 bg-zinc-800/30">
|
||||
<div className="text-2xl mb-2">📊</div>
|
||||
<h3 className="font-medium text-white">Excel</h3>
|
||||
<p className="text-xs text-zinc-500 mt-1">.xlsx, .xls</p>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Formulas
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Styles
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg border border-zinc-800 bg-zinc-800/30">
|
||||
<div className="text-2xl mb-2">📝</div>
|
||||
<h3 className="font-medium text-white">Word</h3>
|
||||
<p className="text-xs text-zinc-500 mt-1">.docx, .doc</p>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Headers
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Tables
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg border border-zinc-800 bg-zinc-800/30">
|
||||
<div className="text-2xl mb-2">📽️</div>
|
||||
<h3 className="font-medium text-white">PowerPoint</h3>
|
||||
<p className="text-xs text-zinc-500 mt-1">.pptx, .ppt</p>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Slides
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Notes
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-zinc-700 text-zinc-400 text-xs">
|
||||
Images
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* API Status */}
|
||||
<Card className="border-zinc-800 bg-zinc-900/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">API Information</CardTitle>
|
||||
<CardDescription>
|
||||
Backend server connection details
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50">
|
||||
<span className="text-zinc-400">API Endpoint</span>
|
||||
<code className="text-teal-400 text-sm">http://localhost:8000</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50">
|
||||
<span className="text-zinc-400">Health Check</span>
|
||||
<code className="text-teal-400 text-sm">/health</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50">
|
||||
<span className="text-zinc-400">Translate Endpoint</span>
|
||||
<code className="text-teal-400 text-sm">/translate</code>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
onClick={handleClearCache}
|
||||
disabled={isClearing}
|
||||
variant="destructive"
|
||||
className="bg-red-600 hover:bg-red-700 text-white px-6"
|
||||
>
|
||||
{isClearing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Clearing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Clear Cache
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white px-8"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user