@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.
- 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/package.json +3 -3
|
@@ -0,0 +1,210 @@
|
|
|
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 {
|
|
14
|
+
Area,
|
|
15
|
+
AreaChart,
|
|
16
|
+
CartesianGrid,
|
|
17
|
+
ResponsiveContainer,
|
|
18
|
+
Tooltip,
|
|
19
|
+
XAxis,
|
|
20
|
+
YAxis,
|
|
21
|
+
} from 'recharts';
|
|
22
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
23
|
+
|
|
24
|
+
function CustomTooltip({
|
|
25
|
+
active,
|
|
26
|
+
payload,
|
|
27
|
+
label,
|
|
28
|
+
}: {
|
|
29
|
+
active?: boolean;
|
|
30
|
+
payload?: Array<{ value: number; dataKey: string }>;
|
|
31
|
+
label?: string;
|
|
32
|
+
}) {
|
|
33
|
+
const t = useTranslations('core.Dashboard');
|
|
34
|
+
if (!active || !payload?.length) return null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="rounded-lg border bg-card px-3 py-2 shadow-xl">
|
|
38
|
+
<p className="mb-1 text-xs font-medium text-card-foreground">{label}</p>
|
|
39
|
+
{payload.map((entry) => (
|
|
40
|
+
<p key={entry.dataKey} className="text-xs text-muted-foreground">
|
|
41
|
+
<span
|
|
42
|
+
className="mr-1.5 inline-block h-2 w-2 rounded-full"
|
|
43
|
+
style={{
|
|
44
|
+
backgroundColor:
|
|
45
|
+
entry.dataKey === 'users' ? '#6366f1' : '#10b981',
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
{entry.dataKey === 'users' ? t('users') : t('sessions')}:{' '}
|
|
49
|
+
{entry.value.toLocaleString('pt-BR')}
|
|
50
|
+
</p>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface UserGrowthChartProps {
|
|
57
|
+
widget?: any;
|
|
58
|
+
onRemove?: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface UserStatsData {
|
|
62
|
+
charts?: {
|
|
63
|
+
userGrowth?: Array<{
|
|
64
|
+
month: string;
|
|
65
|
+
users: number;
|
|
66
|
+
sessions: number;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function UserGrowthChart({
|
|
72
|
+
widget,
|
|
73
|
+
onRemove,
|
|
74
|
+
}: UserGrowthChartProps) {
|
|
75
|
+
const t = useTranslations('core.Dashboard');
|
|
76
|
+
|
|
77
|
+
const {
|
|
78
|
+
data: statsData,
|
|
79
|
+
isLoading,
|
|
80
|
+
isAccessDenied,
|
|
81
|
+
isError,
|
|
82
|
+
} = useWidgetData<UserStatsData>({
|
|
83
|
+
endpoint: '/dashboard-core/stats/overview/users',
|
|
84
|
+
queryKey: 'dashboard-stats-users',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = statsData?.charts?.userGrowth || [];
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<WidgetWrapper
|
|
91
|
+
isLoading={isLoading}
|
|
92
|
+
isAccessDenied={isAccessDenied}
|
|
93
|
+
isError={isError}
|
|
94
|
+
widgetName={widget?.name || t('userGrowthTitle')}
|
|
95
|
+
onRemove={onRemove}
|
|
96
|
+
>
|
|
97
|
+
<Card className="h-full flex flex-col group">
|
|
98
|
+
<div
|
|
99
|
+
className="drag-handle absolute top-3 left-4 z-10"
|
|
100
|
+
style={{ cursor: 'grab' }}
|
|
101
|
+
>
|
|
102
|
+
<IconGripVertical className="text-muted-foreground/50 size-4 shrink-0" />
|
|
103
|
+
</div>
|
|
104
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2 pt-4">
|
|
105
|
+
<div className="pl-6">
|
|
106
|
+
<CardTitle className="text-base font-semibold">
|
|
107
|
+
{t('userGrowthTitle')}
|
|
108
|
+
</CardTitle>
|
|
109
|
+
<CardDescription>{t('userGrowthDescription')}</CardDescription>
|
|
110
|
+
</div>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardContent className="flex-1 pt-0">
|
|
113
|
+
<div className="h-[280px] w-full">
|
|
114
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
115
|
+
<AreaChart
|
|
116
|
+
data={data}
|
|
117
|
+
margin={{ top: 5, right: 10, left: -20, bottom: 0 }}
|
|
118
|
+
>
|
|
119
|
+
<defs>
|
|
120
|
+
<linearGradient
|
|
121
|
+
id="gradientUsuarios"
|
|
122
|
+
x1="0"
|
|
123
|
+
y1="0"
|
|
124
|
+
x2="0"
|
|
125
|
+
y2="1"
|
|
126
|
+
>
|
|
127
|
+
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.5} />
|
|
128
|
+
<stop
|
|
129
|
+
offset="100%"
|
|
130
|
+
stopColor="#6366f1"
|
|
131
|
+
stopOpacity={0.05}
|
|
132
|
+
/>
|
|
133
|
+
</linearGradient>
|
|
134
|
+
<linearGradient
|
|
135
|
+
id="gradientSessoes"
|
|
136
|
+
x1="0"
|
|
137
|
+
y1="0"
|
|
138
|
+
x2="0"
|
|
139
|
+
y2="1"
|
|
140
|
+
>
|
|
141
|
+
<stop offset="0%" stopColor="#10b981" stopOpacity={0.4} />
|
|
142
|
+
<stop
|
|
143
|
+
offset="100%"
|
|
144
|
+
stopColor="#10b981"
|
|
145
|
+
stopOpacity={0.05}
|
|
146
|
+
/>
|
|
147
|
+
</linearGradient>
|
|
148
|
+
</defs>
|
|
149
|
+
<CartesianGrid
|
|
150
|
+
strokeDasharray="3 3"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
opacity={0.1}
|
|
153
|
+
/>
|
|
154
|
+
<XAxis
|
|
155
|
+
dataKey="month"
|
|
156
|
+
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 12 }}
|
|
157
|
+
axisLine={false}
|
|
158
|
+
tickLine={false}
|
|
159
|
+
/>
|
|
160
|
+
<YAxis
|
|
161
|
+
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 12 }}
|
|
162
|
+
axisLine={false}
|
|
163
|
+
tickLine={false}
|
|
164
|
+
/>
|
|
165
|
+
<Tooltip content={<CustomTooltip />} />
|
|
166
|
+
<Area
|
|
167
|
+
type="monotone"
|
|
168
|
+
dataKey="sessions"
|
|
169
|
+
stroke="#10b981"
|
|
170
|
+
strokeWidth={2.5}
|
|
171
|
+
fill="url(#gradientSessoes)"
|
|
172
|
+
animationDuration={1500}
|
|
173
|
+
/>
|
|
174
|
+
<Area
|
|
175
|
+
type="monotone"
|
|
176
|
+
dataKey="users"
|
|
177
|
+
stroke="#6366f1"
|
|
178
|
+
strokeWidth={2.5}
|
|
179
|
+
fill="url(#gradientUsuarios)"
|
|
180
|
+
animationDuration={1500}
|
|
181
|
+
animationBegin={200}
|
|
182
|
+
/>
|
|
183
|
+
</AreaChart>
|
|
184
|
+
</ResponsiveContainer>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="mt-3 flex items-center justify-center gap-6">
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<div
|
|
189
|
+
className="h-2.5 w-2.5 rounded-full"
|
|
190
|
+
style={{ backgroundColor: '#6366f1' }}
|
|
191
|
+
/>
|
|
192
|
+
<span className="text-xs text-muted-foreground">
|
|
193
|
+
{t('users')}
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<div
|
|
198
|
+
className="h-2.5 w-2.5 rounded-full"
|
|
199
|
+
style={{ backgroundColor: '#10b981' }}
|
|
200
|
+
/>
|
|
201
|
+
<span className="text-xs text-muted-foreground">
|
|
202
|
+
{t('sessions')}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
</WidgetWrapper>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from '@/components/ui/card';
|
|
11
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
12
|
+
import type { AllWidgetsData, RoleData } from '@/types/widget-data';
|
|
13
|
+
import { Crown, ShieldCheck } from 'lucide-react';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
16
|
+
|
|
17
|
+
const levelStyles = [
|
|
18
|
+
{
|
|
19
|
+
border: 'border-blue-200 dark:border-blue-800',
|
|
20
|
+
bg: 'bg-blue-50/50 dark:bg-blue-950/30',
|
|
21
|
+
iconBg: 'bg-blue-100 dark:bg-blue-900/50',
|
|
22
|
+
iconColor: 'text-blue-600 dark:text-blue-400',
|
|
23
|
+
badge: 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-400',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
border: 'border-indigo-200 dark:border-indigo-800',
|
|
27
|
+
bg: 'bg-indigo-50/30 dark:bg-indigo-950/20',
|
|
28
|
+
iconBg: 'bg-indigo-100 dark:bg-indigo-900/50',
|
|
29
|
+
iconColor: 'text-indigo-600 dark:text-indigo-400',
|
|
30
|
+
badge:
|
|
31
|
+
'bg-indigo-50 text-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-400',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
border: 'border-emerald-200 dark:border-emerald-800',
|
|
35
|
+
bg: 'bg-emerald-50/30 dark:bg-emerald-950/20',
|
|
36
|
+
iconBg: 'bg-emerald-100 dark:bg-emerald-900/50',
|
|
37
|
+
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
|
38
|
+
badge:
|
|
39
|
+
'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
border: 'border-amber-200 dark:border-amber-800',
|
|
43
|
+
bg: 'bg-amber-50/30 dark:bg-amber-950/20',
|
|
44
|
+
iconBg: 'bg-amber-100 dark:bg-amber-900/50',
|
|
45
|
+
iconColor: 'text-amber-600 dark:text-amber-400',
|
|
46
|
+
badge:
|
|
47
|
+
'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-400',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function RolesContent({ roles }: { roles: RoleData[] }) {
|
|
52
|
+
const t = useTranslations('core.DashboardPage.userRoles');
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Card className="flex h-full flex-col">
|
|
56
|
+
<CardHeader className="shrink-0 pb-3">
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<Crown className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
|
59
|
+
<div>
|
|
60
|
+
<CardTitle className="text-base font-semibold">
|
|
61
|
+
{t('title')}
|
|
62
|
+
</CardTitle>
|
|
63
|
+
<CardDescription>{t('description')}</CardDescription>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent className="flex-1 overflow-auto pt-0">
|
|
68
|
+
<div className="grid grid-cols-2 gap-2">
|
|
69
|
+
{roles.map((role, index) => {
|
|
70
|
+
const style = levelStyles[index % levelStyles.length]!;
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
key={role.id}
|
|
74
|
+
className={`flex items-center gap-3 rounded-xl border p-3 transition-all duration-200 hover:shadow-sm ${style.border} ${style.bg}`}
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${style.iconBg}`}
|
|
78
|
+
>
|
|
79
|
+
<ShieldCheck className={`h-4 w-4 ${style.iconColor}`} />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
82
|
+
<span className="text-sm font-medium text-foreground">
|
|
83
|
+
{role.name}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="text-xs text-muted-foreground">
|
|
86
|
+
{role.slug}
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
<Badge
|
|
90
|
+
variant="secondary"
|
|
91
|
+
className={`shrink-0 text-[10px] ${style.badge}`}
|
|
92
|
+
>
|
|
93
|
+
{t('active')}
|
|
94
|
+
</Badge>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface UserRolesProps {
|
|
105
|
+
widget?: { name?: string };
|
|
106
|
+
onRemove?: () => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function UserRoles({ widget, onRemove }: UserRolesProps) {
|
|
110
|
+
const { data, isLoading, isError, isAccessDenied } = useWidgetData<
|
|
111
|
+
AllWidgetsData,
|
|
112
|
+
RoleData[]
|
|
113
|
+
>({
|
|
114
|
+
endpoint: '/dashboard-core/widgets/me',
|
|
115
|
+
queryKey: 'widget-me',
|
|
116
|
+
select: (d) => d.userRoles,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<WidgetWrapper
|
|
121
|
+
isLoading={isLoading}
|
|
122
|
+
isError={isError}
|
|
123
|
+
isAccessDenied={isAccessDenied}
|
|
124
|
+
widgetName={widget?.name ?? 'user-roles'}
|
|
125
|
+
onRemove={onRemove}
|
|
126
|
+
>
|
|
127
|
+
{data && <RolesContent roles={data} />}
|
|
128
|
+
</WidgetWrapper>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuTrigger,
|
|
17
|
+
} from '@/components/ui/dropdown-menu';
|
|
18
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
19
|
+
import type { AllWidgetsData, SessionData } from '@/types/widget-data';
|
|
20
|
+
import {
|
|
21
|
+
Clock,
|
|
22
|
+
Globe,
|
|
23
|
+
Info,
|
|
24
|
+
LogOut,
|
|
25
|
+
Monitor,
|
|
26
|
+
MoreHorizontal,
|
|
27
|
+
Smartphone,
|
|
28
|
+
Tablet,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import { useTranslations } from 'next-intl';
|
|
31
|
+
import { useRouter } from 'next/navigation';
|
|
32
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
33
|
+
|
|
34
|
+
function detectDeviceType(ua: string): 'desktop' | 'mobile' | 'tablet' {
|
|
35
|
+
const u = ua.toLowerCase();
|
|
36
|
+
if (u.includes('ipad') || u.includes('tablet')) return 'tablet';
|
|
37
|
+
if (u.includes('mobile') || u.includes('iphone') || u.includes('android'))
|
|
38
|
+
return 'mobile';
|
|
39
|
+
return 'desktop';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectBrowser(ua: string): string {
|
|
43
|
+
if (/edg\//i.test(ua)) return 'Edge';
|
|
44
|
+
if (/chrome\//i.test(ua)) return 'Chrome';
|
|
45
|
+
if (/firefox\//i.test(ua)) return 'Firefox';
|
|
46
|
+
if (/safari\//i.test(ua)) return 'Safari';
|
|
47
|
+
if (/msie|trident/i.test(ua)) return 'IE';
|
|
48
|
+
return 'Browser';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectDevice(ua: string): string {
|
|
52
|
+
if (/ipad/i.test(ua)) return 'iPad';
|
|
53
|
+
if (/iphone/i.test(ua)) return 'iPhone';
|
|
54
|
+
if (/android/i.test(ua)) return 'Android';
|
|
55
|
+
if (/macintosh|mac os/i.test(ua)) return 'Mac';
|
|
56
|
+
if (/windows/i.test(ua)) return 'Windows';
|
|
57
|
+
if (/linux/i.test(ua)) return 'Linux';
|
|
58
|
+
return 'Dispositivo';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatRelative(
|
|
62
|
+
dateStr: string,
|
|
63
|
+
tActiveNow: string,
|
|
64
|
+
tMinsAgo: (n: number) => string,
|
|
65
|
+
tHoursAgo: (n: number) => string,
|
|
66
|
+
tDaysAgo: (n: number) => string
|
|
67
|
+
): string {
|
|
68
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
69
|
+
const mins = Math.floor(diff / 60000);
|
|
70
|
+
if (mins < 1) return tActiveNow;
|
|
71
|
+
if (mins < 60) return tMinsAgo(mins);
|
|
72
|
+
const hours = Math.floor(mins / 60);
|
|
73
|
+
if (hours < 24) return tHoursAgo(hours);
|
|
74
|
+
const days = Math.floor(hours / 24);
|
|
75
|
+
return tDaysAgo(days);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const deviceIcons = {
|
|
79
|
+
desktop: Monitor,
|
|
80
|
+
mobile: Smartphone,
|
|
81
|
+
tablet: Tablet,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function SessionsContent({ sessions }: { sessions: SessionData[] }) {
|
|
85
|
+
const t = useTranslations('core.DashboardPage.userSessions');
|
|
86
|
+
const router = useRouter();
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Card className="flex h-full flex-col">
|
|
90
|
+
<CardHeader className="shrink-0 pb-3">
|
|
91
|
+
<div className="flex items-center justify-between">
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
<Globe className="h-5 w-5 text-blue-600" />
|
|
94
|
+
<div>
|
|
95
|
+
<CardTitle className="text-base font-semibold">
|
|
96
|
+
{t('title')}
|
|
97
|
+
</CardTitle>
|
|
98
|
+
<CardDescription>{t('description')}</CardDescription>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<Button
|
|
102
|
+
variant="outline"
|
|
103
|
+
size="sm"
|
|
104
|
+
onClick={() => router.push('/core/account/sessions')}
|
|
105
|
+
>
|
|
106
|
+
<Info className="h-3.5 w-3.5" />
|
|
107
|
+
{t('moreInfo')}
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent className="flex-1 overflow-auto pt-0">
|
|
112
|
+
<div className="flex flex-col gap-2">
|
|
113
|
+
{sessions.map((session, index) => {
|
|
114
|
+
const ua = session.user_agent ?? '';
|
|
115
|
+
const deviceType = detectDeviceType(ua);
|
|
116
|
+
const browser = detectBrowser(ua);
|
|
117
|
+
const device = detectDevice(ua);
|
|
118
|
+
const isCurrent = index === 0;
|
|
119
|
+
const DeviceIcon = deviceIcons[deviceType];
|
|
120
|
+
const ip = session.ip_address
|
|
121
|
+
? session.ip_address.replace(/(\d+\.\d+\.\d+\.)\d+/, '$1***')
|
|
122
|
+
: '—';
|
|
123
|
+
const relativeTime = formatRelative(
|
|
124
|
+
session.created_at,
|
|
125
|
+
t('activeNow'),
|
|
126
|
+
(n) => t('minutesAgo', { count: n }),
|
|
127
|
+
(n) => t('hoursAgo', { count: n }),
|
|
128
|
+
(n) =>
|
|
129
|
+
n === 1
|
|
130
|
+
? t('daysAgo', { count: n })
|
|
131
|
+
: t('daysAgoPlural', { count: n })
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
key={session.id}
|
|
137
|
+
className={`group flex items-center gap-3 rounded-xl border p-3.5 transition-all duration-200 hover:shadow-sm ${
|
|
138
|
+
isCurrent
|
|
139
|
+
? 'border-emerald-200 bg-emerald-50/50 dark:border-emerald-800 dark:bg-emerald-950/30'
|
|
140
|
+
: 'bg-card hover:bg-muted/30'
|
|
141
|
+
}`}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${
|
|
145
|
+
isCurrent
|
|
146
|
+
? 'bg-emerald-100 dark:bg-emerald-900/50'
|
|
147
|
+
: 'bg-muted'
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
<DeviceIcon
|
|
151
|
+
className={`h-5 w-5 ${
|
|
152
|
+
isCurrent
|
|
153
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
154
|
+
: 'text-muted-foreground'
|
|
155
|
+
}`}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
<span className="text-sm font-medium text-foreground">
|
|
161
|
+
{device}
|
|
162
|
+
</span>
|
|
163
|
+
{isCurrent && (
|
|
164
|
+
<Badge className="bg-emerald-100 text-[10px] text-emerald-700 hover:bg-emerald-100 dark:bg-emerald-900/50 dark:text-emerald-400 dark:hover:bg-emerald-900/50">
|
|
165
|
+
{t('thisSession')}
|
|
166
|
+
</Badge>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
<span className="text-xs text-muted-foreground">
|
|
170
|
+
{browser} · {ip}
|
|
171
|
+
</span>
|
|
172
|
+
<div className="flex items-center gap-3 text-[11px] text-muted-foreground/70">
|
|
173
|
+
<span className="flex items-center gap-1">
|
|
174
|
+
<Clock className="h-3 w-3" />
|
|
175
|
+
{relativeTime}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{!isCurrent && (
|
|
180
|
+
<DropdownMenu>
|
|
181
|
+
<DropdownMenuTrigger asChild>
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="icon"
|
|
185
|
+
className="h-8 w-8 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
186
|
+
>
|
|
187
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
188
|
+
</Button>
|
|
189
|
+
</DropdownMenuTrigger>
|
|
190
|
+
<DropdownMenuContent align="end">
|
|
191
|
+
<DropdownMenuItem className="text-destructive">
|
|
192
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
193
|
+
{t('terminateSession')}
|
|
194
|
+
</DropdownMenuItem>
|
|
195
|
+
</DropdownMenuContent>
|
|
196
|
+
</DropdownMenu>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</div>
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface UserSessionsProps {
|
|
208
|
+
widget?: { name?: string };
|
|
209
|
+
onRemove?: () => void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default function UserSessions({ widget, onRemove }: UserSessionsProps) {
|
|
213
|
+
const { data, isLoading, isError, isAccessDenied } = useWidgetData<
|
|
214
|
+
AllWidgetsData,
|
|
215
|
+
SessionData[]
|
|
216
|
+
>({
|
|
217
|
+
endpoint: '/dashboard-core/widgets/me',
|
|
218
|
+
queryKey: 'widget-me',
|
|
219
|
+
select: (d) => d.userSessions,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<WidgetWrapper
|
|
224
|
+
isLoading={isLoading}
|
|
225
|
+
isError={isError}
|
|
226
|
+
isAccessDenied={isAccessDenied}
|
|
227
|
+
widgetName={widget?.name ?? 'user-sessions'}
|
|
228
|
+
onRemove={onRemove}
|
|
229
|
+
>
|
|
230
|
+
{data && <SessionsContent sessions={data} />}
|
|
231
|
+
</WidgetWrapper>
|
|
232
|
+
);
|
|
233
|
+
}
|