✨ feat: Added localized support for user subscription and deletion status, and optimized the subscription form and user interface.
This commit is contained in:
parent
3751f64f73
commit
9f95cec876
@ -25,9 +25,11 @@
|
|||||||
"createSuccess": "Created successfully",
|
"createSuccess": "Created successfully",
|
||||||
"createUser": "Create User",
|
"createUser": "Create User",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"deleted": "Deleted",
|
||||||
"deleteDescription": "This action cannot be undone.",
|
"deleteDescription": "This action cannot be undone.",
|
||||||
"deleteSubscriptionDescription": "This action cannot be undone.",
|
"deleteSubscriptionDescription": "This action cannot be undone.",
|
||||||
"deleteSuccess": "Deleted successfully",
|
"deleteSuccess": "Deleted successfully",
|
||||||
|
"isDeleted": "Status",
|
||||||
"deviceLimit": "Device Limit",
|
"deviceLimit": "Device Limit",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadTraffic": "Download Traffic",
|
"downloadTraffic": "Download Traffic",
|
||||||
@ -51,6 +53,7 @@
|
|||||||
"loginStatus": "Login Status",
|
"loginStatus": "Login Status",
|
||||||
"manager": "Administrator",
|
"manager": "Administrator",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"normal": "Normal",
|
||||||
"notifySettingsTitle": "Notify Settings",
|
"notifySettingsTitle": "Notify Settings",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
|
|||||||
@ -25,9 +25,11 @@
|
|||||||
"createSuccess": "创建成功",
|
"createSuccess": "创建成功",
|
||||||
"createUser": "创建用户",
|
"createUser": "创建用户",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"deleted": "已删除",
|
||||||
"deleteDescription": "此操作无法撤销。",
|
"deleteDescription": "此操作无法撤销。",
|
||||||
"deleteSubscriptionDescription": "此操作无法撤销。",
|
"deleteSubscriptionDescription": "此操作无法撤销。",
|
||||||
"deleteSuccess": "删除成功",
|
"deleteSuccess": "删除成功",
|
||||||
|
"isDeleted": "状态",
|
||||||
"deviceLimit": "IP限制",
|
"deviceLimit": "IP限制",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"downloadTraffic": "下载流量",
|
"downloadTraffic": "下载流量",
|
||||||
@ -51,6 +53,7 @@
|
|||||||
"loginStatus": "登录状态",
|
"loginStatus": "登录状态",
|
||||||
"manager": "管理员",
|
"manager": "管理员",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
|
"normal": "正常",
|
||||||
"notifySettingsTitle": "通知设置",
|
"notifySettingsTitle": "通知设置",
|
||||||
"offline": "离线",
|
"offline": "离线",
|
||||||
"online": "在线",
|
"online": "在线",
|
||||||
|
|||||||
@ -277,7 +277,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
{trigger}
|
{trigger}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-screen-md">
|
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-3xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|||||||
@ -172,6 +172,18 @@ export default function User() {
|
|||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: "ID",
|
header: "ID",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "deleted_at",
|
||||||
|
header: t("isDeleted", "Deleted"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const deletedAt = row.getValue("deleted_at") as number | undefined;
|
||||||
|
return deletedAt ? (
|
||||||
|
<Badge variant="destructive">{t("deleted", "Deleted")}</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">{t("normal", "Normal")}</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "auth_methods",
|
accessorKey: "auth_methods",
|
||||||
header: t("userName", "Username"),
|
header: t("userName", "Username"),
|
||||||
@ -355,16 +367,13 @@ function SubscriptionSheet({ userId }: { userId: number }) {
|
|||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="secondary">{t("subscription", "Subscription")}</Button>
|
<Button variant="secondary">{t("subscription", "Subscription")}</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent
|
<SheetContent className="w-[1000px] max-w-full md:max-w-7xl" side="right">
|
||||||
className="w-[1000px] max-w-full md:max-w-screen-xl"
|
|
||||||
side="right"
|
|
||||||
>
|
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
{t("subscriptionList", "Subscription List")} · ID: {userId}
|
{t("subscriptionList", "Subscription List")} · ID: {userId}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="mt-2">
|
<div className="mt-2 px-4">
|
||||||
<UserSubscription userId={userId} />
|
<UserSubscription userId={userId} />
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const ref = useRef<ProTableActions>(null);
|
const ref = useRef<ProTableActions>(null);
|
||||||
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
||||||
@ -59,105 +58,13 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
title={t("editSubscription", "Edit Subscription")}
|
title={t("editSubscription", "Edit Subscription")}
|
||||||
trigger={t("edit", "Edit")}
|
trigger={t("edit", "Edit")}
|
||||||
/>,
|
/>,
|
||||||
<Button
|
<RowMoreActions
|
||||||
key="copy"
|
key="more"
|
||||||
onClick={async () => {
|
refresh={() => ref.current?.refresh()}
|
||||||
await navigator.clipboard.writeText(
|
row={row}
|
||||||
getUserSubscribeUrls(row.token)[0] || ""
|
token={row.token}
|
||||||
);
|
userId={userId}
|
||||||
toast.success(t("copySuccess", "Copied successfully"));
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{t("copySubscription", "Copy Subscription")}
|
|
||||||
</Button>,
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"resetTokenDescription",
|
|
||||||
"This will reset the subscription address and regenerate a new token."
|
|
||||||
)}
|
|
||||||
key="resetToken"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await resetUserSubscribeToken({ user_subscribe_id: row.id });
|
|
||||||
toast.success(
|
|
||||||
t(
|
|
||||||
"resetTokenSuccess",
|
|
||||||
"Subscription address reset successfully"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("confirmResetToken", "Confirm Reset Subscription Address")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
{t("resetToken", "Reset Subscription Address")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
/>,
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={
|
|
||||||
row.status === 5
|
|
||||||
? t(
|
|
||||||
"resumeSubscribeDescription",
|
|
||||||
"This will resume the subscription and allow the user to use it."
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"stopSubscribeDescription",
|
|
||||||
"This will stop the subscription temporarily. User will not be able to use it."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
key="toggle"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await toggleUserSubscribeStatus({ user_subscribe_id: row.id });
|
|
||||||
toast.success(
|
|
||||||
row.status === 5
|
|
||||||
? t(
|
|
||||||
"resumeSubscribeSuccess",
|
|
||||||
"Subscription resumed successfully"
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"stopSubscribeSuccess",
|
|
||||||
"Subscription stopped successfully"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
row.status === 5
|
|
||||||
? t("confirmResumeSubscribe", "Confirm Resume Subscription")
|
|
||||||
: t("confirmStopSubscribe", "Confirm Stop Subscription")
|
|
||||||
}
|
|
||||||
trigger={
|
|
||||||
<Button variant="secondary">
|
|
||||||
{row.status === 5
|
|
||||||
? t("resumeSubscribe", "Resume Subscription")
|
|
||||||
: t("stopSubscribe", "Stop Subscription")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"deleteSubscriptionDescription",
|
|
||||||
"This action cannot be undone."
|
|
||||||
)}
|
|
||||||
key="delete"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
|
||||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("confirmDelete", "Confirm Delete")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<RowMoreActions key="more" subId={row.id} userId={userId} />,
|
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
@ -175,6 +82,11 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
header: t("status", "Status"),
|
header: t("status", "Status"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.getValue("status") as number;
|
const status = row.getValue("status") as number;
|
||||||
|
const expireTime = row.original.expire_time;
|
||||||
|
|
||||||
|
// 如果过期时间为0,说明是永久订阅,应该显示为激活状态
|
||||||
|
const displayStatus = status === 3 && expireTime === 0 ? 1 : status;
|
||||||
|
|
||||||
const statusMap: Record<
|
const statusMap: Record<
|
||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
@ -201,7 +113,7 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const statusInfo = statusMap[status] || {
|
const statusInfo = statusMap[displayStatus] || {
|
||||||
label: "Unknown",
|
label: "Unknown",
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
};
|
};
|
||||||
@ -261,10 +173,12 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
{
|
{
|
||||||
accessorKey: "expire_time",
|
accessorKey: "expire_time",
|
||||||
header: t("expireTime", "Expire Time"),
|
header: t("expireTime", "Expire Time"),
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) => {
|
||||||
row.getValue("expire_time")
|
const expireTime = row.getValue("expire_time") as number;
|
||||||
? formatDate(row.getValue("expire_time"))
|
return expireTime && expireTime !== 0
|
||||||
: t("permanent", "Permanent"),
|
? formatDate(expireTime)
|
||||||
|
: t("permanent", "Permanent");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "created_at",
|
accessorKey: "created_at",
|
||||||
@ -308,9 +222,24 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
function RowMoreActions({
|
||||||
|
userId,
|
||||||
|
row,
|
||||||
|
token,
|
||||||
|
refresh,
|
||||||
|
}: {
|
||||||
|
userId: number;
|
||||||
|
row: API.UserSubscribe;
|
||||||
|
token: string;
|
||||||
|
refresh: () => void;
|
||||||
|
}) {
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const resetTokenRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const toggleStatusRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const deleteRef = useRef<HTMLButtonElement>(null);
|
||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
|
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex">
|
<div className="inline-flex">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -318,9 +247,47 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
<Button variant="outline">{t("more", "More")}</Button>
|
<Button variant="outline">{t("more", "More")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
getUserSubscribeUrls(token)[0] || ""
|
||||||
|
);
|
||||||
|
toast.success(t("copySuccess", "Copied successfully"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("copySubscription", "Copy Subscription")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
resetTokenRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("resetToken", "Reset Subscription Address")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleStatusRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.status === 5
|
||||||
|
? t("resumeSubscribe", "Resume Subscription")
|
||||||
|
: t("stopSubscribe", "Stop Subscription")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive"
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("delete", "Delete")}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/subscribe"
|
to="/dashboard/log/subscribe"
|
||||||
>
|
>
|
||||||
{t("subscriptionLogs", "Subscription Logs")}
|
{t("subscriptionLogs", "Subscription Logs")}
|
||||||
@ -328,7 +295,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/reset-subscribe"
|
to="/dashboard/log/reset-subscribe"
|
||||||
>
|
>
|
||||||
{t("resetLogs", "Reset Logs")}
|
{t("resetLogs", "Reset Logs")}
|
||||||
@ -336,7 +303,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/subscribe-traffic"
|
to="/dashboard/log/subscribe-traffic"
|
||||||
>
|
>
|
||||||
{t("trafficStats", "Traffic Stats")}
|
{t("trafficStats", "Traffic Stats")}
|
||||||
@ -344,7 +311,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, subscribe_id: subId }}
|
search={{ user_id: userId, subscribe_id: row.id }}
|
||||||
to="/dashboard/log/traffic-details"
|
to="/dashboard/log/traffic-details"
|
||||||
>
|
>
|
||||||
{t("trafficDetails", "Traffic Details")}
|
{t("trafficDetails", "Traffic Details")}
|
||||||
@ -361,8 +328,78 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Hidden triggers for confirm dialogs */}
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={t(
|
||||||
|
"resetTokenDescription",
|
||||||
|
"This will reset the subscription address and regenerate a new token."
|
||||||
|
)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await resetUserSubscribeToken({
|
||||||
|
user_subscribe_id: row.subscribe_id,
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
t("resetTokenSuccess", "Subscription address reset successfully")
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={t("confirmResetToken", "Confirm Reset Subscription Address")}
|
||||||
|
trigger={<Button className="hidden" ref={resetTokenRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={
|
||||||
|
row.status === 5
|
||||||
|
? t(
|
||||||
|
"resumeSubscribeDescription",
|
||||||
|
"This will resume the subscription and allow the user to use it."
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"stopSubscribeDescription",
|
||||||
|
"This will stop the subscription temporarily. User will not be able to use it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await toggleUserSubscribeStatus({
|
||||||
|
user_subscribe_id: row.subscribe_id,
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
row.status === 5
|
||||||
|
? t("resumeSubscribeSuccess", "Subscription resumed successfully")
|
||||||
|
: t("stopSubscribeSuccess", "Subscription stopped successfully")
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
row.status === 5
|
||||||
|
? t("confirmResumeSubscribe", "Confirm Resume Subscription")
|
||||||
|
: t("confirmStopSubscribe", "Confirm Stop Subscription")
|
||||||
|
}
|
||||||
|
trigger={<Button className="hidden" ref={toggleStatusRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={t(
|
||||||
|
"deleteSubscriptionDescription",
|
||||||
|
"This action cannot be undone."
|
||||||
|
)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||||
|
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={t("confirmDelete", "Confirm Delete")}
|
||||||
|
trigger={<Button className="hidden" ref={deleteRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
<SubscriptionDetail
|
<SubscriptionDetail
|
||||||
subscriptionId={subId}
|
subscriptionId={row.id}
|
||||||
trigger={<Button className="hidden" ref={triggerRef} />}
|
trigger={<Button className="hidden" ref={triggerRef} />}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export function SubscriptionForm({
|
|||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]">
|
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-4">
|
||||||
<div className="pr-4">
|
<div className="pr-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@ -218,337 +218,354 @@ export default function Content() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{userSubscribe.map((item) => (
|
{userSubscribe.map((item) => {
|
||||||
<Card
|
// 如果过期时间为0,说明是永久订阅,不应该显示过期状态
|
||||||
className={cn("relative", {
|
const isActuallyExpired =
|
||||||
"relative opacity-80 grayscale": item.status === 3,
|
item.status === 3 && item.expire_time !== 0;
|
||||||
"relative hidden opacity-60 blur-[0.3px] grayscale":
|
const shouldShowWatermark =
|
||||||
item.status === 4,
|
item.status === 2 || item.status === 4 || isActuallyExpired;
|
||||||
})}
|
|
||||||
key={item.id}
|
|
||||||
>
|
|
||||||
{item.status >= 2 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute top-0 left-0 z-10 h-full w-full overflow-hidden mix-blend-difference brightness-150 contrast-200 invert-[0.2]",
|
|
||||||
{
|
|
||||||
"text-destructive": item.status === 2,
|
|
||||||
"text-white": item.status === 3 || item.status === 4,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
{Array.from({ length: 16 }).map((_, i) => {
|
|
||||||
const row = Math.floor(i / 4);
|
|
||||||
const col = i % 4;
|
|
||||||
const top = 10 + row * 25 + (col % 2 === 0 ? 5 : -5);
|
|
||||||
const left = 5 + col * 30 + (row % 2 === 0 ? 0 : 10);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<Card
|
||||||
className="absolute rotate-[-30deg] whitespace-nowrap font-black text-lg opacity-40 shadow-[0px_0px_1px_rgba(255,255,255,0.5)]"
|
className={cn("relative", {
|
||||||
key={i}
|
"relative opacity-80 grayscale": isActuallyExpired,
|
||||||
style={{
|
"relative hidden opacity-60 blur-[0.3px] grayscale":
|
||||||
top: `${top}%`,
|
item.status === 4,
|
||||||
left: `${left}%`,
|
})}
|
||||||
}}
|
key={item.id}
|
||||||
>
|
>
|
||||||
{
|
{shouldShowWatermark && (
|
||||||
statusWatermarks[
|
<div
|
||||||
item.status as keyof typeof statusWatermarks
|
className={cn(
|
||||||
]
|
"pointer-events-none absolute top-0 left-0 z-10 h-full w-full overflow-hidden mix-blend-difference brightness-150 contrast-200 invert-[0.2]",
|
||||||
}
|
{
|
||||||
</span>
|
"text-destructive": item.status === 2,
|
||||||
);
|
"text-white": isActuallyExpired || item.status === 4,
|
||||||
})}
|
}
|
||||||
</div>
|
)}
|
||||||
</div>
|
>
|
||||||
)}
|
<div className="absolute inset-0">
|
||||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0">
|
{Array.from({ length: 16 }).map((_, i) => {
|
||||||
<CardTitle className="font-medium">
|
const row = Math.floor(i / 4);
|
||||||
{item.subscribe.name}
|
const col = i % 4;
|
||||||
<p className="mt-1 text-foreground/50 text-sm">
|
const top = 10 + row * 25 + (col % 2 === 0 ? 5 : -5);
|
||||||
{formatDate(item.start_time)}
|
const left = 5 + col * 30 + (row % 2 === 0 ? 0 : 10);
|
||||||
</p>
|
|
||||||
</CardTitle>
|
return (
|
||||||
{item.status !== 4 && (
|
<span
|
||||||
<div className="flex flex-wrap gap-2">
|
className="absolute rotate-[-30deg] whitespace-nowrap font-black text-lg opacity-40 shadow-[0px_0px_1px_rgba(255,255,255,0.5)]"
|
||||||
<AlertDialog>
|
key={i}
|
||||||
<AlertDialogTrigger asChild>
|
style={{
|
||||||
<Button size="sm" variant="destructive">
|
top: `${top}%`,
|
||||||
{t("resetSubscription", "Reset Subscription")}
|
left: `${left}%`,
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t("prompt", "Prompt")}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t(
|
|
||||||
"confirmResetSubscription",
|
|
||||||
"Are you sure you want to reset your subscription?"
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await resetUserSubscribeToken({
|
|
||||||
user_subscribe_id: item.id,
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
toast.success(t("resetSuccess", "Reset Success"));
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("confirm", "Confirm")}
|
{
|
||||||
</AlertDialogAction>
|
statusWatermarks[
|
||||||
</AlertDialogFooter>
|
item.status as keyof typeof statusWatermarks
|
||||||
</AlertDialogContent>
|
]
|
||||||
</AlertDialog>
|
}
|
||||||
<ResetTraffic
|
</span>
|
||||||
id={item.id}
|
);
|
||||||
replacement={item.subscribe.replacement}
|
})}
|
||||||
/>
|
</div>
|
||||||
{item.expire_time !== 0 && (
|
|
||||||
<Renewal id={item.id} subscribe={item.subscribe} />
|
|
||||||
)}
|
|
||||||
<Unsubscribe
|
|
||||||
allowDeduction={item.subscribe.allow_deduction}
|
|
||||||
id={item.id}
|
|
||||||
onSuccess={refetch}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0">
|
||||||
<CardContent>
|
<CardTitle className="font-medium">
|
||||||
<ul className="grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4">
|
{item.subscribe.name}
|
||||||
<li>
|
<p className="mt-1 text-foreground/50 text-sm">
|
||||||
<span className="text-muted-foreground">
|
{formatDate(item.start_time)}
|
||||||
{t("used", "Used")}
|
</p>
|
||||||
</span>
|
</CardTitle>
|
||||||
<span className="font-bold text-2xl">
|
{item.status !== 4 && (
|
||||||
<Display
|
<div className="flex flex-wrap gap-2">
|
||||||
type="traffic"
|
<AlertDialog>
|
||||||
unlimited={!item.traffic}
|
<AlertDialogTrigger asChild>
|
||||||
value={item.upload + item.download}
|
<Button size="sm" variant="destructive">
|
||||||
/>
|
{t("resetSubscription", "Reset Subscription")}
|
||||||
</span>
|
</Button>
|
||||||
</li>
|
</AlertDialogTrigger>
|
||||||
<li>
|
<AlertDialogContent>
|
||||||
<span className="text-muted-foreground">
|
<AlertDialogHeader>
|
||||||
{t("totalTraffic", "Total Traffic")}
|
<AlertDialogTitle>
|
||||||
</span>
|
{t("prompt", "Prompt")}
|
||||||
<span className="font-bold text-2xl">
|
</AlertDialogTitle>
|
||||||
<Display
|
<AlertDialogDescription>
|
||||||
type="traffic"
|
{t(
|
||||||
unlimited={!item.traffic}
|
"confirmResetSubscription",
|
||||||
value={item.traffic}
|
"Are you sure you want to reset your subscription?"
|
||||||
/>
|
)}
|
||||||
</span>
|
</AlertDialogDescription>
|
||||||
</li>
|
</AlertDialogHeader>
|
||||||
<li>
|
<AlertDialogFooter>
|
||||||
<span className="text-muted-foreground">
|
<AlertDialogCancel>
|
||||||
{t("nextResetDays", "Next Reset Days")}
|
{t("cancel", "Cancel")}
|
||||||
</span>
|
</AlertDialogCancel>
|
||||||
<span className="font-semibold text-2xl">
|
<AlertDialogAction
|
||||||
{item.reset_time
|
onClick={async () => {
|
||||||
? differenceInDays(
|
await resetUserSubscribeToken({
|
||||||
new Date(item.reset_time),
|
user_subscribe_id: item.id,
|
||||||
new Date()
|
});
|
||||||
)
|
await refetch();
|
||||||
: t("noReset", "No Reset")}
|
toast.success(
|
||||||
</span>
|
t("resetSuccess", "Reset Success")
|
||||||
</li>
|
);
|
||||||
<li>
|
}}
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t("expirationDays", "Expiration Days")}
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold text-2xl">
|
|
||||||
{}
|
|
||||||
{item.expire_time
|
|
||||||
? differenceInDays(
|
|
||||||
new Date(item.expire_time),
|
|
||||||
new Date()
|
|
||||||
) || t("unknown", "Unknown")
|
|
||||||
: t("noLimit", "No Limit")}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Separator className="mt-4" />
|
|
||||||
<Accordion
|
|
||||||
className="w-full"
|
|
||||||
collapsible
|
|
||||||
defaultValue="0"
|
|
||||||
type="single"
|
|
||||||
>
|
|
||||||
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
|
|
||||||
<AccordionItem key={url} value={String(index)}>
|
|
||||||
<AccordionTrigger className="hover:no-underline">
|
|
||||||
<div className="flex w-full flex-row items-center justify-between">
|
|
||||||
<CardTitle className="font-medium text-sm">
|
|
||||||
{t("subscriptionUrl", "Subscription URL")}{" "}
|
|
||||||
{index + 1}
|
|
||||||
</CardTitle>
|
|
||||||
|
|
||||||
<CopyToClipboard
|
|
||||||
onCopy={(_, result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success(t("copySuccess", "Copy Success"));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
text={url}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mr-4 flex cursor-pointer rounded p-2 text-primary text-sm hover:bg-accent"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<Icon className="mr-2 size-5" icon="uil:copy" />
|
{t("confirm", "Confirm")}
|
||||||
{t("copy", "Copy")}
|
</AlertDialogAction>
|
||||||
</span>
|
</AlertDialogFooter>
|
||||||
</CopyToClipboard>
|
</AlertDialogContent>
|
||||||
</div>
|
</AlertDialog>
|
||||||
</AccordionTrigger>
|
<ResetTraffic
|
||||||
<AccordionContent>
|
id={item.id}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
replacement={item.subscribe.replacement}
|
||||||
{applications
|
/>
|
||||||
?.filter(
|
{item.expire_time !== 0 && (
|
||||||
(application) =>
|
<Renewal id={item.id} subscribe={item.subscribe} />
|
||||||
!!(
|
)}
|
||||||
application.download_link?.[platform] &&
|
<Unsubscribe
|
||||||
application.scheme
|
allowDeduction={item.subscribe.allow_deduction}
|
||||||
)
|
id={item.id}
|
||||||
|
onSuccess={refetch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4">
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("used", "Used")}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-2xl">
|
||||||
|
<Display
|
||||||
|
type="traffic"
|
||||||
|
unlimited={!item.traffic}
|
||||||
|
value={item.upload + item.download}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("totalTraffic", "Total Traffic")}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-2xl">
|
||||||
|
<Display
|
||||||
|
type="traffic"
|
||||||
|
unlimited={!item.traffic}
|
||||||
|
value={item.traffic}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("nextResetDays", "Next Reset Days")}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-2xl">
|
||||||
|
{item.reset_time
|
||||||
|
? differenceInDays(
|
||||||
|
new Date(item.reset_time),
|
||||||
|
new Date()
|
||||||
)
|
)
|
||||||
.map((application) => {
|
: t("noReset", "No Reset")}
|
||||||
const downloadUrl =
|
</span>
|
||||||
application.download_link?.[platform];
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("expirationDays", "Expiration Days")}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-2xl">
|
||||||
|
{}
|
||||||
|
{item.expire_time
|
||||||
|
? differenceInDays(
|
||||||
|
new Date(item.expire_time),
|
||||||
|
new Date()
|
||||||
|
) || t("unknown", "Unknown")
|
||||||
|
: t("noLimit", "No Limit")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
<Accordion
|
||||||
|
className="w-full"
|
||||||
|
collapsible
|
||||||
|
defaultValue="0"
|
||||||
|
type="single"
|
||||||
|
>
|
||||||
|
{getUserSubscribe(item.token, protocol)?.map(
|
||||||
|
(url, index) => (
|
||||||
|
<AccordionItem key={url} value={String(index)}>
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<CardTitle className="font-medium text-sm">
|
||||||
|
{t("subscriptionUrl", "Subscription URL")}{" "}
|
||||||
|
{index + 1}
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
const handleCopy = (
|
<CopyToClipboard
|
||||||
_: string,
|
onCopy={(_, result) => {
|
||||||
result: boolean
|
if (result) {
|
||||||
) => {
|
|
||||||
if (result) {
|
|
||||||
const href = getAppSubLink(
|
|
||||||
url,
|
|
||||||
application.scheme
|
|
||||||
);
|
|
||||||
const showSuccessMessage = () => {
|
|
||||||
toast.success(
|
toast.success(
|
||||||
<>
|
t("copySuccess", "Copy Success")
|
||||||
<p>
|
|
||||||
{t("copySuccess", "Copy Success")}
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
"manualImportMessage",
|
|
||||||
"Please import manually"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
text={url}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mr-4 flex cursor-pointer rounded p-2 text-primary text-sm hover:bg-accent"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="mr-2 size-5"
|
||||||
|
icon="uil:copy"
|
||||||
|
/>
|
||||||
|
{t("copy", "Copy")}
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||||
|
{applications
|
||||||
|
?.filter(
|
||||||
|
(application) =>
|
||||||
|
!!(
|
||||||
|
application.download_link?.[platform] &&
|
||||||
|
application.scheme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((application) => {
|
||||||
|
const downloadUrl =
|
||||||
|
application.download_link?.[platform];
|
||||||
|
|
||||||
|
const handleCopy = (
|
||||||
|
_: string,
|
||||||
|
result: boolean
|
||||||
|
) => {
|
||||||
|
if (result) {
|
||||||
|
const href = getAppSubLink(
|
||||||
|
url,
|
||||||
|
application.scheme
|
||||||
|
);
|
||||||
|
const showSuccessMessage = () => {
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t("copySuccess", "Copy Success")}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
"manualImportMessage",
|
||||||
|
"Please import manually"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBrowser() && href) {
|
||||||
|
window.location.href = href;
|
||||||
|
const checkRedirect = setTimeout(() => {
|
||||||
|
if (window.location.href !== href) {
|
||||||
|
showSuccessMessage();
|
||||||
|
}
|
||||||
|
clearTimeout(checkRedirect);
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isBrowser() && href) {
|
return (
|
||||||
window.location.href = href;
|
<div
|
||||||
const checkRedirect = setTimeout(() => {
|
className="flex size-full flex-col items-center justify-between gap-2 text-muted-foreground text-xs"
|
||||||
if (window.location.href !== href) {
|
key={application.name}
|
||||||
showSuccessMessage();
|
>
|
||||||
}
|
<span>{application.name}</span>
|
||||||
clearTimeout(checkRedirect);
|
|
||||||
}, 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccessMessage();
|
{application.icon && (
|
||||||
}
|
<img
|
||||||
};
|
alt={application.name}
|
||||||
|
className="p-1"
|
||||||
return (
|
height={64}
|
||||||
<div
|
src={application.icon}
|
||||||
className="flex size-full flex-col items-center justify-between gap-2 text-muted-foreground text-xs"
|
width={64}
|
||||||
key={application.name}
|
/>
|
||||||
>
|
)}
|
||||||
<span>{application.name}</span>
|
<div className="flex">
|
||||||
|
{downloadUrl && (
|
||||||
{application.icon && (
|
<Button
|
||||||
<img
|
asChild
|
||||||
alt={application.name}
|
className={
|
||||||
className="p-1"
|
application.scheme
|
||||||
height={64}
|
? "rounded-r-none px-1.5"
|
||||||
src={application.icon}
|
: "px-1.5"
|
||||||
width={64}
|
}
|
||||||
/>
|
size="sm"
|
||||||
)}
|
variant="secondary"
|
||||||
<div className="flex">
|
>
|
||||||
{downloadUrl && (
|
<a
|
||||||
<Button
|
href={downloadUrl}
|
||||||
asChild
|
rel="noopener noreferrer"
|
||||||
className={
|
target="_blank"
|
||||||
application.scheme
|
>
|
||||||
? "rounded-r-none px-1.5"
|
{t("download", "Download")}
|
||||||
: "px-1.5"
|
</a>
|
||||||
}
|
</Button>
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={downloadUrl}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{t("download", "Download")}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{application.scheme && (
|
|
||||||
<CopyToClipboard
|
|
||||||
onCopy={handleCopy}
|
|
||||||
text={getAppSubLink(
|
|
||||||
url,
|
|
||||||
application.scheme
|
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<Button
|
{application.scheme && (
|
||||||
className={
|
<CopyToClipboard
|
||||||
downloadUrl
|
onCopy={handleCopy}
|
||||||
? "rounded-l-none p-2"
|
text={getAppSubLink(
|
||||||
: "p-2"
|
url,
|
||||||
}
|
application.scheme
|
||||||
size="sm"
|
)}
|
||||||
>
|
>
|
||||||
{t("import", "Import")}
|
<Button
|
||||||
</Button>
|
className={
|
||||||
</CopyToClipboard>
|
downloadUrl
|
||||||
)}
|
? "rounded-l-none p-2"
|
||||||
</div>
|
: "p-2"
|
||||||
</div>
|
}
|
||||||
);
|
size="sm"
|
||||||
})}
|
>
|
||||||
<div className="hidden size-full flex-col items-center justify-between gap-2 text-muted-foreground text-sm lg:flex">
|
{t("import", "Import")}
|
||||||
<span>{t("qrCode", "QR Code")}</span>
|
</Button>
|
||||||
<QRCodeCanvas
|
</CopyToClipboard>
|
||||||
bgColor="transparent"
|
)}
|
||||||
fgColor="rgb(59, 130, 246)"
|
</div>
|
||||||
size={80}
|
</div>
|
||||||
value={url}
|
);
|
||||||
/>
|
})}
|
||||||
<span className="text-center">
|
<div className="hidden size-full flex-col items-center justify-between gap-2 text-muted-foreground text-sm lg:flex">
|
||||||
{t("scanToSubscribe", "Scan to Subscribe")}
|
<span>{t("qrCode", "QR Code")}</span>
|
||||||
</span>
|
<QRCodeCanvas
|
||||||
</div>
|
bgColor="transparent"
|
||||||
</div>
|
fgColor="rgb(59, 130, 246)"
|
||||||
</AccordionContent>
|
size={80}
|
||||||
</AccordionItem>
|
value={url}
|
||||||
))}
|
/>
|
||||||
</Accordion>
|
<span className="text-center">
|
||||||
</CardContent>
|
{t("scanToSubscribe", "Scan to Subscribe")}
|
||||||
</Card>
|
</span>
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -34,6 +34,15 @@ export function DatePicker({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDate(undefined);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -47,16 +56,14 @@ export function DatePicker({
|
|||||||
{value ? intlFormat(value) : <span>{placeholder}</span>}
|
{value ? intlFormat(value) : <span>{placeholder}</span>}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{value && (
|
{value && (
|
||||||
<X
|
<button
|
||||||
className="size-4 opacity-50 hover:opacity-100"
|
className="flex items-center"
|
||||||
onClick={(e) => {
|
onClick={handleClear}
|
||||||
e.stopPropagation();
|
onMouseDown={handleClear}
|
||||||
setDate(undefined);
|
type="button"
|
||||||
if (onChange) {
|
>
|
||||||
onChange(0);
|
<X className="size-4 opacity-50 hover:opacity-100" />
|
||||||
}
|
</button>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<CalendarIcon className="size-4" />
|
<CalendarIcon className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user