🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
@@ -0,0 +1,92 @@
"use client";
import { Separator } from "@workspace/ui/components/separator";
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
interface SubscribeBillingProps {
order?: Partial<
API.OrderDetail & {
unit_price: number;
unit_time: string;
subscribe_discount: number;
}
>;
}
export function SubscribeBilling({ order }: Readonly<SubscribeBillingProps>) {
const { t } = useTranslation("subscribe");
return (
<>
<div className="font-semibold">
{t("billing.billingTitle", "Billing Detail")}
</div>
<ul className="grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1">
{order?.type && [1, 2].includes(order?.type) && (
<li>
<span className="text-muted-foreground">
{t("billing.duration", "Duration")}
</span>
<span>
{order?.quantity || 1}{" "}
{t(order?.unit_time || "Month", order?.unit_time || "Month")}
</span>
</li>
)}
<li>
<span className="text-muted-foreground">
{t("billing.price", "Price")}
</span>
<span>
<Display
type="currency"
value={order?.price || order?.unit_price}
/>
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("billing.productDiscount", "Product Discount")}
</span>
<span>
<Display type="currency" value={order?.discount} />
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("billing.couponDiscount", "Coupon Discount")}
</span>
<span>
<Display type="currency" value={order?.coupon_discount} />
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("billing.fee", "Fee")}
</span>
<span>
<Display type="currency" value={order?.fee_amount} />
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("billing.gift", "Gift")}
</span>
<span>
<Display type="currency" value={order?.gift_amount} />
</span>
</li>
</ul>
<Separator />
<div className="flex items-center justify-between font-semibold">
<span className="text-muted-foreground">
{t("billing.total", "Total")}
</span>
<span>
<Display type="currency" value={order?.amount} />
</span>
</div>
</>
);
}
@@ -0,0 +1,27 @@
"use client";
import { Input } from "@workspace/ui/components/input";
import type React from "react";
import { useTranslation } from "react-i18next";
interface CouponInputProps {
coupon?: string;
onChange: (value: string) => void;
}
const CouponInput: React.FC<CouponInputProps> = ({ coupon, onChange }) => {
const { t } = useTranslation("subscribe");
return (
<>
<div className="font-semibold">{t("coupon", "Coupon")}</div>
<Input
onChange={(e) => onChange(e.target.value.trim())}
placeholder={t("enterCoupon", "Enter Coupon")}
value={coupon}
/>
</>
);
};
export default CouponInput;
@@ -0,0 +1,63 @@
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
interface SubscribeDetailProps {
subscribe?: Partial<
API.Subscribe & {
name: string;
quantity: number;
}
>;
}
export function SubscribeDetail({ subscribe }: Readonly<SubscribeDetailProps>) {
const { t } = useTranslation("subscribe");
return (
<>
<div className="font-semibold">
{t("detail.productDetail", "Product Detail")}
</div>
<ul className="grid grid-cols-1 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1">
{subscribe?.name && (
<li className="flex items-center justify-between">
<span className="line-clamp-2 flex-1 text-muted-foreground">
{subscribe?.name}
</span>
<span>
x <span>{subscribe?.quantity || 1}</span>
</span>
</li>
)}
<li>
<span className="text-muted-foreground">
{t("detail.availableTraffic", "Available Traffic")}
</span>
<span>
<Display type="traffic" unlimited value={subscribe?.traffic} />
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("detail.connectionSpeed", "Connection Speed")}
</span>
<span>
<Display
type="trafficSpeed"
unlimited
value={subscribe?.speed_limit}
/>
</span>
</li>
<li>
<span className="text-muted-foreground">
{t("detail.connectedDevices", "Connected Devices")}
</span>
<span>
<Display type="number" unlimited value={subscribe?.device_limit} />
</span>
</li>
</ul>
</>
);
}
@@ -0,0 +1,91 @@
"use client";
import { Badge } from "@workspace/ui/components/badge";
import { Label } from "@workspace/ui/components/label";
import {
RadioGroup,
RadioGroupItem,
} from "@workspace/ui/components/radio-group";
import type React from "react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
interface DurationSelectorProps {
quantity: number;
unitTime?: string;
discounts?: Array<{ quantity: number; discount: number }>;
onChange: (value: number) => void;
}
const DurationSelector: React.FC<DurationSelectorProps> = ({
quantity,
unitTime = "Month",
discounts = [],
onChange,
}) => {
const { t } = useTranslation("subscribe");
const handleChange = useCallback(
(value: string) => {
onChange(Number(value));
},
[onChange]
);
const DurationOption: React.FC<{ value: string; label: string }> = ({
value,
label,
}) => (
<div className="relative">
<RadioGroupItem className="peer sr-only" id={value} value={value} />
<Label
className="relative flex h-full flex-col items-center justify-center gap-2 rounded-md border-2 border-muted bg-popover p-2 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary"
htmlFor={value}
>
{label}
</Label>
</div>
);
const currentDiscount = discounts?.find(
(item) => item.quantity === quantity
)?.discount;
const discountPercentage = currentDiscount ? 100 - currentDiscount : 0;
return (
<>
<div className="font-semibold">
{t("purchaseDuration", "Purchase Duration")}
</div>
<RadioGroup
className="flex flex-wrap gap-3"
onValueChange={handleChange}
value={String(quantity)}
>
{unitTime !== "Minute" && (
<DurationOption label={`1 / ${t(unitTime)}`} value="1" />
)}
{discounts?.map((item) => (
<DurationOption
key={item.quantity}
label={`${item.quantity} / ${t(unitTime)}`}
value={String(item.quantity)}
/>
))}
</RadioGroup>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">
{t("discountInfo", "Discount Info")}:
</span>
{discountPercentage > 0 ? (
<Badge className="h-6 text-sm" variant="destructive">
-{discountPercentage}% {t("discount", "Discount")}
</Badge>
) : (
<span className="h-6 text-muted-foreground text-sm">--</span>
)}
</div>
</>
);
};
export default DurationSelector;
+150
View File
@@ -0,0 +1,150 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@workspace/ui/components/card";
import { Separator } from "@workspace/ui/components/separator";
import Empty from "@workspace/ui/composed/empty";
import { Icon } from "@workspace/ui/composed/icon";
import { cn } from "@workspace/ui/lib/utils";
import { querySubscribeList } from "@workspace/ui/services/user/subscribe";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
import { SubscribeDetail } from "./detail";
import Purchase from "./purchase";
export default function Subscribe() {
const { t, i18n } = useTranslation("subscribe");
const unitTimeMap: Record<string, string> = {
Day: t("Day", "Day"),
Hour: t("Hour", "Hour"),
Minute: t("Minute", "Minute"),
Month: t("Month", "Month"),
NoLimit: t("NoLimit", "No Limit"),
Year: t("Year", "Year"),
};
const locale = i18n.language;
const [subscribe, setSubscribe] = useState<API.Subscribe>();
const { data } = useQuery({
queryKey: ["querySubscribeList", locale],
queryFn: async () => {
console.log("Fetching subscription list...");
const { data } = await querySubscribeList({ language: locale });
return data.data?.list || [];
},
});
const filteredData = data?.filter((item) => item.show);
return (
<>
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
{filteredData?.map((item) => (
<Card className="relative flex flex-col" key={item.id}>
<CardHeader className="font-medium text-xl">
{item.name}
</CardHeader>
<CardContent className="*:!text-sm flex flex-grow flex-col gap-3">
{/* <div className='font-semibold'>{t('productDescription')}</div> */}
<ul className="flex flex-grow flex-col gap-3">
{(() => {
let parsedDescription: {
description: string;
features: Array<{
icon: string;
label: string;
type: "default" | "success" | "destructive";
}>;
};
try {
parsedDescription = JSON.parse(item.description);
} catch {
parsedDescription = { description: "", features: [] };
}
const { description, features } = parsedDescription;
return (
<>
{description && (
<li className="text-muted-foreground">
{description}
</li>
)}
{features?.map(
(
feature: {
icon: string;
label: string;
type: "default" | "success" | "destructive";
},
index: number
) => (
<li
className={cn("flex items-center gap-1", {
"text-muted-foreground line-through":
feature.type === "destructive",
})}
key={index}
>
{feature.icon && (
<Icon
className={cn("size-5 text-primary", {
"text-green-500":
feature.type === "success",
"text-destructive":
feature.type === "destructive",
})}
icon={feature.icon}
/>
)}
{feature.label}
</li>
)
)}
</>
);
})()}
</ul>
<SubscribeDetail
subscribe={{
...item,
name: undefined,
}}
/>
</CardContent>
<Separator />
<CardFooter className="flex flex-col gap-2">
<h2 className="pb-8 font-semibold text-2xl sm:text-3xl">
<Display type="currency" value={item.unit_price} />
<span className="font-medium text-base">
/
{unitTimeMap[item.unit_time!] ||
t(item.unit_time || "Month", item.unit_time || "Month")}
</span>
</h2>
<Button
className="absolute bottom-0 w-full rounded-t-none rounded-b-xl"
onClick={() => {
setSubscribe(item);
}}
>
{t("buy", "Buy")}
</Button>
</CardFooter>
</Card>
))}
</div>
{filteredData?.length === 0 && <Empty />}
</div>
<Purchase setSubscribe={setSubscribe} subscribe={subscribe} />
</>
);
}
@@ -0,0 +1,85 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Label } from "@workspace/ui/components/label";
import {
RadioGroup,
RadioGroupItem,
} from "@workspace/ui/components/radio-group";
import { cn } from "@workspace/ui/lib/utils";
import { getAvailablePaymentMethods } from "@workspace/ui/services/user/portal";
import type React from "react";
import { memo } from "react";
import { useTranslation } from "react-i18next";
interface PaymentMethodsProps {
value: number;
onChange: (value: number) => void;
balance?: boolean;
}
const PaymentMethods: React.FC<PaymentMethodsProps> = ({
value,
onChange,
balance = true,
}) => {
const { t } = useTranslation("subscribe");
const { data } = useQuery({
queryKey: ["getAvailablePaymentMethods", { balance }],
queryFn: async () => {
const { data } = await getAvailablePaymentMethods();
const list = data.data?.list || [];
const methods = balance ? list : list.filter((item) => item.id !== -1);
const defaultMethod = methods.find((item) => item.id)?.id;
if (defaultMethod) onChange(defaultMethod);
return methods;
},
});
return (
<>
<div className="font-semibold">
{t("paymentMethod", "Payment Method")}
</div>
<RadioGroup
className="grid grid-cols-2 gap-2 md:grid-cols-5"
onValueChange={(val) => {
console.log(val);
onChange(Number(val));
}}
value={String(value)}
>
{data?.map((item) => (
<div className="relative" key={item.id}>
<RadioGroupItem
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
id={String(item.id)}
value={String(item.id)}
/>
<Label
className={cn(
"flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover py-2 hover:bg-accent hover:text-accent-foreground",
String(value) === String(item.id) ? "border-primary" : ""
)}
htmlFor={String(item.id)}
>
<div className="flex size-12 items-center justify-center">
<img
alt={item.name}
height={48}
src={item.icon || "/payment/balance.svg"}
width={48}
/>
</div>
<span className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-center">
{item.name}
</span>
</Label>
</div>
))}
</RadioGroup>
</>
);
};
export default memo(PaymentMethods);
@@ -0,0 +1,168 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "@tanstack/react-router";
import { Button } from "@workspace/ui/components/button";
import { Card, CardContent } from "@workspace/ui/components/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@workspace/ui/components/dialog";
import { Separator } from "@workspace/ui/components/separator";
import { preCreateOrder, purchase } from "@workspace/ui/services/user/order";
import { LoaderCircle } from "lucide-react";
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import CouponInput from "@/sections/subscribe/coupon-input";
import DurationSelector from "@/sections/subscribe/duration-selector";
import PaymentMethods from "@/sections/subscribe/payment-methods";
import { useGlobalStore } from "@/stores/global";
import { SubscribeBilling } from "./billing";
import { SubscribeDetail } from "./detail";
interface PurchaseProps {
subscribe?: API.Subscribe;
setSubscribe: (subscribe?: API.Subscribe) => void;
}
export default function Purchase({
subscribe,
setSubscribe,
}: Readonly<PurchaseProps>) {
const { t } = useTranslation("subscribe");
const { getUserInfo } = useGlobalStore();
const router = useRouter();
const [params, setParams] = useState<Partial<API.PurchaseOrderRequest>>({
quantity: 1,
subscribe_id: 0,
payment: -1,
coupon: "",
});
const [loading, startTransition] = useTransition();
const lastSuccessOrderRef = useRef<any>(null);
const { data: order } = useQuery({
enabled: !!subscribe?.id,
queryKey: ["preCreateOrder", params],
queryFn: async () => {
try {
const { data } = await preCreateOrder({
...params,
subscribe_id: subscribe?.id as number,
} as API.PurchaseOrderRequest);
const result = data.data;
if (result) {
lastSuccessOrderRef.current = result;
}
return result;
} catch (error) {
if (lastSuccessOrderRef.current) {
return lastSuccessOrderRef.current;
}
throw error;
}
},
});
useEffect(() => {
if (subscribe) {
setParams((prev) => ({
...prev,
quantity: 1,
subscribe_id: subscribe?.id,
}));
}
}, [subscribe]);
const handleChange = useCallback(
(field: keyof typeof params, value: string | number) => {
setParams((prev) => ({
...prev,
[field]: value,
}));
},
[]
);
const handleSubmit = useCallback(async () => {
startTransition(async () => {
try {
const response = await purchase(params as API.PurchaseOrderRequest);
const orderNo = response.data.data?.order_no;
if (orderNo) {
getUserInfo();
router.navigate({ to: "/payment", search: { order_no: orderNo } });
}
} catch (_error) {
/* empty */
}
});
}, [params, router, getUserInfo]);
return (
<Dialog
onOpenChange={(open) => {
if (!open) setSubscribe(undefined);
}}
open={!!subscribe?.id}
>
<DialogContent className="flex h-full flex-col overflow-hidden border-none p-0 md:h-auto md:max-w-screen-lg">
<DialogHeader className="p-6 pb-0">
<DialogTitle>{t("buySubscription", "Buy Subscription")}</DialogTitle>
</DialogHeader>
<div className="grid w-full flex-grow gap-3 overflow-auto p-6 pt-0 lg:grid-cols-2">
<Card className="border-transparent shadow-none md:border-inherit md:shadow">
<CardContent className="grid gap-3 text-sm">
<SubscribeDetail
subscribe={{
...subscribe,
quantity: params.quantity,
}}
/>
<Separator />
<SubscribeBilling
order={{
...order,
quantity: params.quantity,
unit_price: subscribe?.unit_price,
}}
/>
</CardContent>
</Card>
<div className="flex flex-col justify-between text-sm">
<div className="mb-6 grid gap-3">
<DurationSelector
discounts={subscribe?.discount}
onChange={(value) => {
handleChange("quantity", value);
}}
quantity={params.quantity as number}
unitTime={subscribe?.unit_time}
/>
<CouponInput
coupon={params.coupon}
onChange={(value) => handleChange("coupon", value)}
/>
<PaymentMethods
onChange={(value) => {
handleChange("payment", value);
}}
value={params.payment as number}
/>
</div>
<Button
className="fixed bottom-0 left-0 w-full md:relative md:mt-6"
disabled={loading}
onClick={handleSubmit}
>
{loading && <LoaderCircle className="mr-2 animate-spin" />}
{t("buyNow", "Buy Now")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,105 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { recharge } from "@workspace/ui/services/user/order";
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
import { LoaderCircle } from "lucide-react";
import type React from "react";
import { useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
import PaymentMethods from "./payment-methods";
export default function Recharge(
props: Readonly<React.ComponentProps<typeof Button>>
) {
const { t } = useTranslation("subscribe");
const { common } = useGlobalStore();
const { currency } = common;
const [open, setOpen] = useState<boolean>(false);
const [loading, startTransition] = useTransition();
const [params, setParams] = useState<API.RechargeOrderRequest>({
amount: 0,
payment: 1,
});
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button {...props}>{t("recharge", "Recharge")}</Button>
</DialogTrigger>
<DialogContent className="flex h-full flex-col overflow-hidden md:h-auto">
<DialogHeader>
<DialogTitle>{t("balanceRecharge", "Balance Recharge")}</DialogTitle>
<DialogDescription>
{t("rechargeDescription", "Recharge your account balance")}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col justify-between text-sm">
<div className="grid gap-3">
<div className="font-semibold">
{t("rechargeAmount", "Recharge Amount")}
</div>
<div className="flex">
<EnhancedInput
formatInput={(value) => unitConversion("centsToDollars", value)}
formatOutput={(value) =>
unitConversion("dollarsToCents", value)
}
min={0}
onValueChange={(value) => {
setParams((prev) => ({
...prev,
amount: value as number,
}));
}}
placeholder={t("enterAmount", "Enter Amount")}
prefix={currency.currency_symbol}
suffix={currency.currency_unit}
type="number"
value={params.amount}
/>
</div>
<PaymentMethods
balance={false}
onChange={(value) => setParams({ ...params, payment: value })}
value={params.payment}
/>
</div>
<Button
className="fixed bottom-0 left-0 w-full rounded-none md:relative md:mt-6"
disabled={loading || !params.amount}
onClick={() => {
startTransition(async () => {
try {
const response = await recharge(params);
const orderNo = response.data.data?.order_no;
if (orderNo) {
window.location.href = `/payment?order_no=${orderNo}`;
setOpen(false);
}
} catch (_error) {
/* empty */
}
});
}}
>
{loading && <LoaderCircle className="mr-2 animate-spin" />}
{t("rechargeNow", "Recharge Now")}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,165 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import { Card, CardContent } from "@workspace/ui/components/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import { Separator } from "@workspace/ui/components/separator";
import { preCreateOrder, renewal } from "@workspace/ui/services/user/order";
import { LoaderCircle } from "lucide-react";
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import CouponInput from "@/sections/subscribe/coupon-input";
import DurationSelector from "@/sections/subscribe/duration-selector";
import PaymentMethods from "@/sections/subscribe/payment-methods";
import { useGlobalStore } from "@/stores/global";
import { SubscribeBilling } from "./billing";
import { SubscribeDetail } from "./detail";
interface RenewalProps {
id: number;
subscribe: API.Subscribe;
}
export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
const { t } = useTranslation("subscribe");
const { getUserInfo } = useGlobalStore();
const [open, setOpen] = useState<boolean>(false);
const [params, setParams] = useState<Partial<API.RenewalOrderRequest>>({
quantity: 1,
payment: -1,
coupon: "",
user_subscribe_id: id,
});
const [loading, startTransition] = useTransition();
const lastSuccessOrderRef = useRef<any>(null);
const { data: order } = useQuery({
enabled: !!subscribe.id && open,
queryKey: ["preCreateOrder", params],
queryFn: async () => {
try {
const { data } = await preCreateOrder({
...params,
subscribe_id: subscribe.id,
} as API.PurchaseOrderRequest);
const result = data.data;
if (result) {
lastSuccessOrderRef.current = result;
}
return result;
} catch (_error) {
if (lastSuccessOrderRef.current) {
return lastSuccessOrderRef.current;
}
}
},
});
useEffect(() => {
if (subscribe.id && id) {
setParams((prev) => ({
...prev,
quantity: 1,
subscribe_id: subscribe.id,
user_subscribe_id: id,
}));
}
}, [subscribe.id, id]);
const handleChange = useCallback(
(field: keyof typeof params, value: string | number) => {
setParams((prev) => ({
...prev,
[field]: value,
}));
},
[]
);
const handleSubmit = useCallback(async () => {
startTransition(async () => {
try {
const response = await renewal(params as API.RenewalOrderRequest);
const orderNo = response.data.data?.order_no;
if (orderNo) {
getUserInfo();
window.location.href = `/payment?order_no=${orderNo}`;
}
} catch (_error) {
/* empty */
}
});
}, [params, getUserInfo]);
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button size="sm">{t("renew", "Renew")}</Button>
</DialogTrigger>
<DialogContent className="flex h-full flex-col overflow-hidden md:h-auto md:max-w-screen-lg">
<DialogHeader>
<DialogTitle>
{t("renewSubscription", "Renew Subscription")}
</DialogTitle>
</DialogHeader>
<div className="grid w-full gap-3 lg:grid-cols-2">
<Card className="border-transparent shadow-none md:border-inherit md:shadow">
<CardContent className="grid gap-3 p-0 text-sm md:p-6">
<SubscribeDetail
subscribe={{
...subscribe,
quantity: params.quantity,
}}
/>
<Separator />
<SubscribeBilling
order={{
...order,
quantity: params.quantity,
unit_price: subscribe?.unit_price,
}}
/>
</CardContent>
</Card>
<div className="flex flex-col justify-between text-sm">
<div className="mb-6 grid gap-3">
<DurationSelector
discounts={subscribe?.discount}
onChange={(value) => {
handleChange("quantity", value);
}}
quantity={params.quantity!}
unitTime={subscribe?.unit_time}
/>
<CouponInput
coupon={params.coupon}
onChange={(value) => handleChange("coupon", value)}
/>
<PaymentMethods
onChange={(value) => {
handleChange("payment", value);
}}
value={params.payment as number}
/>
</div>
<Button
className="fixed bottom-0 left-0 w-full md:relative md:mt-6"
disabled={loading}
onClick={handleSubmit}
>
{loading && <LoaderCircle className="mr-2 animate-spin" />}
{t("buyNow", "Buy Now")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,106 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import { resetTraffic } from "@workspace/ui/services/user/order";
import { LoaderCircle } from "lucide-react";
import { useEffect, useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
import { useGlobalStore } from "@/stores/global";
import PaymentMethods from "./payment-methods";
interface ResetTrafficProps {
id: number;
replacement?: number;
}
export default function ResetTraffic({
id,
replacement,
}: Readonly<ResetTrafficProps>) {
const { t } = useTranslation("subscribe");
const { getUserInfo } = useGlobalStore();
const [open, setOpen] = useState<boolean>(false);
const [params, setParams] = useState<API.ResetTrafficOrderRequest>({
payment: -1,
user_subscribe_id: id,
});
const [loading, startTransition] = useTransition();
useEffect(() => {
if (id) {
setParams((prev) => ({
...prev,
quantity: 1,
user_subscribe_id: id,
}));
}
}, [id]);
if (!replacement) return;
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button size="sm" variant="secondary">
{t("resetTraffic", "Reset Traffic")}
</Button>
</DialogTrigger>
<DialogContent className="flex h-full flex-col overflow-hidden md:h-auto">
<DialogHeader>
<DialogTitle>{t("resetTrafficTitle", "Reset Traffic")}</DialogTitle>
<DialogDescription>
{t("resetTrafficDescription", "Reset your subscription traffic")}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col justify-between text-sm">
<div className="grid gap-3">
<div className="flex justify-between font-semibold">
<span>{t("resetPrice", "Reset Price")}</span>
<span>
<Display type="currency" value={replacement} />
</span>
</div>
<PaymentMethods
onChange={(value) => {
setParams({
...params,
payment: value,
});
}}
value={params.payment}
/>
</div>
<Button
className="fixed bottom-0 left-0 w-full rounded-none md:relative md:mt-6"
disabled={loading}
onClick={async () => {
startTransition(async () => {
try {
const response = await resetTraffic(params);
const orderNo = response.data.data?.order_no;
if (orderNo) {
getUserInfo();
window.location.href = `/payment?order_no=${orderNo}`;
}
} catch (error) {
console.log(error);
}
});
}}
>
{loading && <LoaderCircle className="mr-2 animate-spin" />}
{t("buyNow", "Buy Now")}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,103 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import { preUnsubscribe, unsubscribe } from "@workspace/ui/services/user/user";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Display } from "@/components/display";
import { useGlobalStore } from "@/stores/global";
interface UnsubscribeProps {
id: number;
allowDeduction?: boolean;
}
export default function Unsubscribe({
id,
allowDeduction,
}: Readonly<UnsubscribeProps>) {
const { t } = useTranslation("subscribe");
const { common, getUserInfo } = useGlobalStore();
const single_model = common.subscribe.single_model;
const [open, setOpen] = useState(false);
const { data } = useQuery({
enabled: Boolean(open && id && allowDeduction),
queryKey: ["preUnsubscribe", id],
queryFn: async () => {
const { data } = await preUnsubscribe({ id });
return data.data?.deduction_amount;
},
});
const handleSubmit = async () => {
try {
await unsubscribe(
{ id },
{
skipErrorHandler: true,
}
);
toast.success(t("unsubscribe.success", "Unsubscribed successfully"));
await getUserInfo();
setOpen(false);
} catch (_error) {
toast.error(t("unsubscribe.failed", "Unsubscribe failed"));
}
};
if (!(single_model || allowDeduction)) return null;
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button size="sm" variant="destructive">
{t("unsubscribe.unsubscribe", "Unsubscribe")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t("unsubscribe.confirmUnsubscribe", "Confirm Unsubscribe")}
</DialogTitle>
<DialogDescription>
{t(
"unsubscribe.confirmUnsubscribeDescription",
"Are you sure you want to unsubscribe?"
)}
</DialogDescription>
</DialogHeader>
<p>{t("unsubscribe.residualValue", "Residual Value")}</p>
<p className="font-semibold text-2xl text-primary">
<Display type="currency" value={data} />
</p>
<p className="text-muted-foreground text-sm">
{t(
"unsubscribe.unsubscribeDescription",
"The residual value will be refunded to your account"
)}
</p>
<DialogFooter>
<Button onClick={() => setOpen(false)} variant="outline">
{t("unsubscribe.cancel", "Cancel")}
</Button>
<Button onClick={handleSubmit}>
{t("unsubscribe.confirm", "Confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}