@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,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
+ }