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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user