@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,383 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
10
|
+
Separator,
|
|
11
|
+
cn,
|
|
12
|
+
} from '@imansi/ui';
|
|
13
|
+
|
|
14
|
+
export function CheckoutPage({
|
|
15
|
+
brandName = 'Imansi',
|
|
16
|
+
brandLogo,
|
|
17
|
+
title = 'Finalizar compra',
|
|
18
|
+
steps = ['Información', 'Pago', 'Confirmación'],
|
|
19
|
+
currentStep = 1,
|
|
20
|
+
|
|
21
|
+
// Items
|
|
22
|
+
items = [],
|
|
23
|
+
// [{ id, name, description, price, quantity, image }]
|
|
24
|
+
|
|
25
|
+
// Totales
|
|
26
|
+
subtotal,
|
|
27
|
+
tax = '$0',
|
|
28
|
+
discount = '$0',
|
|
29
|
+
total,
|
|
30
|
+
|
|
31
|
+
// Formulario
|
|
32
|
+
emailLabel = 'Correo electrónico',
|
|
33
|
+
cardNumberLabel = 'Número de tarjeta',
|
|
34
|
+
cardNameLabel = 'Nombre en la tarjeta',
|
|
35
|
+
expiryLabel = 'Vencimiento',
|
|
36
|
+
cvcLabel = 'CVC',
|
|
37
|
+
billingAddressLabel = 'Dirección de facturación',
|
|
38
|
+
countryLabel = 'País',
|
|
39
|
+
submitText = 'Pagar',
|
|
40
|
+
submitLoadingText = 'Procesando...',
|
|
41
|
+
successTitle = '¡Pago exitoso!',
|
|
42
|
+
successMessage = 'Recibirás un correo con los detalles de tu compra.',
|
|
43
|
+
|
|
44
|
+
onBack,
|
|
45
|
+
onSubmit,
|
|
46
|
+
loading = false,
|
|
47
|
+
success = false,
|
|
48
|
+
className,
|
|
49
|
+
}) {
|
|
50
|
+
const [form, setForm] = useState({
|
|
51
|
+
email: '',
|
|
52
|
+
cardNumber: '',
|
|
53
|
+
cardName: '',
|
|
54
|
+
expiry: '',
|
|
55
|
+
cvc: '',
|
|
56
|
+
address: '',
|
|
57
|
+
city: '',
|
|
58
|
+
zip: '',
|
|
59
|
+
country: 'AR',
|
|
60
|
+
});
|
|
61
|
+
const [localError, setLocalError] = useState(null);
|
|
62
|
+
|
|
63
|
+
async function handleSubmit(e) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
setLocalError(null);
|
|
66
|
+
if (!form.email || !form.cardNumber) {
|
|
67
|
+
setLocalError('Completá los datos de pago.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (onSubmit) await onSubmit(form);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={cn('min-h-dvh bg-background text-foreground flex flex-col', className)}>
|
|
75
|
+
{/* Header */}
|
|
76
|
+
<header className="border-b border-border bg-card">
|
|
77
|
+
<div className="imansi-container h-16 flex items-center justify-between">
|
|
78
|
+
<div className="flex items-center gap-2.5">
|
|
79
|
+
{brandLogo || (
|
|
80
|
+
<div className="h-9 w-9 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
|
|
81
|
+
{brandName.charAt(0)}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
<span className="text-h4 font-semibold tracking-tight">{brandName}</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex items-center gap-1.5 text-caption text-muted-foreground">
|
|
87
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
88
|
+
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
89
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
90
|
+
</svg>
|
|
91
|
+
Pago seguro
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
{/* Stepper */}
|
|
97
|
+
<div className="border-b border-border bg-card">
|
|
98
|
+
<div className="imansi-container py-4">
|
|
99
|
+
<div className="flex items-center justify-center gap-2 sm:gap-4">
|
|
100
|
+
{steps.map((label, i) => {
|
|
101
|
+
const stepNumber = i + 1;
|
|
102
|
+
const isActive = stepNumber === currentStep;
|
|
103
|
+
const isDone = stepNumber < currentStep;
|
|
104
|
+
return (
|
|
105
|
+
<div key={label} className="flex items-center gap-2 sm:gap-4">
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<div
|
|
108
|
+
className={cn(
|
|
109
|
+
'flex h-7 w-7 items-center justify-center rounded-full text-caption font-semibold transition-colors',
|
|
110
|
+
isDone && 'bg-success text-success-foreground',
|
|
111
|
+
isActive && 'bg-primary text-primary-foreground',
|
|
112
|
+
!isDone && !isActive && 'bg-muted text-muted-foreground'
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{isDone ? (
|
|
116
|
+
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
117
|
+
<polyline points="2,6 5,9 10,3" />
|
|
118
|
+
</svg>
|
|
119
|
+
) : (
|
|
120
|
+
stepNumber
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
<span
|
|
124
|
+
className={cn(
|
|
125
|
+
'text-small font-medium hidden sm:block',
|
|
126
|
+
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{label}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
{i < steps.length - 1 && (
|
|
133
|
+
<div className={cn('h-0.5 w-6 sm:w-12', isDone ? 'bg-success' : 'bg-border')} />
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Contenido */}
|
|
143
|
+
<main className="flex-1">
|
|
144
|
+
<div className="imansi-container py-10 max-w-6xl">
|
|
145
|
+
{success ? (
|
|
146
|
+
<div className="max-w-lg mx-auto text-center space-y-6">
|
|
147
|
+
<div className="inline-flex h-20 w-20 items-center justify-center rounded-full bg-success/10 text-success">
|
|
148
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
149
|
+
<polyline points="4,12 10,18 20,6" />
|
|
150
|
+
</svg>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<h1 className="text-h1 font-bold tracking-tight">{successTitle}</h1>
|
|
154
|
+
<p className="text-body text-muted-foreground">{successMessage}</p>
|
|
155
|
+
</div>
|
|
156
|
+
<Button size="lg">Volver al inicio</Button>
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="grid gap-8 lg:grid-cols-[1.4fr_1fr]">
|
|
160
|
+
{/* FORMULARIO */}
|
|
161
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
162
|
+
{localError && (
|
|
163
|
+
<Alert variant="destructive" title="Error">{localError}</Alert>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Contacto */}
|
|
167
|
+
<Card>
|
|
168
|
+
<CardContent className="p-6 space-y-5">
|
|
169
|
+
<h2 className="text-h4 font-semibold">Información de contacto</h2>
|
|
170
|
+
<div className="space-y-1.5">
|
|
171
|
+
<Label htmlFor="checkout-email">{emailLabel}</Label>
|
|
172
|
+
<Input
|
|
173
|
+
id="checkout-email"
|
|
174
|
+
type="email"
|
|
175
|
+
placeholder="tu@correo.com"
|
|
176
|
+
value={form.email}
|
|
177
|
+
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
178
|
+
autoComplete="email"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</CardContent>
|
|
182
|
+
</Card>
|
|
183
|
+
|
|
184
|
+
{/* Pago */}
|
|
185
|
+
<Card>
|
|
186
|
+
<CardContent className="p-6 space-y-5">
|
|
187
|
+
<div className="flex items-center justify-between">
|
|
188
|
+
<h2 className="text-h4 font-semibold">Método de pago</h2>
|
|
189
|
+
<div className="flex gap-1.5">
|
|
190
|
+
<span className="h-6 w-10 rounded border border-border bg-card" />
|
|
191
|
+
<span className="h-6 w-10 rounded border border-border bg-card" />
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="space-y-1.5">
|
|
196
|
+
<Label htmlFor="checkout-card">{cardNumberLabel}</Label>
|
|
197
|
+
<Input
|
|
198
|
+
id="checkout-card"
|
|
199
|
+
placeholder="1234 5678 9012 3456"
|
|
200
|
+
value={form.cardNumber}
|
|
201
|
+
onChange={(e) => setForm({ ...form, cardNumber: e.target.value })}
|
|
202
|
+
autoComplete="cc-number"
|
|
203
|
+
inputMode="numeric"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="space-y-1.5">
|
|
208
|
+
<Label htmlFor="checkout-name">{cardNameLabel}</Label>
|
|
209
|
+
<Input
|
|
210
|
+
id="checkout-name"
|
|
211
|
+
placeholder="JUAN PEREZ"
|
|
212
|
+
value={form.cardName}
|
|
213
|
+
onChange={(e) => setForm({ ...form, cardName: e.target.value })}
|
|
214
|
+
autoComplete="cc-name"
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="grid grid-cols-2 gap-4">
|
|
219
|
+
<div className="space-y-1.5">
|
|
220
|
+
<Label htmlFor="checkout-expiry">{expiryLabel}</Label>
|
|
221
|
+
<Input
|
|
222
|
+
id="checkout-expiry"
|
|
223
|
+
placeholder="MM/AA"
|
|
224
|
+
value={form.expiry}
|
|
225
|
+
onChange={(e) => setForm({ ...form, expiry: e.target.value })}
|
|
226
|
+
autoComplete="cc-exp"
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="space-y-1.5">
|
|
230
|
+
<Label htmlFor="checkout-cvc">{cvcLabel}</Label>
|
|
231
|
+
<Input
|
|
232
|
+
id="checkout-cvc"
|
|
233
|
+
placeholder="123"
|
|
234
|
+
value={form.cvc}
|
|
235
|
+
onChange={(e) => setForm({ ...form, cvc: e.target.value })}
|
|
236
|
+
autoComplete="cc-csc"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</CardContent>
|
|
241
|
+
</Card>
|
|
242
|
+
|
|
243
|
+
{/* Dirección */}
|
|
244
|
+
<Card>
|
|
245
|
+
<CardContent className="p-6 space-y-5">
|
|
246
|
+
<h2 className="text-h4 font-semibold">{billingAddressLabel}</h2>
|
|
247
|
+
<div className="space-y-1.5">
|
|
248
|
+
<Label htmlFor="checkout-address">Dirección</Label>
|
|
249
|
+
<Input
|
|
250
|
+
id="checkout-address"
|
|
251
|
+
placeholder="Av. Corrientes 1234"
|
|
252
|
+
value={form.address}
|
|
253
|
+
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
|
254
|
+
autoComplete="street-address"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
<div className="grid grid-cols-2 gap-4">
|
|
258
|
+
<div className="space-y-1.5">
|
|
259
|
+
<Label htmlFor="checkout-city">Ciudad</Label>
|
|
260
|
+
<Input
|
|
261
|
+
id="checkout-city"
|
|
262
|
+
value={form.city}
|
|
263
|
+
onChange={(e) => setForm({ ...form, city: e.target.value })}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="space-y-1.5">
|
|
267
|
+
<Label htmlFor="checkout-zip">Código postal</Label>
|
|
268
|
+
<Input
|
|
269
|
+
id="checkout-zip"
|
|
270
|
+
value={form.zip}
|
|
271
|
+
onChange={(e) => setForm({ ...form, zip: e.target.value })}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="space-y-1.5">
|
|
276
|
+
<Label htmlFor="checkout-country">{countryLabel}</Label>
|
|
277
|
+
<select
|
|
278
|
+
id="checkout-country"
|
|
279
|
+
value={form.country}
|
|
280
|
+
onChange={(e) => setForm({ ...form, country: e.target.value })}
|
|
281
|
+
className="flex h-10 w-full rounded-md border border-border bg-input px-3 py-2 text-body focus:outline-none focus:ring-2 focus:ring-ring"
|
|
282
|
+
>
|
|
283
|
+
<option value="AR">Argentina</option>
|
|
284
|
+
<option value="MX">México</option>
|
|
285
|
+
<option value="ES">España</option>
|
|
286
|
+
<option value="US">Estados Unidos</option>
|
|
287
|
+
</select>
|
|
288
|
+
</div>
|
|
289
|
+
</CardContent>
|
|
290
|
+
</Card>
|
|
291
|
+
|
|
292
|
+
{/* Submit mobile */}
|
|
293
|
+
<div className="lg:hidden">
|
|
294
|
+
<Button type="submit" fullWidth size="lg" disabled={loading}>
|
|
295
|
+
{loading ? submitLoadingText : `${submitText} ${total}`}
|
|
296
|
+
</Button>
|
|
297
|
+
</div>
|
|
298
|
+
</form>
|
|
299
|
+
|
|
300
|
+
{/* RESUMEN */}
|
|
301
|
+
<aside className="space-y-6">
|
|
302
|
+
<Card className="lg:sticky lg:top-24">
|
|
303
|
+
<CardContent className="p-6 space-y-5">
|
|
304
|
+
<h2 className="text-h4 font-semibold">Resumen del pedido</h2>
|
|
305
|
+
|
|
306
|
+
<Separator />
|
|
307
|
+
|
|
308
|
+
{/* Items */}
|
|
309
|
+
<ul className="space-y-4">
|
|
310
|
+
{items.map((item) => (
|
|
311
|
+
<li key={item.id} className="flex gap-3">
|
|
312
|
+
<div className="h-12 w-12 shrink-0 rounded-md border border-border bg-muted flex items-center justify-center text-muted-foreground">
|
|
313
|
+
{item.image ? (
|
|
314
|
+
<img src={item.image} alt="" className="h-full w-full object-cover rounded-md" />
|
|
315
|
+
) : (
|
|
316
|
+
<span className="text-caption">{item.quantity}×</span>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex-1 min-w-0">
|
|
320
|
+
<p className="text-small font-medium truncate">{item.name}</p>
|
|
321
|
+
{item.description && (
|
|
322
|
+
<p className="text-caption text-muted-foreground truncate">
|
|
323
|
+
{item.description}
|
|
324
|
+
</p>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
<p className="text-small font-medium shrink-0">{item.price}</p>
|
|
328
|
+
</li>
|
|
329
|
+
))}
|
|
330
|
+
</ul>
|
|
331
|
+
|
|
332
|
+
<Separator />
|
|
333
|
+
|
|
334
|
+
{/* Totales */}
|
|
335
|
+
<div className="space-y-2 text-small">
|
|
336
|
+
<div className="flex justify-between text-muted-foreground">
|
|
337
|
+
<span>Subtotal</span>
|
|
338
|
+
<span>{subtotal}</span>
|
|
339
|
+
</div>
|
|
340
|
+
{discount && discount !== '$0' && (
|
|
341
|
+
<div className="flex justify-between text-success">
|
|
342
|
+
<span>Descuento</span>
|
|
343
|
+
<span>-{discount}</span>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
<div className="flex justify-between text-muted-foreground">
|
|
347
|
+
<span>Impuestos</span>
|
|
348
|
+
<span>{tax}</span>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<Separator />
|
|
353
|
+
|
|
354
|
+
<div className="flex justify-between items-baseline">
|
|
355
|
+
<span className="text-small font-semibold">Total</span>
|
|
356
|
+
<span className="text-h2 font-bold tracking-tight">{total}</span>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{/* Submit desktop */}
|
|
360
|
+
<div className="hidden lg:block pt-2">
|
|
361
|
+
<Button type="submit" fullWidth size="lg" disabled={loading} onClick={handleSubmit}>
|
|
362
|
+
{loading ? submitLoadingText : submitText}
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Seguridad */}
|
|
367
|
+
<div className="flex items-center justify-center gap-2 text-caption text-muted-foreground pt-2">
|
|
368
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
369
|
+
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
370
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
371
|
+
</svg>
|
|
372
|
+
Pago encriptado SSL
|
|
373
|
+
</div>
|
|
374
|
+
</CardContent>
|
|
375
|
+
</Card>
|
|
376
|
+
</aside>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
</main>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Alert, Button, Input, cn } from '@imansi/ui';
|
|
3
|
+
|
|
4
|
+
export function ComingSoonPage({
|
|
5
|
+
brandName = 'Imansi',
|
|
6
|
+
brandLogo,
|
|
7
|
+
badge = 'Próximamente',
|
|
8
|
+
title = 'Algo grande está en camino',
|
|
9
|
+
subtitle = 'Estamos construyendo algo nuevo. Dejanos tu correo y te avisamos cuando lancemos.',
|
|
10
|
+
emailPlaceholder = 'tu@correo.com',
|
|
11
|
+
submitText = 'Avisame',
|
|
12
|
+
submitLoadingText = 'Enviando...',
|
|
13
|
+
successTitle = '¡Listo!',
|
|
14
|
+
successMessage = 'Te avisaremos en cuanto lancemos.',
|
|
15
|
+
socialLinks = [],
|
|
16
|
+
footerNote = 'Sin spam. Te avisamos una sola vez cuando esté listo.',
|
|
17
|
+
onSubmit,
|
|
18
|
+
className,
|
|
19
|
+
}) {
|
|
20
|
+
const [email, setEmail] = useState('');
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const [success, setSuccess] = useState(false);
|
|
24
|
+
|
|
25
|
+
async function handleSubmit(e) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setError(null);
|
|
28
|
+
if (!email.includes('@')) {
|
|
29
|
+
setError('Ingresá un correo válido.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setLoading(true);
|
|
33
|
+
if (onSubmit) await onSubmit({ email });
|
|
34
|
+
else await new Promise((r) => setTimeout(r, 800));
|
|
35
|
+
setLoading(false);
|
|
36
|
+
setSuccess(true);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn('min-h-dvh flex flex-col bg-background text-foreground relative overflow-hidden', className)}>
|
|
41
|
+
{/* Decoración de fondo */}
|
|
42
|
+
<div
|
|
43
|
+
className="absolute inset-0 opacity-[0.04] pointer-events-none"
|
|
44
|
+
style={{
|
|
45
|
+
backgroundImage: `
|
|
46
|
+
radial-gradient(circle at 30% 20%, currentColor 0, transparent 40%),
|
|
47
|
+
radial-gradient(circle at 70% 80%, currentColor 0, transparent 40%)
|
|
48
|
+
`,
|
|
49
|
+
}}
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
{/* Header */}
|
|
54
|
+
<header className="relative imansi-container py-6">
|
|
55
|
+
<div className="flex items-center justify-center gap-2.5">
|
|
56
|
+
{brandLogo || (
|
|
57
|
+
<div className="h-10 w-10 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
|
|
58
|
+
{brandName.charAt(0)}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
<span className="text-h4 font-semibold tracking-tight">{brandName}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
{/* Contenido */}
|
|
66
|
+
<main className="relative flex-1 flex items-center justify-center p-6">
|
|
67
|
+
<div className="max-w-xl w-full text-center space-y-8">
|
|
68
|
+
{/* Badge */}
|
|
69
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-4 py-1.5 text-caption font-medium uppercase tracking-widest text-muted-foreground">
|
|
70
|
+
<span className="inline-block h-2 w-2 rounded-full bg-primary animate-pulse-soft" />
|
|
71
|
+
{badge}
|
|
72
|
+
</span>
|
|
73
|
+
|
|
74
|
+
{/* Título */}
|
|
75
|
+
<div className="space-y-4">
|
|
76
|
+
<h1 className="text-display font-bold tracking-tight leading-[1.05]">
|
|
77
|
+
{title}
|
|
78
|
+
</h1>
|
|
79
|
+
<p className="text-body text-muted-foreground leading-relaxed max-w-md mx-auto">
|
|
80
|
+
{subtitle}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Form o success */}
|
|
85
|
+
<div className="max-w-md mx-auto pt-4">
|
|
86
|
+
{success ? (
|
|
87
|
+
<div className="rounded-lg border border-success/30 bg-success/10 p-6 space-y-2">
|
|
88
|
+
<div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-success text-success-foreground">
|
|
89
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
90
|
+
<polyline points="4,12 10,18 20,6" />
|
|
91
|
+
</svg>
|
|
92
|
+
</div>
|
|
93
|
+
<h3 className="text-h4 font-semibold text-success">{successTitle}</h3>
|
|
94
|
+
<p className="text-small text-muted-foreground">{successMessage}</p>
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
<>
|
|
98
|
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-2">
|
|
99
|
+
<Input
|
|
100
|
+
type="email"
|
|
101
|
+
placeholder={emailPlaceholder}
|
|
102
|
+
value={email}
|
|
103
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
104
|
+
disabled={loading}
|
|
105
|
+
className="flex-1"
|
|
106
|
+
/>
|
|
107
|
+
<Button type="submit" disabled={loading}>
|
|
108
|
+
{loading ? submitLoadingText : submitText}
|
|
109
|
+
</Button>
|
|
110
|
+
</form>
|
|
111
|
+
{error && (
|
|
112
|
+
<Alert variant="destructive" className="mt-4">
|
|
113
|
+
{error}
|
|
114
|
+
</Alert>
|
|
115
|
+
)}
|
|
116
|
+
<p className="text-caption text-muted-foreground mt-3">
|
|
117
|
+
{footerNote}
|
|
118
|
+
</p>
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Social */}
|
|
124
|
+
{socialLinks.length > 0 && (
|
|
125
|
+
<div className="flex justify-center gap-3 pt-6">
|
|
126
|
+
{socialLinks.map((s, i) => (
|
|
127
|
+
<a
|
|
128
|
+
key={i}
|
|
129
|
+
href={s.href}
|
|
130
|
+
aria-label={s.label}
|
|
131
|
+
className="flex h-10 w-10 items-center justify-center rounded-md border border-border text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
|
132
|
+
>
|
|
133
|
+
{s.icon}
|
|
134
|
+
</a>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</main>
|
|
140
|
+
|
|
141
|
+
{/* Footer */}
|
|
142
|
+
<footer className="relative imansi-container py-6 text-center text-caption text-muted-foreground">
|
|
143
|
+
© {new Date().getFullYear()} {brandName}
|
|
144
|
+
</footer>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import { Button, cn } from '@imansi/ui';
|
|
3
|
+
|
|
4
|
+
export function ErrorPage({
|
|
5
|
+
brandName = 'Imansi',
|
|
6
|
+
brandLogo,
|
|
7
|
+
code = '404',
|
|
8
|
+
title = 'Página no encontrada',
|
|
9
|
+
subtitle = 'Lo sentimos, la página que buscás no existe o fue movida.',
|
|
10
|
+
primaryCTA = { label: 'Volver al inicio', href: '/' },
|
|
11
|
+
secondaryCTA = { label: 'Contactar soporte', href: '/contacto' },
|
|
12
|
+
helpText,
|
|
13
|
+
className,
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('min-h-dvh flex flex-col bg-background text-foreground', className)}>
|
|
17
|
+
{/* Header minimal */}
|
|
18
|
+
<header className="imansi-container py-6">
|
|
19
|
+
<Link to="/" className="inline-flex items-center gap-2.5">
|
|
20
|
+
{brandLogo || (
|
|
21
|
+
<div className="h-9 w-9 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
|
|
22
|
+
{brandName.charAt(0)}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<span className="text-h4 font-semibold tracking-tight">{brandName}</span>
|
|
26
|
+
</Link>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
{/* Contenido */}
|
|
30
|
+
<main className="flex-1 flex items-center justify-center p-6">
|
|
31
|
+
<div className="max-w-lg w-full text-center space-y-8">
|
|
32
|
+
{/* Código grande */}
|
|
33
|
+
<div className="relative">
|
|
34
|
+
<div
|
|
35
|
+
className="absolute inset-0 blur-3xl opacity-30"
|
|
36
|
+
style={{
|
|
37
|
+
background: `radial-gradient(circle, var(--color-primary) 0%, transparent 70%)`,
|
|
38
|
+
}}
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
/>
|
|
41
|
+
<p className="relative text-[8rem] sm:text-[10rem] font-bold tracking-tighter leading-none text-primary/20 select-none">
|
|
42
|
+
{code}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Texto */}
|
|
47
|
+
<div className="space-y-3">
|
|
48
|
+
<h1 className="text-h1 font-bold tracking-tight">{title}</h1>
|
|
49
|
+
<p className="text-body text-muted-foreground leading-relaxed max-w-md mx-auto">
|
|
50
|
+
{subtitle}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* CTAs */}
|
|
55
|
+
<div className="flex flex-wrap justify-center gap-3">
|
|
56
|
+
{primaryCTA && (
|
|
57
|
+
<Link to={primaryCTA.href}>
|
|
58
|
+
<Button size="lg">
|
|
59
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
60
|
+
<path d="M3 12L12 3l9 9" />
|
|
61
|
+
<path d="M5 10v10h14V10" />
|
|
62
|
+
</svg>
|
|
63
|
+
{primaryCTA.label}
|
|
64
|
+
</Button>
|
|
65
|
+
</Link>
|
|
66
|
+
)}
|
|
67
|
+
{secondaryCTA && (
|
|
68
|
+
<Link to={secondaryCTA.href}>
|
|
69
|
+
<Button size="lg" variant="outline">
|
|
70
|
+
{secondaryCTA.label}
|
|
71
|
+
</Button>
|
|
72
|
+
</Link>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Help */}
|
|
77
|
+
{helpText && (
|
|
78
|
+
<p className="text-caption text-muted-foreground pt-4">
|
|
79
|
+
{helpText}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</main>
|
|
84
|
+
|
|
85
|
+
{/* Footer minimal */}
|
|
86
|
+
<footer className="imansi-container py-6 text-center text-caption text-muted-foreground">
|
|
87
|
+
© {new Date().getFullYear()} {brandName}. Todos los derechos reservados.
|
|
88
|
+
</footer>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|