@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +557 -0
  3. package/package.json +35 -0
  4. package/src/auth/ForgotPasswordPage.jsx +176 -0
  5. package/src/auth/LoginPage.jsx +265 -0
  6. package/src/auth/RegisterPage.jsx +302 -0
  7. package/src/auth/ResetPasswordPage.jsx +158 -0
  8. package/src/auth/Verify2FAPage.jsx +175 -0
  9. package/src/dashboard/BillingPage.jsx +345 -0
  10. package/src/dashboard/DashboardHome.jsx +246 -0
  11. package/src/dashboard/NotificationsPage.jsx +230 -0
  12. package/src/dashboard/ProfilePage.jsx +272 -0
  13. package/src/dashboard/SettingsPage.jsx +415 -0
  14. package/src/dashboard/UsersTablePage.jsx +317 -0
  15. package/src/extra/CareersPage.jsx +166 -0
  16. package/src/extra/ChangelogPage.jsx +127 -0
  17. package/src/extra/CheckoutPage.jsx +383 -0
  18. package/src/extra/ComingSoonPage.jsx +147 -0
  19. package/src/extra/ErrorPage.jsx +91 -0
  20. package/src/extra/InvoicePage.jsx +241 -0
  21. package/src/extra/LegalPage.jsx +115 -0
  22. package/src/extra/MaintenancePage.jsx +172 -0
  23. package/src/extra/OnboardingPage.jsx +197 -0
  24. package/src/extra/StatusPage.jsx +181 -0
  25. package/src/index.js +53 -0
  26. package/src/layouts/AuthLayout.jsx +104 -0
  27. package/src/layouts/DashboardLayout.jsx +249 -0
  28. package/src/layouts/MarketingLayout.jsx +184 -0
  29. package/src/marketing/AboutPage.jsx +189 -0
  30. package/src/marketing/BlogPage.jsx +182 -0
  31. package/src/marketing/BlogPostPage.jsx +192 -0
  32. package/src/marketing/ContactPage.jsx +546 -0
  33. package/src/marketing/DocsPage.jsx +256 -0
  34. package/src/marketing/HomePage.jsx +702 -0
  35. package/src/marketing/LandingPage.jsx +755 -0
  36. package/src/marketing/PricingPage.jsx +205 -0
@@ -0,0 +1,546 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Button,
5
+ Card,
6
+ CardContent,
7
+ Input,
8
+ Label,
9
+ Separator,
10
+ Textarea,
11
+ } from '@imansi/ui';
12
+ import { MarketingLayout } from '../layouts/MarketingLayout.jsx';
13
+
14
+ export function ContactPage({
15
+ brandName = 'Imansi',
16
+ brandLogo,
17
+ navLinks = [],
18
+ headerCTA,
19
+ headerSecondaryCTA,
20
+ footerColumns = [],
21
+
22
+ /* ===== Hero ===== */
23
+ badge = 'Contacto',
24
+ title = 'Hablemos',
25
+ subtitle = 'Nuestro equipo está disponible para ayudarte con cualquier consulta sobre el producto, precios o partnership.',
26
+
27
+ /* ===== Cards de métodos ===== */
28
+ contactMethods = [],
29
+
30
+ /* ===== Formulario ===== */
31
+ formTitle = 'Enviá tu consulta',
32
+ formNameLabel = 'Nombre completo',
33
+ formEmailLabel = 'Correo electrónico',
34
+ formCompanyLabel = 'Empresa',
35
+ formSubjectLabel = 'Asunto',
36
+ formSubjectOptions = [],
37
+ formMessageLabel = 'Mensaje',
38
+ formMessagePlaceholder = 'Contanos sobre tu proyecto, consulta o problema...',
39
+ formPrivacyNote = 'Al enviar aceptás nuestra Política de Privacidad.',
40
+ formSubmitText = 'Enviar mensaje',
41
+ formSubmitLoadingText = 'Enviando...',
42
+ formSuccessTitle = '¡Mensaje enviado!',
43
+ formSuccessMessage = 'Gracias por escribirnos. Te responderemos en menos de 24 horas.',
44
+
45
+ /* ===== Sidebar: info ===== */
46
+ infoTitle = 'Información de contacto',
47
+ infoItems = [],
48
+
49
+ /* ===== Sidebar: horario ===== */
50
+ hoursTitle = 'Horario de atención',
51
+ hoursItems = [],
52
+
53
+ /* ===== Oficinas ===== */
54
+ officesTitle = 'Nuestras oficinas',
55
+ officesSubtitle = 'Estamos presentes en tres continentes.',
56
+ offices = [],
57
+
58
+ onSubmit,
59
+ error: externalError,
60
+ loading = false,
61
+ success: externalSuccess = false,
62
+ }) {
63
+ const [nombre, setNombre] = useState('');
64
+ const [email, setEmail] = useState('');
65
+ const [empresa, setEmpresa] = useState('');
66
+ const [asunto, setAsunto] = useState('');
67
+ const [mensaje, setMensaje] = useState('');
68
+ const [localError, setLocalError] = useState(null);
69
+ const [localSuccess, setLocalSuccess] = useState(false);
70
+
71
+ const showingSuccess = externalSuccess || localSuccess;
72
+ const error = localError || externalError;
73
+
74
+ async function handleSubmit(e) {
75
+ e.preventDefault();
76
+ setLocalError(null);
77
+
78
+ if (!nombre || !email || !mensaje) {
79
+ setLocalError('Completá los campos obligatorios.');
80
+ return;
81
+ }
82
+
83
+ if (!email.includes('@')) {
84
+ setLocalError('Ingresá un correo válido.');
85
+ return;
86
+ }
87
+
88
+ if (onSubmit) {
89
+ const result = await onSubmit({ nombre, email, empresa, asunto, mensaje });
90
+ if (result?.ok) setLocalSuccess(true);
91
+ } else {
92
+ setLocalSuccess(true);
93
+ }
94
+ }
95
+
96
+ function resetForm() {
97
+ setNombre('');
98
+ setEmail('');
99
+ setEmpresa('');
100
+ setAsunto('');
101
+ setMensaje('');
102
+ setLocalSuccess(false);
103
+ }
104
+
105
+ return (
106
+ <MarketingLayout
107
+ brandName={brandName}
108
+ brandLogo={brandLogo}
109
+ navLinks={navLinks}
110
+ headerCTA={headerCTA}
111
+ headerSecondaryCTA={headerSecondaryCTA}
112
+ footerColumns={footerColumns}
113
+ >
114
+ {/* ============ HERO ============ */}
115
+ <section className="imansi-container pt-24 pb-16">
116
+ <div className="text-center max-w-2xl mx-auto space-y-5">
117
+ <span className="inline-flex items-center rounded-full border border-border bg-card px-4 py-1.5 text-caption font-medium uppercase tracking-widest text-muted-foreground">
118
+ {badge}
119
+ </span>
120
+ <h1 className="text-display font-bold tracking-tight leading-[1.05]">
121
+ {title}
122
+ </h1>
123
+ <p className="text-body text-muted-foreground leading-relaxed">
124
+ {subtitle}
125
+ </p>
126
+ </div>
127
+ </section>
128
+
129
+ {/* ============ MÉTODOS DE CONTACTO ============ */}
130
+ {contactMethods.length > 0 && (
131
+ <section className="imansi-container pb-20">
132
+ <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
133
+ {contactMethods.map((method, i) => (
134
+ <a
135
+ key={i}
136
+ href={method.href || '#'}
137
+ className="group flex flex-col gap-4 rounded-xl border border-border bg-card p-6 transition-all hover:border-primary/40 hover:shadow-lg"
138
+ >
139
+ <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
140
+ {method.icon}
141
+ </div>
142
+ <div className="space-y-1.5 flex-1">
143
+ <h3 className="text-h4 font-semibold text-foreground">
144
+ {method.title}
145
+ </h3>
146
+ <p className="text-small text-muted-foreground leading-relaxed">
147
+ {method.description}
148
+ </p>
149
+ </div>
150
+ {method.action && (
151
+ <span className="inline-flex items-center gap-1.5 text-small font-medium text-primary">
152
+ {method.action}
153
+ <svg
154
+ width="14"
155
+ height="14"
156
+ viewBox="0 0 14 14"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ strokeWidth="1.5"
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ className="transition-transform group-hover:translate-x-0.5"
163
+ >
164
+ <path d="M5 3L9 7L5 11" />
165
+ </svg>
166
+ </span>
167
+ )}
168
+ </a>
169
+ ))}
170
+ </div>
171
+ </section>
172
+ )}
173
+
174
+ {/* ============ FORM + INFO ============ */}
175
+ <section className="imansi-container pb-20">
176
+ <div className="grid gap-6 lg:grid-cols-[1.5fr_1fr] max-w-5xl mx-auto items-start">
177
+ {/* ---------- FORMULARIO ---------- */}
178
+ <Card>
179
+ <CardContent className="p-7 sm:p-9">
180
+ {showingSuccess ? (
181
+ <div className="text-center space-y-6 py-14">
182
+ <div className="inline-flex h-16 w-16 items-center justify-center rounded-full bg-success/10 text-success">
183
+ <svg
184
+ width="30"
185
+ height="30"
186
+ viewBox="0 0 24 24"
187
+ fill="none"
188
+ stroke="currentColor"
189
+ strokeWidth="2.5"
190
+ strokeLinecap="round"
191
+ strokeLinejoin="round"
192
+ >
193
+ <polyline points="4,12 10,18 20,6" />
194
+ </svg>
195
+ </div>
196
+ <div className="space-y-2 max-w-md mx-auto">
197
+ <h2 className="text-h3 font-semibold">{formSuccessTitle}</h2>
198
+ <p className="text-small text-muted-foreground leading-relaxed">
199
+ {formSuccessMessage}
200
+ </p>
201
+ </div>
202
+ <Button variant="outline" onClick={resetForm}>
203
+ Enviar otro mensaje
204
+ </Button>
205
+ </div>
206
+ ) : (
207
+ <div className="space-y-7">
208
+ {/* Header form */}
209
+ <div className="space-y-2">
210
+ <h2 className="text-h3 font-semibold tracking-tight">
211
+ {formTitle}
212
+ </h2>
213
+ <p className="text-small text-muted-foreground">
214
+ Los campos marcados con <span className="text-destructive">*</span> son obligatorios.
215
+ </p>
216
+ </div>
217
+
218
+ {error && (
219
+ <Alert variant="destructive" title="Error">
220
+ {error}
221
+ </Alert>
222
+ )}
223
+
224
+ <form onSubmit={handleSubmit} className="space-y-5">
225
+ {/* Nombre + Email */}
226
+ <div className="grid gap-5 sm:grid-cols-2">
227
+ <div className="space-y-2">
228
+ <Label
229
+ htmlFor="contact-nombre"
230
+ className="text-small font-medium"
231
+ >
232
+ {formNameLabel}{' '}
233
+ <span className="text-destructive">*</span>
234
+ </Label>
235
+ <Input
236
+ id="contact-nombre"
237
+ value={nombre}
238
+ onChange={(e) => setNombre(e.target.value)}
239
+ placeholder="Juan Pérez"
240
+ autoComplete="name"
241
+ disabled={loading}
242
+ />
243
+ </div>
244
+
245
+ <div className="space-y-2">
246
+ <Label
247
+ htmlFor="contact-email"
248
+ className="text-small font-medium"
249
+ >
250
+ {formEmailLabel}{' '}
251
+ <span className="text-destructive">*</span>
252
+ </Label>
253
+ <Input
254
+ id="contact-email"
255
+ type="email"
256
+ value={email}
257
+ onChange={(e) => setEmail(e.target.value)}
258
+ placeholder="correo@ejemplo.com"
259
+ autoComplete="email"
260
+ disabled={loading}
261
+ />
262
+ </div>
263
+ </div>
264
+
265
+ {/* Empresa + Asunto */}
266
+ <div className="grid gap-5 sm:grid-cols-2">
267
+ <div className="space-y-2">
268
+ <Label
269
+ htmlFor="contact-empresa"
270
+ className="text-small font-medium"
271
+ >
272
+ {formCompanyLabel}
273
+ </Label>
274
+ <Input
275
+ id="contact-empresa"
276
+ value={empresa}
277
+ onChange={(e) => setEmpresa(e.target.value)}
278
+ placeholder="Acme Inc."
279
+ autoComplete="organization"
280
+ disabled={loading}
281
+ />
282
+ </div>
283
+
284
+ <div className="space-y-2">
285
+ <Label
286
+ htmlFor="contact-asunto"
287
+ className="text-small font-medium"
288
+ >
289
+ {formSubjectLabel}
290
+ </Label>
291
+ {formSubjectOptions.length > 0 ? (
292
+ <select
293
+ id="contact-asunto"
294
+ value={asunto}
295
+ onChange={(e) => setAsunto(e.target.value)}
296
+ disabled={loading}
297
+ className="flex h-10 w-full rounded-md border border-border bg-input px-3 py-2 text-body text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50"
298
+ >
299
+ <option value="">Seleccioná una opción</option>
300
+ {formSubjectOptions.map((opt) => (
301
+ <option key={opt} value={opt}>
302
+ {opt}
303
+ </option>
304
+ ))}
305
+ </select>
306
+ ) : (
307
+ <Input
308
+ id="contact-asunto"
309
+ value={asunto}
310
+ onChange={(e) => setAsunto(e.target.value)}
311
+ placeholder="¿En qué podemos ayudarte?"
312
+ disabled={loading}
313
+ />
314
+ )}
315
+ </div>
316
+ </div>
317
+
318
+ {/* Mensaje */}
319
+ <div className="space-y-2">
320
+ <Label
321
+ htmlFor="contact-mensaje"
322
+ className="text-small font-medium"
323
+ >
324
+ {formMessageLabel}{' '}
325
+ <span className="text-destructive">*</span>
326
+ </Label>
327
+ <Textarea
328
+ id="contact-mensaje"
329
+ placeholder={formMessagePlaceholder}
330
+ value={mensaje}
331
+ onChange={(e) => setMensaje(e.target.value)}
332
+ rows={6}
333
+ disabled={loading}
334
+ />
335
+ </div>
336
+
337
+ <Separator />
338
+
339
+ {/* Footer form */}
340
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
341
+ <p className="text-caption text-muted-foreground max-w-xs leading-relaxed">
342
+ {formPrivacyNote}
343
+ </p>
344
+ <Button
345
+ type="submit"
346
+ disabled={loading}
347
+ className="sm:min-w-[180px]"
348
+ >
349
+ {loading ? formSubmitLoadingText : formSubmitText}
350
+ </Button>
351
+ </div>
352
+ </form>
353
+ </div>
354
+ )}
355
+ </CardContent>
356
+ </Card>
357
+
358
+ {/* ---------- SIDEBAR ---------- */}
359
+ <div className="space-y-6">
360
+ {/* Info de contacto */}
361
+ {infoItems.length > 0 && (
362
+ <Card>
363
+ <CardContent className="p-6 space-y-5">
364
+ <h3 className="text-h4 font-semibold">{infoTitle}</h3>
365
+ <Separator />
366
+ <ul className="space-y-5">
367
+ {infoItems.map((item, i) => (
368
+ <li key={i} className="flex items-start gap-3">
369
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-foreground">
370
+ {item.icon}
371
+ </div>
372
+ <div className="space-y-1 min-w-0 pt-0.5">
373
+ <p className="text-caption font-semibold uppercase tracking-wider text-muted-foreground">
374
+ {item.label}
375
+ </p>
376
+ {item.href ? (
377
+ <a
378
+ href={item.href}
379
+ className="block text-small text-foreground hover:text-primary transition-colors break-words"
380
+ >
381
+ {item.value}
382
+ </a>
383
+ ) : (
384
+ <p className="text-small text-foreground break-words">
385
+ {item.value}
386
+ </p>
387
+ )}
388
+ </div>
389
+ </li>
390
+ ))}
391
+ </ul>
392
+ </CardContent>
393
+ </Card>
394
+ )}
395
+
396
+ {/* Horario */}
397
+ {hoursItems.length > 0 && (
398
+ <Card>
399
+ <CardContent className="p-6 space-y-5">
400
+ <h3 className="text-h4 font-semibold">{hoursTitle}</h3>
401
+ <Separator />
402
+ <ul className="space-y-3">
403
+ {hoursItems.map((item, i) => (
404
+ <li
405
+ key={i}
406
+ className="flex items-center justify-between gap-3 text-small"
407
+ >
408
+ <span className="text-muted-foreground">
409
+ {item.day}
410
+ </span>
411
+ <span className="font-medium text-foreground">
412
+ {item.hours}
413
+ </span>
414
+ </li>
415
+ ))}
416
+ </ul>
417
+ </CardContent>
418
+ </Card>
419
+ )}
420
+ </div>
421
+ </div>
422
+ </section>
423
+
424
+ {/* ============ OFICINAS ============ */}
425
+ {offices.length > 0 && (
426
+ <section className="border-t border-border bg-muted/30">
427
+ <div className="imansi-container py-24">
428
+ <div className="text-center max-w-2xl mx-auto space-y-4 mb-16">
429
+ <h2 className="text-h1 font-bold tracking-tight">
430
+ {officesTitle}
431
+ </h2>
432
+ <p className="text-body text-muted-foreground">
433
+ {officesSubtitle}
434
+ </p>
435
+ </div>
436
+
437
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
438
+ {offices.map((office, i) => (
439
+ <Card key={i} className="transition-shadow hover:shadow-md">
440
+ <CardContent className="p-6 space-y-5">
441
+ <div className="flex items-start gap-3">
442
+ <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
443
+ <svg
444
+ width="20"
445
+ height="20"
446
+ viewBox="0 0 24 24"
447
+ fill="none"
448
+ stroke="currentColor"
449
+ strokeWidth="2"
450
+ strokeLinecap="round"
451
+ strokeLinejoin="round"
452
+ >
453
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
454
+ <circle cx="12" cy="10" r="3" />
455
+ </svg>
456
+ </div>
457
+ <div className="space-y-1 min-w-0">
458
+ <h3 className="text-h4 font-semibold leading-tight">
459
+ {office.city}
460
+ </h3>
461
+ {office.country && (
462
+ <p className="text-caption text-muted-foreground uppercase tracking-wider">
463
+ {office.country}
464
+ </p>
465
+ )}
466
+ </div>
467
+ </div>
468
+
469
+ <Separator />
470
+
471
+ <div className="space-y-3">
472
+ <div className="flex items-start gap-2.5 text-small">
473
+ <svg
474
+ width="14"
475
+ height="14"
476
+ viewBox="0 0 24 24"
477
+ fill="none"
478
+ stroke="currentColor"
479
+ strokeWidth="2"
480
+ strokeLinecap="round"
481
+ strokeLinejoin="round"
482
+ className="text-muted-foreground shrink-0 mt-0.5"
483
+ >
484
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
485
+ <circle cx="12" cy="10" r="3" />
486
+ </svg>
487
+ <span className="text-muted-foreground whitespace-pre-line leading-relaxed">
488
+ {office.address}
489
+ </span>
490
+ </div>
491
+
492
+ {office.phone && (
493
+ <a
494
+ href={`tel:${office.phone.replace(/\s/g, '')}`}
495
+ className="flex items-start gap-2.5 text-small text-muted-foreground hover:text-primary transition-colors"
496
+ >
497
+ <svg
498
+ width="14"
499
+ height="14"
500
+ viewBox="0 0 24 24"
501
+ fill="none"
502
+ stroke="currentColor"
503
+ strokeWidth="2"
504
+ strokeLinecap="round"
505
+ strokeLinejoin="round"
506
+ className="shrink-0 mt-0.5"
507
+ >
508
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
509
+ </svg>
510
+ {office.phone}
511
+ </a>
512
+ )}
513
+
514
+ {office.email && (
515
+ <a
516
+ href={`mailto:${office.email}`}
517
+ className="flex items-start gap-2.5 text-small text-muted-foreground hover:text-primary transition-colors"
518
+ >
519
+ <svg
520
+ width="14"
521
+ height="14"
522
+ viewBox="0 0 24 24"
523
+ fill="none"
524
+ stroke="currentColor"
525
+ strokeWidth="2"
526
+ strokeLinecap="round"
527
+ strokeLinejoin="round"
528
+ className="shrink-0 mt-0.5"
529
+ >
530
+ <rect x="2" y="4" width="20" height="16" rx="2" />
531
+ <path d="m22 6-10 7L2 6" />
532
+ </svg>
533
+ {office.email}
534
+ </a>
535
+ )}
536
+ </div>
537
+ </CardContent>
538
+ </Card>
539
+ ))}
540
+ </div>
541
+ </div>
542
+ </section>
543
+ )}
544
+ </MarketingLayout>
545
+ );
546
+ }