forked from 77media/video-flow
372 lines
13 KiB
TypeScript
372 lines
13 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useMemo } from "react";
|
||
import { useRouter, useSearchParams } 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";
|
||
import { DashboardLayout } from "@/components/layout/dashboard-layout";
|
||
import { trackEvent, trackPageView, trackPayment } from "@/utils/analytics";
|
||
|
||
export default function PricingPage() {
|
||
// 页面访问跟踪
|
||
useEffect(() => {
|
||
trackPageView('/pricing', 'Pricing Plans');
|
||
}, []);
|
||
|
||
return (
|
||
<DashboardLayout>
|
||
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]" id="pricing-page">
|
||
{/* Main Content */}
|
||
<HomeModule5 />
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
}
|
||
/**价格方案 */
|
||
function HomeModule5() {
|
||
const [billingType, setBillingType] = useState<"month" | "year">("year");
|
||
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
|
||
const [loadingPlan, setLoadingPlan] = useState<string | null>(null); // 跟踪哪个计划正在加载
|
||
|
||
// 监听支付完成消息
|
||
useEffect(() => {
|
||
const handlePaymentResult = (ev: MessageEvent) => {
|
||
if (ev.data.type === 'payment-result') {
|
||
// 支付完成后清除loading状态
|
||
setLoadingPlan(null);
|
||
}
|
||
};
|
||
|
||
window.addEventListener('message', handlePaymentResult);
|
||
return () => window.removeEventListener('message', handlePaymentResult);
|
||
}, []);
|
||
|
||
// 从后端获取订阅计划数据
|
||
useEffect(() => {
|
||
const loadPlans = async () => {
|
||
try {
|
||
const plansData = await fetchSubscriptionPlans();
|
||
setPlans(plansData);
|
||
} catch (err) {
|
||
console.error("加载订阅计划失败:", err);
|
||
}
|
||
};
|
||
|
||
loadPlans();
|
||
}, []);
|
||
|
||
const pricingPlans = useMemo<
|
||
{
|
||
title: string;
|
||
price: number;
|
||
originalPrice: number;
|
||
monthlyPrice: number;
|
||
discountMsg: string;
|
||
credits: string;
|
||
buttonText: string;
|
||
features: string[];
|
||
issubscribed: boolean;
|
||
}[]
|
||
>(() => {
|
||
return plans.map((plan) => {
|
||
return {
|
||
title: plan.display_name || plan.name,
|
||
price:
|
||
billingType === "month"
|
||
? plan.price_month / 100
|
||
: plan.price_year / 100,
|
||
originalPrice: plan.price_month / 100,
|
||
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100,
|
||
discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`,
|
||
credits: plan.description,
|
||
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
|
||
issubscribed: plan.is_subscribed,
|
||
features: plan.features || [],
|
||
};
|
||
});
|
||
}, [plans, billingType]);
|
||
|
||
const handleSubscribe = async (planName: string) => {
|
||
setLoadingPlan(planName);
|
||
|
||
// 跟踪订阅按钮点击事件
|
||
trackEvent('subscription_button_click', {
|
||
event_category: 'subscription',
|
||
event_label: planName,
|
||
custom_parameters: {
|
||
plan_name: planName,
|
||
billing_type: billingType,
|
||
},
|
||
});
|
||
|
||
// 改为直接携带参数打开 pay-redirect,由其内部完成创建与跳转
|
||
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
|
||
const win = window.open(url, '_blank');
|
||
// 通知当前窗口等待支付(显示loading模态框)
|
||
window.postMessage({
|
||
type: 'waiting-payment',
|
||
paymentType: 'subscription',
|
||
}, '*');
|
||
if (!win) {
|
||
setLoadingPlan(null);
|
||
throw new Error('Unable to open redirect window, please check popup settings');
|
||
}
|
||
};
|
||
return (
|
||
<div
|
||
data-alt="core-value-section"
|
||
className="home-module5 relative flex flex-col items-center justify-center w-full bg-black snap-start
|
||
min-h-[100vh] py-8
|
||
sm:min-h-[100vh] sm:py-12
|
||
md:h-[1000px] md:py-16
|
||
lg:h-[1100px] lg:py-20
|
||
xl:h-[1200px] xl:py-24
|
||
2xl:h-[1500px] 2xl:py-32"
|
||
>
|
||
<div
|
||
data-alt="core-value-content"
|
||
className="center z-10 flex flex-col items-center
|
||
mb-8 px-4
|
||
sm:mb-12 sm:px-6
|
||
md:mb-[4rem] md:px-8
|
||
lg:mb-[4rem] lg:px-12
|
||
xl:mb-[4rem] xl:px-16
|
||
2xl:mb-[4rem] 2xl:px-20"
|
||
>
|
||
<h2
|
||
className="text-white font-normal text-center
|
||
text-[1.5rem] leading-[110%] mb-4
|
||
sm:text-[2rem] sm:leading-[110%] sm:mb-6
|
||
md:text-[2.5rem] md:leading-[110%] md:mb-[1.5rem]
|
||
lg:text-[3rem] lg:leading-[110%] lg:mb-[1.5rem]
|
||
xl:text-[3.375rem] xl:leading-[110%] xl:mb-[1.5rem]
|
||
2xl:text-[3.5rem] 2xl:leading-[110%] 2xl:mb-[1.5rem]"
|
||
>
|
||
Pick a plan and make it yours
|
||
</h2>
|
||
|
||
{/* 计费切换 */}
|
||
<div
|
||
className="flex bg-black rounded-full border border-white/20
|
||
h-[2.5rem] p-[0.0625rem] mt-4
|
||
sm:h-[3rem] sm:mt-6
|
||
md:h-[3.375rem] md:mt-[1.5rem]
|
||
lg:h-[3.375rem] lg:mt-[1.5rem]
|
||
xl:h-[3.375rem] xl:mt-[1.5rem]
|
||
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
|
||
>
|
||
<button
|
||
onClick={() => {
|
||
setBillingType("month");
|
||
trackEvent('billing_toggle', {
|
||
event_category: 'subscription',
|
||
event_label: 'month',
|
||
custom_parameters: { billing_type: 'month' },
|
||
});
|
||
}}
|
||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||
billingType === "month"
|
||
? "bg-white text-black"
|
||
: "text-white hover:text-gray-300"
|
||
}
|
||
w-[4.5rem] text-sm
|
||
sm:w-[5rem] sm:text-sm
|
||
md:w-[6rem] md:text-base
|
||
lg:w-[6rem] lg:text-base
|
||
xl:w-[6rem] xl:text-base
|
||
2xl:w-[6rem] 2xl:text-base`}
|
||
>
|
||
Monthly
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setBillingType("year");
|
||
trackEvent('billing_toggle', {
|
||
event_category: 'subscription',
|
||
event_label: 'year',
|
||
custom_parameters: { billing_type: 'year' },
|
||
});
|
||
}}
|
||
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
|
||
billingType === "year"
|
||
? "bg-white text-black"
|
||
: "text-white hover:text-gray-300"
|
||
}
|
||
w-[5.5rem] text-sm
|
||
sm:w-[6rem] sm:text-sm
|
||
md:w-[7.125rem] md:text-base
|
||
lg:w-[7.125rem] lg:text-base
|
||
xl:w-[7.125rem] xl:text-base
|
||
2xl:w-[7.125rem] 2xl:text-base`}
|
||
>
|
||
Yearly
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主要价格卡片 */}
|
||
<div
|
||
className="w-full max-w-[95%] mx-auto px-4
|
||
grid grid-cols-1 gap-4
|
||
sm:grid-cols-2 sm:gap-6 sm:px-6
|
||
md:grid-cols-3 md:gap-8 md:px-8
|
||
lg:gap-10 lg:px-12
|
||
xl:gap-12 xl:px-16
|
||
2xl:gap-16 2xl:px-20"
|
||
>
|
||
{pricingPlans.map((plan, index) => (
|
||
<div
|
||
key={index}
|
||
className="bg-black rounded-2xl border border-white/20
|
||
p-4 min-h-[28rem]
|
||
sm:p-5 sm:min-h-[32rem]
|
||
md:p-6 md:min-h-[36rem]
|
||
lg:p-[1.375rem] lg:min-h-[37rem]
|
||
xl:p-[1.5rem] xl:min-h-[38.125rem]
|
||
2xl:p-[1.75rem] 2xl:min-h-[40rem]"
|
||
>
|
||
<h3
|
||
className="text-white font-normal
|
||
text-lg mb-3
|
||
sm:text-xl sm:mb-4
|
||
md:text-xl md:mb-4
|
||
lg:text-2xl lg:mb-[1rem]
|
||
xl:text-2xl xl:mb-[1rem]
|
||
2xl:text-3xl 2xl:mb-[1.25rem]"
|
||
>
|
||
{plan.title}
|
||
</h3>
|
||
<div
|
||
className="mb-3
|
||
sm:mb-4
|
||
md:mb-4
|
||
lg:mb-[1rem]
|
||
xl:mb-[1rem]
|
||
2xl:mb-[1.25rem]"
|
||
>
|
||
<div className="flex items-baseline">
|
||
<span
|
||
className="text-white font-bold
|
||
text-2xl
|
||
sm:text-3xl
|
||
md:text-[2.5rem]
|
||
lg:text-[3rem]
|
||
xl:text-[3.375rem]
|
||
2xl:text-[3.75rem]"
|
||
>
|
||
${plan.monthlyPrice || plan.price}
|
||
</span>
|
||
<span
|
||
className="text-white ml-2 whitespace-nowrap
|
||
text-xs
|
||
sm:text-xs
|
||
md:text-xs
|
||
lg:text-xs
|
||
xl:text-xs
|
||
2xl:text-sm"
|
||
>
|
||
/ month
|
||
</span>
|
||
</div>
|
||
{plan.originalPrice !== plan.price ? (<div className="pt-2 text-white text-sm line-through">
|
||
${plan.originalPrice}
|
||
</div>) : null}
|
||
</div>
|
||
<p
|
||
className="text-white mb-4
|
||
text-sm
|
||
sm:text-sm
|
||
md:text-[0.875rem]
|
||
lg:text-[0.875rem]
|
||
xl:text-[0.875rem]
|
||
2xl:text-base"
|
||
>
|
||
{plan.credits}
|
||
</p>
|
||
{plan.issubscribed ? (
|
||
<button
|
||
disabled
|
||
className="w-full bg-gray-400 text-gray-600 rounded-full cursor-not-allowed border border-gray-300
|
||
py-2 mb-4 text-sm
|
||
sm:py-3 sm:mb-4 sm:text-base
|
||
md:py-[0.75rem] md:mb-[1rem] md:text-base
|
||
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
|
||
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
|
||
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
|
||
>
|
||
Already Owned
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={() => handleSubscribe(plan.title)}
|
||
className="w-full bg-white text-black rounded-full hover:bg-black hover:text-white transition-colors border border-white/20
|
||
py-2 mb-4 text-sm
|
||
sm:py-3 sm:mb-4 sm:text-base
|
||
md:py-[0.75rem] md:mb-[1rem] md:text-base
|
||
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
|
||
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
|
||
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
|
||
>
|
||
{plan.buttonText}
|
||
</button>
|
||
)}
|
||
<p
|
||
className="w-full text-center text-white/60 mb-4
|
||
text-xs
|
||
sm:text-xs
|
||
md:text-[0.75rem] md:mb-[2rem]
|
||
lg:text-[0.75rem] lg:mb-[2rem]
|
||
xl:text-[0.75rem] xl:mb-[2rem]
|
||
2xl:text-sm 2xl:mb-[2.5rem]"
|
||
>
|
||
* {plan.discountMsg}
|
||
</p>
|
||
<ul
|
||
className="space-y-2
|
||
sm:space-y-3
|
||
md:space-y-[1rem]
|
||
lg:space-y-[1rem]
|
||
xl:space-y-[1rem]
|
||
2xl:space-y-[1.25rem]"
|
||
>
|
||
{plan.features.map((feature, featureIndex) => (
|
||
<li
|
||
key={featureIndex}
|
||
className="flex items-center text-white
|
||
text-sm
|
||
sm:text-sm
|
||
md:text-[0.875rem]
|
||
lg:text-[0.875rem]
|
||
xl:text-[0.875rem]
|
||
2xl:text-base"
|
||
>
|
||
<span
|
||
className="text-[#C73BFF] mr-2
|
||
text-sm
|
||
sm:mr-2 sm:text-base
|
||
md:mr-[0.5rem] md:text-base
|
||
lg:mr-[0.5rem] lg:text-base
|
||
xl:mr-[0.5rem] xl:text-base
|
||
2xl:mr-[0.625rem] 2xl:text-lg"
|
||
>
|
||
✓
|
||
</span>
|
||
{feature}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|