@hed-hog/core 0.0.215 → 0.0.216

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 (36) 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/package.json +3 -3
@@ -0,0 +1,296 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from '@/components/ui/card';
12
+ import {
13
+ Collapsible,
14
+ CollapsibleContent,
15
+ CollapsibleTrigger,
16
+ } from '@/components/ui/collapsible';
17
+ import { Input } from '@/components/ui/input';
18
+ import { Label } from '@/components/ui/label';
19
+ import { Switch } from '@/components/ui/switch';
20
+ import {
21
+ CheckCircle2,
22
+ ChevronDown,
23
+ ExternalLink,
24
+ KeyRound,
25
+ XCircle,
26
+ } from 'lucide-react';
27
+ import { useState } from 'react';
28
+
29
+ interface OAuthProvider {
30
+ id: string;
31
+ name: string;
32
+ color: string;
33
+ bgColor: string;
34
+ icon: React.ReactNode;
35
+ enabled: boolean;
36
+ clientId: string;
37
+ clientSecret: string;
38
+ callbackUrl: string;
39
+ scopes: string;
40
+ docUrl: string;
41
+ }
42
+
43
+ const googleSvg = (
44
+ <svg className="h-4 w-4" viewBox="0 0 24 24">
45
+ <path
46
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
47
+ fill="#4285F4"
48
+ />
49
+ <path
50
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
51
+ fill="#34A853"
52
+ />
53
+ <path
54
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
55
+ fill="#FBBC05"
56
+ />
57
+ <path
58
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
59
+ fill="#EA4335"
60
+ />
61
+ </svg>
62
+ );
63
+
64
+ const githubSvg = (
65
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
66
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.08 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.82.58C20.57 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z" />
67
+ </svg>
68
+ );
69
+
70
+ const facebookSvg = (
71
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="#1877F2">
72
+ <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
73
+ </svg>
74
+ );
75
+
76
+ const microsoftSvg = (
77
+ <svg className="h-4 w-4" viewBox="0 0 24 24">
78
+ <rect width="10.5" height="10.5" x="1" y="1" fill="#F25022" />
79
+ <rect width="10.5" height="10.5" x="12.5" y="1" fill="#7FBA00" />
80
+ <rect width="10.5" height="10.5" x="1" y="12.5" fill="#00A4EF" />
81
+ <rect width="10.5" height="10.5" x="12.5" y="12.5" fill="#FFB900" />
82
+ </svg>
83
+ );
84
+
85
+ const initialProviders: OAuthProvider[] = [
86
+ {
87
+ id: 'google',
88
+ name: 'Google',
89
+ color: 'text-foreground',
90
+ bgColor: 'bg-red-50',
91
+ icon: googleSvg,
92
+ enabled: true,
93
+ clientId: '123456789.apps.googleusercontent.com',
94
+ clientSecret: '',
95
+ callbackUrl: 'https://app.heroadmin.com/auth/callback/google',
96
+ scopes: 'openid email profile',
97
+ docUrl: 'https://console.cloud.google.com/apis/credentials',
98
+ },
99
+ {
100
+ id: 'github',
101
+ name: 'GitHub',
102
+ color: 'text-foreground',
103
+ bgColor: 'bg-gray-50',
104
+ icon: githubSvg,
105
+ enabled: true,
106
+ clientId: 'Iv1.abc123def456',
107
+ clientSecret: '',
108
+ callbackUrl: 'https://app.heroadmin.com/auth/callback/github',
109
+ scopes: 'read:user user:email',
110
+ docUrl: 'https://github.com/settings/developers',
111
+ },
112
+ {
113
+ id: 'facebook',
114
+ name: 'Facebook',
115
+ color: 'text-foreground',
116
+ bgColor: 'bg-blue-50',
117
+ icon: facebookSvg,
118
+ enabled: false,
119
+ clientId: '',
120
+ clientSecret: '',
121
+ callbackUrl: 'https://app.heroadmin.com/auth/callback/facebook',
122
+ scopes: 'email public_profile',
123
+ docUrl: 'https://developers.facebook.com/apps/',
124
+ },
125
+ {
126
+ id: 'microsoft',
127
+ name: 'Microsoft',
128
+ color: 'text-foreground',
129
+ bgColor: 'bg-sky-50',
130
+ icon: microsoftSvg,
131
+ enabled: false,
132
+ clientId: '',
133
+ clientSecret: '',
134
+ callbackUrl: 'https://app.heroadmin.com/auth/callback/microsoft',
135
+ scopes: 'openid email profile User.Read',
136
+ docUrl:
137
+ 'https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade',
138
+ },
139
+ ];
140
+
141
+ export default function OAuthConfig() {
142
+ const [providers, setProviders] = useState(initialProviders);
143
+ const [openId, setOpenId] = useState<string | null>(null);
144
+
145
+ function updateProvider(id: string, updates: Partial<OAuthProvider>) {
146
+ setProviders((prev) =>
147
+ prev.map((p) => (p.id === id ? { ...p, ...updates } : p))
148
+ );
149
+ }
150
+
151
+ return (
152
+ <Card className="h-full">
153
+ <CardHeader>
154
+ <div className="flex items-center gap-3">
155
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-50">
156
+ <KeyRound className="h-5 w-5 text-amber-600" />
157
+ </div>
158
+ <div>
159
+ <CardTitle className="text-base">Autenticacao OAuth2</CardTitle>
160
+ <CardDescription>
161
+ Configure provedores de login social
162
+ </CardDescription>
163
+ </div>
164
+ </div>
165
+ </CardHeader>
166
+ <CardContent className="space-y-3">
167
+ {providers.map((p) => (
168
+ <Collapsible
169
+ key={p.id}
170
+ open={openId === p.id}
171
+ onOpenChange={(isOpen) => setOpenId(isOpen ? p.id : null)}
172
+ >
173
+ <div className="rounded-lg border">
174
+ <CollapsibleTrigger asChild>
175
+ <button
176
+ type="button"
177
+ className="flex w-full items-center gap-3 p-4 text-left transition-colors hover:bg-muted/50"
178
+ >
179
+ <div
180
+ className={`flex h-9 w-9 items-center justify-center rounded-lg ${p.bgColor}`}
181
+ >
182
+ {p.icon}
183
+ </div>
184
+ <div className="flex-1">
185
+ <div className="flex items-center gap-2">
186
+ <span className="text-sm font-medium">{p.name}</span>
187
+ {p.enabled ? (
188
+ <Badge
189
+ variant="secondary"
190
+ className="bg-emerald-50 text-emerald-700 text-[10px]"
191
+ >
192
+ <CheckCircle2 className="mr-1 h-2.5 w-2.5" />
193
+ Ativo
194
+ </Badge>
195
+ ) : (
196
+ <Badge
197
+ variant="secondary"
198
+ className="text-[10px] text-muted-foreground"
199
+ >
200
+ <XCircle className="mr-1 h-2.5 w-2.5" />
201
+ Inativo
202
+ </Badge>
203
+ )}
204
+ </div>
205
+ <span className="text-xs text-muted-foreground">
206
+ {p.clientId
207
+ ? `Client ID: ${p.clientId.slice(0, 16)}...`
208
+ : 'Nao configurado'}
209
+ </span>
210
+ </div>
211
+ <ChevronDown
212
+ className={`h-4 w-4 text-muted-foreground transition-transform ${openId === p.id ? 'rotate-180' : ''}`}
213
+ />
214
+ </button>
215
+ </CollapsibleTrigger>
216
+ <CollapsibleContent>
217
+ <div className="border-t px-4 pb-4 pt-4 space-y-4">
218
+ <div className="flex items-center justify-between">
219
+ <Label htmlFor={`${p.id}-enabled`} className="text-sm">
220
+ Habilitar {p.name}
221
+ </Label>
222
+ <Switch
223
+ id={`${p.id}-enabled`}
224
+ checked={p.enabled}
225
+ onCheckedChange={(checked) =>
226
+ updateProvider(p.id, { enabled: checked })
227
+ }
228
+ />
229
+ </div>
230
+ <div className="space-y-2">
231
+ <Label htmlFor={`${p.id}-client-id`}>Client ID</Label>
232
+ <Input
233
+ id={`${p.id}-client-id`}
234
+ value={p.clientId}
235
+ onChange={(e) =>
236
+ updateProvider(p.id, { clientId: e.target.value })
237
+ }
238
+ className="font-mono text-sm"
239
+ />
240
+ </div>
241
+ <div className="space-y-2">
242
+ <Label htmlFor={`${p.id}-client-secret`}>
243
+ Client Secret
244
+ </Label>
245
+ <Input
246
+ id={`${p.id}-client-secret`}
247
+ type="password"
248
+ value={p.clientSecret}
249
+ onChange={(e) =>
250
+ updateProvider(p.id, { clientSecret: e.target.value })
251
+ }
252
+ className="font-mono text-sm"
253
+ placeholder="********"
254
+ />
255
+ </div>
256
+ <div className="space-y-2">
257
+ <Label htmlFor={`${p.id}-callback`}>Callback URL</Label>
258
+ <Input
259
+ id={`${p.id}-callback`}
260
+ value={p.callbackUrl}
261
+ readOnly
262
+ className="bg-muted font-mono text-sm"
263
+ />
264
+ </div>
265
+ <div className="space-y-2">
266
+ <Label htmlFor={`${p.id}-scopes`}>Scopes</Label>
267
+ <Input
268
+ id={`${p.id}-scopes`}
269
+ value={p.scopes}
270
+ onChange={(e) =>
271
+ updateProvider(p.id, { scopes: e.target.value })
272
+ }
273
+ className="font-mono text-sm"
274
+ />
275
+ </div>
276
+ <div className="flex items-center justify-between border-t pt-3">
277
+ <a
278
+ href={p.docUrl}
279
+ target="_blank"
280
+ rel="noopener noreferrer"
281
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
282
+ >
283
+ <ExternalLink className="h-3 w-3" />
284
+ Abrir console do provedor
285
+ </a>
286
+ <Button size="sm">Salvar</Button>
287
+ </div>
288
+ </div>
289
+ </CollapsibleContent>
290
+ </div>
291
+ </Collapsible>
292
+ ))}
293
+ </CardContent>
294
+ </Card>
295
+ );
296
+ }
@@ -0,0 +1,61 @@
1
+ import { useWidgetData } from '@/hooks/use-widget-data';
2
+ import { Shield } from 'lucide-react';
3
+ import { useTranslations } from 'next-intl';
4
+ import StatCard from '../stats';
5
+ import { WidgetWrapper } from '../widget-wrapper';
6
+
7
+ interface PermissionsCardProps {
8
+ widget?: any;
9
+ onRemove?: () => void;
10
+ }
11
+
12
+ interface UserStatsData {
13
+ cards?: {
14
+ roles?: {
15
+ value: number;
16
+ change: number | null;
17
+ };
18
+ };
19
+ }
20
+
21
+ export default function PermissionsCard({
22
+ widget,
23
+ onRemove,
24
+ }: PermissionsCardProps) {
25
+ const t = useTranslations('core.Dashboard');
26
+
27
+ const { data, isLoading, isAccessDenied, isError } =
28
+ useWidgetData<UserStatsData>({
29
+ endpoint: '/dashboard-core/stats/overview/users',
30
+ queryKey: 'dashboard-stats-users',
31
+ });
32
+
33
+ const value = data?.cards?.roles?.value?.toLocaleString('pt-BR') || '0';
34
+ const change = data?.cards?.roles?.change;
35
+ const changeType =
36
+ change !== null && change !== undefined && change >= 0 ? 'up' : 'down';
37
+
38
+ return (
39
+ <WidgetWrapper
40
+ isLoading={isLoading}
41
+ isAccessDenied={isAccessDenied}
42
+ isError={isError}
43
+ widgetName={widget?.name || t('permissions')}
44
+ onRemove={onRemove}
45
+ >
46
+ <StatCard
47
+ title={t('permissions')}
48
+ value={value}
49
+ change={
50
+ change !== null && change !== undefined
51
+ ? `${change > 0 ? '+' : ''}${change}%`
52
+ : undefined
53
+ }
54
+ changeType={changeType}
55
+ icon={<Shield className="h-6 w-6 text-rose-500" />}
56
+ iconBg="bg-rose-500/10"
57
+ delay={200}
58
+ />
59
+ </WidgetWrapper>
60
+ );
61
+ }
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@/components/ui/card';
10
+ import { useWidgetData } from '@/hooks/use-widget-data';
11
+ import { IconGripVertical } from '@tabler/icons-react';
12
+ import { useTranslations } from 'next-intl';
13
+ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
14
+ import { WidgetWrapper } from '../widget-wrapper';
15
+
16
+ function CustomTooltip({
17
+ active,
18
+ payload,
19
+ }: {
20
+ active?: boolean;
21
+ payload?: Array<{ name: string; value: number; payload: { color: string } }>;
22
+ }) {
23
+ const t = useTranslations('core.Dashboard');
24
+ if (!active || !payload?.length) return null;
25
+
26
+ const entry = payload[0];
27
+ return (
28
+ <div className="rounded-lg border bg-card px-3 py-2 shadow-xl">
29
+ <p className="text-xs text-muted-foreground">
30
+ <span
31
+ className="mr-1.5 inline-block h-2 w-2 rounded-full"
32
+ style={{ backgroundColor: entry?.payload.color }}
33
+ />
34
+ {entry?.name}: {entry?.value} {t('permissionsLowercase')}
35
+ </p>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ interface PermissionsChartProps {
41
+ widget?: any;
42
+ onRemove?: () => void;
43
+ }
44
+
45
+ interface UserStatsData {
46
+ charts?: {
47
+ permissionDistribution?: Array<{
48
+ name: string;
49
+ value: number;
50
+ color: string;
51
+ }>;
52
+ };
53
+ }
54
+
55
+ export default function PermissionsChart({
56
+ widget,
57
+ onRemove,
58
+ }: PermissionsChartProps) {
59
+ const t = useTranslations('core.Dashboard');
60
+
61
+ const {
62
+ data: statsData,
63
+ isLoading,
64
+ isAccessDenied,
65
+ isError,
66
+ } = useWidgetData<UserStatsData>({
67
+ endpoint: '/dashboard-core/stats/overview/users',
68
+ queryKey: 'dashboard-stats-users',
69
+ });
70
+
71
+ const data = statsData?.charts?.permissionDistribution || [];
72
+ const total = data.reduce((sum: number, item: any) => sum + item.value, 0);
73
+
74
+ return (
75
+ <WidgetWrapper
76
+ isLoading={isLoading}
77
+ isAccessDenied={isAccessDenied}
78
+ isError={isError}
79
+ widgetName={widget?.name || t('permissionsDistributionTitle')}
80
+ onRemove={onRemove}
81
+ >
82
+ <Card className="h-full flex flex-col group">
83
+ <div
84
+ className="drag-handle absolute top-3 left-4 z-10"
85
+ style={{ cursor: 'grab' }}
86
+ >
87
+ <IconGripVertical className="text-muted-foreground/50 size-4 shrink-0" />
88
+ </div>
89
+ <CardHeader className="pb-2 pt-4 pl-10">
90
+ <CardTitle className="text-base font-semibold">
91
+ {t('permissionsDistributionTitle')}
92
+ </CardTitle>
93
+ <CardDescription>
94
+ {t('permissionsDistributionDescription')}
95
+ </CardDescription>
96
+ </CardHeader>
97
+ <CardContent className="flex-1 pt-0">
98
+ <div className="flex justify-between items-center gap-4">
99
+ <div className="relative h-[280px] w-[280px] shrink-0">
100
+ <ResponsiveContainer width="100%" height="100%">
101
+ <PieChart>
102
+ <Pie
103
+ data={data}
104
+ cx="50%"
105
+ cy="50%"
106
+ innerRadius={70}
107
+ outerRadius={110}
108
+ paddingAngle={4}
109
+ dataKey="value"
110
+ animationDuration={1200}
111
+ strokeWidth={0}
112
+ >
113
+ {data.map((entry: any, index: number) => (
114
+ <Cell key={`cell-${index}`} fill={entry.color} />
115
+ ))}
116
+ </Pie>
117
+ <Tooltip content={<CustomTooltip />} />
118
+ </PieChart>
119
+ </ResponsiveContainer>
120
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
121
+ <span className="text-2xl font-bold text-foreground">
122
+ {total}
123
+ </span>
124
+ <span className="text-[10px] text-muted-foreground">
125
+ {t('total')}
126
+ </span>
127
+ </div>
128
+ </div>
129
+ <div className="grid grid-cols-2 gap-3">
130
+ {data.map((item: any) => (
131
+ <div key={item.name} className="flex items-center gap-3">
132
+ <div
133
+ className="h-3 w-3 rounded-sm"
134
+ style={{ backgroundColor: item.color }}
135
+ />
136
+ <div className="flex flex-col">
137
+ <span className="text-sm font-medium text-foreground">
138
+ {item.name}
139
+ </span>
140
+ <span className="text-xs text-muted-foreground">
141
+ {item.value} ({Math.round((item.value / total) * 100)}%)
142
+ </span>
143
+ </div>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ </CardContent>
149
+ </Card>
150
+ </WidgetWrapper>
151
+ );
152
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent } from '@/components/ui/card';
7
+ import { Separator } from '@/components/ui/separator';
8
+ import { useWidgetData } from '@/hooks/use-widget-data';
9
+ import { formatDateTime } from '@/lib/format-date';
10
+ import { getPhotoUrl } from '@/lib/get-photo-url';
11
+ import type { AllWidgetsData, ProfileData } from '@/types/widget-data';
12
+ import { useApp } from '@hed-hog/next-app-provider';
13
+ import {
14
+ Calendar,
15
+ CheckCircle2,
16
+ Clock,
17
+ Edit3,
18
+ Globe,
19
+ Mail,
20
+ Shield,
21
+ } from 'lucide-react';
22
+ import { useTranslations } from 'next-intl';
23
+ import { useRouter } from 'next/navigation';
24
+ import { WidgetWrapper } from '../widget-wrapper';
25
+
26
+ function getInitials(name: string): string {
27
+ return name
28
+ .split(' ')
29
+ .filter(Boolean)
30
+ .slice(0, 2)
31
+ .map((n) => n[0]?.toUpperCase() ?? '')
32
+ .join('');
33
+ }
34
+
35
+ function ProfileContent({ profile }: { profile: ProfileData }) {
36
+ const { getSettingValue } = useApp();
37
+ const router = useRouter();
38
+ const t = useTranslations('core.DashboardPage.profileCard');
39
+
40
+ return (
41
+ <Card className="relative flex h-full flex-col overflow-hidden">
42
+ <div className="absolute inset-x-0 top-0 h-24 bg-linear-to-br from-muted to-muted/50" />
43
+ <CardContent className="relative flex flex-col flex-1 overflow-hidden pt-6 pb-10">
44
+ <div className="flex flex-col items-center gap-4 sm:flex-row sm:items-end">
45
+ <div className="relative">
46
+ <Avatar className="h-20 w-20 border-4 border-card shadow-md">
47
+ {profile.photo_id && (
48
+ <AvatarImage
49
+ src={getPhotoUrl(profile.photo_id)}
50
+ alt={profile.name}
51
+ />
52
+ )}
53
+ <AvatarFallback className="bg-foreground text-background text-xl font-bold">
54
+ {getInitials(profile.name)}
55
+ </AvatarFallback>
56
+ </Avatar>
57
+ <div className="absolute -bottom-0.5 -right-0.5 h-5 w-5 rounded-full border-2 border-card bg-emerald-500" />
58
+ </div>
59
+ <div className="flex flex-1 flex-col items-center gap-1 text-center sm:items-start sm:text-left">
60
+ <div className="flex items-center gap-2">
61
+ <h2 className="text-xl font-bold text-foreground">
62
+ {profile.name}
63
+ </h2>
64
+ <CheckCircle2 className="h-4 w-4 text-blue-500" />
65
+ </div>
66
+ <p className="text-sm text-muted-foreground">{profile.role}</p>
67
+ <div className="flex flex-wrap items-center gap-2 pt-1">
68
+ <Badge
69
+ variant="outline"
70
+ className="border-emerald-200 bg-emerald-50 text-xs text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-400"
71
+ >
72
+ {t('online')}
73
+ </Badge>
74
+ </div>
75
+ </div>
76
+ <Button
77
+ onClick={() => router.push('/core/account/profile')}
78
+ variant="outline"
79
+ size="sm"
80
+ className="shrink-0 gap-1.5"
81
+ >
82
+ <Edit3 className="h-3.5 w-3.5" />
83
+ {t('editProfile')}
84
+ </Button>
85
+ </div>
86
+
87
+ <Separator className="my-5" />
88
+
89
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
90
+ <div className="flex items-center gap-2.5 rounded-lg p-2 transition-colors hover:bg-muted/50">
91
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted">
92
+ <Mail className="h-4 w-4 text-muted-foreground" />
93
+ </div>
94
+ <div className="flex min-w-0 flex-col">
95
+ <span className="text-[11px] text-muted-foreground">
96
+ {t('email')}
97
+ </span>
98
+ <span className="truncate text-xs font-medium text-foreground">
99
+ {profile.email}
100
+ </span>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="flex items-center gap-2.5 rounded-lg p-2 transition-colors hover:bg-muted/50">
105
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted">
106
+ <Calendar className="h-4 w-4 text-muted-foreground" />
107
+ </div>
108
+ <div className="flex min-w-0 flex-col">
109
+ <span className="text-[11px] text-muted-foreground">
110
+ {t('memberSinceLabel')}
111
+ </span>
112
+ <span className="truncate text-xs font-medium text-foreground">
113
+ {formatDateTime(profile.memberSince, getSettingValue)}
114
+ </span>
115
+ </div>
116
+ </div>
117
+
118
+ <div className="flex items-center gap-2.5 rounded-lg p-2 transition-colors hover:bg-muted/50">
119
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-50 dark:bg-blue-950/40">
120
+ <Globe className="h-4 w-4 text-blue-600 dark:text-blue-400" />
121
+ </div>
122
+ <div className="flex min-w-0 flex-col">
123
+ <span className="text-[11px] text-muted-foreground">
124
+ {t('activeSessions')}
125
+ </span>
126
+ <span className="text-xs font-semibold text-foreground">
127
+ {profile.totalSessions}
128
+ </span>
129
+ </div>
130
+ </div>
131
+
132
+ <div className="flex items-center gap-2.5 rounded-lg p-2 transition-colors hover:bg-muted/50">
133
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-50 dark:bg-amber-950/40">
134
+ <Shield className="h-4 w-4 text-amber-600 dark:text-amber-400" />
135
+ </div>
136
+ <div className="flex min-w-0 flex-col">
137
+ <span className="text-[11px] text-muted-foreground">
138
+ {t('roles')}
139
+ </span>
140
+ <span className="text-xs font-semibold text-foreground">
141
+ {profile.totalRoles}
142
+ </span>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </CardContent>
147
+
148
+ <div className="absolute bottom-0 inset-x-0 flex items-center gap-2 border-t bg-card px-4 py-2">
149
+ <Clock className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
150
+ <span className="truncate text-xs text-muted-foreground">
151
+ {t('lastLogin', {
152
+ date: formatDateTime(profile.lastLogin, getSettingValue),
153
+ })}
154
+ </span>
155
+ </div>
156
+ </Card>
157
+ );
158
+ }
159
+
160
+ interface ProfileCardProps {
161
+ widget?: { name?: string };
162
+ onRemove?: () => void;
163
+ }
164
+
165
+ export default function ProfileCard({ widget, onRemove }: ProfileCardProps) {
166
+ const { data, isLoading, isError, isAccessDenied } = useWidgetData<
167
+ AllWidgetsData,
168
+ ProfileData
169
+ >({
170
+ endpoint: '/dashboard-core/widgets/me',
171
+ queryKey: 'widget-me',
172
+ select: (d) => d.profile,
173
+ });
174
+
175
+ return (
176
+ <WidgetWrapper
177
+ isLoading={isLoading}
178
+ isError={isError}
179
+ isAccessDenied={isAccessDenied}
180
+ widgetName={widget?.name ?? 'profile-card'}
181
+ onRemove={onRemove}
182
+ >
183
+ {data && <ProfileContent profile={data} />}
184
+ </WidgetWrapper>
185
+ );
186
+ }