ar-saas 0.3.1 → 0.3.2

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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +338 -314
  3. package/dist/cli.js +19 -0
  4. package/dist/generator.js +166 -55
  5. package/package.json +52 -50
  6. package/templates/backend/.env.example +67 -67
  7. package/templates/backend/.prettierrc +4 -4
  8. package/templates/backend/README.md +249 -168
  9. package/templates/backend/eslint.config.mjs +35 -35
  10. package/templates/backend/nest-cli.json +8 -8
  11. package/templates/backend/package-lock.json +10979 -10979
  12. package/templates/backend/package.json +88 -88
  13. package/templates/backend/src/app.controller.spec.ts +24 -24
  14. package/templates/backend/src/app.controller.ts +15 -15
  15. package/templates/backend/src/app.module.ts +40 -40
  16. package/templates/backend/src/app.service.ts +11 -11
  17. package/templates/backend/src/common/base/base.repository.ts +221 -221
  18. package/templates/backend/src/common/base/base.schema.ts +24 -24
  19. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
  20. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
  21. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
  22. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
  23. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
  24. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
  25. package/templates/backend/src/main.ts +51 -51
  26. package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
  27. package/templates/backend/src/modules/auth/auth.module.ts +20 -20
  28. package/templates/backend/src/modules/auth/auth.service.ts +257 -257
  29. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
  30. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
  31. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
  32. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
  33. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
  34. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
  35. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
  36. package/templates/backend/src/modules/mail/mail.module.ts +9 -9
  37. package/templates/backend/src/modules/mail/mail.service.ts +141 -141
  38. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
  39. package/templates/backend/src/modules/users/users.module.ts +14 -14
  40. package/templates/backend/src/modules/users/users.repository.ts +51 -51
  41. package/templates/backend/src/modules/users/users.service.ts +104 -104
  42. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
  43. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
  44. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
  45. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
  46. package/templates/backend/test/app.e2e-spec.ts +25 -25
  47. package/templates/backend/test/jest-e2e.json +9 -9
  48. package/templates/backend/tsconfig.build.json +4 -4
  49. package/templates/backend/tsconfig.json +26 -26
  50. package/templates/frontend/.env.local.example +1 -1
  51. package/templates/frontend/README.md +152 -0
  52. package/templates/frontend/components.json +20 -20
  53. package/templates/frontend/eslint.config.mjs +14 -14
  54. package/templates/frontend/next.config.ts +5 -5
  55. package/templates/frontend/package-lock.json +6722 -6722
  56. package/templates/frontend/package.json +48 -48
  57. package/templates/frontend/pnpm-lock.yaml +5012 -5012
  58. package/templates/frontend/pnpm-workspace.yaml +3 -3
  59. package/templates/frontend/postcss.config.mjs +7 -7
  60. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
  61. package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
  62. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
  63. package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
  64. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
  65. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
  66. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  67. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
  68. package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
  69. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
  70. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
  71. package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
  72. package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
  73. package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
  74. package/templates/frontend/src/app/globals.css +81 -81
  75. package/templates/frontend/src/app/layout.tsx +26 -26
  76. package/templates/frontend/src/app/page.tsx +5 -45
  77. package/templates/frontend/src/app/setup/page.tsx +371 -275
  78. package/templates/frontend/src/components/dashboard/header.tsx +89 -89
  79. package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
  80. package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
  81. package/templates/frontend/src/components/landing/faq.tsx +39 -39
  82. package/templates/frontend/src/components/landing/features.tsx +54 -54
  83. package/templates/frontend/src/components/landing/footer.tsx +76 -76
  84. package/templates/frontend/src/components/landing/hero.tsx +72 -72
  85. package/templates/frontend/src/components/landing/navbar.tsx +78 -78
  86. package/templates/frontend/src/components/landing/pricing.tsx +90 -90
  87. package/templates/frontend/src/components/ui/accordion.tsx +52 -52
  88. package/templates/frontend/src/components/ui/avatar.tsx +46 -46
  89. package/templates/frontend/src/components/ui/badge.tsx +30 -30
  90. package/templates/frontend/src/components/ui/button.tsx +52 -52
  91. package/templates/frontend/src/components/ui/card.tsx +50 -50
  92. package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
  93. package/templates/frontend/src/components/ui/dialog.tsx +100 -100
  94. package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
  95. package/templates/frontend/src/components/ui/form.tsx +158 -158
  96. package/templates/frontend/src/components/ui/input.tsx +21 -21
  97. package/templates/frontend/src/components/ui/label.tsx +22 -22
  98. package/templates/frontend/src/components/ui/separator.tsx +25 -25
  99. package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
  100. package/templates/frontend/src/components/ui/switch.tsx +28 -28
  101. package/templates/frontend/src/components/ui/tabs.tsx +54 -54
  102. package/templates/frontend/src/components/ui/textarea.tsx +20 -20
  103. package/templates/frontend/src/components/ui/toast.tsx +109 -109
  104. package/templates/frontend/src/components/ui/toaster.tsx +30 -30
  105. package/templates/frontend/src/config/site.ts +197 -197
  106. package/templates/frontend/src/hooks/use-toast.ts +116 -116
  107. package/templates/frontend/src/lib/api/auth.ts +39 -39
  108. package/templates/frontend/src/lib/api/client.ts +66 -66
  109. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
  110. package/templates/frontend/src/lib/utils.ts +6 -6
  111. package/templates/frontend/src/providers/auth-provider.tsx +60 -60
  112. package/templates/frontend/src/types/api.ts +12 -12
  113. package/templates/frontend/src/types/auth.ts +27 -27
  114. package/templates/frontend/tsconfig.json +23 -23
@@ -1,275 +1,371 @@
1
- 'use client'
2
-
3
- import { useEffect, useState } from 'react'
4
- import { useRouter } from 'next/navigation'
5
-
6
- const BACKEND_REQUIRED = [
7
- {
8
- key: 'MONGODB_URI',
9
- example: 'mongodb://localhost:27017/mi-saas',
10
- desc: 'URI de conexión a MongoDB. Para desarrollo local instalar MongoDB o usar Atlas.',
11
- },
12
- {
13
- key: 'JWT_ACCESS_SECRET',
14
- example: '$(openssl rand -hex 64)',
15
- desc: 'Secreto para firmar access tokens. Generar con OpenSSL.',
16
- },
17
- {
18
- key: 'JWT_REFRESH_SECRET',
19
- example: '$(openssl rand -hex 64)',
20
- desc: 'Secreto para firmar refresh tokens. Usar un valor distinto al anterior.',
21
- },
22
- {
23
- key: 'RESEND_API_KEY',
24
- example: 're_xxxxxxxxxxxxxxxx',
25
- desc: 'API Key de Resend para enviar emails. Crear cuenta en resend.com.',
26
- },
27
- {
28
- key: 'RESEND_FROM_EMAIL',
29
- example: 'noreply@tudominio.com',
30
- desc: 'Email remitente. Debe estar verificado en tu cuenta de Resend.',
31
- },
32
- {
33
- key: 'APP_URL',
34
- example: 'http://localhost:3001',
35
- desc: 'URL del frontend. Se usa en los links de los emails de verificación y reset.',
36
- },
37
- {
38
- key: 'CORS_ORIGINS',
39
- example: 'http://localhost:3001',
40
- desc: 'URL del frontend separada por coma. Debe coincidir con APP_URL.',
41
- },
42
- ]
43
-
44
- const BACKEND_OPTIONAL = [
45
- { key: 'PORT', example: '3000', desc: 'Puerto del servidor (default: 3000).' },
46
- { key: 'JWT_ACCESS_EXPIRES_IN', example: '15m', desc: 'Duración del access token (default: 15m).' },
47
- { key: 'JWT_REFRESH_EXPIRES_IN', example: '7d', desc: 'Duración del refresh token (default: 7d).' },
48
- { key: 'RESEND_FROM_NAME', example: '"Mi SaaS"', desc: 'Nombre del remitente que ven los destinatarios.' },
49
- { key: 'COOKIE_SECRET', example: '$(openssl rand -hex 32)', desc: 'Secreto para firmar cookies.' },
50
- ]
51
-
52
- const FRONTEND_REQUIRED = [
53
- {
54
- key: 'NEXT_PUBLIC_API_URL',
55
- example: 'http://localhost:3000',
56
- desc: 'URL base del backend. Debe coincidir con el PORT del backend.',
57
- },
58
- ]
59
-
60
- function EnvVar({ varKey, example, desc }: { varKey: string; example: string; desc: string }) {
61
- const [copied, setCopied] = useState(false)
62
-
63
- const copy = () => {
64
- navigator.clipboard.writeText(`${varKey}=${example}`)
65
- setCopied(true)
66
- setTimeout(() => setCopied(false), 1500)
67
- }
68
-
69
- return (
70
- <div className="py-3 border-b border-zinc-100 last:border-0">
71
- <div className="flex items-start justify-between gap-4">
72
- <div className="flex-1 min-w-0">
73
- <div className="flex items-center gap-2 mb-0.5">
74
- <code className="text-sm font-mono font-semibold text-zinc-900">{varKey}</code>
75
- </div>
76
- <p className="text-xs text-zinc-500 leading-relaxed">{desc}</p>
77
- <code className="text-xs text-zinc-400 font-mono">{example}</code>
78
- </div>
79
- <button
80
- onClick={copy}
81
- className="shrink-0 text-xs text-zinc-400 hover:text-zinc-700 transition-colors px-2 py-1 rounded border border-zinc-200 hover:border-zinc-300"
82
- >
83
- {copied ? '✓' : 'copiar'}
84
- </button>
85
- </div>
86
- </div>
87
- )
88
- }
89
-
90
- function CodeBlock({ children }: { children: string }) {
91
- const [copied, setCopied] = useState(false)
92
-
93
- const copy = () => {
94
- navigator.clipboard.writeText(children)
95
- setCopied(true)
96
- setTimeout(() => setCopied(false), 1500)
97
- }
98
-
99
- return (
100
- <div className="relative group">
101
- <pre className="bg-zinc-950 text-zinc-100 rounded-lg px-4 py-3 text-sm font-mono overflow-x-auto">
102
- {children}
103
- </pre>
104
- <button
105
- onClick={copy}
106
- className="absolute top-2 right-2 text-xs text-zinc-500 hover:text-zinc-300 transition-colors bg-zinc-800 px-2 py-0.5 rounded"
107
- >
108
- {copied ? '✓' : 'copiar'}
109
- </button>
110
- </div>
111
- )
112
- }
113
-
114
- export default function SetupPage() {
115
- const router = useRouter()
116
- const [dismissed, setDismissed] = useState(false)
117
- const [mounted, setMounted] = useState(false)
118
-
119
- useEffect(() => {
120
- setMounted(true)
121
- }, [])
122
-
123
- const handleDone = () => {
124
- localStorage.setItem('setup_dismissed', 'true')
125
- setDismissed(true)
126
- router.push('/login')
127
- }
128
-
129
- if (!mounted || dismissed) return null
130
-
131
- return (
132
- <div className="min-h-screen bg-zinc-50">
133
- <div className="max-w-3xl mx-auto px-4 py-12">
134
-
135
- {/* Header */}
136
- <div className="mb-10">
137
- <div className="inline-flex items-center gap-2 bg-zinc-900 text-white text-xs font-mono px-3 py-1.5 rounded-full mb-4">
138
- <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
139
- create-saas-ar
140
- </div>
141
- <h1 className="text-3xl font-bold text-zinc-900 mb-2">
142
- Tu proyecto está listo
143
- </h1>
144
- <p className="text-zinc-500 text-lg">
145
- Completá la configuración antes de empezar a desarrollar.
146
- Son 5 minutos y después tenés todo andando.
147
- </p>
148
- </div>
149
-
150
- {/* Backend */}
151
- <section className="mb-8">
152
- <div className="flex items-center gap-3 mb-4">
153
- <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">1</div>
154
- <h2 className="text-lg font-semibold text-zinc-900">Backend — configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env</code></h2>
155
- </div>
156
-
157
- <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
158
- <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
159
- <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
160
- </div>
161
- <div className="px-4">
162
- {BACKEND_REQUIRED.map((v) => (
163
- <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
164
- ))}
165
- </div>
166
- </div>
167
-
168
- <details className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
169
- <summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
170
- Variables opcionales
171
- </summary>
172
- <div className="px-4">
173
- {BACKEND_OPTIONAL.map((v) => (
174
- <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
175
- ))}
176
- </div>
177
- </details>
178
-
179
- <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
180
- <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
181
- <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el backend</span>
182
- </div>
183
- <div className="p-4 space-y-3">
184
- <CodeBlock>{`cd backend
185
- cp .env.example .env
186
- # Completar las variables en .env
187
- npm install
188
- npm run start:dev`}</CodeBlock>
189
- <p className="text-xs text-zinc-400">
190
- El servidor levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000</code>.
191
- Swagger en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000/api/docs</code>.
192
- </p>
193
- </div>
194
- </div>
195
- </section>
196
-
197
- {/* Frontend */}
198
- <section className="mb-8">
199
- <div className="flex items-center gap-3 mb-4">
200
- <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">2</div>
201
- <h2 className="text-lg font-semibold text-zinc-900">Frontend configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env.local</code></h2>
202
- </div>
203
-
204
- <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
205
- <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
206
- <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
207
- </div>
208
- <div className="px-4">
209
- {FRONTEND_REQUIRED.map((v) => (
210
- <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
211
- ))}
212
- </div>
213
- </div>
214
-
215
- <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
216
- <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
217
- <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el frontend</span>
218
- </div>
219
- <div className="p-4 space-y-3">
220
- <CodeBlock>{`cd frontend
221
- cp .env.local.example .env.local
222
- # Ajustar NEXT_PUBLIC_API_URL si cambiaste el PORT del backend
223
- npm install
224
- npm run dev`}</CodeBlock>
225
- <p className="text-xs text-zinc-400">
226
- El frontend levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3001</code> (o el puerto que indique Next.js).
227
- </p>
228
- </div>
229
- </div>
230
- </section>
231
-
232
- {/* Verificación */}
233
- <section className="mb-10">
234
- <div className="flex items-center gap-3 mb-4">
235
- <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">3</div>
236
- <h2 className="text-lg font-semibold text-zinc-900">Verificar que todo funciona</h2>
237
- </div>
238
-
239
- <div className="bg-white border border-zinc-200 rounded-xl p-4 space-y-2">
240
- {[
241
- { label: 'Backend corre sin errores', hint: 'npm run start:dev no muestra errores rojos' },
242
- { label: 'Swagger disponible', hint: 'http://localhost:3000/api/docs carga correctamente' },
243
- { label: 'MongoDB conectado', hint: 'El log dice "Connected to MongoDB"' },
244
- { label: 'Registro funciona', hint: 'POST /api/auth/register crea un usuario' },
245
- { label: 'Email de verificación llega', hint: 'Revisar spam si no aparece en inbox' },
246
- ].map((item) => (
247
- <div key={item.label} className="flex items-start gap-3 py-1.5">
248
- <div className="w-4 h-4 rounded border-2 border-zinc-300 mt-0.5 shrink-0" />
249
- <div>
250
- <p className="text-sm text-zinc-800 font-medium">{item.label}</p>
251
- <p className="text-xs text-zinc-400">{item.hint}</p>
252
- </div>
253
- </div>
254
- ))}
255
- </div>
256
- </section>
257
-
258
- {/* CTA */}
259
- <div className="flex items-center justify-between">
260
- <p className="text-xs text-zinc-400">
261
- Esta pantalla no vuelve a aparecer. Podés volver desde{' '}
262
- <code className="bg-zinc-100 px-1 py-0.5 rounded">/setup</code>.
263
- </p>
264
- <button
265
- onClick={handleDone}
266
- className="bg-zinc-900 hover:bg-zinc-700 text-white font-medium text-sm px-6 py-2.5 rounded-lg transition-colors"
267
- >
268
- Todo listo, ir al login →
269
- </button>
270
- </div>
271
-
272
- </div>
273
- </div>
274
- )
275
- }
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+
6
+ const BACKEND_REQUIRED = [
7
+ {
8
+ key: 'MONGODB_URI',
9
+ example: 'mongodb://localhost:27017/mi-saas',
10
+ desc: 'URI de conexión a MongoDB. Para desarrollo local instalar MongoDB o usar Atlas.',
11
+ },
12
+ {
13
+ key: 'JWT_ACCESS_SECRET',
14
+ example: '$(openssl rand -hex 64)',
15
+ desc: 'Secreto para firmar access tokens. Generar con OpenSSL.',
16
+ },
17
+ {
18
+ key: 'JWT_REFRESH_SECRET',
19
+ example: '$(openssl rand -hex 64)',
20
+ desc: 'Secreto para firmar refresh tokens. Usar un valor distinto al anterior.',
21
+ },
22
+ {
23
+ key: 'RESEND_API_KEY',
24
+ example: 're_xxxxxxxxxxxxxxxx',
25
+ desc: 'API Key de Resend para enviar emails. Crear cuenta en resend.com.',
26
+ },
27
+ {
28
+ key: 'RESEND_FROM_EMAIL',
29
+ example: 'noreply@tudominio.com',
30
+ desc: 'Email remitente. Debe estar verificado en tu cuenta de Resend.',
31
+ },
32
+ {
33
+ key: 'APP_URL',
34
+ example: 'http://localhost:3001',
35
+ desc: 'URL del frontend. Se usa en los links de los emails de verificación y reset.',
36
+ },
37
+ {
38
+ key: 'CORS_ORIGINS',
39
+ example: 'http://localhost:3001',
40
+ desc: 'URL del frontend separada por coma. Debe coincidir con APP_URL.',
41
+ },
42
+ ]
43
+
44
+ const BACKEND_OPTIONAL = [
45
+ { key: 'PORT', example: '3000', desc: 'Puerto del servidor (default: 3000).' },
46
+ { key: 'JWT_ACCESS_EXPIRES_IN', example: '15m', desc: 'Duración del access token (default: 15m).' },
47
+ { key: 'JWT_REFRESH_EXPIRES_IN', example: '7d', desc: 'Duración del refresh token (default: 7d).' },
48
+ { key: 'RESEND_FROM_NAME', example: '"Mi SaaS"', desc: 'Nombre del remitente que ven los destinatarios.' },
49
+ { key: 'COOKIE_SECRET', example: '$(openssl rand -hex 32)', desc: 'Secreto para firmar cookies.' },
50
+ ]
51
+
52
+ const FRONTEND_REQUIRED = [
53
+ {
54
+ key: 'NEXT_PUBLIC_API_URL',
55
+ example: 'http://localhost:3000',
56
+ desc: 'URL base del backend. Debe coincidir con el PORT del backend.',
57
+ },
58
+ ]
59
+
60
+ function EnvVar({ varKey, example, desc }: { varKey: string; example: string; desc: string }) {
61
+ const [copied, setCopied] = useState(false)
62
+
63
+ const copy = () => {
64
+ navigator.clipboard.writeText(`${varKey}=${example}`)
65
+ setCopied(true)
66
+ setTimeout(() => setCopied(false), 1500)
67
+ }
68
+
69
+ return (
70
+ <div className="py-3 border-b border-zinc-100 last:border-0">
71
+ <div className="flex items-start justify-between gap-4">
72
+ <div className="flex-1 min-w-0">
73
+ <div className="flex items-center gap-2 mb-0.5">
74
+ <code className="text-sm font-mono font-semibold text-zinc-900">{varKey}</code>
75
+ </div>
76
+ <p className="text-xs text-zinc-500 leading-relaxed">{desc}</p>
77
+ <code className="text-xs text-zinc-400 font-mono">{example}</code>
78
+ </div>
79
+ <button
80
+ onClick={copy}
81
+ className="shrink-0 text-xs text-zinc-400 hover:text-zinc-700 transition-colors px-2 py-1 rounded border border-zinc-200 hover:border-zinc-300"
82
+ >
83
+ {copied ? '✓' : 'copiar'}
84
+ </button>
85
+ </div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ function CodeBlock({ children }: { children: string }) {
91
+ const [copied, setCopied] = useState(false)
92
+
93
+ const copy = () => {
94
+ navigator.clipboard.writeText(children)
95
+ setCopied(true)
96
+ setTimeout(() => setCopied(false), 1500)
97
+ }
98
+
99
+ return (
100
+ <div className="relative group">
101
+ <pre className="bg-zinc-950 text-zinc-100 rounded-lg px-4 py-3 text-sm font-mono overflow-x-auto">
102
+ {children}
103
+ </pre>
104
+ <button
105
+ onClick={copy}
106
+ className="absolute top-2 right-2 text-xs text-zinc-500 hover:text-zinc-300 transition-colors bg-zinc-800 px-2 py-0.5 rounded"
107
+ >
108
+ {copied ? '✓' : 'copiar'}
109
+ </button>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ export default function SetupPage() {
115
+ const router = useRouter()
116
+ const [dismissed, setDismissed] = useState(false)
117
+ const [mounted, setMounted] = useState(false)
118
+
119
+ useEffect(() => {
120
+ setMounted(true)
121
+ if (typeof window !== 'undefined' && localStorage.getItem('setup_dismissed') === 'true') {
122
+ router.replace('/login')
123
+ }
124
+ }, [router])
125
+
126
+ const handleDone = () => {
127
+ localStorage.setItem('setup_dismissed', 'true')
128
+ setDismissed(true)
129
+ router.push('/login')
130
+ }
131
+
132
+ if (!mounted || dismissed) return null
133
+
134
+ return (
135
+ <div className="min-h-screen bg-zinc-50">
136
+ <div className="max-w-3xl mx-auto px-4 py-12">
137
+
138
+ {/* Header */}
139
+ <div className="mb-10">
140
+ <div className="inline-flex items-center gap-2 bg-zinc-900 text-white text-xs font-mono px-3 py-1.5 rounded-full mb-4">
141
+ <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
142
+ create-saas-ar
143
+ </div>
144
+ <h1 className="text-3xl font-bold text-zinc-900 mb-2">
145
+ Tu proyecto está listo
146
+ </h1>
147
+ <p className="text-zinc-500 text-lg">
148
+ Completá la configuración antes de empezar a desarrollar.
149
+ Son 5 minutos y después tenés todo andando.
150
+ </p>
151
+ </div>
152
+
153
+ {/* MongoDB */}
154
+ <section className="mb-8">
155
+ <div className="flex items-center gap-3 mb-4">
156
+ <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">1</div>
157
+ <h2 className="text-lg font-semibold text-zinc-900">Levantar MongoDB</h2>
158
+ </div>
159
+
160
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-3">
161
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 flex items-center gap-2">
162
+ <span className="text-xs font-semibold text-zinc-700">Opción A — Docker</span>
163
+ <span className="text-xs bg-zinc-900 text-white px-1.5 py-0.5 rounded font-mono">recomendado</span>
164
+ </div>
165
+ <div className="p-4 space-y-3">
166
+ <p className="text-xs text-zinc-500">El proyecto ya incluye un <code className="bg-zinc-100 px-1 py-0.5 rounded">docker-compose.dev.yml</code> en la raíz. Solo necesitás tener Docker instalado.</p>
167
+ <CodeBlock>{`# Desde la raíz del proyecto (no desde /backend)
168
+ docker compose -f docker-compose.dev.yml up -d
169
+
170
+ # Verificar que está corriendo
171
+ docker ps`}</CodeBlock>
172
+ <p className="text-xs text-zinc-400">MongoDB queda disponible en <code className="bg-zinc-100 px-1 py-0.5 rounded">mongodb://localhost:27017</code>. Los datos persisten en un volumen de Docker.</p>
173
+ </div>
174
+ </div>
175
+
176
+ <details className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-3">
177
+ <summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
178
+ Opción B — MongoDB Atlas (cloud gratuito)
179
+ </summary>
180
+ <div className="p-4 space-y-2">
181
+ <ol className="text-xs text-zinc-600 space-y-1.5 list-none">
182
+ {[
183
+ 'Crear cuenta gratis en mongodb.com/atlas',
184
+ 'Crear un cluster gratuito (M0 Free Tier)',
185
+ 'En "Database Access" → crear un usuario con contraseña',
186
+ 'En "Network Access" agregar tu IP (o 0.0.0.0/0 para dev)',
187
+ 'En "Connect" → elegir "Connect your application" → copiar la URI',
188
+ 'Reemplazar <password> en la URI con la contraseña del paso 3',
189
+ 'Pegar la URI completa en MONGODB_URI del .env del backend',
190
+ ].map((step, i) => (
191
+ <li key={i} className="flex gap-2">
192
+ <span className="shrink-0 w-4 h-4 rounded-full bg-zinc-200 text-zinc-600 flex items-center justify-center font-bold text-[10px]">{i + 1}</span>
193
+ {step}
194
+ </li>
195
+ ))}
196
+ </ol>
197
+ </div>
198
+ </details>
199
+
200
+ <details className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
201
+ <summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
202
+ Opción C — MongoDB local sin Docker
203
+ </summary>
204
+ <div className="p-4">
205
+ <p className="text-xs text-zinc-500">
206
+ Instalá MongoDB Community Edition siguiendo las instrucciones oficiales para tu sistema operativo:{' '}
207
+ <span className="font-mono text-zinc-700">docs.mongodb.com/manual/installation</span>
208
+ </p>
209
+ </div>
210
+ </details>
211
+ </section>
212
+
213
+ {/* Backend */}
214
+ <section className="mb-8">
215
+ <div className="flex items-center gap-3 mb-4">
216
+ <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">2</div>
217
+ <h2 className="text-lg font-semibold text-zinc-900">Backend configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env</code></h2>
218
+ </div>
219
+
220
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
221
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
222
+ <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
223
+ </div>
224
+ <div className="px-4">
225
+ {BACKEND_REQUIRED.map((v) => (
226
+ <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
227
+ ))}
228
+ </div>
229
+ </div>
230
+
231
+ <div className="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-4">
232
+ <p className="text-xs font-semibold text-amber-800 mb-2">Generar JWT secrets</p>
233
+ <p className="text-xs text-amber-700 mb-2">Usá cualquiera de estos comandos para generar valores seguros para <code className="bg-amber-100 px-1 rounded">JWT_ACCESS_SECRET</code> y <code className="bg-amber-100 px-1 rounded">JWT_REFRESH_SECRET</code>. Usá un valor distinto para cada uno.</p>
234
+ <CodeBlock>{`# Con Node.js (disponible si tenés Node instalado)
235
+ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
236
+
237
+ # Con openssl
238
+ openssl rand -hex 64`}</CodeBlock>
239
+ </div>
240
+
241
+ <details className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
242
+ <summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
243
+ Variables opcionales
244
+ </summary>
245
+ <div className="px-4">
246
+ {BACKEND_OPTIONAL.map((v) => (
247
+ <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
248
+ ))}
249
+ </div>
250
+ </details>
251
+
252
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
253
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
254
+ <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Configurar Resend (emails)</span>
255
+ </div>
256
+ <div className="p-4 space-y-2">
257
+ <ol className="text-xs text-zinc-600 space-y-1.5 list-none">
258
+ {[
259
+ 'Crear cuenta gratis en resend.com (3 000 emails/mes gratis)',
260
+ 'Ir a API Keys → "Create API Key" → copiar la key',
261
+ 'Pegar en RESEND_API_KEY del .env',
262
+ 'Para dev podés usar el dominio sandbox de Resend (solo envía a tu propio email)',
263
+ 'Para prod: agregar y verificar tu dominio en "Domains"',
264
+ ].map((step, i) => (
265
+ <li key={i} className="flex gap-2">
266
+ <span className="shrink-0 w-4 h-4 rounded-full bg-zinc-200 text-zinc-600 flex items-center justify-center font-bold text-[10px]">{i + 1}</span>
267
+ {step}
268
+ </li>
269
+ ))}
270
+ </ol>
271
+ </div>
272
+ </div>
273
+
274
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
275
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
276
+ <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el backend</span>
277
+ </div>
278
+ <div className="p-4 space-y-3">
279
+ <CodeBlock>{`cd backend
280
+ # .env ya fue copiado automáticamente desde .env.example
281
+ # Completar las variables faltantes (JWT secrets, Resend key)
282
+ npm install
283
+ npm run start:dev`}</CodeBlock>
284
+ <p className="text-xs text-zinc-400">
285
+ El servidor levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000</code>.
286
+ Swagger en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000/api/docs</code>.
287
+ </p>
288
+ </div>
289
+ </div>
290
+ </section>
291
+
292
+ {/* Frontend */}
293
+ <section className="mb-8">
294
+ <div className="flex items-center gap-3 mb-4">
295
+ <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">3</div>
296
+ <h2 className="text-lg font-semibold text-zinc-900">Frontend — configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env.local</code></h2>
297
+ </div>
298
+
299
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
300
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
301
+ <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
302
+ </div>
303
+ <div className="px-4">
304
+ {FRONTEND_REQUIRED.map((v) => (
305
+ <EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
306
+ ))}
307
+ </div>
308
+ </div>
309
+
310
+ <div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
311
+ <div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
312
+ <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el frontend</span>
313
+ </div>
314
+ <div className="p-4 space-y-3">
315
+ <CodeBlock>{`cd frontend
316
+ # .env.local ya fue copiado automáticamente desde .env.local.example
317
+ # Ajustar NEXT_PUBLIC_API_URL si cambiaste el PORT del backend
318
+ npm install
319
+ npm run dev`}</CodeBlock>
320
+ <p className="text-xs text-zinc-400">
321
+ El frontend levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3001</code> (o el puerto que indique Next.js).
322
+ </p>
323
+ </div>
324
+ </div>
325
+ </section>
326
+
327
+ {/* Verificación */}
328
+ <section className="mb-10">
329
+ <div className="flex items-center gap-3 mb-4">
330
+ <div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">4</div>
331
+ <h2 className="text-lg font-semibold text-zinc-900">Verificar que todo funciona</h2>
332
+ </div>
333
+
334
+ <div className="bg-white border border-zinc-200 rounded-xl p-4 space-y-2">
335
+ {[
336
+ { label: 'MongoDB corriendo', hint: 'docker ps muestra el contenedor activo, o verificá que el proceso mongod esté levantado. Si el backend da ECONNREFUSED, MongoDB no está corriendo.' },
337
+ { label: 'Backend corre sin errores', hint: 'npm run start:dev no muestra errores rojos. Errores de JWT_SECRET faltante o MONGODB_URI indican variables sin completar.' },
338
+ { label: 'Swagger disponible', hint: 'http://localhost:3000/api/docs carga correctamente y lista los endpoints de auth.' },
339
+ { label: 'MongoDB conectado', hint: 'El log del backend dice "Connected to MongoDB successfully". Si no aparece, verificá MONGODB_URI en backend/.env.' },
340
+ { label: 'Registro funciona', hint: 'Ir a http://localhost:3001/register y crear una cuenta. Si falla con CORS, verificar que CORS_ORIGINS en backend/.env incluya http://localhost:3001.' },
341
+ { label: 'Email de verificación llega', hint: 'Revisar spam si no aparece en inbox. Si no llega, verificar RESEND_API_KEY y RESEND_FROM_EMAIL en backend/.env.' },
342
+ ].map((item) => (
343
+ <div key={item.label} className="flex items-start gap-3 py-1.5">
344
+ <div className="w-4 h-4 rounded border-2 border-zinc-300 mt-0.5 shrink-0" />
345
+ <div>
346
+ <p className="text-sm text-zinc-800 font-medium">{item.label}</p>
347
+ <p className="text-xs text-zinc-400">{item.hint}</p>
348
+ </div>
349
+ </div>
350
+ ))}
351
+ </div>
352
+ </section>
353
+
354
+ {/* CTA */}
355
+ <div className="flex items-center justify-between">
356
+ <p className="text-xs text-zinc-400">
357
+ Esta pantalla no vuelve a aparecer. Podés volver desde{' '}
358
+ <code className="bg-zinc-100 px-1 py-0.5 rounded">/setup</code>.
359
+ </p>
360
+ <button
361
+ onClick={handleDone}
362
+ className="bg-zinc-900 hover:bg-zinc-700 text-white font-medium text-sm px-6 py-2.5 rounded-lg transition-colors"
363
+ >
364
+ Todo listo, ir al login →
365
+ </button>
366
+ </div>
367
+
368
+ </div>
369
+ </div>
370
+ )
371
+ }