diff --git a/dashboard/src/components/charts/index.ts b/dashboard/src/components/charts/index.ts new file mode 100644 index 0000000..bc7f0c1 --- /dev/null +++ b/dashboard/src/components/charts/index.ts @@ -0,0 +1 @@ +export * from "./pie"; diff --git a/dashboard/src/components/charts/pie/index.tsx b/dashboard/src/components/charts/pie/index.tsx new file mode 100644 index 0000000..9878354 --- /dev/null +++ b/dashboard/src/components/charts/pie/index.tsx @@ -0,0 +1,67 @@ +import { FC, useState } from 'react'; +import { Pie } from '@visx/shape'; +import { Group } from '@visx/group'; +import { Text } from '@visx/text'; + +export type PieChartDataRecord = { + symbol: string, + amount: number, + color: string +}; + +export interface PieChartProps { + data: PieChartDataRecord[]; + width: number; + height: number; + total: number; + label: string; +} + +export const PieChart: FC = ({ data, width, height, total, label }) => { + const half = width / 2; + const [active, setActive] = useState(null); + + return ( + + + data.amount} + outerRadius={half} + innerRadius={({ data }) => (active && (active.symbol === data.symbol) ? half - 14 : half - 8)} + padAngle={0.01} + > + {pie => { + return pie.arcs.map(arc => { + return ( + setActive(arc.data)} + onMouseLeave={() => setActive(null)} + > + + + ) + }) + }} + + + {active ? active.amount : total} + + + {label} + + + + ); +}; diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts index e0ea146..5356af5 100644 --- a/dashboard/src/components/index.ts +++ b/dashboard/src/components/index.ts @@ -9,5 +9,6 @@ export * from "./entity-table"; export * from "./copy-button"; export * from "./page"; export * from "./stacks"; -export * from "./stats-widgets"; +export * from "./widgets"; +export * from "./charts"; export * from "./support-us"; diff --git a/dashboard/src/components/stats-widgets/mini-widget/index.tsx b/dashboard/src/components/stats-widgets/mini-widget/index.tsx deleted file mode 100644 index b4b76b6..0000000 --- a/dashboard/src/components/stats-widgets/mini-widget/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle -} from '@marzneshin/components' -import { FC } from 'react' - -export interface MiniWidgetProps { - title: JSX.Element | string; - content: JSX.Element | string; - footer?: JSX.Element | string; -} - -export const MiniWidget: FC = ({ footer, content, title }) => { - return ( - - - - {title} - - - - {content} - - {footer && {footer} } - - ) -} diff --git a/dashboard/src/components/stats-widgets/index.ts b/dashboard/src/components/widgets/index.ts similarity index 100% rename from dashboard/src/components/stats-widgets/index.ts rename to dashboard/src/components/widgets/index.ts diff --git a/dashboard/src/components/widgets/mini-widget/index.tsx b/dashboard/src/components/widgets/mini-widget/index.tsx new file mode 100644 index 0000000..74a4dc2 --- /dev/null +++ b/dashboard/src/components/widgets/mini-widget/index.tsx @@ -0,0 +1,30 @@ +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@marzneshin/components'; +import type { FC, PropsWithChildren } from 'react'; + +export interface MiniWidgetProps extends PropsWithChildren { + title: JSX.Element | string; + content?: JSX.Element | string; + footer?: JSX.Element | string; +} + +export const MiniWidget: FC = ({ footer, content, children, title }) => { + return ( + + + + {title} + + + + {content || children} + + {footer && {footer} } + + ) +} diff --git a/dashboard/src/features/users/services/index.ts b/dashboard/src/features/users/services/index.ts index 3887a8b..6a9df23 100644 --- a/dashboard/src/features/users/services/index.ts +++ b/dashboard/src/features/users/services/index.ts @@ -1,7 +1,8 @@ -export * from './users.query' -export * from './update-user.mutate' -export * from './create-user.mutate' -export * from './delete-user.mutate' -export * from './user-usage-reset.command' -export * from './user-subscription-revoke.command' -export * from './user-status.mutate' +export * from "./users.query"; +export * from "./users-stats.query"; +export * from "./update-user.mutate"; +export * from "./create-user.mutate"; +export * from "./delete-user.mutate"; +export * from "./user-usage-reset.command"; +export * from "./user-subscription-revoke.command"; +export * from "./user-status.mutate"; diff --git a/dashboard/src/features/users/widgets/users-stats-widget/index.tsx b/dashboard/src/features/users/widgets/users-stats-widget/index.tsx index 33e92b7..92d6405 100644 --- a/dashboard/src/features/users/widgets/users-stats-widget/index.tsx +++ b/dashboard/src/features/users/widgets/users-stats-widget/index.tsx @@ -1,96 +1,50 @@ -import { MiniWidget } from '@marzneshin/components/stats-widgets'; -import { UsersIcon } from 'lucide-react'; -import { Pie } from '@visx/shape'; -import { Group } from '@visx/group'; -import { Text } from '@visx/text'; import { FC } from 'react'; -import { useUsersStatsQuery } from '../../services/users-stats.query'; -import { useState } from 'react'; +import { UsersIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { UsersStatsLegend } from './legend' +import { + MiniWidget, + PieChart, +} from "@marzneshin/components"; +import { UsersStatsLegend } from './legend'; -const Header = () => { - const { t } = useTranslation() - return ( -
- - {t('users')} -
- ) +interface UsersStatsWidgetProps { + active: number; + online: number; + expired: number; + on_hold: number; + limited: number; } -type DataItem = { symbol: string, amount: number, color: string } - -const Content = () => { - const width = 200; - const half = width / 2; - const { data: stats } = useUsersStatsQuery() - const [active, setActive] = useState(null); - const { t } = useTranslation() +export const UsersStatsWidget: FC = ({ + active, online, expired, on_hold, limited +}) => { + const { t } = useTranslation(); - const data: DataItem[] = [ - { symbol: "Active", amount: stats.active, color: "fill-blue-500" }, - { symbol: "Online", amount: stats.online, color: "fill-emerald-500" }, - { symbol: "Expired", amount: stats.expired, color: "fill-gray-600" }, - { symbol: "On Hold", amount: stats.on_hold, color: "fill-amber-500" }, - { symbol: "Limited", amount: stats.limited, color: "fill-red-500" }, + const data = [ + { symbol: "Active", amount: active, color: "fill-blue-500" }, + { symbol: "Online", amount: online, color: "fill-emerald-500" }, + { symbol: "Expired", amount: expired, color: "fill-gray-600" }, + { symbol: "On Hold", amount: on_hold, color: "fill-purple-500" }, + { symbol: "Limited", amount: limited, color: "fill-red-500" }, ]; - return ( -
- - - data.amount} - outerRadius={half} - innerRadius={({ data }) => (active && (active.symbol === data.symbol) ? half - 14 : half - 8)} - padAngle={0.01} - > - {pie => { - return pie.arcs.map(arc => { - return ( - setActive(arc.data)} - onMouseLeave={() => setActive(null)} - > - - - ) - }) - }} - - - {active ? active.amount : stats.total} - - - {t('users')} - - - - -
- ) -} + const total = active + online + expired + on_hold + limited -const Widget: FC = () => { return ( } - content={} - /> - ) -} + title={<> {t('users')} } + > +
+ + +
+
+ ); +}; -export const UsersStatsWidget = Widget; diff --git a/dashboard/src/features/users/widgets/users-stats-widget/legend.tsx b/dashboard/src/features/users/widgets/users-stats-widget/legend.tsx index d4142f5..9d20633 100644 --- a/dashboard/src/features/users/widgets/users-stats-widget/legend.tsx +++ b/dashboard/src/features/users/widgets/users-stats-widget/legend.tsx @@ -3,16 +3,17 @@ import { scaleOrdinal } from '@visx/scale'; import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'; const legendGlyphSize = 15; + const ordinalColorScale = scaleOrdinal({ domain: ["Active", "Online", "Expired", "On Hold", "Limited"], - range: ['#3b82f6', '#10b981', '#4b5563', '#f59e0b', '#ef4444'], + range: ['#3b82f6', '#10b981', '#4b5563', '#A855F7', '#ef4444'], }); export const UsersStatsLegend: FC = () => { return ( {(labels) => ( -
+
{labels.map((label, i) => ( = { + title: "Features/Users/Users Stats", + component: UsersStatsWidget, + tags: ["autodocs"] +}; + +export default meta; + +type WidgetStory = StoryObj; + +/** + * Users chart widget which shows the active, + * online, expired, on hold, and limited users. + * */ +export const UsersChart: WidgetStory = { + args: { + active: 50, + online: 30, + expired: 10, + on_hold: 5, + limited: 5, + } +} diff --git a/dashboard/src/routes/_dashboard/index.tsx b/dashboard/src/routes/_dashboard/index.tsx index b181c52..7a4bad0 100644 --- a/dashboard/src/routes/_dashboard/index.tsx +++ b/dashboard/src/routes/_dashboard/index.tsx @@ -6,12 +6,14 @@ import { CardTitle, Page, } from '@marzneshin/components'; -import { UsersStatsWidget } from '@marzneshin/features/users'; +import { UsersStatsWidget, useUsersStatsQuery } from '@marzneshin/features/users'; import { useTranslation } from 'react-i18next'; import { FC } from 'react'; export const DashboardPage: FC = () => { const { t } = useTranslation(); + const { data: stats } = useUsersStatsQuery() + return ( @@ -21,7 +23,7 @@ export const DashboardPage: FC = () => { - +