@imansi/templates 0.1.0
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/LICENSE +21 -0
- package/README.md +557 -0
- package/package.json +35 -0
- package/src/auth/ForgotPasswordPage.jsx +176 -0
- package/src/auth/LoginPage.jsx +265 -0
- package/src/auth/RegisterPage.jsx +302 -0
- package/src/auth/ResetPasswordPage.jsx +158 -0
- package/src/auth/Verify2FAPage.jsx +175 -0
- package/src/dashboard/BillingPage.jsx +345 -0
- package/src/dashboard/DashboardHome.jsx +246 -0
- package/src/dashboard/NotificationsPage.jsx +230 -0
- package/src/dashboard/ProfilePage.jsx +272 -0
- package/src/dashboard/SettingsPage.jsx +415 -0
- package/src/dashboard/UsersTablePage.jsx +317 -0
- package/src/extra/CareersPage.jsx +166 -0
- package/src/extra/ChangelogPage.jsx +127 -0
- package/src/extra/CheckoutPage.jsx +383 -0
- package/src/extra/ComingSoonPage.jsx +147 -0
- package/src/extra/ErrorPage.jsx +91 -0
- package/src/extra/InvoicePage.jsx +241 -0
- package/src/extra/LegalPage.jsx +115 -0
- package/src/extra/MaintenancePage.jsx +172 -0
- package/src/extra/OnboardingPage.jsx +197 -0
- package/src/extra/StatusPage.jsx +181 -0
- package/src/index.js +53 -0
- package/src/layouts/AuthLayout.jsx +104 -0
- package/src/layouts/DashboardLayout.jsx +249 -0
- package/src/layouts/MarketingLayout.jsx +184 -0
- package/src/marketing/AboutPage.jsx +189 -0
- package/src/marketing/BlogPage.jsx +182 -0
- package/src/marketing/BlogPostPage.jsx +192 -0
- package/src/marketing/ContactPage.jsx +546 -0
- package/src/marketing/DocsPage.jsx +256 -0
- package/src/marketing/HomePage.jsx +702 -0
- package/src/marketing/LandingPage.jsx +755 -0
- package/src/marketing/PricingPage.jsx +205 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
AvatarFallback,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
Tabs,
|
|
10
|
+
TabsList,
|
|
11
|
+
TabsTrigger,
|
|
12
|
+
cn,
|
|
13
|
+
} from '@imansi/ui';
|
|
14
|
+
import { DashboardLayout } from '../layouts/DashboardLayout.jsx';
|
|
15
|
+
|
|
16
|
+
export function NotificationsPage({
|
|
17
|
+
brandName,
|
|
18
|
+
brandLogo,
|
|
19
|
+
navSections,
|
|
20
|
+
user,
|
|
21
|
+
userMenuItems,
|
|
22
|
+
notificationsCount,
|
|
23
|
+
onLogout,
|
|
24
|
+
|
|
25
|
+
title = 'Notificaciones',
|
|
26
|
+
subtitle = 'Todas tus notificaciones en un solo lugar.',
|
|
27
|
+
markAllText = 'Marcar todas como leídas',
|
|
28
|
+
|
|
29
|
+
notifications = [],
|
|
30
|
+
// [{ id, type: 'message'|'system'|'mention'|'success'|'warning',
|
|
31
|
+
// user?: { name, avatar }, title, description, time, read, href }]
|
|
32
|
+
|
|
33
|
+
tabs = [
|
|
34
|
+
{ value: 'all', label: 'Todas' },
|
|
35
|
+
{ value: 'unread', label: 'Sin leer' },
|
|
36
|
+
{ value: 'mentions', label: 'Menciones' },
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
emptyMessage = 'No tenés notificaciones.',
|
|
40
|
+
|
|
41
|
+
onMarkAll,
|
|
42
|
+
onMarkRead,
|
|
43
|
+
onNotificationClick,
|
|
44
|
+
}) {
|
|
45
|
+
const [tab, setTab] = useState('all');
|
|
46
|
+
|
|
47
|
+
const filtered = notifications.filter((n) => {
|
|
48
|
+
if (tab === 'unread') return !n.read;
|
|
49
|
+
if (tab === 'mentions') return n.type === 'mention';
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const unreadCount = notifications.filter((n) => !n.read).length;
|
|
54
|
+
|
|
55
|
+
const typeIcons = {
|
|
56
|
+
message: {
|
|
57
|
+
color: 'bg-info/10 text-info',
|
|
58
|
+
icon: (
|
|
59
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
60
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
61
|
+
</svg>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
system: {
|
|
65
|
+
color: 'bg-muted text-muted-foreground',
|
|
66
|
+
icon: (
|
|
67
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
68
|
+
<circle cx="12" cy="12" r="10" />
|
|
69
|
+
<line x1="12" y1="16" x2="12" y2="12" />
|
|
70
|
+
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
71
|
+
</svg>
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
mention: {
|
|
75
|
+
color: 'bg-primary/10 text-primary',
|
|
76
|
+
icon: <span className="text-body font-bold">@</span>,
|
|
77
|
+
},
|
|
78
|
+
success: {
|
|
79
|
+
color: 'bg-success/10 text-success',
|
|
80
|
+
icon: (
|
|
81
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
82
|
+
<polyline points="4,12 10,18 20,6" />
|
|
83
|
+
</svg>
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
warning: {
|
|
87
|
+
color: 'bg-warning/10 text-warning',
|
|
88
|
+
icon: (
|
|
89
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
90
|
+
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
91
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
92
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
93
|
+
</svg>
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<DashboardLayout
|
|
100
|
+
brandName={brandName}
|
|
101
|
+
brandLogo={brandLogo}
|
|
102
|
+
navSections={navSections}
|
|
103
|
+
user={user}
|
|
104
|
+
userMenuItems={userMenuItems}
|
|
105
|
+
notificationsCount={notificationsCount}
|
|
106
|
+
onLogout={onLogout}
|
|
107
|
+
title={title}
|
|
108
|
+
>
|
|
109
|
+
<div className="space-y-6 max-w-4xl mx-auto">
|
|
110
|
+
{/* Header */}
|
|
111
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
112
|
+
<div className="space-y-1">
|
|
113
|
+
<h1 className="text-h2 font-bold tracking-tight">{title}</h1>
|
|
114
|
+
<p className="text-small text-muted-foreground">{subtitle}</p>
|
|
115
|
+
</div>
|
|
116
|
+
{unreadCount > 0 && onMarkAll && (
|
|
117
|
+
<Button variant="outline" size="sm" onClick={onMarkAll}>
|
|
118
|
+
{markAllText}
|
|
119
|
+
</Button>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Tabs */}
|
|
124
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
125
|
+
<TabsList>
|
|
126
|
+
{tabs.map((t) => {
|
|
127
|
+
const count =
|
|
128
|
+
t.value === 'all'
|
|
129
|
+
? notifications.length
|
|
130
|
+
: t.value === 'unread'
|
|
131
|
+
? unreadCount
|
|
132
|
+
: notifications.filter((n) => n.type === 'mention').length;
|
|
133
|
+
return (
|
|
134
|
+
<TabsTrigger key={t.value} value={t.value}>
|
|
135
|
+
{t.label}
|
|
136
|
+
<span className="ml-1.5 text-caption text-muted-foreground">
|
|
137
|
+
{count}
|
|
138
|
+
</span>
|
|
139
|
+
</TabsTrigger>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</TabsList>
|
|
143
|
+
</Tabs>
|
|
144
|
+
|
|
145
|
+
{/* Lista */}
|
|
146
|
+
<Card>
|
|
147
|
+
<CardContent className="p-0">
|
|
148
|
+
{filtered.length > 0 ? (
|
|
149
|
+
<ul className="divide-y divide-border">
|
|
150
|
+
{filtered.map((n) => {
|
|
151
|
+
const typeInfo = typeIcons[n.type] || typeIcons.system;
|
|
152
|
+
return (
|
|
153
|
+
<li key={n.id}>
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => {
|
|
156
|
+
onMarkRead?.(n.id);
|
|
157
|
+
onNotificationClick?.(n);
|
|
158
|
+
}}
|
|
159
|
+
className={cn(
|
|
160
|
+
'w-full flex items-start gap-4 p-4 text-left transition-colors hover:bg-muted/50',
|
|
161
|
+
!n.read && 'bg-primary/[0.03]'
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{/* Icono de tipo */}
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
|
|
168
|
+
typeInfo.color
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{typeInfo.icon}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Contenido */}
|
|
175
|
+
<div className="flex-1 min-w-0 space-y-1">
|
|
176
|
+
<div className="flex items-start justify-between gap-3">
|
|
177
|
+
<p className="text-small font-medium leading-snug">
|
|
178
|
+
{n.title}
|
|
179
|
+
</p>
|
|
180
|
+
{!n.read && (
|
|
181
|
+
<span className="shrink-0 mt-1.5 h-2 w-2 rounded-full bg-primary" />
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
{n.description && (
|
|
185
|
+
<p className="text-caption text-muted-foreground leading-relaxed line-clamp-2">
|
|
186
|
+
{n.description}
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
<div className="flex items-center gap-2 pt-1">
|
|
190
|
+
{n.user && (
|
|
191
|
+
<span className="flex items-center gap-1.5 text-caption text-muted-foreground">
|
|
192
|
+
<Avatar size="xs">
|
|
193
|
+
{n.user.avatar ? (
|
|
194
|
+
<img src={n.user.avatar} alt="" />
|
|
195
|
+
) : (
|
|
196
|
+
<AvatarFallback>
|
|
197
|
+
{n.user.name?.charAt(0) || 'U'}
|
|
198
|
+
</AvatarFallback>
|
|
199
|
+
)}
|
|
200
|
+
</Avatar>
|
|
201
|
+
{n.user.name}
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
<span className="text-caption text-muted-foreground">
|
|
205
|
+
{n.time}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</button>
|
|
210
|
+
</li>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</ul>
|
|
214
|
+
) : (
|
|
215
|
+
<div className="text-center py-20 space-y-3">
|
|
216
|
+
<div className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
217
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
218
|
+
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
|
219
|
+
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
|
220
|
+
</svg>
|
|
221
|
+
</div>
|
|
222
|
+
<p className="text-small text-muted-foreground">{emptyMessage}</p>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</CardContent>
|
|
226
|
+
</Card>
|
|
227
|
+
</div>
|
|
228
|
+
</DashboardLayout>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
AvatarFallback,
|
|
5
|
+
AvatarImage,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Separator,
|
|
15
|
+
Textarea,
|
|
16
|
+
} from '@imansi/ui';
|
|
17
|
+
import { DashboardLayout } from '../layouts/DashboardLayout.jsx';
|
|
18
|
+
|
|
19
|
+
export function ProfilePage({
|
|
20
|
+
brandName,
|
|
21
|
+
brandLogo,
|
|
22
|
+
navSections,
|
|
23
|
+
user,
|
|
24
|
+
userMenuItems,
|
|
25
|
+
notificationsCount,
|
|
26
|
+
onLogout,
|
|
27
|
+
|
|
28
|
+
title = 'Mi perfil',
|
|
29
|
+
subtitle = 'Así te ven otros usuarios en la plataforma.',
|
|
30
|
+
|
|
31
|
+
profile,
|
|
32
|
+
// { name, email, role, avatar, bio, company, website, location }
|
|
33
|
+
|
|
34
|
+
stats = [],
|
|
35
|
+
// [{ label, value }]
|
|
36
|
+
|
|
37
|
+
onSave,
|
|
38
|
+
onUploadAvatar,
|
|
39
|
+
onChangePasswordHref = '#',
|
|
40
|
+
}) {
|
|
41
|
+
const [saving, setSaving] = useState(false);
|
|
42
|
+
const [form, setForm] = useState({
|
|
43
|
+
name: profile?.name || user?.name || '',
|
|
44
|
+
email: profile?.email || user?.email || '',
|
|
45
|
+
bio: profile?.bio || '',
|
|
46
|
+
company: profile?.company || '',
|
|
47
|
+
website: profile?.website || '',
|
|
48
|
+
location: profile?.location || '',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function handleSave() {
|
|
52
|
+
if (!onSave) return;
|
|
53
|
+
setSaving(true);
|
|
54
|
+
await onSave(form);
|
|
55
|
+
setSaving(false);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const displayName = form.name || user?.name || 'Usuario';
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<DashboardLayout
|
|
62
|
+
brandName={brandName}
|
|
63
|
+
brandLogo={brandLogo}
|
|
64
|
+
navSections={navSections}
|
|
65
|
+
user={user}
|
|
66
|
+
userMenuItems={userMenuItems}
|
|
67
|
+
notificationsCount={notificationsCount}
|
|
68
|
+
onLogout={onLogout}
|
|
69
|
+
title={title}
|
|
70
|
+
>
|
|
71
|
+
<div className="space-y-6 max-w-5xl mx-auto">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
<h1 className="text-h2 font-bold tracking-tight">{title}</h1>
|
|
75
|
+
<p className="text-small text-muted-foreground">{subtitle}</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
|
|
79
|
+
{/* Formulario principal */}
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle>Información personal</CardTitle>
|
|
83
|
+
<CardDescription>
|
|
84
|
+
Actualizá tus datos y foto de perfil.
|
|
85
|
+
</CardDescription>
|
|
86
|
+
</CardHeader>
|
|
87
|
+
<CardContent className="space-y-6">
|
|
88
|
+
{/* Avatar */}
|
|
89
|
+
<div className="flex flex-wrap items-center gap-5">
|
|
90
|
+
<Avatar size="2xl">
|
|
91
|
+
{profile?.avatar ? (
|
|
92
|
+
<AvatarImage src={profile.avatar} alt={displayName} />
|
|
93
|
+
) : null}
|
|
94
|
+
<AvatarFallback>
|
|
95
|
+
{displayName.charAt(0).toUpperCase()}
|
|
96
|
+
</AvatarFallback>
|
|
97
|
+
</Avatar>
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<div className="flex flex-wrap gap-2">
|
|
100
|
+
<Button
|
|
101
|
+
variant="outline"
|
|
102
|
+
size="sm"
|
|
103
|
+
onClick={onUploadAvatar}
|
|
104
|
+
>
|
|
105
|
+
Cambiar foto
|
|
106
|
+
</Button>
|
|
107
|
+
<Button variant="ghost" size="sm">
|
|
108
|
+
Eliminar
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
<p className="text-caption text-muted-foreground">
|
|
112
|
+
JPG, PNG o GIF. Máximo 2 MB.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<Separator />
|
|
118
|
+
|
|
119
|
+
<div className="grid gap-5 sm:grid-cols-2">
|
|
120
|
+
<div className="space-y-1.5">
|
|
121
|
+
<Label htmlFor="profile-name">Nombre</Label>
|
|
122
|
+
<Input
|
|
123
|
+
id="profile-name"
|
|
124
|
+
value={form.name}
|
|
125
|
+
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="space-y-1.5">
|
|
129
|
+
<Label htmlFor="profile-email">Email</Label>
|
|
130
|
+
<Input
|
|
131
|
+
id="profile-email"
|
|
132
|
+
type="email"
|
|
133
|
+
value={form.email}
|
|
134
|
+
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="space-y-1.5">
|
|
140
|
+
<Label htmlFor="profile-bio">Biografía</Label>
|
|
141
|
+
<Textarea
|
|
142
|
+
id="profile-bio"
|
|
143
|
+
rows={3}
|
|
144
|
+
placeholder="Contanos sobre vos..."
|
|
145
|
+
value={form.bio}
|
|
146
|
+
onChange={(e) => setForm({ ...form, bio: e.target.value })}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="grid gap-5 sm:grid-cols-2">
|
|
151
|
+
<div className="space-y-1.5">
|
|
152
|
+
<Label htmlFor="profile-company">Empresa</Label>
|
|
153
|
+
<Input
|
|
154
|
+
id="profile-company"
|
|
155
|
+
value={form.company}
|
|
156
|
+
onChange={(e) =>
|
|
157
|
+
setForm({ ...form, company: e.target.value })
|
|
158
|
+
}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="space-y-1.5">
|
|
162
|
+
<Label htmlFor="profile-location">Ubicación</Label>
|
|
163
|
+
<Input
|
|
164
|
+
id="profile-location"
|
|
165
|
+
value={form.location}
|
|
166
|
+
onChange={(e) =>
|
|
167
|
+
setForm({ ...form, location: e.target.value })
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="space-y-1.5">
|
|
174
|
+
<Label htmlFor="profile-website">Sitio web</Label>
|
|
175
|
+
<Input
|
|
176
|
+
id="profile-website"
|
|
177
|
+
type="url"
|
|
178
|
+
placeholder="https://..."
|
|
179
|
+
value={form.website}
|
|
180
|
+
onChange={(e) => setForm({ ...form, website: e.target.value })}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<Separator />
|
|
185
|
+
|
|
186
|
+
<div className="flex justify-end gap-2">
|
|
187
|
+
<Button variant="ghost">Cancelar</Button>
|
|
188
|
+
<Button disabled={saving} onClick={handleSave}>
|
|
189
|
+
{saving ? 'Guardando...' : 'Guardar cambios'}
|
|
190
|
+
</Button>
|
|
191
|
+
</div>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
|
|
195
|
+
{/* Sidebar con info */}
|
|
196
|
+
<div className="space-y-6">
|
|
197
|
+
{/* Card de resumen */}
|
|
198
|
+
<Card>
|
|
199
|
+
<CardContent className="p-6 text-center space-y-3">
|
|
200
|
+
<Avatar size="xl" className="mx-auto">
|
|
201
|
+
{profile?.avatar ? (
|
|
202
|
+
<AvatarImage src={profile.avatar} alt={displayName} />
|
|
203
|
+
) : null}
|
|
204
|
+
<AvatarFallback>
|
|
205
|
+
{displayName.charAt(0).toUpperCase()}
|
|
206
|
+
</AvatarFallback>
|
|
207
|
+
</Avatar>
|
|
208
|
+
<div className="space-y-0.5">
|
|
209
|
+
<p className="text-h4 font-semibold">{displayName}</p>
|
|
210
|
+
{profile?.role && (
|
|
211
|
+
<p className="text-caption text-muted-foreground">
|
|
212
|
+
{profile.role}
|
|
213
|
+
</p>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
{stats.length > 0 && (
|
|
217
|
+
<>
|
|
218
|
+
<Separator />
|
|
219
|
+
<div className="grid grid-cols-3 gap-2 pt-1">
|
|
220
|
+
{stats.map((s, i) => (
|
|
221
|
+
<div key={i} className="text-center">
|
|
222
|
+
<p className="text-h4 font-bold text-foreground">
|
|
223
|
+
{s.value}
|
|
224
|
+
</p>
|
|
225
|
+
<p className="text-caption text-muted-foreground">
|
|
226
|
+
{s.label}
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</>
|
|
232
|
+
)}
|
|
233
|
+
</CardContent>
|
|
234
|
+
</Card>
|
|
235
|
+
|
|
236
|
+
{/* Card de seguridad */}
|
|
237
|
+
<Card>
|
|
238
|
+
<CardHeader>
|
|
239
|
+
<CardTitle className="text-h4">Seguridad</CardTitle>
|
|
240
|
+
</CardHeader>
|
|
241
|
+
<CardContent className="space-y-4">
|
|
242
|
+
<div className="flex items-center justify-between gap-3">
|
|
243
|
+
<div className="space-y-0.5 min-w-0">
|
|
244
|
+
<p className="text-small font-medium">Contraseña</p>
|
|
245
|
+
<p className="text-caption text-muted-foreground">
|
|
246
|
+
Última actualización: hace 3 meses
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
<Button variant="outline" size="sm" asChild>
|
|
250
|
+
<a href={onChangePasswordHref}>Cambiar</a>
|
|
251
|
+
</Button>
|
|
252
|
+
</div>
|
|
253
|
+
<Separator />
|
|
254
|
+
<div className="flex items-center justify-between gap-3">
|
|
255
|
+
<div className="space-y-0.5 min-w-0">
|
|
256
|
+
<p className="text-small font-medium">2FA</p>
|
|
257
|
+
<p className="text-caption text-muted-foreground">
|
|
258
|
+
Autenticación en dos pasos
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
<Button variant="outline" size="sm">
|
|
262
|
+
Configurar
|
|
263
|
+
</Button>
|
|
264
|
+
</div>
|
|
265
|
+
</CardContent>
|
|
266
|
+
</Card>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</DashboardLayout>
|
|
271
|
+
);
|
|
272
|
+
}
|