Prmbr/src/app/pricing/page.tsx
2025-08-05 23:29:32 +08:00

256 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useTranslations } from 'next-intl'
import { useAuth } from '@/hooks/useAuth'
import { useUser } from '@/hooks/useUser'
import { Header } from '@/components/layout/Header'
import { Button } from '@/components/ui/button'
import { Check, Crown, Star } from 'lucide-react'
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
import { useEffect, useState } from 'react'
import type { SubscriptionPlan } from '@prisma/client'
import {
isPlanPro,
isPlanFree,
getPlanLimits,
getPlanFeatures,
formatPlanPrice,
getPlanTheme
} from '@/lib/subscription-utils'
export default function PricingPage() {
const { user } = useAuth()
const { userData } = useUser()
const t = useTranslations('pricing')
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
const [loading, setLoading] = useState(true)
const fetchPlans = async () => {
try {
const response = await fetch('/api/subscription-plans')
if (response.ok) {
const data = await response.json()
// 过滤套餐:只显示真正的免费套餐、用户当前套餐,以及有 stripePriceId 的付费套餐
const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => {
// 只显示官方的免费套餐ID为'free'或名称为'free'
if (isPlanFree(plan) && (plan.id === 'free' || plan.name.toLowerCase() === 'free')) {
return true
}
// 用户当前套餐总是显示(即使是异常套餐)
if (userData && isCurrentPlan(plan.id)) return true
// 付费套餐必须有 stripePriceId 才能显示(可订阅)
if (!isPlanFree(plan) && plan.stripePriceId && plan.stripePriceId.trim() !== '') {
return true
}
return false
})
setPlans(filteredPlans)
}
} catch (error) {
console.error('Error fetching plans:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchPlans()
}, [userData]) // 依赖 userData确保用户数据加载后再过滤套餐
const isCurrentPlan = (planId: string) => {
if (!userData) return false
return userData.subscriptionPlanId === planId
}
if (loading) {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto"></div>
<p className="mt-4 text-muted-foreground">Loading pricing plans...</p>
</div>
</main>
</div>
)
}
return (
<div className="min-h-screen bg-background">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
{/* Header */}
<div className="text-center mb-16">
<h1 className="text-4xl font-bold text-foreground mb-4">
{t('title')}
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
{t('subtitle')}
</p>
</div>
{/* Pricing Cards */}
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{plans.map((plan) => {
const limits = getPlanLimits(plan)
const features = getPlanFeatures(plan)
const isFree = isPlanFree(plan)
const isPro = isPlanPro(plan)
const isCurrent = isCurrentPlan(plan.id)
const theme = getPlanTheme(plan)
return (
<div
key={plan.id}
className={`bg-card p-8 rounded-lg shadow-sm border relative ${
isPro ? `${theme.borderColor} shadow-lg` : ''
}`}
>
{isCurrent && (
<div className="absolute top-4 right-4 bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300 px-3 py-1 rounded-full text-xs font-semibold">
{t('currentPlan')}
</div>
)}
{isPro && (
<div className="absolute top-4 left-4 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-semibold">
{t('popular')}
</div>
)}
<div className="text-center mb-6">
<div className="flex items-center justify-center mb-4">
<div className={`p-3 rounded-full ${theme.iconGradient}`}>
{isPro ? (
<Crown className="w-6 h-6 text-white" />
) : (
<Star className="w-6 h-6 text-white" />
)}
</div>
</div>
<h3 className="text-2xl font-bold text-card-foreground mb-2">
{plan.displayName}
</h3>
<div className="text-4xl font-bold text-card-foreground mb-2">
{formatPlanPrice(plan, t)}
</div>
<p className="text-muted-foreground">
{plan.description}
</p>
</div>
<ul className="space-y-3 mb-8">
<li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">
{limits.promptLimit} Prompt Limit
</span>
</li>
<li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">
{limits.maxVersionLimit} Versions per Prompt
</span>
</li>
{features.includes('prioritySupport') && (
<li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">Priority Support</span>
</li>
)}
{features.includes('advancedAnalytics') && (
<li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">Advanced Analytics</span>
</li>
)}
{features.includes('apiAccess') && (
<li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">API Access</span>
</li>
)}
</ul>
{(() => {
// 免费套餐逻辑
if (isFree) {
// 如果是当前套餐,显示"当前套餐"按钮
if (isCurrent) {
return (
<Button
className="w-full"
variant="outline"
disabled
>
{t('currentPlan')}
</Button>
)
}
// 免费套餐且非当前套餐,不显示按钮
return null
}
// 付费套餐逻辑
if (isCurrent) {
// 当前套餐,显示"当前套餐"按钮
return (
<Button
className="w-full"
variant="outline"
disabled
>
{t('currentPlan')}
</Button>
)
}
// 可订阅的付费套餐,显示"立即订阅"按钮
if (plan.stripePriceId && plan.stripePriceId.trim() !== '') {
return (
<SubscribeButton
priceId={plan.stripePriceId}
planName={plan.displayName}
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
>
{t('subscribeNow')}
</SubscribeButton>
)
}
// 没有 stripePriceId 的套餐不显示按钮(理论上不会到这里,因为已经过滤了)
return null
})()}
</div>
)
})}
</div>
{/* Additional Info */}
{user && (
<div className="text-center mt-12">
<p className="text-muted-foreground mb-4">
Need to manage your subscription?
</p>
<Button
variant="outline"
onClick={() => window.location.href = '/subscription'}
>
{t('manageSubscription')}
</Button>
</div>
)}
</main>
</div>
)
}