🎉 feat: initialization
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { Card, CardContent, CardHeader } from "@workspace/ui/components/card";
|
||||
import { Separator } from "@workspace/ui/components/separator";
|
||||
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { prePurchaseOrder, purchase } from "@workspace/ui/services/user/portal";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SubscribeBilling } from "@/sections/subscribe/billing";
|
||||
import CouponInput from "@/sections/subscribe/coupon-input";
|
||||
import { SubscribeDetail } from "@/sections/subscribe/detail";
|
||||
import DurationSelector from "@/sections/subscribe/duration-selector";
|
||||
import PaymentMethods from "@/sections/subscribe/payment-methods";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
export default function Content({
|
||||
subscription,
|
||||
}: {
|
||||
subscription?: API.Subscribe;
|
||||
}) {
|
||||
const { t } = 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 { common } = useGlobalStore();
|
||||
const navigate = useNavigate();
|
||||
const [params, setParams] = useState<API.PortalPurchaseRequest>({
|
||||
quantity: 1,
|
||||
subscribe_id: 0,
|
||||
payment: -1,
|
||||
coupon: "",
|
||||
auth_type: "email",
|
||||
identifier: "",
|
||||
password: "",
|
||||
});
|
||||
const [loading, startTransition] = useTransition();
|
||||
const [isEmailValid, setIsEmailValid] = useState({
|
||||
valid: false,
|
||||
message: "",
|
||||
});
|
||||
|
||||
const { data: order } = useQuery({
|
||||
enabled: !!subscription?.id && !!params.payment,
|
||||
queryKey: [
|
||||
"preCreateOrder",
|
||||
params.coupon,
|
||||
params.quantity,
|
||||
params.payment,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const { data } = await prePurchaseOrder({
|
||||
...params,
|
||||
subscribe_id: subscription?.id as number,
|
||||
} as API.PrePurchaseOrderRequest);
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
quantity: 1,
|
||||
subscribe_id: subscription?.id,
|
||||
}));
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(field: keyof typeof params, value: string | number) => {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const { data } = await purchase(params);
|
||||
const { order_no } = data.data!;
|
||||
if (order_no) {
|
||||
localStorage.setItem(
|
||||
order_no,
|
||||
JSON.stringify({
|
||||
auth_type: params.auth_type,
|
||||
identifier: params.identifier,
|
||||
})
|
||||
);
|
||||
navigate({ to: "/purchasing/order", search: { order_no } });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}, [params, navigate]);
|
||||
|
||||
if (!subscription) {
|
||||
return (
|
||||
<div className="p-6 text-center">
|
||||
{t("subscriptionNotFound", "Subscription not found")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-8 flex max-w-4xl flex-col gap-8 md:grid md:grid-cols-2 md:flex-row">
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{t(
|
||||
"emailInputTitle",
|
||||
"Enter the email address for your {{siteName}} account",
|
||||
{
|
||||
siteName: common.site.site_name,
|
||||
}
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<EnhancedInput
|
||||
className={cn({
|
||||
"border-destructive":
|
||||
!isEmailValid.valid && params.identifier !== "",
|
||||
})}
|
||||
onValueChange={(value: string) => {
|
||||
const email = value as string;
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
identifier: email,
|
||||
}));
|
||||
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!reg.test(email)) {
|
||||
setIsEmailValid({
|
||||
valid: false,
|
||||
message: t(
|
||||
"invalidEmail",
|
||||
"Please enter a valid email address"
|
||||
),
|
||||
});
|
||||
} else if (common.auth.email.enable_domain_suffix) {
|
||||
const domain = email.split("@")[1];
|
||||
const isValid = common.auth.email?.domain_suffix_list
|
||||
.split("\n")
|
||||
.includes(domain || "");
|
||||
if (!isValid) {
|
||||
setIsEmailValid({
|
||||
valid: false,
|
||||
message: t(
|
||||
"emailDomainNotAllowed",
|
||||
"Email domain is not in the whitelist"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setIsEmailValid({
|
||||
valid: true,
|
||||
message: "",
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder="Email"
|
||||
required
|
||||
type="email"
|
||||
value={params.identifier || ""}
|
||||
/>
|
||||
<p
|
||||
className={cn("text-muted-foreground text-xs", {
|
||||
"text-destructive":
|
||||
!isEmailValid.valid && params.identifier !== "",
|
||||
})}
|
||||
>
|
||||
{isEmailValid.message ||
|
||||
t("emailRequired", "Please enter your email address.")}
|
||||
</p>
|
||||
</div>
|
||||
{params.identifier && isEmailValid.valid && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<EnhancedInput
|
||||
onValueChange={(value: string) =>
|
||||
handleChange("password", value)
|
||||
}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value={params.password || ""}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(
|
||||
"passwordHint",
|
||||
"If you do not enter a password, we will automatically generate one and send it to your email."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* <div>
|
||||
<OAuthMethods />
|
||||
</div> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="grid gap-3 text-sm">
|
||||
<h2 className="font-semibold text-xl">{subscription.name}</h2>
|
||||
<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(subscription.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={{
|
||||
...subscription,
|
||||
quantity: params.quantity,
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...order,
|
||||
quantity: params.quantity,
|
||||
unit_price: subscription?.unit_price,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="grid gap-6">
|
||||
<DurationSelector
|
||||
discounts={subscription?.discount}
|
||||
onChange={(value: number) => handleChange("quantity", value)}
|
||||
quantity={params.quantity!}
|
||||
unitTime={
|
||||
unitTimeMap[subscription.unit_time!] || subscription.unit_time
|
||||
}
|
||||
/>
|
||||
<CouponInput
|
||||
coupon={params.coupon}
|
||||
onChange={(value: string) => handleChange("coupon", value)}
|
||||
/>
|
||||
<PaymentMethods
|
||||
balance={false}
|
||||
onChange={(value: number) => handleChange("payment", value)}
|
||||
value={params.payment!}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!isEmailValid.valid || loading}
|
||||
onClick={handleSubmit}
|
||||
size="lg"
|
||||
>
|
||||
{loading && <LoaderCircle className="mr-2 animate-spin" />}
|
||||
{t("buyNow", "Buy Now")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { getSubscription } from "@workspace/ui/services/user/portal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Content from "./content";
|
||||
|
||||
export default function Purchasing() {
|
||||
const { id } = useSearch({ from: "/(main)/purchasing/" }) as { id: string };
|
||||
const { i18n } = useTranslation();
|
||||
const { data } = useQuery({
|
||||
queryKey: ["subscription", i18n.language],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscription(
|
||||
{
|
||||
language: i18n.language,
|
||||
},
|
||||
{
|
||||
skipErrorHandler: true,
|
||||
}
|
||||
);
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
|
||||
const subscription = data?.find(
|
||||
(item: API.Subscribe) => item.id === Number(id)
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container space-y-16">
|
||||
<Content subscription={subscription} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@workspace/ui/components/card";
|
||||
import { Separator } from "@workspace/ui/components/separator";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import {
|
||||
purchaseCheckout,
|
||||
queryPurchaseOrder,
|
||||
} from "@workspace/ui/services/user/portal";
|
||||
import { formatDate } from "@workspace/ui/utils/formatting";
|
||||
import { useCountDown } from "ahooks";
|
||||
import { addMinutes, format } from "date-fns";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Display } from "@/components/display";
|
||||
import { SubscribeBilling } from "@/sections/subscribe/billing";
|
||||
import { SubscribeDetail } from "@/sections/subscribe/detail";
|
||||
import StripePayment from "@/sections/user/payment/stripe";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
import { setAuthorization } from "@/utils/common";
|
||||
|
||||
export default function Order() {
|
||||
const { t } = useTranslation("order");
|
||||
const { getUserInfo } = useGlobalStore();
|
||||
const [orderNo, setOrderNo] = useState<string>();
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const search = useSearch({ from: "/(main)/purchasing/order/" });
|
||||
|
||||
const { data } = useQuery({
|
||||
enabled,
|
||||
queryKey: ["queryPurchaseOrder", orderNo],
|
||||
queryFn: async () => {
|
||||
if (!orderNo) return;
|
||||
const params = localStorage.getItem(orderNo);
|
||||
const authParams = params ? JSON.parse(params) : {};
|
||||
const { data } = await queryPurchaseOrder({
|
||||
order_no: orderNo,
|
||||
...authParams,
|
||||
});
|
||||
if (data?.data?.status !== 1) {
|
||||
setEnabled(false);
|
||||
if (data?.data?.token) {
|
||||
setAuthorization(data?.data?.token);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await getUserInfo();
|
||||
}
|
||||
}
|
||||
return data?.data;
|
||||
},
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const { data: payment } = useQuery({
|
||||
enabled: !!orderNo && data?.status === 1,
|
||||
queryKey: ["purchaseCheckout", orderNo],
|
||||
queryFn: async () => {
|
||||
const { data } = await purchaseCheckout({
|
||||
orderNo: orderNo || "",
|
||||
returnUrl: window.location.href,
|
||||
});
|
||||
if (data.data?.type === "url" && data.data?.checkout_url) {
|
||||
window.open(data.data.checkout_url, "_blank");
|
||||
}
|
||||
return data?.data;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (search.order_no) {
|
||||
setOrderNo(search.order_no);
|
||||
setEnabled(true);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
const [countDown, formattedRes] = useCountDown({
|
||||
targetDate:
|
||||
data &&
|
||||
format(addMinutes(data?.created_at, 15), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"),
|
||||
});
|
||||
|
||||
const { hours, minutes, seconds } = formattedRes;
|
||||
|
||||
const countdownDisplay =
|
||||
countDown > 0 ? (
|
||||
<>
|
||||
{hours.toString().length === 1 ? `0${hours}` : hours} :{" "}
|
||||
{minutes.toString().length === 1 ? `0${minutes}` : minutes} :{" "}
|
||||
{seconds.toString().length === 1 ? `0${seconds}` : seconds}
|
||||
</>
|
||||
) : (
|
||||
t("timeExpired", "Time Expired")
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container lg:mt-16">
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<Card className="order-2 xl:order-1">
|
||||
<CardHeader className="flex flex-row items-start bg-muted/50">
|
||||
<div className="grid gap-0.5">
|
||||
<CardTitle className="flex flex-col text-lg">
|
||||
{t("orderNumber", "Order Number")}
|
||||
<span>{data?.order_no}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("createdAt", "Created At")}: {formatDate(data?.created_at)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 p-6 text-sm">
|
||||
<div className="font-semibold">
|
||||
{t("paymentMethod", "Payment Method")}
|
||||
</div>
|
||||
<dl className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<dt className="text-muted-foreground">
|
||||
<Badge>{data?.payment.name || data?.payment.platform}</Badge>
|
||||
</dt>
|
||||
</div>
|
||||
</dl>
|
||||
<Separator />
|
||||
|
||||
{data?.status && [1, 2].includes(data.status) && (
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...data?.subscribe,
|
||||
quantity: data?.quantity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{data?.status === 3 && (
|
||||
<>
|
||||
<div className="font-semibold">
|
||||
{t("resetTraffic", "Reset Traffic")}
|
||||
</div>
|
||||
<ul className="grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1">
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="line-clamp-2 flex-1 text-muted-foreground">
|
||||
{t("resetPrice", "Reset Price")}
|
||||
</span>
|
||||
<span>
|
||||
<Display type="currency" value={data.amount} />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{data?.status === 4 && (
|
||||
<>
|
||||
<div className="font-semibold">
|
||||
{t("balanceRecharge", "Balance Recharge")}
|
||||
</div>
|
||||
<ul className="grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1">
|
||||
<li className="flex items-center justify-between">
|
||||
<span className="line-clamp-2 flex-1 text-muted-foreground">
|
||||
{t("rechargeAmount", "Recharge Amount")}
|
||||
</span>
|
||||
<span>
|
||||
<Display type="currency" value={data.amount} />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<Separator />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...data,
|
||||
unit_price: data?.subscribe?.unit_price,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="order-1 flex flex-auto items-center justify-center xl:order-2">
|
||||
<CardContent className="py-16">
|
||||
{data?.status && [2, 5].includes(data?.status) && (
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
{t("paymentSuccess", "Payment Successful")}
|
||||
</h3>
|
||||
<Icon
|
||||
className="text-7xl text-green-500"
|
||||
icon="mdi:success-circle-outline"
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Button asChild>
|
||||
<Link to="/dashboard">
|
||||
{t("subscribeNow", "Subscribe Now")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Link to="/document">
|
||||
{t("viewDocument", "View Document")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.status === 1 && payment?.type === "url" && (
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
{t("waitingForPayment", "Waiting for Payment")}
|
||||
</h3>
|
||||
<p className="flex items-center font-bold text-3xl">
|
||||
{countdownDisplay}
|
||||
</p>
|
||||
<Icon
|
||||
className="text-7xl text-muted-foreground"
|
||||
icon="mdi:access-time"
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (payment?.checkout_url) {
|
||||
window.location.href = payment?.checkout_url;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("goToPayment", "Go to Payment")}
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Link search={{ id: 0 }} to="/">
|
||||
{t("productList", "Product List")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.status === 1 && payment?.type === "qr" && (
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
{t("scanToPay", "Scan to Pay")}
|
||||
</h3>
|
||||
<p className="flex items-center font-bold text-3xl">
|
||||
{countdownDisplay}
|
||||
</p>
|
||||
<QRCodeCanvas
|
||||
imageSettings={{
|
||||
src: "/payment/alipay_f2f.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
size={208}
|
||||
value={payment?.checkout_url || ""}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Button asChild>
|
||||
<Link to="/subscribe">
|
||||
{t("productList", "Product List")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link to="/order">{t("orderList", "Order List")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.status === 1 && payment?.type === "stripe" && (
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
{t("waitingForPayment", "Waiting for Payment")}
|
||||
</h3>
|
||||
<p className="flex items-center font-bold text-3xl">
|
||||
{countdownDisplay}
|
||||
</p>
|
||||
{payment.stripe && <StripePayment {...payment.stripe} />}
|
||||
{/* <div className='flex gap-4'>
|
||||
<Button asChild>
|
||||
<Link to='/subscribe'>{t('productList')}</Link>
|
||||
</Button>
|
||||
<Button asChild variant='outline'>
|
||||
<Link to='/order'>{t('orderList')}</Link>
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.status && [3, 4].includes(data?.status) && (
|
||||
<div className="flex flex-col items-center gap-8 text-center">
|
||||
<h3 className="font-bold text-2xl tracking-tight">
|
||||
{t("orderClosed", "Order Closed")}
|
||||
</h3>
|
||||
<Icon className="text-7xl text-red-500" icon="mdi:cancel" />
|
||||
<div className="flex gap-4">
|
||||
<Button asChild>
|
||||
<Link to="/subscribe">
|
||||
{t("productList", "Product List")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link to="/order">{t("orderList", "Order List")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user