mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card, CardContent, CardDescription,
|
||||
@@ -17,15 +17,16 @@ import ResponsiveModal from "@/components/generic/ResponsiveModal";
|
||||
import { toast } from "sonner";
|
||||
import api from "@/utils/api.util";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatPrice(price, currency = "USD") {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
}).format(price);
|
||||
const REFUND_WINDOW_SECS = 5 * 60;
|
||||
|
||||
function formatCountdown(secs) {
|
||||
const m = Math.floor(secs / 60);
|
||||
const s = secs % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatDuration(days) {
|
||||
@@ -93,7 +94,8 @@ const PlanSkeleton = () => (
|
||||
|
||||
// ─── Plan Card ────────────────────────────────────────────────────────────────
|
||||
|
||||
const PlanCard = ({ plan, myTier, onSelect, onView, onRefund }) => {
|
||||
const PlanCard = ({ plan, myTier, onSelect, onView, onRefund, refundSecsLeft }) => {
|
||||
const { fmtCurrency } = useDateFormat();
|
||||
const style = TIER_STYLES[plan.tier] ?? TIER_STYLES.free;
|
||||
const Icon = style.icon;
|
||||
const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active";
|
||||
@@ -116,7 +118,7 @@ const PlanCard = ({ plan, myTier, onSelect, onView, onRefund }) => {
|
||||
</div>
|
||||
<CardDescription>
|
||||
<span className="text-3xl font-bold text-foreground">
|
||||
{formatPrice(plan.price, plan.currency)}
|
||||
{fmtCurrency(plan.price, plan.currency)}
|
||||
</span>
|
||||
{duration && (
|
||||
<span className="text-sm text-muted-foreground ml-1">/ {duration}</span>
|
||||
@@ -170,15 +172,16 @@ const PlanCard = ({ plan, myTier, onSelect, onView, onRefund }) => {
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
{isCurrent ? (
|
||||
{isCurrent && refundSecsLeft > 0 ? (
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="destructive"
|
||||
onClick={() => onRefund(plan)}
|
||||
>
|
||||
<RotateCcw className="size-4" /> Refund
|
||||
<RotateCcw className="size-4" />
|
||||
Refund ({formatCountdown(refundSecsLeft)})
|
||||
</Button>
|
||||
) : (
|
||||
) : !isCurrent ? (
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant={style.button}
|
||||
@@ -186,7 +189,7 @@ const PlanCard = ({ plan, myTier, onSelect, onView, onRefund }) => {
|
||||
>
|
||||
{plan.tier === "free" ? "Current" : `Get ${style.label}`}
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
@@ -197,15 +200,34 @@ const PlanCard = ({ plan, myTier, onSelect, onView, onRefund }) => {
|
||||
export default function PlanList() {
|
||||
const navigate = useNavigate();
|
||||
const { plans, plansLoading, myTier, tierLoading, getPlans, getMyTier, resetMyTier } = useClientTiers();
|
||||
const { fmtCurrency, fmtDate } = useDateFormat();
|
||||
|
||||
const [refundPlan, setRefundPlan] = useState(null); // plan being refunded
|
||||
const [refundLoading, setRefundLoading] = useState(false);
|
||||
const [refundSecsLeft, setRefundSecsLeft] = useState(0);
|
||||
const refundTimerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
getPlans();
|
||||
getMyTier();
|
||||
}, [getPlans, getMyTier]);
|
||||
|
||||
useEffect(() => {
|
||||
clearInterval(refundTimerRef.current);
|
||||
if (!myTier?.starts_at) { setRefundSecsLeft(0); return; }
|
||||
const compute = () => {
|
||||
const elapsed = Math.floor((Date.now() - new Date(myTier.starts_at).getTime()) / 1000);
|
||||
return Math.max(0, REFUND_WINDOW_SECS - elapsed);
|
||||
};
|
||||
setRefundSecsLeft(compute());
|
||||
refundTimerRef.current = setInterval(() => {
|
||||
const left = compute();
|
||||
setRefundSecsLeft(left);
|
||||
if (left === 0) clearInterval(refundTimerRef.current);
|
||||
}, 1000);
|
||||
return () => clearInterval(refundTimerRef.current);
|
||||
}, [myTier?.starts_at]);
|
||||
|
||||
const handleViewPlan = (plan) => navigate(`/plans/view/${plan.plan_id}`);
|
||||
|
||||
const handleSelectPlan = (plan) => navigate(`/plans/checkout?plan_id=${plan.plan_id}`);
|
||||
@@ -216,7 +238,7 @@ export default function PlanList() {
|
||||
setRefundLoading(true);
|
||||
try {
|
||||
const { data } = await api.post("/client/tiers/checkout/refund");
|
||||
toast.success(data.message ?? "Refund processed. Access remains until end of billing period.");
|
||||
toast.success(data.message ?? "Refund processed. Your access has been revoked.");
|
||||
setRefundPlan(null);
|
||||
resetMyTier();
|
||||
getMyTier();
|
||||
@@ -234,7 +256,7 @@ export default function PlanList() {
|
||||
<div className="lg:container lg:mx-auto space-y-8 p-6">
|
||||
|
||||
{/* Advertisement Banner */}
|
||||
<Card className="overflow-hidden border-primary/20 bg-gradient-to-r from-primary/10 via-primary/5 to-background">
|
||||
{/* <Card className="overflow-hidden border-primary/20 bg-gradient-to-r from-primary/10 via-primary/5 to-background">
|
||||
<CardContent className="flex flex-col gap-4 py-20 md:flex-row md:items-center md:justify-between pl-10">
|
||||
<div className="space-y-2">
|
||||
<Badge>
|
||||
@@ -251,10 +273,10 @@ export default function PlanList() {
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card> */}
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="text-center">
|
||||
<div className="text-center mt-6">
|
||||
<h2 className="text-3xl font-bold">Available Plans</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Choose a subscription that matches your goals.
|
||||
@@ -279,6 +301,7 @@ export default function PlanList() {
|
||||
onSelect={handleSelectPlan}
|
||||
onView={handleViewPlan}
|
||||
onRefund={handleRefundClick}
|
||||
refundSecsLeft={refundSecsLeft}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -305,7 +328,7 @@ export default function PlanList() {
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleConfirmRefund}
|
||||
disabled={refundLoading}
|
||||
disabled={refundLoading || refundSecsLeft === 0}
|
||||
>
|
||||
<RotateCcw className="size-4" />
|
||||
{refundLoading ? "Processing..." : "Confirm Refund"}
|
||||
@@ -322,30 +345,41 @@ export default function PlanList() {
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Refund amount</span>
|
||||
<span className="font-medium">
|
||||
{refundPlan ? formatPrice(refundPlan.price, refundPlan.currency) : "—"}
|
||||
{refundPlan ? fmtCurrency(refundPlan.price, refundPlan.currency) : "—"}
|
||||
</span>
|
||||
</div>
|
||||
{myTier?.expires_at && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Access until</span>
|
||||
<span className="font-medium">
|
||||
{new Date(myTier.expires_at).toLocaleDateString("en-US", {
|
||||
month: "long", day: "numeric", year: "numeric",
|
||||
})}
|
||||
{fmtDate(myTier.expires_at)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center pt-1 border-t">
|
||||
<span className="text-muted-foreground">Refund window</span>
|
||||
{refundSecsLeft > 0 ? (
|
||||
<span className="font-semibold tabular-nums text-destructive">
|
||||
{formatCountdown(refundSecsLeft)} remaining
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-semibold text-muted-foreground">Expired</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your refund will be processed through PayPal. You will retain access to your current plan until{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{myTier?.expires_at
|
||||
? new Date(myTier.expires_at).toLocaleDateString("en-US", {
|
||||
month: "long", day: "numeric", year: "numeric",
|
||||
})
|
||||
: "the end of the billing period"}
|
||||
</span>.
|
||||
</p>
|
||||
{refundSecsLeft > 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your refund will be processed through PayPal.{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
Access will be revoked immediately
|
||||
</span>{" "}
|
||||
and your account will be downgraded to Free.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-destructive">
|
||||
The 5-minute refund window has expired. Refunds are no longer available for this payment.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ResponsiveModal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user