forked from 77media/video-flow
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { Check, ArrowLeft } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe';
|
||
|
||
export default function PricingPage() {
|
||
const router = useRouter();
|
||
const [billingCycle, setBillingCycle] = useState<'month' | 'year'>('month');
|
||
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||
|
||
// 从后端获取订阅计划数据
|
||
useEffect(() => {
|
||
const loadPlans = async () => {
|
||
try {
|
||
const plansData = await fetchSubscriptionPlans();
|
||
setPlans(plansData);
|
||
} catch (err) {
|
||
console.error('加载订阅计划失败:', err);
|
||
}
|
||
};
|
||
|
||
loadPlans();
|
||
}, []);
|
||
|
||
// 转换后端数据为前端显示格式,保持原有的数据结构
|
||
const transformPlanForDisplay = (plan: SubscriptionPlan) => {
|
||
const monthlyPrice = plan.price_month / 100; // 后端存储的是分,转换为元
|
||
const yearlyPrice = plan.price_year / 100;
|
||
|
||
return {
|
||
name: plan.name,
|
||
displayName: plan.display_name,
|
||
price: {
|
||
month: monthlyPrice,
|
||
year: yearlyPrice
|
||
},
|
||
description: plan.description,
|
||
features: plan.features || [],
|
||
popular: plan.is_popular, // 使用后端返回的 is_popular 字段
|
||
buttonText: plan.is_free ? 'Start Free Trial' : 'Subscribe',
|
||
buttonVariant: plan.is_free ? 'outline' as const : 'default' as const
|
||
};
|
||
};
|
||
|
||
const handleSubscribe = async (planName: string) => {
|
||
if (planName === 'hobby') {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 使用新的Checkout Session方案(更简单!)
|
||
const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe');
|
||
|
||
// 从localStorage获取当前用户信息
|
||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||
|
||
if (!User.id) {
|
||
throw new Error("无法获取用户ID,请重新登录");
|
||
}
|
||
|
||
// 1. 创建Checkout Session
|
||
const result = await createCheckoutSession({
|
||
user_id: String(User.id),
|
||
plan_name: planName,
|
||
billing_cycle: billingCycle
|
||
});
|
||
|
||
if (!result.successful || !result.data) {
|
||
throw new Error("create checkout session failed");
|
||
}
|
||
|
||
// 2. 直接跳转到Stripe托管页面(就这么简单!)
|
||
redirectToCheckout(result.data.checkout_url);
|
||
|
||
} catch (error) {
|
||
throw new Error("create checkout session failed, please try again later");
|
||
}
|
||
};
|
||
|
||
// 如果还没有加载到数据,显示加载状态但保持原有样式
|
||
if (plans.length === 0) {
|
||
return (
|
||
<div className="min-h-screen bg-black text-white">
|
||
<main className="container mx-auto px-6 py-16">
|
||
<div className="mb-8">
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => router.back()}
|
||
className="text-gray-400 hover:text-white"
|
||
>
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
Back
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="text-center mb-16">
|
||
<h1 className="text-6xl font-bold mb-6">Pricing</h1>
|
||
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p>
|
||
|
||
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1">
|
||
<button
|
||
onClick={() => setBillingCycle('year')}
|
||
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
|
||
billingCycle === 'year'
|
||
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
|
||
: 'text-gray-300 hover:text-white'
|
||
}`}
|
||
>
|
||
Pay Yearly
|
||
<span className={`ml-2 text-sm ${
|
||
billingCycle === 'year'
|
||
? 'text-white/90'
|
||
: 'text-gray-400'
|
||
}`}>
|
||
Up to 20% off 🔥
|
||
</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setBillingCycle('month')}
|
||
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
|
||
billingCycle === 'month'
|
||
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
|
||
: 'text-gray-300 hover:text-white'
|
||
}`}
|
||
>
|
||
Pay Monthly
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-16">
|
||
<div className="text-center text-gray-400">
|
||
<p>正在加载订阅计划...</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-black text-white">
|
||
{/* Main Content */}
|
||
<main className="container mx-auto px-6 py-16">
|
||
{/* Back Button */}
|
||
<div className="mb-8">
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => router.back()}
|
||
className="text-gray-400 hover:text-white"
|
||
>
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
Back
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="text-center mb-16">
|
||
<h1 className="text-6xl font-bold mb-6">Pricing</h1>
|
||
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p>
|
||
|
||
{/* Billing Toggle */}
|
||
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1">
|
||
<button
|
||
onClick={() => setBillingCycle('year')}
|
||
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
|
||
billingCycle === 'year'
|
||
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
|
||
: 'text-gray-300 hover:text-white'
|
||
}`}
|
||
>
|
||
Pay Yearly
|
||
<span className={`ml-2 text-sm ${
|
||
billingCycle === 'year'
|
||
? 'text-white/90'
|
||
: 'text-gray-400'
|
||
}`}>
|
||
Up to 20% off 🔥
|
||
</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setBillingCycle('month')}
|
||
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
|
||
billingCycle === 'month'
|
||
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
|
||
: 'text-gray-300 hover:text-white'
|
||
}`}
|
||
>
|
||
Pay Monthly
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Plans Section */}
|
||
<div className="mb-16">
|
||
<div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
||
{plans.map((plan) => {
|
||
const displayPlan = transformPlanForDisplay(plan);
|
||
|
||
return (
|
||
<div
|
||
key={plan.id}
|
||
className={`flex flex-col h-full transition-all duration-300 ${
|
||
displayPlan.popular ? 'hover:scale-105 hover:shadow-2xl' : ''
|
||
}`}
|
||
style={{ minHeight: '540px' }}
|
||
>
|
||
{/* 预留标签空间,确保所有卡片组合高度一致 */}
|
||
<div className="h-10 flex items-center justify-center">
|
||
{displayPlan.popular && (
|
||
<div className="bg-gradient-to-r from-pink-500 to-purple-600 text-white py-2 text-center text-sm font-medium rounded-t-2xl w-full shadow-lg">
|
||
Most Popular
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Card
|
||
className={`bg-gray-900/50 ${displayPlan.popular ? 'border-l border-r border-b border-gray-700/50 rounded-b-2xl rounded-t-none' : 'border border-gray-700/50 rounded-2xl'} overflow-hidden ${displayPlan.popular ? '' : 'transition-all duration-300 hover:scale-105 hover:shadow-2xl'} flex flex-col flex-grow ${
|
||
displayPlan.popular ? 'ring-2 ring-pink-500/50 shadow-pink-500/20' : ''
|
||
}`}
|
||
>
|
||
|
||
<CardHeader className="pt-8 pb-6 flex-shrink-0 text-center">
|
||
<CardTitle className="text-white text-2xl font-bold mb-4">{displayPlan.displayName}</CardTitle>
|
||
|
||
<div className="mb-4">
|
||
<div className="text-4xl font-bold text-white">
|
||
{displayPlan.price[billingCycle] === 0 ? (
|
||
'Free'
|
||
) : (
|
||
<>
|
||
<span className="text-5xl">${displayPlan.price[billingCycle]}</span>
|
||
<span className="text-lg text-gray-400 font-normal">
|
||
/{billingCycle === 'month' ? 'month' : 'year'}
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<CardDescription className="text-gray-300 text-base px-4">
|
||
{displayPlan.description}
|
||
</CardDescription>
|
||
</CardHeader>
|
||
|
||
<CardContent className="space-y-8 pb-10 flex-grow flex flex-col justify-between">
|
||
<div className="space-y-5">
|
||
{displayPlan.features.map((feature, index) => (
|
||
<div key={index} className="flex items-start space-x-3">
|
||
<div className="w-5 h-5 bg-green-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<Check className="w-3 h-3 text-green-400" />
|
||
</div>
|
||
<span className="text-gray-300 text-sm leading-relaxed">{feature}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<Button
|
||
onClick={() => handleSubscribe(displayPlan.name)}
|
||
variant={displayPlan.buttonVariant}
|
||
className={`w-full py-4 rounded-xl font-medium text-base transition-all duration-300 mt-6 ${
|
||
displayPlan.buttonText === 'Start Free Trial'
|
||
? 'border-2 border-gray-600 text-white hover:border-pink-500 hover:text-pink-400 hover:bg-gray-800/50'
|
||
: 'bg-gradient-to-r from-pink-500 to-purple-600 text-white hover:from-pink-600 hover:to-purple-700 shadow-lg hover:shadow-xl'
|
||
}`}
|
||
>
|
||
{displayPlan.buttonText}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|