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:
2025-12-31 10:43:31 +01:00
parent 721b18dbbd
commit c4d6cae735
27 changed files with 7824 additions and 2181 deletions

View File

@@ -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>
);
}