@hed-hog/core 0.0.215 → 0.0.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
- package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
- package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
- package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
- package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
- package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
- package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
- package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
- package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
- package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
- package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
- package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
- package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
- package/hedhog/frontend/messages/en.json +143 -1
- package/hedhog/frontend/messages/pt.json +143 -1
- package/hedhog/table/dashboard_user.yaml +2 -10
- package/package.json +2 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
4
|
+
import { IconGripVertical } from '@tabler/icons-react';
|
|
5
|
+
import { ArrowRight, TrendingDown, TrendingUp } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
interface StatCardProps {
|
|
10
|
+
title: string;
|
|
11
|
+
value: string;
|
|
12
|
+
change?: string;
|
|
13
|
+
changeType: 'up' | 'down';
|
|
14
|
+
icon: React.ReactNode;
|
|
15
|
+
iconBg: string;
|
|
16
|
+
delay: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function StatCard({
|
|
20
|
+
title,
|
|
21
|
+
value,
|
|
22
|
+
change,
|
|
23
|
+
changeType,
|
|
24
|
+
icon,
|
|
25
|
+
iconBg,
|
|
26
|
+
delay,
|
|
27
|
+
}: StatCardProps) {
|
|
28
|
+
const t = useTranslations('core.Dashboard');
|
|
29
|
+
const [mounted, setMounted] = useState(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const timer = setTimeout(() => setMounted(true), delay);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}, [delay]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Card
|
|
38
|
+
className={`h-full flex flex-col group relative overflow-hidden transition-all duration-300 hover:shadow-lg ${
|
|
39
|
+
mounted ? 'animate-fade-in-up' : 'opacity-0'
|
|
40
|
+
}`}
|
|
41
|
+
>
|
|
42
|
+
<div className="absolute inset-0 animate-shimmer opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
|
43
|
+
<div
|
|
44
|
+
className="drag-handle absolute top-2 left-3 z-10"
|
|
45
|
+
style={{ cursor: 'grab' }}
|
|
46
|
+
>
|
|
47
|
+
<IconGripVertical className="text-muted-foreground/50 size-4 shrink-0" />
|
|
48
|
+
</div>
|
|
49
|
+
<CardContent className="flex-1 p-6">
|
|
50
|
+
<div className="flex items-start justify-between">
|
|
51
|
+
<div className="flex flex-col gap-1">
|
|
52
|
+
<span className="text-sm font-medium text-muted-foreground">
|
|
53
|
+
{title}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="text-3xl font-bold tracking-tight text-foreground animate-count-up">
|
|
56
|
+
{value}
|
|
57
|
+
</span>
|
|
58
|
+
{change !== undefined && (
|
|
59
|
+
<div className="flex items-center gap-1 pt-1">
|
|
60
|
+
{changeType === 'up' ? (
|
|
61
|
+
<TrendingUp className="h-3.5 w-3.5 text-emerald-500" />
|
|
62
|
+
) : (
|
|
63
|
+
<TrendingDown className="h-3.5 w-3.5 text-destructive" />
|
|
64
|
+
)}
|
|
65
|
+
<span
|
|
66
|
+
className={`text-xs font-medium ${
|
|
67
|
+
changeType === 'up'
|
|
68
|
+
? 'text-emerald-500'
|
|
69
|
+
: 'text-destructive'
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
{change}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="text-xs text-muted-foreground">
|
|
75
|
+
{t('vsPreviousMonth')}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<div
|
|
81
|
+
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-xl ${iconBg}`}
|
|
82
|
+
>
|
|
83
|
+
{icon}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="mt-4 flex items-center gap-1 text-xs text-muted-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
|
87
|
+
<span>{t('viewDetails')}</span>
|
|
88
|
+
<ArrowRight className="h-3 w-3" />
|
|
89
|
+
</div>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
|
+
import { IconGripVertical, IconX } from '@tabler/icons-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
|
|
8
|
+
interface WidgetWrapperProps {
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
isAccessDenied: boolean;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
widgetName: string;
|
|
13
|
+
onRemove?: () => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function WidgetWrapper({
|
|
18
|
+
isLoading,
|
|
19
|
+
isAccessDenied,
|
|
20
|
+
isError,
|
|
21
|
+
widgetName,
|
|
22
|
+
onRemove,
|
|
23
|
+
children,
|
|
24
|
+
}: WidgetWrapperProps) {
|
|
25
|
+
const t = useTranslations('core.DashboardPage');
|
|
26
|
+
|
|
27
|
+
if (isLoading) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative h-full w-full">
|
|
30
|
+
{onRemove && (
|
|
31
|
+
<div className="absolute top-3 right-4 z-10">
|
|
32
|
+
<Button
|
|
33
|
+
variant="ghost"
|
|
34
|
+
size="icon"
|
|
35
|
+
className="size-6 shrink-0 no-drag"
|
|
36
|
+
onClick={(e) => {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
onRemove();
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<IconX className="size-3" />
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
<Skeleton className="h-full w-full" />
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isAccessDenied) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="group relative flex h-full w-full flex-col items-center justify-center rounded-lg border border-destructive/50 bg-destructive/5 p-4 text-center">
|
|
54
|
+
<div
|
|
55
|
+
className="drag-handle absolute left-4 top-3 z-10"
|
|
56
|
+
style={{ cursor: 'grab' }}
|
|
57
|
+
>
|
|
58
|
+
<IconGripVertical className="size-4 shrink-0 text-muted-foreground/50" />
|
|
59
|
+
</div>
|
|
60
|
+
{onRemove && (
|
|
61
|
+
<Button
|
|
62
|
+
variant="ghost"
|
|
63
|
+
size="icon"
|
|
64
|
+
className="no-drag absolute right-4 top-3 z-10 size-6 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
65
|
+
onClick={(e) => {
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
onRemove();
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<IconX className="size-3" />
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
<div className="mb-2 text-4xl text-destructive">🔒</div>
|
|
75
|
+
<h3 className="mb-2 text-sm font-semibold text-destructive">
|
|
76
|
+
{widgetName}
|
|
77
|
+
</h3>
|
|
78
|
+
<p className="mb-1 text-xs font-medium text-destructive">
|
|
79
|
+
{t('accessDenied')}
|
|
80
|
+
</p>
|
|
81
|
+
<p className="text-xs text-muted-foreground">
|
|
82
|
+
{t('widgetDataUnavailable')}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isError) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="group relative flex h-full w-full flex-col items-center justify-center rounded-lg border border-destructive/50 bg-destructive/5 p-4 text-center">
|
|
91
|
+
<div
|
|
92
|
+
className="drag-handle absolute left-4 top-3 z-10"
|
|
93
|
+
style={{ cursor: 'grab' }}
|
|
94
|
+
>
|
|
95
|
+
<IconGripVertical className="size-4 shrink-0 text-muted-foreground/50" />
|
|
96
|
+
</div>
|
|
97
|
+
{onRemove && (
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon"
|
|
101
|
+
className="no-drag absolute right-4 top-3 z-10 size-6 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
102
|
+
onClick={(e) => {
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
onRemove();
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<IconX className="size-3" />
|
|
109
|
+
</Button>
|
|
110
|
+
)}
|
|
111
|
+
<div className="mb-2 text-4xl text-destructive">⚠️</div>
|
|
112
|
+
<h3 className="mb-2 text-sm font-semibold text-destructive">
|
|
113
|
+
{widgetName}
|
|
114
|
+
</h3>
|
|
115
|
+
<p className="mb-1 text-xs font-medium text-destructive">
|
|
116
|
+
{t('renderError')}
|
|
117
|
+
</p>
|
|
118
|
+
<p className="text-xs text-muted-foreground">
|
|
119
|
+
{t('widgetRenderError')}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="group relative h-full w-full">
|
|
127
|
+
<div
|
|
128
|
+
className="drag-handle absolute left-3 top-3 z-20"
|
|
129
|
+
style={{ cursor: 'grab' }}
|
|
130
|
+
>
|
|
131
|
+
<IconGripVertical className="size-4 shrink-0 text-muted-foreground/30 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
132
|
+
</div>
|
|
133
|
+
{onRemove && (
|
|
134
|
+
<Button
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="icon"
|
|
137
|
+
className="no-drag absolute right-3 top-3 z-20 size-6 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
onRemove();
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<IconX className="size-3" />
|
|
145
|
+
</Button>
|
|
146
|
+
)}
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from '@/components/ui/card';
|
|
11
|
+
import { Progress } from '@/components/ui/progress';
|
|
12
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
13
|
+
import type { AllWidgetsData } from '@/types/widget-data';
|
|
14
|
+
import {
|
|
15
|
+
AlertTriangle,
|
|
16
|
+
CheckCircle2,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
Lock,
|
|
19
|
+
Mail,
|
|
20
|
+
ShieldCheck,
|
|
21
|
+
Smartphone,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
import { useTranslations } from 'next-intl';
|
|
24
|
+
import { useEffect, useState } from 'react';
|
|
25
|
+
|
|
26
|
+
import type { AccountSecurityData } from '@/types/widget-data';
|
|
27
|
+
import { useRouter } from 'next/navigation';
|
|
28
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
29
|
+
|
|
30
|
+
const ICON_MAP: Record<string, React.ElementType> = {
|
|
31
|
+
password: Lock,
|
|
32
|
+
'2fa': Smartphone,
|
|
33
|
+
email: Mail,
|
|
34
|
+
sessions: AlertTriangle,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
|
|
38
|
+
const t = useTranslations('core.DashboardPage.accountSecurity');
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const [score, setScore] = useState(0);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const timer = setTimeout(() => setScore(data.score), 300);
|
|
44
|
+
return () => clearTimeout(timer);
|
|
45
|
+
}, [data.score]);
|
|
46
|
+
|
|
47
|
+
const scoreColor =
|
|
48
|
+
score >= 80
|
|
49
|
+
? 'text-emerald-600'
|
|
50
|
+
: score >= 50
|
|
51
|
+
? 'text-amber-600'
|
|
52
|
+
: 'text-red-600';
|
|
53
|
+
|
|
54
|
+
const progressColor =
|
|
55
|
+
score >= 80
|
|
56
|
+
? 'hsl(160, 84%, 39%)'
|
|
57
|
+
: score >= 50
|
|
58
|
+
? 'hsl(38, 92%, 50%)'
|
|
59
|
+
: 'hsl(0, 84%, 60%)';
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card className="flex h-full flex-col">
|
|
63
|
+
<CardHeader className="shrink-0 pb-3">
|
|
64
|
+
<div className="flex items-center gap-2">
|
|
65
|
+
<ShieldCheck className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
|
66
|
+
<div>
|
|
67
|
+
<CardTitle className="text-base font-semibold">
|
|
68
|
+
{t('title')}
|
|
69
|
+
</CardTitle>
|
|
70
|
+
<CardDescription>{t('description')}</CardDescription>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</CardHeader>
|
|
74
|
+
<CardContent className="flex-1 overflow-auto pt-0">
|
|
75
|
+
<div className="mb-5 flex flex-col items-center gap-3 rounded-xl bg-muted/50 p-5">
|
|
76
|
+
<div className="flex items-baseline gap-1">
|
|
77
|
+
<span className={`text-5xl font-bold tracking-tight ${scoreColor}`}>
|
|
78
|
+
{score}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="text-lg text-muted-foreground">/100</span>
|
|
81
|
+
</div>
|
|
82
|
+
<Progress
|
|
83
|
+
value={score}
|
|
84
|
+
className="h-2.5 w-full max-w-xs"
|
|
85
|
+
style={
|
|
86
|
+
{
|
|
87
|
+
'--progress-foreground': progressColor,
|
|
88
|
+
} as any
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
<p className="text-xs text-muted-foreground">
|
|
92
|
+
{score >= 80 ? t('wellProtected') : t('recommendProtections')}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="flex flex-col gap-1.5">
|
|
97
|
+
{data.checks.map((item) => {
|
|
98
|
+
const Icon = ICON_MAP[item.id] ?? ShieldCheck;
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
key={item.id}
|
|
102
|
+
className="group flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-muted/50"
|
|
103
|
+
>
|
|
104
|
+
<div
|
|
105
|
+
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
|
|
106
|
+
item.enabled
|
|
107
|
+
? 'bg-emerald-50 dark:bg-emerald-950/40'
|
|
108
|
+
: 'bg-muted'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
<Icon
|
|
112
|
+
className={`h-4 w-4 ${
|
|
113
|
+
item.enabled
|
|
114
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
115
|
+
: 'text-muted-foreground'
|
|
116
|
+
}`}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<span className="text-sm font-medium text-foreground">
|
|
122
|
+
{t(`labels.${item.labelKey}` as any) || item.labelKey}
|
|
123
|
+
</span>
|
|
124
|
+
{item.enabled ? (
|
|
125
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
|
|
126
|
+
) : (
|
|
127
|
+
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
<span className="text-xs text-muted-foreground">
|
|
131
|
+
{t(`descriptions.${item.descriptionKey}` as any) ||
|
|
132
|
+
item.descriptionKey}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
{!item.enabled && (
|
|
136
|
+
<Button
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="sm"
|
|
139
|
+
onClick={() => router.push('/core/account/2fa')}
|
|
140
|
+
className="shrink-0 gap-1 text-xs"
|
|
141
|
+
>
|
|
142
|
+
{t('activate')}
|
|
143
|
+
<ChevronRight className="h-3 w-3" />
|
|
144
|
+
</Button>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface AccountSecurityProps {
|
|
156
|
+
widget?: { name?: string };
|
|
157
|
+
onRemove?: () => void;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default function AccountSecurity({
|
|
161
|
+
widget,
|
|
162
|
+
onRemove,
|
|
163
|
+
}: AccountSecurityProps) {
|
|
164
|
+
const { data, isLoading, isError, isAccessDenied } = useWidgetData<
|
|
165
|
+
AllWidgetsData,
|
|
166
|
+
AccountSecurityData
|
|
167
|
+
>({
|
|
168
|
+
endpoint: '/dashboard-core/widgets/me',
|
|
169
|
+
queryKey: 'widget-me',
|
|
170
|
+
select: (d) => d.accountSecurity,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<WidgetWrapper
|
|
175
|
+
isLoading={isLoading}
|
|
176
|
+
isError={isError}
|
|
177
|
+
isAccessDenied={isAccessDenied}
|
|
178
|
+
widgetName={widget?.name ?? 'account-security'}
|
|
179
|
+
onRemove={onRemove}
|
|
180
|
+
>
|
|
181
|
+
{data && <AccountSecurityContent data={data} />}
|
|
182
|
+
</WidgetWrapper>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
2
|
+
import { Users } from 'lucide-react';
|
|
3
|
+
import { useTranslations } from 'next-intl';
|
|
4
|
+
import StatCard from '../stats';
|
|
5
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
6
|
+
|
|
7
|
+
interface ActiveUsersProps {
|
|
8
|
+
widget?: any;
|
|
9
|
+
onRemove?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UserStatsData {
|
|
13
|
+
cards?: {
|
|
14
|
+
activeUsers?: {
|
|
15
|
+
value: number;
|
|
16
|
+
change: number | null;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function ActiveUsers({ widget, onRemove }: ActiveUsersProps) {
|
|
22
|
+
const t = useTranslations('core.Dashboard');
|
|
23
|
+
|
|
24
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
25
|
+
useWidgetData<UserStatsData>({
|
|
26
|
+
endpoint: '/dashboard-core/stats/overview/users',
|
|
27
|
+
queryKey: 'dashboard-stats-users',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const value = data?.cards?.activeUsers?.value?.toLocaleString('pt-BR') || '0';
|
|
31
|
+
const change = data?.cards?.activeUsers?.change;
|
|
32
|
+
const changeType =
|
|
33
|
+
change !== null && change !== undefined && change >= 0 ? 'up' : 'down';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<WidgetWrapper
|
|
37
|
+
isLoading={isLoading}
|
|
38
|
+
isAccessDenied={isAccessDenied}
|
|
39
|
+
isError={isError}
|
|
40
|
+
widgetName={widget?.name || t('activeUsers')}
|
|
41
|
+
onRemove={onRemove}
|
|
42
|
+
>
|
|
43
|
+
<StatCard
|
|
44
|
+
title={t('activeUsers')}
|
|
45
|
+
value={value}
|
|
46
|
+
change={
|
|
47
|
+
change !== null && change !== undefined
|
|
48
|
+
? `${change > 0 ? '+' : ''}${change}%`
|
|
49
|
+
: undefined
|
|
50
|
+
}
|
|
51
|
+
changeType={changeType}
|
|
52
|
+
icon={<Users className="h-6 w-6 text-blue-500" />}
|
|
53
|
+
iconBg="bg-blue-500/10"
|
|
54
|
+
delay={50}
|
|
55
|
+
/>
|
|
56
|
+
</WidgetWrapper>
|
|
57
|
+
);
|
|
58
|
+
}
|