@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.
Files changed (37) hide show
  1. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
  2. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
  3. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
  4. package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
  5. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
  6. package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
  7. package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
  8. package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
  9. package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
  10. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
  11. package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
  12. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
  13. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
  14. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
  15. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
  16. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
  17. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
  18. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
  19. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
  20. package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
  21. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
  22. package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
  23. package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
  24. package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
  25. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
  26. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
  27. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
  28. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
  29. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
  30. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
  31. package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
  32. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
  33. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
  34. package/hedhog/frontend/messages/en.json +143 -1
  35. package/hedhog/frontend/messages/pt.json +143 -1
  36. package/hedhog/table/dashboard_user.yaml +2 -10
  37. 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
+ }