import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import * as LucideIcons from "lucide-react";
import {
Card, CardContent, CardDescription,
CardFooter, CardHeader, CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import {
BookOpen, Clock, Check,
Tag, RotateCcw, LaptopMinimal, Table as TableIcon,
} from "lucide-react";
import { useClientTiers } from "@/contexts/ClientTiersProvider";
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";
import { useClientAdvertisements } from "@/contexts/ClientAdvertisementContext";
import { Banner, BannerSkeleton } from "@/components/generic/Blocks/Client/Advertisements/Banner";
import { resolveTierBadge } from "@/utils/tierBadge.util";
import PlanComparisonTable from "../components/PlanComparisonTable";
// ─── Helpers ──────────────────────────────────────────────────────────────────
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, unit) {
if (!days) return null;
const UNIT_TO_DAYS = { minute: 1 / 1440, hour: 1 / 24, day: 1, month: 30, year: 365 };
const multiplier = UNIT_TO_DAYS[unit] ?? 1;
const value = Math.round((days / multiplier) * 1000) / 1000;
const label = unit ?? "day";
return `${value} ${label}${value !== 1 ? "s" : ""}`;
}
function formatCourseDuration(seconds = 0) {
if (!seconds) return null;
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h && m) return `${h}h ${m}m`;
if (h) return `${h}h`;
return `${m}m`;
}
// ─── Skeleton ──────────────────────────────────────────────────────────────────
const PlanSkeleton = () => (
{Array.from({ length: 3 }).map((_, i) => (
))}
);
// ─── Plan Card ────────────────────────────────────────────────────────────────
const PREVIEW_COURSE_LIMIT = 2;
const PlanCard = ({ plan, myTier, tierMap, onSelect, onView, onRefund, refundSecsLeft }) => {
const { fmtCurrency } = useDateFormat();
const [coursesOpen, setCoursesOpen] = useState(false);
const [notAvailableOpen, setNotAvailableOpen] = useState(false);
const { label: tierLabel, cls: badgeCls, rank } = resolveTierBadge(plan.tier, tierMap);
const Icon = LucideIcons[tierMap[plan.tier]?.badge_icon] ?? Tag;
const ring = rank > 0 ? "ring-2 ring-primary/30" : "";
const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active";
const duration = formatDuration(plan.duration_days, plan.duration_unit);
const features = plan.features ?? [];
const previewCourses = plan.courses?.slice(0, PREVIEW_COURSE_LIMIT) ?? [];
const extraCount = (plan.courses?.length ?? 0) - PREVIEW_COURSE_LIMIT;
return (
<>
{plan.label}
{isCurrent && (
Current Plan
)}
{tierLabel}
{fmtCurrency(plan.price, plan.currency)}
{duration && (
/ {duration}
)}
{features.length > 0 && (
{features.slice(0, 4).map((f, i) => (
-
{f.text}
))}
)}
{plan.courses?.length > 0 ? (
Course{plan.course_count !== 1 ? "s" : ""} Included
{previewCourses.map((course) => (
-
{course.title}
{course.level && (
{course.level}
)}
{formatCourseDuration(course.duration_seconds) && (
{formatCourseDuration(course.duration_seconds)}
)}
))}
{extraCount > 0 && (
setCoursesOpen(true)}
// DIALOG DISABLED
variant="secondary"
>
+{extraCount} more course{extraCount !== 1 ? "s" : ""}
)}
) : !plan.description ? (
Access to all free course content.
) : null}
{isCurrent && refundSecsLeft > 0 ? (
) : !plan.is_active ? (
) : !isCurrent ? (
) : null}
{/* ── Not Available Dialog ──────────────────────────────────────── */}
{/* ── Plan Detail Dialog ─────────────────────────────────────────── */}
{/* Current disabled from line 170 to 172 */}
>
);
};
// ─── Page ─────────────────────────────────────────────────────────────────────
export default function PlanList() {
const navigate = useNavigate();
const { plans, plansLoading, myTier, tierLoading, tierMap, getPlans, getMyTier, getTierCategories, resetMyTier } = useClientTiers();
const { fmtDate, fmtCurrency } = useDateFormat();
const { advertisements, loading: adLoading, getActiveAdvertisement, handleAdCtaClick } = useClientAdvertisements();
const [view, setView] = useState("grid");
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();
getTierCategories();
getActiveAdvertisement("tier_plans.banner");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getPlans, getMyTier]);
const bannerAd = advertisements["tier_plans.banner"] ?? null;
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}`);
const handleRefundClick = (plan) => setRefundPlan(plan);
const handleConfirmRefund = async () => {
setRefundLoading(true);
try {
const { data } = await api.post("/client/tiers/checkout/refund");
toast(data.message ?? "Refund processed. Your access has been revoked.");
setRefundPlan(null);
resetMyTier();
getMyTier();
} catch (err) {
toast(err?.response?.data?.message ?? "Refund failed. Please try again.");
} finally {
setRefundLoading(false);
}
};
return (
{/* Advertisement Banner */}
{adLoading["tier_plans.banner"] ? (
) : (
)}
{/* Section Header */}
Available Plans
Choose a subscription that matches your goals.
{!plansLoading && plans.length > 0 && (
)}
{/* Plan Cards / Comparison Table */}
{plansLoading || tierLoading ? (
{Array.from({ length: 3 }).map((_, i) =>
)}
) : plans.length === 0 ? (
No plans available at the moment.
) : view === "table" ? (
) : (
{plans.map((plan) => (
))}
)}
{/* ── Refund Confirmation Modal ──────────────────────────────────── */}
!v && setRefundPlan(null)}
title="Request Refund"
description={`Are you sure you want to refund your ${refundPlan?.label ?? "current"} plan?`}
footer={
<>
>
}
>
Plan
{refundPlan?.tier}
Refund amount
{refundPlan ? fmtCurrency(refundPlan.price, refundPlan.currency) : "—"}
{myTier?.expires_at && (
Access until
{fmtDate(myTier.expires_at)}
)}
Refund window
{refundSecsLeft > 0 ? (
{formatCountdown(refundSecsLeft)} remaining
) : (
Expired
)}
{refundSecsLeft > 0 ? (
Your refund will be processed through PayPal.{" "}
Access will be revoked immediately
{" "}
and your account will be downgraded to Free.
) : (
The 5-minute refund window has expired. Refunds are no longer available for this payment.
)}
);
}