@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
Heading,
|
||||
Text,
|
||||
Flex,
|
||||
Card,
|
||||
Button,
|
||||
Spinner,
|
||||
HStack,
|
||||
Textarea,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import {
|
||||
useMySubscription,
|
||||
useCancelSubscription,
|
||||
} from "@/lib/api/subscriptions/use-hooks";
|
||||
import { PlanBadge } from "./plan-badge";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { LuCalendar, LuTriangleAlert, LuX, LuCheck } from "react-icons/lu";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
|
||||
/**
|
||||
* Subscription info card for the Profile page.
|
||||
* Shows current plan, billing dates, cancel option.
|
||||
*/
|
||||
export function SubscriptionCard() {
|
||||
const t = useTranslations("subscription");
|
||||
const tCommon = useTranslations("common");
|
||||
const { data: session } = useSession();
|
||||
const { data: subData, isLoading } = useMySubscription(!!session);
|
||||
const router = useRouter();
|
||||
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||
|
||||
const [showCancelForm, setShowCancelForm] = useState(false);
|
||||
const [cancelReason, setCancelReason] = useState("");
|
||||
const cancelMutation = useCancelSubscription();
|
||||
|
||||
const subscription = subData?.data ?? null;
|
||||
const plan = subscription?.plan ?? session?.user?.subscriptionPlan ?? "free";
|
||||
const isFree = plan === "free";
|
||||
|
||||
const handleCancel = async () => {
|
||||
await cancelMutation.mutateAsync({ reason: cancelReason || undefined });
|
||||
setShowCancelForm(false);
|
||||
setCancelReason("");
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Body>
|
||||
<Flex justify="center" py={8}>
|
||||
<Spinner size="sm" color="primary.500" />
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Header>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Heading as="h3" size="sm">
|
||||
{t("title")}
|
||||
</Heading>
|
||||
<PlanBadge plan={plan} />
|
||||
</Flex>
|
||||
</Card.Header>
|
||||
<Card.Body pt={0}>
|
||||
<VStack gap={4} align="stretch">
|
||||
{/* Subscription Details */}
|
||||
{subscription && !isFree && (
|
||||
<>
|
||||
{subscription.currentPeriodEnd && (
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack gap={2} color="fg.muted">
|
||||
<LuCalendar size={14} />
|
||||
<Text fontSize="sm">{t("next-billing")}</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
{new Date(
|
||||
subscription.currentPeriodEnd,
|
||||
).toLocaleDateString()}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{subscription.cancelEffectiveDate && (
|
||||
<Box
|
||||
p={3}
|
||||
bg="orange.50"
|
||||
_dark={{ bg: "orange.950" }}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<LuTriangleAlert size={14} color="orange" />
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="orange.600"
|
||||
_dark={{ color: "orange.300" }}
|
||||
>
|
||||
{t("cancelled-info", {
|
||||
date: new Date(
|
||||
subscription.cancelEffectiveDate,
|
||||
).toLocaleDateString(),
|
||||
})}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Flex gap={2} direction={{ base: "column", sm: "row" }}>
|
||||
{isFree ? (
|
||||
<Button
|
||||
onClick={() => router.push("/pricing")}
|
||||
colorPalette="primary"
|
||||
variant="solid"
|
||||
size="sm"
|
||||
borderRadius="lg"
|
||||
flex={1}
|
||||
>
|
||||
{t("upgrade-cta")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => router.push("/pricing")}
|
||||
colorPalette="primary"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
borderRadius="lg"
|
||||
flex={1}
|
||||
>
|
||||
{t("manage")}
|
||||
</Button>
|
||||
|
||||
{!subscription?.cancelEffectiveDate && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
colorPalette="red"
|
||||
onClick={() => setShowCancelForm(true)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Cancel Confirmation */}
|
||||
{showCancelForm && (
|
||||
<Box
|
||||
p={4}
|
||||
borderWidth="1px"
|
||||
borderColor="red.200"
|
||||
_dark={{ borderColor: "red.800" }}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<VStack gap={3} align="stretch">
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
color="red.600"
|
||||
_dark={{ color: "red.300" }}
|
||||
>
|
||||
{t("cancel-confirm-title")}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{t("cancel-confirm-message")}
|
||||
</Text>
|
||||
<Textarea
|
||||
size="sm"
|
||||
placeholder={t("cancel-reason-placeholder")}
|
||||
value={cancelReason}
|
||||
onChange={(e) => setCancelReason(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
<HStack gap={2} justify="flex-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowCancelForm(false);
|
||||
setCancelReason("");
|
||||
}}
|
||||
>
|
||||
<LuX />
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
colorPalette="red"
|
||||
variant="solid"
|
||||
size="sm"
|
||||
loading={cancelMutation.isPending}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<LuCheck />
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user