diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
@@ -133,7 +133,7 @@
"settings": {
"subscription_link": {
"copied": "Subscription Link Copied",
- "copy": "Copy Subscription Link",
+ "copy": "Subscription Link",
"error": "Unable to copy Subscription Link"
}
},
diff --git a/dashboard/src/components/copy-button/index.tsx b/dashboard/src/components/copy-button/index.tsx
@@ -1,10 +1,15 @@
-import { Button, Tooltip, TooltipContent, TooltipTrigger } from '@marzneshin/components';
-import CopyToClipboard from 'react-copy-to-clipboard';
-import { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { CopyCheck, CopyX, ClipboardCopy, LucideIcon } from 'lucide-react';
-import { toast } from 'sonner';
-import { cn } from '@marzneshin/utils';
+import {
+ Button,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@marzneshin/components";
+import CopyToClipboard from "react-copy-to-clipboard";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { CopyCheck, CopyX, ClipboardCopy, LucideIcon } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@marzneshin/utils";
interface CopyToClipboardButtonProps {
text: string;
@@ -23,8 +28,9 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
successMessage,
copyIcon,
copyLabel,
- errorLabel, className,
- tooltipMsg
+ errorLabel,
+ className,
+ tooltipMsg,
}) => {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
@@ -40,7 +46,7 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
}, [copied, successMessage, t]);
useEffect(() => {
- setHealthy(text !== '');
+ setHealthy(text !== "");
}, [text]);
const handleClick = () => {
@@ -49,26 +55,35 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
}
};
- const tooltip = tooltipMsg ? tooltipMsg : (copyLabel ? copyLabel : undefined)
+ const tooltip = tooltipMsg ? tooltipMsg : copyLabel ? copyLabel : undefined;
- const Icon = copied ? CopyCheck : (healthy ? (copyIcon ? copyIcon : ClipboardCopy) : CopyX)
- const isWithLabel = successMessage && copyLabel && errorLabel
+ const Icon = copied
+ ? CopyCheck
+ : healthy
+ ? copyIcon
+ ? copyIcon
+ : ClipboardCopy
+ : CopyX;
+ const isWithLabel = successMessage && copyLabel && errorLabel;
return (
<Tooltip>
- <TooltipTrigger>
+ <TooltipTrigger
+ className={cn(className, "flex flex-row items-center gap-2")}
+ >
<CopyToClipboard text={text}>
- <Button className={cn(className, "p-2 flex flex-row items-center gap-2")} disabled={disabled || !healthy} onClick={handleClick}>
+ <Button
+ className="w-full"
+ disabled={disabled || !healthy}
+ onClick={handleClick}
+ >
<Icon />
- {isWithLabel && t(copied ? successMessage : (healthy ? copyLabel : errorLabel))}
+ {isWithLabel &&
+ t(copied ? successMessage : healthy ? copyLabel : errorLabel)}
</Button>
</CopyToClipboard>
</TooltipTrigger>
- {tooltip &&
- <TooltipContent>
- {tooltip}
- </TooltipContent>
- }
- </Tooltip >
+ {tooltip && <TooltipContent>{tooltip}</TooltipContent>}
+ </Tooltip>
);
};
diff --git a/dashboard/src/components/stacks/horizontal.tsx b/dashboard/src/components/stacks/horizontal.tsx
@@ -1,5 +1,9 @@
-import { FC, PropsWithChildren } from "react";
+import type { FC, PropsWithChildren, HTMLAttributes } from "react";
+import { cn } from "@marzneshin/utils";
-export const HStack: FC<PropsWithChildren> = ({ children }) => {
- return <div className="flex flex-row gap-2">{children}</div>;
+export const HStack: FC<PropsWithChildren & HTMLAttributes<HTMLDivElement>> = ({
+ children,
+ className,
+}) => {
+ return <div className={cn("flex flex-row gap-2", className)}>{children}</div>;
};
diff --git a/dashboard/src/features/users/dialogs/settings/index.tsx b/dashboard/src/features/users/dialogs/settings/index.tsx
@@ -8,39 +8,53 @@ import {
TabsContent,
TabsList,
TabsTrigger,
-} from "@marzneshin/components"
-import { FC } from "react"
-import { useTranslation } from "react-i18next"
-import { UserServicesTable, UserType } from "@marzneshin/features/users"
-import { UserInfoTable, QRCodeSection } from "./user-info"
+} from "@marzneshin/components";
+import { UserServicesTable } from "@marzneshin/features/users";
+import type { UserType } from "@marzneshin/features/users";
+import type { FC } from "react";
+import { useTranslation } from "react-i18next";
+import { QRCodeSection, UserInfoTable } from "./user-info";
+import { SubscriptionActions } from "./user-info/subscription-actions";
interface UsersSettingsDialogProps {
- onOpenChange: (state: boolean) => void
- open: boolean
- entity: UserType | null
+ onOpenChange: (state: boolean) => void;
+ open: boolean;
+ entity: UserType | null;
}
-export const UsersSettingsDialog: FC<UsersSettingsDialogProps> = ({ onOpenChange, open, entity }) => {
+export const UsersSettingsDialog: FC<UsersSettingsDialogProps> = ({
+ onOpenChange,
+ open,
+ entity,
+}) => {
const { t } = useTranslation();
if (entity) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:min-w-full md:min-w-[700px]" >
- <SheetHeader>
- <SheetTitle>
- {t('settings')}
- </SheetTitle>
- </SheetHeader>
+ <SheetContent className="sm:min-w-full md:min-w-[700px]">
<ScrollArea className="flex flex-col gap-4 m-3 h-full">
+ <SheetHeader className="my-1">
+ <SheetTitle>{t("settings")}</SheetTitle>
+ </SheetHeader>
<Tabs defaultValue="info" className="w-full h-full">
<TabsList className="w-full bg-accent">
- <TabsTrigger className="w-full" value="info"> {t('user_info')} </TabsTrigger>
- <TabsTrigger className="w-full" value="services"> {t('services')} </TabsTrigger>
+ <TabsTrigger className="w-full" value="info">
+ {" "}
+ {t("user_info")}{" "}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="services">
+ {" "}
+ {t("services")}{" "}
+ </TabsTrigger>
</TabsList>
- <TabsContent value="info" className="flex flex-col gap-4">
+ <TabsContent
+ value="info"
+ className="flex flex-col gap-2 w-full h-full"
+ >
<UserInfoTable entity={entity} />
<QRCodeSection entity={entity} />
+ <SubscriptionActions entity={entity} />
</TabsContent>
<TabsContent value="services">
<UserServicesTable user={entity} />
@@ -49,6 +63,6 @@ export const UsersSettingsDialog: FC<UsersSettingsDialogProps> = ({ onOpenChange
</ScrollArea>
</SheetContent>
</Sheet>
- )
+ );
}
-}
+};
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/qrcode.tsx b/dashboard/src/features/users/dialogs/settings/user-info/qrcode.tsx
@@ -1,53 +1,34 @@
-
-import {
- useUserSubscriptionRevokeCmd,
- UserType
-} from '@marzneshin/features/users';
+import type { UserType } from "@marzneshin/features/users";
import QRCode from "react-qr-code";
import { QrCodeIcon } from "lucide-react";
-import { FC } from 'react'
+import type { FC } from "react";
import {
- Button,
Card,
CardContent,
CardHeader,
- CardTitle
-} from '@marzneshin/components';
-import { useTranslation } from 'react-i18next';
-import { SubscriptionLinkButton } from './subcription-link-button';
-import { getSubscriptionLink } from '@marzneshin/utils';
+ CardTitle,
+} from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import { getSubscriptionLink } from "@marzneshin/utils";
-interface QRCodeProps {
- entity: UserType
+export interface QRCodeProps {
+ entity: UserType;
}
-export const QRCodeSection: FC<QRCodeProps> = (
- { entity }
-) => {
+export const QRCodeSection: FC<QRCodeProps> = ({ entity }) => {
const subscribeQrLink = getSubscriptionLink(entity.subscription_url);
- const { mutate: revokeSubscription } = useUserSubscriptionRevokeCmd()
- const { t } = useTranslation()
+ const { t } = useTranslation();
return (
<Card>
<CardHeader className="flex flex-row justify-between items-center w-full">
<CardTitle className="flex flex-row gap-4 justify-start">
<QrCodeIcon />
- {t('subscription')}
+ {t("subscription")} QRCode
</CardTitle>
- <Button className="w-[9rem]" onClick={() => revokeSubscription(entity)}>
- {t('page.users.revoke_subscription')}
- </Button>
</CardHeader>
- <CardContent className='flex flex-col justify-center items-center py-10 w-full h-full'>
- <div className="flex flex-col gap-3 justify-center items-center w-1/2 h-1/2">
- <QRCode
- size={150}
- className="w-full h-full"
- value={subscribeQrLink}
- />
- <SubscriptionLinkButton subscriptionLink={subscribeQrLink} />
- </div>
+ <CardContent className="flex flex-col justify-center items-center py-10 w-full h-full">
+ <QRCode size={150} className="size-2/3" value={subscribeQrLink} />
</CardContent>
</Card>
);
-}
+};
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/subcription-link-button.tsx b/dashboard/src/features/users/dialogs/settings/user-info/subcription-link-button.tsx
@@ -1,24 +1,26 @@
-import { useTranslation } from 'react-i18next';
-import { CopyToClipboardButton } from '@marzneshin/components';
-import { FC } from 'react';
-import { SquareCode } from 'lucide-react';
-import { getSubscriptionLink } from '@marzneshin/utils';
+import { useTranslation } from "react-i18next";
+import { CopyToClipboardButton } from "@marzneshin/components";
+import type { FC } from "react";
+import { SquareCode } from "lucide-react";
+import { getSubscriptionLink } from "@marzneshin/utils";
interface SubscriptionLinkButtonProps {
- subscriptionLink: string;
+ subscriptionLink: string;
}
-export const SubscriptionLinkButton: FC<SubscriptionLinkButtonProps> = ({ subscriptionLink }) => {
- const { t } = useTranslation();
+export const SubscriptionLinkButton: FC<SubscriptionLinkButtonProps> = ({
+ subscriptionLink,
+}) => {
+ const { t } = useTranslation();
- return (
- <CopyToClipboardButton
- text={getSubscriptionLink(subscriptionLink)}
- successMessage={t('page.users.settings.subscription_link.copied')}
- copyLabel={t('page.users.settings.subscription_link.copy')}
- errorLabel={t('page.users.settings.subscription_link.error')}
- copyIcon={SquareCode}
- className="w-full"
- />
- );
+ return (
+ <CopyToClipboardButton
+ text={getSubscriptionLink(subscriptionLink)}
+ successMessage={t("page.users.settings.subscription_link.copied")}
+ copyLabel={t("page.users.settings.subscription_link.copy")}
+ errorLabel={t("page.users.settings.subscription_link.error")}
+ copyIcon={SquareCode}
+ className="w-full"
+ />
+ );
};
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/subscription-actions.tsx b/dashboard/src/features/users/dialogs/settings/user-info/subscription-actions.tsx
@@ -0,0 +1,21 @@
+import { Button, HStack } from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import { getSubscriptionLink } from "@marzneshin/utils";
+import { useUserSubscriptionRevokeCmd } from "@marzneshin/features/users";
+import type { QRCodeProps } from "./qrcode";
+import type { FC } from 'react';
+
+import { SubscriptionLinkButton } from "./subcription-link-button";
+export const SubscriptionActions: FC<QRCodeProps> = ({ entity }) => {
+ const subscribeQrLink = getSubscriptionLink(entity.subscription_url);
+ const { mutate: revokeSubscription } = useUserSubscriptionRevokeCmd();
+ const { t } = useTranslation();
+ return (
+ <HStack className="w-full items-center">
+ <SubscriptionLinkButton subscriptionLink={subscribeQrLink} />
+ <Button className="w-full" onClick={() => revokeSubscription(entity)}>
+ {t("page.users.revoke_subscription")}
+ </Button>
+ </HStack>
+ );
+};
diff --git a/dashboard/src/features/users/dialogs/settings/user-info/table.tsx b/dashboard/src/features/users/dialogs/settings/user-info/table.tsx
@@ -7,15 +7,15 @@ import {
Table,
TableBody,
TableCell,
- TableRow
+ TableRow,
} from "@marzneshin/components";
-import { FC } from 'react';
+import { FC } from "react";
import {
useUserUsageResetCmd,
UserType,
UsersStatus,
UsersStatusBadge,
-} from '@marzneshin/features/users';
+} from "@marzneshin/features/users";
import { CircularProgress } from "@nextui-org/progress";
import { useTranslation } from "react-i18next";
import { format, isDate, isValid } from "date-fns";
@@ -25,68 +25,101 @@ interface UserInfoTableProps {
entity: UserType;
}
-const TableRowWithCell: FC<{ label: string; value: string | number | JSX.Element }> = ({ label, value }) => (
+const TableRowWithCell: FC<{
+ label: string;
+ value: string | number | JSX.Element;
+}> = ({ label, value }) => (
<TableRow>
<TableCell>{label}</TableCell>
<TableCell>{value}</TableCell>
</TableRow>
);
-const DateTableRow: FC<{ label: string; date: Date | string }> = ({ label, date }) => {
- let formattedDate = '';
+const DateTableRow: FC<{ label: string; date: Date | string }> = ({
+ label,
+ date,
+}) => {
+ let formattedDate = "";
if (date && isValid(date)) {
formattedDate = format(date, "PPP");
}
- return (
- <TableRowWithCell label={label} value={formattedDate} />
- );
+ return <TableRowWithCell label={label} value={formattedDate} />;
};
-const CircularProgressBarRow: FC<{ label: string; value: number; limit: number }> = ({ label, value, limit }) => (
+const CircularProgressBarRow: FC<{
+ label: string;
+ value: number;
+ limit: number;
+}> = ({ label, value, limit }) => (
<TableRow>
<TableCell>{label}</TableCell>
- <TableCell><CircularProgress size="sm" value={value / limit * 100} /></TableCell>
+ <TableCell>
+ <CircularProgress size="sm" value={(value / limit) * 100} />
+ </TableCell>
</TableRow>
);
export const UserInfoTable: FC<UserInfoTableProps> = ({ entity }) => {
const { t } = useTranslation();
- const { mutate: resetUsage } = useUserUsageResetCmd()
- const expireDate = entity.expire ? (isDate(entity.expire) ? entity.expire : new Date(entity.expire)) : null;
+ const { mutate: resetUsage } = useUserUsageResetCmd();
+ const expireDate = entity.expire
+ ? isDate(entity.expire)
+ ? entity.expire
+ : new Date(entity.expire)
+ : null;
return (
<Card>
<CardHeader className="flex flex-row justify-between items-center w-full">
- <CardTitle>{t('user_info')}</CardTitle>
- <div className="flex flex-row justify-center items-center gap-3">
- <Button onClick={() => resetUsage(entity)}>
- {t('page.users.reset_usage')}
- </Button>
- <UserStatusEnableButton user={entity} />
- </div>
+ <CardTitle>{t("user_info")}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
- <TableRowWithCell label={t('username')} value={entity.username} />
- <TableRowWithCell label={t('status')} value={<UsersStatusBadge status={UsersStatus[entity.status]} />} />
- {(expireDate && entity.on_hold_expire_duration === 0) ? (
- <DateTableRow label={t('page.users.expire_date')} date={expireDate} />
+ <TableRowWithCell label={t("username")} value={entity.username} />
+ <TableRowWithCell
+ label={t("status")}
+ value={<UsersStatusBadge status={UsersStatus[entity.status]} />}
+ />
+ {expireDate && entity.on_hold_expire_duration === 0 ? (
+ <DateTableRow
+ label={t("page.users.expire_date")}
+ date={expireDate}
+ />
) : (
<>
- <TableRowWithCell label={t('page.users.on_hold_expire_duration')} value={entity.on_hold_expire_duration} />
- {entity.on_hold_timeout &&
- <DateTableRow label={t('page.users.expire_date')} date={entity.on_hold_timeout} />
- }
+ <TableRowWithCell
+ label={t("page.users.on_hold_expire_duration")}
+ value={entity.on_hold_expire_duration}
+ />
+ {entity.on_hold_timeout && (
+ <DateTableRow
+ label={t("page.users.expire_date")}
+ date={entity.on_hold_timeout}
+ />
+ )}
</>
)}
- {entity.data_limit &&
- <CircularProgressBarRow label={t('page.users.used_traffic')} value={entity.used_traffic} limit={entity.data_limit} />
- }
- <DateTableRow label={t('page.users.online_at')} date={entity.online_at} />
- <TableRowWithCell label={t('note')} value={entity.note} />
+ {entity.data_limit && (
+ <CircularProgressBarRow
+ label={t("page.users.used_traffic")}
+ value={entity.used_traffic}
+ limit={entity.data_limit}
+ />
+ )}
+ <DateTableRow
+ label={t("page.users.online_at")}
+ date={entity.online_at}
+ />
+ <TableRowWithCell label={t("note")} value={entity.note} />
</TableBody>
</Table>
+ <div className="flex flex-row justify-center items-center gap-2">
+ <Button className="w-full" onClick={() => resetUsage(entity)}>
+ {t("page.users.reset_usage")}
+ </Button>
+ <UserStatusEnableButton user={entity} />
+ </div>
</CardContent>
</Card>
);