@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,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
Separator,
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
cn,
|
|
17
|
+
} from '@imansi/ui';
|
|
18
|
+
import { DashboardLayout } from '../layouts/DashboardLayout.jsx';
|
|
19
|
+
|
|
20
|
+
export function BillingPage({
|
|
21
|
+
brandName,
|
|
22
|
+
brandLogo,
|
|
23
|
+
navSections,
|
|
24
|
+
user,
|
|
25
|
+
userMenuItems,
|
|
26
|
+
notificationsCount,
|
|
27
|
+
onLogout,
|
|
28
|
+
|
|
29
|
+
title = 'Facturación',
|
|
30
|
+
subtitle = 'Administrá tu plan, método de pago y facturas.',
|
|
31
|
+
|
|
32
|
+
currentPlan,
|
|
33
|
+
// { name, price, period, features, renewsOn }
|
|
34
|
+
|
|
35
|
+
availablePlans = [],
|
|
36
|
+
// [{ name, price, period, description, features, popular }]
|
|
37
|
+
|
|
38
|
+
paymentMethod,
|
|
39
|
+
// { brand, last4, expires, holder }
|
|
40
|
+
|
|
41
|
+
invoices = [],
|
|
42
|
+
// [{ id, date, amount, status: 'paid'|'pending'|'failed' }]
|
|
43
|
+
|
|
44
|
+
statusMap = {
|
|
45
|
+
paid: { label: 'Pagada', variant: 'success' },
|
|
46
|
+
pending: { label: 'Pendiente', variant: 'warning' },
|
|
47
|
+
failed: { label: 'Fallida', variant: 'destructive' },
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
onUpgrade,
|
|
51
|
+
onCancelPlan,
|
|
52
|
+
onUpdatePayment,
|
|
53
|
+
onDownloadInvoice,
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<DashboardLayout
|
|
57
|
+
brandName={brandName}
|
|
58
|
+
brandLogo={brandLogo}
|
|
59
|
+
navSections={navSections}
|
|
60
|
+
user={user}
|
|
61
|
+
userMenuItems={userMenuItems}
|
|
62
|
+
notificationsCount={notificationsCount}
|
|
63
|
+
onLogout={onLogout}
|
|
64
|
+
title={title}
|
|
65
|
+
>
|
|
66
|
+
<div className="space-y-6 max-w-6xl mx-auto">
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<div className="space-y-1">
|
|
69
|
+
<h1 className="text-h2 font-bold tracking-tight">{title}</h1>
|
|
70
|
+
<p className="text-small text-muted-foreground">{subtitle}</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Plan actual + método de pago */}
|
|
74
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
75
|
+
{/* Plan actual */}
|
|
76
|
+
{currentPlan && (
|
|
77
|
+
<Card className="lg:col-span-2">
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<div className="flex items-start justify-between gap-4">
|
|
80
|
+
<div className="space-y-1">
|
|
81
|
+
<CardTitle className="text-h4">
|
|
82
|
+
Plan {currentPlan.name}
|
|
83
|
+
</CardTitle>
|
|
84
|
+
<CardDescription>
|
|
85
|
+
Se renueva el {currentPlan.renewsOn}
|
|
86
|
+
</CardDescription>
|
|
87
|
+
</div>
|
|
88
|
+
<Badge variant="success">Activo</Badge>
|
|
89
|
+
</div>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent className="space-y-5">
|
|
92
|
+
<div className="flex items-baseline gap-1">
|
|
93
|
+
<span className="text-display font-bold tracking-tight">
|
|
94
|
+
{currentPlan.price}
|
|
95
|
+
</span>
|
|
96
|
+
{currentPlan.period && (
|
|
97
|
+
<span className="text-small text-muted-foreground">
|
|
98
|
+
/{currentPlan.period}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{currentPlan.features?.length > 0 && (
|
|
104
|
+
<ul className="grid gap-2 sm:grid-cols-2">
|
|
105
|
+
{currentPlan.features.map((f, i) => (
|
|
106
|
+
<li
|
|
107
|
+
key={i}
|
|
108
|
+
className="flex items-start gap-2 text-small text-muted-foreground"
|
|
109
|
+
>
|
|
110
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-success/10 text-success">
|
|
111
|
+
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
112
|
+
<polyline points="2,6 5,9 10,3" />
|
|
113
|
+
</svg>
|
|
114
|
+
</span>
|
|
115
|
+
{f}
|
|
116
|
+
</li>
|
|
117
|
+
))}
|
|
118
|
+
</ul>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<Separator />
|
|
122
|
+
|
|
123
|
+
<div className="flex flex-wrap gap-2">
|
|
124
|
+
{onUpgrade && <Button onClick={onUpgrade}>Cambiar plan</Button>}
|
|
125
|
+
{onCancelPlan && (
|
|
126
|
+
<Button variant="ghost" onClick={onCancelPlan}>
|
|
127
|
+
Cancelar suscripción
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Método de pago */}
|
|
136
|
+
<Card>
|
|
137
|
+
<CardHeader>
|
|
138
|
+
<CardTitle className="text-h4">Método de pago</CardTitle>
|
|
139
|
+
</CardHeader>
|
|
140
|
+
<CardContent className="space-y-5">
|
|
141
|
+
{paymentMethod ? (
|
|
142
|
+
<>
|
|
143
|
+
<div className="rounded-lg border border-border bg-muted/30 p-4 space-y-3">
|
|
144
|
+
<div className="flex items-center justify-between">
|
|
145
|
+
<span className="text-small font-semibold uppercase tracking-wider text-muted-foreground">
|
|
146
|
+
{paymentMethod.brand}
|
|
147
|
+
</span>
|
|
148
|
+
<svg
|
|
149
|
+
width="32"
|
|
150
|
+
height="20"
|
|
151
|
+
viewBox="0 0 32 20"
|
|
152
|
+
fill="none"
|
|
153
|
+
className="text-muted-foreground"
|
|
154
|
+
>
|
|
155
|
+
<rect
|
|
156
|
+
x="0.5"
|
|
157
|
+
y="0.5"
|
|
158
|
+
width="31"
|
|
159
|
+
height="19"
|
|
160
|
+
rx="2.5"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
/>
|
|
163
|
+
<line
|
|
164
|
+
x1="0"
|
|
165
|
+
y1="7"
|
|
166
|
+
x2="32"
|
|
167
|
+
y2="7"
|
|
168
|
+
stroke="currentColor"
|
|
169
|
+
/>
|
|
170
|
+
</svg>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="space-y-0.5">
|
|
173
|
+
<p className="text-small font-mono tracking-wider">
|
|
174
|
+
•••• •••• •••• {paymentMethod.last4}
|
|
175
|
+
</p>
|
|
176
|
+
<p className="text-caption text-muted-foreground">
|
|
177
|
+
Expira {paymentMethod.expires}
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
{paymentMethod.holder && (
|
|
181
|
+
<p className="text-caption text-muted-foreground pt-1 border-t border-border">
|
|
182
|
+
{paymentMethod.holder}
|
|
183
|
+
</p>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
<Button
|
|
187
|
+
variant="outline"
|
|
188
|
+
fullWidth
|
|
189
|
+
onClick={onUpdatePayment}
|
|
190
|
+
>
|
|
191
|
+
Actualizar tarjeta
|
|
192
|
+
</Button>
|
|
193
|
+
</>
|
|
194
|
+
) : (
|
|
195
|
+
<div className="text-center py-4 space-y-3">
|
|
196
|
+
<p className="text-small text-muted-foreground">
|
|
197
|
+
No hay método de pago configurado.
|
|
198
|
+
</p>
|
|
199
|
+
<Button fullWidth onClick={onUpdatePayment}>
|
|
200
|
+
Agregar tarjeta
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</CardContent>
|
|
205
|
+
</Card>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Planes disponibles */}
|
|
209
|
+
{availablePlans.length > 0 && (
|
|
210
|
+
<div className="space-y-4">
|
|
211
|
+
<h3 className="text-h4 font-semibold">Planes disponibles</h3>
|
|
212
|
+
<div
|
|
213
|
+
className={cn(
|
|
214
|
+
'grid gap-5',
|
|
215
|
+
availablePlans.length === 2 && 'md:grid-cols-2',
|
|
216
|
+
availablePlans.length === 3 && 'md:grid-cols-3',
|
|
217
|
+
availablePlans.length >= 4 && 'md:grid-cols-2 lg:grid-cols-4'
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{availablePlans.map((plan, i) => (
|
|
221
|
+
<div
|
|
222
|
+
key={i}
|
|
223
|
+
className={cn(
|
|
224
|
+
'rounded-xl border bg-card p-6 space-y-5 flex flex-col',
|
|
225
|
+
plan.popular && 'border-primary ring-1 ring-primary'
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
{plan.popular && (
|
|
229
|
+
<Badge variant="primary" className="self-start">
|
|
230
|
+
Recomendado
|
|
231
|
+
</Badge>
|
|
232
|
+
)}
|
|
233
|
+
<div className="space-y-1">
|
|
234
|
+
<h4 className="text-h4 font-semibold">{plan.name}</h4>
|
|
235
|
+
{plan.description && (
|
|
236
|
+
<p className="text-caption text-muted-foreground">
|
|
237
|
+
{plan.description}
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-baseline gap-1">
|
|
242
|
+
<span className="text-h1 font-bold tracking-tight">
|
|
243
|
+
{plan.price}
|
|
244
|
+
</span>
|
|
245
|
+
{plan.period && (
|
|
246
|
+
<span className="text-small text-muted-foreground">
|
|
247
|
+
/{plan.period}
|
|
248
|
+
</span>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
{plan.features?.length > 0 && (
|
|
252
|
+
<ul className="space-y-2 flex-1">
|
|
253
|
+
{plan.features.map((f, j) => (
|
|
254
|
+
<li
|
|
255
|
+
key={j}
|
|
256
|
+
className="flex items-start gap-2 text-small text-muted-foreground"
|
|
257
|
+
>
|
|
258
|
+
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-success/10 text-success">
|
|
259
|
+
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
260
|
+
<polyline points="2,6 5,9 10,3" />
|
|
261
|
+
</svg>
|
|
262
|
+
</span>
|
|
263
|
+
{f}
|
|
264
|
+
</li>
|
|
265
|
+
))}
|
|
266
|
+
</ul>
|
|
267
|
+
)}
|
|
268
|
+
<Button
|
|
269
|
+
variant={plan.popular ? 'primary' : 'outline'}
|
|
270
|
+
fullWidth
|
|
271
|
+
onClick={() => onUpgrade?.(plan)}
|
|
272
|
+
>
|
|
273
|
+
Elegir plan
|
|
274
|
+
</Button>
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Historial de facturas */}
|
|
282
|
+
{invoices.length > 0 && (
|
|
283
|
+
<Card>
|
|
284
|
+
<CardHeader>
|
|
285
|
+
<CardTitle className="text-h4">Historial de facturas</CardTitle>
|
|
286
|
+
<CardDescription>
|
|
287
|
+
Descargá tus facturas anteriores.
|
|
288
|
+
</CardDescription>
|
|
289
|
+
</CardHeader>
|
|
290
|
+
<CardContent className="p-0">
|
|
291
|
+
<Table>
|
|
292
|
+
<TableHeader>
|
|
293
|
+
<TableRow>
|
|
294
|
+
<TableHead>Factura</TableHead>
|
|
295
|
+
<TableHead>Fecha</TableHead>
|
|
296
|
+
<TableHead>Monto</TableHead>
|
|
297
|
+
<TableHead>Estado</TableHead>
|
|
298
|
+
<TableHead className="text-right">Acciones</TableHead>
|
|
299
|
+
</TableRow>
|
|
300
|
+
</TableHeader>
|
|
301
|
+
<TableBody>
|
|
302
|
+
{invoices.map((invoice) => {
|
|
303
|
+
const status = statusMap[invoice.status] || statusMap.pending;
|
|
304
|
+
return (
|
|
305
|
+
<TableRow key={invoice.id}>
|
|
306
|
+
<TableCell className="font-mono text-small">
|
|
307
|
+
{invoice.id}
|
|
308
|
+
</TableCell>
|
|
309
|
+
<TableCell className="text-muted-foreground">
|
|
310
|
+
{invoice.date}
|
|
311
|
+
</TableCell>
|
|
312
|
+
<TableCell className="font-medium">
|
|
313
|
+
{invoice.amount}
|
|
314
|
+
</TableCell>
|
|
315
|
+
<TableCell>
|
|
316
|
+
<Badge variant={status.variant}>
|
|
317
|
+
{status.label}
|
|
318
|
+
</Badge>
|
|
319
|
+
</TableCell>
|
|
320
|
+
<TableCell className="text-right">
|
|
321
|
+
<Button
|
|
322
|
+
variant="ghost"
|
|
323
|
+
size="sm"
|
|
324
|
+
onClick={() => onDownloadInvoice?.(invoice)}
|
|
325
|
+
>
|
|
326
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
327
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
328
|
+
<polyline points="7 10 12 15 17 10" />
|
|
329
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
330
|
+
</svg>
|
|
331
|
+
Descargar
|
|
332
|
+
</Button>
|
|
333
|
+
</TableCell>
|
|
334
|
+
</TableRow>
|
|
335
|
+
);
|
|
336
|
+
})}
|
|
337
|
+
</TableBody>
|
|
338
|
+
</Table>
|
|
339
|
+
</CardContent>
|
|
340
|
+
</Card>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</DashboardLayout>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
AvatarFallback,
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
Progress,
|
|
12
|
+
cn,
|
|
13
|
+
} from '@imansi/ui';
|
|
14
|
+
import { DashboardLayout } from '../layouts/DashboardLayout.jsx';
|
|
15
|
+
|
|
16
|
+
export function DashboardHome({
|
|
17
|
+
brandName,
|
|
18
|
+
brandLogo,
|
|
19
|
+
navSections,
|
|
20
|
+
user,
|
|
21
|
+
userMenuItems,
|
|
22
|
+
notificationsCount,
|
|
23
|
+
onLogout,
|
|
24
|
+
|
|
25
|
+
title = 'Panel',
|
|
26
|
+
subtitle = 'Resumen general de tu actividad.',
|
|
27
|
+
|
|
28
|
+
// Stats
|
|
29
|
+
stats = [],
|
|
30
|
+
// [{ label, value, change, trend: 'up'|'down'|'flat', icon }]
|
|
31
|
+
|
|
32
|
+
// Gráficos
|
|
33
|
+
chartTitle = 'Ingresos de los últimos 7 días',
|
|
34
|
+
chartData = [],
|
|
35
|
+
// [{ label, value }] con value entre 0-100
|
|
36
|
+
|
|
37
|
+
// Actividad
|
|
38
|
+
activityTitle = 'Actividad reciente',
|
|
39
|
+
activityItems = [],
|
|
40
|
+
// [{ id, user: { name, avatar }, action, target, time }]
|
|
41
|
+
|
|
42
|
+
// Top items
|
|
43
|
+
topTitle = 'Top productos',
|
|
44
|
+
topItems = [],
|
|
45
|
+
// [{ id, name, value, progress }]
|
|
46
|
+
|
|
47
|
+
onViewAllActivity,
|
|
48
|
+
onViewAllTop,
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<DashboardLayout
|
|
52
|
+
brandName={brandName}
|
|
53
|
+
brandLogo={brandLogo}
|
|
54
|
+
navSections={navSections}
|
|
55
|
+
user={user}
|
|
56
|
+
userMenuItems={userMenuItems}
|
|
57
|
+
notificationsCount={notificationsCount}
|
|
58
|
+
onLogout={onLogout}
|
|
59
|
+
title={title}
|
|
60
|
+
>
|
|
61
|
+
<div className="space-y-8 max-w-7xl mx-auto">
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
64
|
+
<div className="space-y-1">
|
|
65
|
+
<h1 className="text-h2 font-bold tracking-tight">{title}</h1>
|
|
66
|
+
<p className="text-small text-muted-foreground">{subtitle}</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex gap-2">
|
|
69
|
+
<Button variant="outline" size="sm">
|
|
70
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
71
|
+
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
72
|
+
<line x1="16" y1="2" x2="16" y2="6" />
|
|
73
|
+
<line x1="8" y1="2" x2="8" y2="6" />
|
|
74
|
+
<line x1="3" y1="10" x2="21" y2="10" />
|
|
75
|
+
</svg>
|
|
76
|
+
Últimos 7 días
|
|
77
|
+
</Button>
|
|
78
|
+
<Button size="sm">
|
|
79
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
80
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
81
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
82
|
+
</svg>
|
|
83
|
+
Nuevo
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Stats */}
|
|
89
|
+
{stats.length > 0 && (
|
|
90
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
91
|
+
{stats.map((stat, i) => (
|
|
92
|
+
<Card key={i}>
|
|
93
|
+
<CardContent className="p-5 space-y-3">
|
|
94
|
+
<div className="flex items-start justify-between">
|
|
95
|
+
<div className="space-y-1">
|
|
96
|
+
<p className="text-caption uppercase tracking-wider font-semibold text-muted-foreground">
|
|
97
|
+
{stat.label}
|
|
98
|
+
</p>
|
|
99
|
+
<p className="text-h2 font-bold tracking-tight">
|
|
100
|
+
{stat.value}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
{stat.icon && (
|
|
104
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary shrink-0">
|
|
105
|
+
{stat.icon}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
{stat.change && (
|
|
110
|
+
<div className="flex items-center gap-1.5 text-caption">
|
|
111
|
+
<span
|
|
112
|
+
className={cn(
|
|
113
|
+
'inline-flex items-center gap-0.5 font-medium',
|
|
114
|
+
stat.trend === 'up' && 'text-success',
|
|
115
|
+
stat.trend === 'down' && 'text-destructive',
|
|
116
|
+
(!stat.trend || stat.trend === 'flat') &&
|
|
117
|
+
'text-muted-foreground'
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{stat.trend === 'up' && '↑'}
|
|
121
|
+
{stat.trend === 'down' && '↓'}
|
|
122
|
+
{stat.trend === 'flat' && '→'}
|
|
123
|
+
{stat.change}
|
|
124
|
+
</span>
|
|
125
|
+
<span className="text-muted-foreground">
|
|
126
|
+
vs. período anterior
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</CardContent>
|
|
131
|
+
</Card>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{/* Chart + Activity */}
|
|
137
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
138
|
+
{/* Chart */}
|
|
139
|
+
{chartData.length > 0 && (
|
|
140
|
+
<Card className="lg:col-span-2">
|
|
141
|
+
<CardHeader>
|
|
142
|
+
<CardTitle className="text-h4">{chartTitle}</CardTitle>
|
|
143
|
+
</CardHeader>
|
|
144
|
+
<CardContent>
|
|
145
|
+
{/* Barras simples */}
|
|
146
|
+
<div className="flex items-end justify-between gap-2 h-56">
|
|
147
|
+
{chartData.map((item, i) => (
|
|
148
|
+
<div
|
|
149
|
+
key={i}
|
|
150
|
+
className="flex-1 flex flex-col items-center gap-2 group"
|
|
151
|
+
>
|
|
152
|
+
<div className="w-full flex items-end justify-center h-full">
|
|
153
|
+
<div
|
|
154
|
+
className="w-full max-w-[40px] rounded-t-md bg-primary/80 transition-all group-hover:bg-primary"
|
|
155
|
+
style={{ height: `${item.value}%` }}
|
|
156
|
+
title={`${item.value}%`}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<span className="text-caption text-muted-foreground">
|
|
160
|
+
{item.label}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Activity */}
|
|
170
|
+
{activityItems.length > 0 && (
|
|
171
|
+
<Card>
|
|
172
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
173
|
+
<CardTitle className="text-h4">{activityTitle}</CardTitle>
|
|
174
|
+
{onViewAllActivity && (
|
|
175
|
+
<button
|
|
176
|
+
onClick={onViewAllActivity}
|
|
177
|
+
className="text-caption text-primary hover:underline"
|
|
178
|
+
>
|
|
179
|
+
Ver todo
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
</CardHeader>
|
|
183
|
+
<CardContent className="space-y-4">
|
|
184
|
+
{activityItems.slice(0, 5).map((item) => (
|
|
185
|
+
<div key={item.id} className="flex items-start gap-3">
|
|
186
|
+
<Avatar size="sm">
|
|
187
|
+
{item.user?.avatar ? (
|
|
188
|
+
<img src={item.user.avatar} alt="" />
|
|
189
|
+
) : (
|
|
190
|
+
<AvatarFallback>
|
|
191
|
+
{item.user?.name?.charAt(0) || 'U'}
|
|
192
|
+
</AvatarFallback>
|
|
193
|
+
)}
|
|
194
|
+
</Avatar>
|
|
195
|
+
<div className="flex-1 min-w-0 space-y-0.5">
|
|
196
|
+
<p className="text-small leading-snug">
|
|
197
|
+
<span className="font-medium">
|
|
198
|
+
{item.user?.name}
|
|
199
|
+
</span>{' '}
|
|
200
|
+
<span className="text-muted-foreground">
|
|
201
|
+
{item.action}
|
|
202
|
+
</span>{' '}
|
|
203
|
+
<span className="font-medium">{item.target}</span>
|
|
204
|
+
</p>
|
|
205
|
+
<p className="text-caption text-muted-foreground">
|
|
206
|
+
{item.time}
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
</CardContent>
|
|
212
|
+
</Card>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Top items */}
|
|
217
|
+
{topItems.length > 0 && (
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
220
|
+
<CardTitle className="text-h4">{topTitle}</CardTitle>
|
|
221
|
+
{onViewAllTop && (
|
|
222
|
+
<button
|
|
223
|
+
onClick={onViewAllTop}
|
|
224
|
+
className="text-caption text-primary hover:underline"
|
|
225
|
+
>
|
|
226
|
+
Ver todo
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardContent className="space-y-4">
|
|
231
|
+
{topItems.map((item) => (
|
|
232
|
+
<div key={item.id} className="space-y-2">
|
|
233
|
+
<div className="flex items-center justify-between text-small">
|
|
234
|
+
<span className="font-medium">{item.name}</span>
|
|
235
|
+
<span className="text-muted-foreground">{item.value}</span>
|
|
236
|
+
</div>
|
|
237
|
+
<Progress value={item.progress} size="sm" />
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</CardContent>
|
|
241
|
+
</Card>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
</DashboardLayout>
|
|
245
|
+
);
|
|
246
|
+
}
|