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,197 +1,197 @@
1
- // ─────────────────────────────────────────────────────────────────────────────
2
- // Archivo de configuración central del SaaS.
3
- // Editá este archivo para personalizar textos, links y contenido de toda la app.
4
- // Los valores con __PLACEHOLDER__ son reemplazados automáticamente por el CLI.
5
- // ─────────────────────────────────────────────────────────────────────────────
6
-
7
- export const siteConfig = {
8
- // ── Identidad ──────────────────────────────────────────────────────────────
9
- name: '__SITE_NAME__',
10
- tagline: '__SITE_TAGLINE__',
11
- description: '__SITE_DESCRIPTION__',
12
- url: 'https://tu-dominio.com',
13
- supportEmail: '__SUPPORT_EMAIL__',
14
-
15
- // ── Navegación pública (landing) ───────────────────────────────────────────
16
- nav: {
17
- links: [
18
- { label: 'Funcionalidades', href: '#features' },
19
- { label: 'Precios', href: '#pricing' },
20
- { label: 'FAQ', href: '#faq' },
21
- ],
22
- },
23
-
24
- // ── Hero ───────────────────────────────────────────────────────────────────
25
- hero: {
26
- headline: '__SITE_NAME__',
27
- subheadline: '__SITE_TAGLINE__',
28
- description: '__SITE_DESCRIPTION__',
29
- cta: { label: 'Empezar gratis', href: '/register' },
30
- ctaSecondary: { label: 'Ver funcionalidades', href: '#features' },
31
- },
32
-
33
- // ── Funcionalidades ────────────────────────────────────────────────────────
34
- features: [
35
- {
36
- icon: 'Zap',
37
- title: 'Rápido y confiable',
38
- description: 'Infraestructura diseñada para escalar con tu negocio sin fricciones.',
39
- },
40
- {
41
- icon: 'Shield',
42
- title: 'Seguridad primero',
43
- description: 'Autenticación robusta, tokens JWT y encriptación en toda la plataforma.',
44
- },
45
- {
46
- icon: 'BarChart3',
47
- title: 'Analytics en tiempo real',
48
- description: 'Tomá decisiones basadas en datos con métricas actualizadas al instante.',
49
- },
50
- {
51
- icon: 'Users',
52
- title: 'Trabajo en equipo',
53
- description: 'Invitá colaboradores y gestioná permisos de forma simple y segura.',
54
- },
55
- {
56
- icon: 'Globe',
57
- title: 'Multi-workspace',
58
- description: 'Administrá múltiples proyectos o clientes desde una sola cuenta.',
59
- },
60
- {
61
- icon: 'Headphones',
62
- title: 'Soporte dedicado',
63
- description: 'Estamos disponibles para ayudarte a resolver cualquier duda.',
64
- },
65
- ],
66
-
67
- // ── Precios ────────────────────────────────────────────────────────────────
68
- pricing: [
69
- {
70
- name: 'Free',
71
- price: '$0',
72
- period: '/mes',
73
- description: 'Para empezar y explorar la plataforma.',
74
- features: [
75
- '1 workspace',
76
- 'Hasta 3 usuarios',
77
- '5 GB de almacenamiento',
78
- 'Soporte por email',
79
- ],
80
- cta: 'Empezar gratis',
81
- href: '/register',
82
- highlight: false,
83
- },
84
- {
85
- name: 'Pro',
86
- price: '$29',
87
- period: '/mes',
88
- description: 'Para equipos que necesitan más poder.',
89
- features: [
90
- 'Workspaces ilimitados',
91
- 'Usuarios ilimitados',
92
- '50 GB de almacenamiento',
93
- 'Analytics avanzados',
94
- 'Soporte prioritario',
95
- 'Integraciones premium',
96
- ],
97
- cta: 'Comenzar prueba gratis',
98
- href: '/register',
99
- highlight: true,
100
- },
101
- {
102
- name: 'Enterprise',
103
- price: 'A consultar',
104
- period: '',
105
- description: 'Soluciones a medida para grandes organizaciones.',
106
- features: [
107
- 'Todo lo de Pro',
108
- 'SLA garantizado',
109
- 'Onboarding dedicado',
110
- 'SSO y SAML',
111
- 'Auditoría y compliance',
112
- 'Contrato personalizado',
113
- ],
114
- cta: 'Hablar con ventas',
115
- href: 'mailto:__SUPPORT_EMAIL__',
116
- highlight: false,
117
- },
118
- ],
119
-
120
- // ── FAQ ────────────────────────────────────────────────────────────────────
121
- faq: [
122
- {
123
- question: '¿Puedo cambiar de plan en cualquier momento?',
124
- answer:
125
- 'Sí, podés upgradear o downgradear tu plan cuando quieras. Los cambios se aplican al próximo ciclo de facturación.',
126
- },
127
- {
128
- question: '¿Qué métodos de pago aceptan?',
129
- answer:
130
- 'Aceptamos tarjetas de crédito y débito (Visa, Mastercard, Amex) y transferencias bancarias para planes Enterprise.',
131
- },
132
- {
133
- question: '¿Hay un período de prueba gratuito?',
134
- answer:
135
- 'El plan Free está disponible sin límite de tiempo. Los planes pagos incluyen 14 días de prueba sin necesidad de tarjeta.',
136
- },
137
- {
138
- question: '¿Cómo es la seguridad de mis datos?',
139
- answer:
140
- 'Todos los datos se almacenan encriptados en reposo y en tránsito. Cumplimos con los estándares de seguridad más exigentes.',
141
- },
142
- {
143
- question: '¿Puedo cancelar en cualquier momento?',
144
- answer:
145
- 'Sí, sin penalidades ni cargos adicionales. Si cancelás, seguís teniendo acceso hasta el fin del período pagado.',
146
- },
147
- {
148
- question: '¿Tienen soporte en español?',
149
- answer: 'Sí, todo nuestro soporte es en español. Respondemos dentro de las 24 horas hábiles.',
150
- },
151
- ],
152
-
153
- // ── Footer ─────────────────────────────────────────────────────────────────
154
- footer: {
155
- columns: [
156
- {
157
- title: 'Producto',
158
- links: [
159
- { label: 'Funcionalidades', href: '#features' },
160
- { label: 'Precios', href: '#pricing' },
161
- { label: 'FAQ', href: '#faq' },
162
- { label: 'Changelog', href: '#' },
163
- ],
164
- },
165
- {
166
- title: 'Legal',
167
- links: [
168
- { label: 'Términos y condiciones', href: '/terms' },
169
- { label: 'Política de privacidad', href: '/privacy' },
170
- ],
171
- },
172
- {
173
- title: 'Soporte',
174
- links: [
175
- { label: 'Contacto', href: 'mailto:__SUPPORT_EMAIL__' },
176
- { label: 'Documentación', href: '#' },
177
- { label: 'Estado del servicio', href: '#' },
178
- ],
179
- },
180
- ],
181
- social: {
182
- twitter: '',
183
- github: '',
184
- linkedin: '',
185
- },
186
- copyright: '© 2024 __SITE_NAME__. Todos los derechos reservados.',
187
- },
188
-
189
- // ── Legal ──────────────────────────────────────────────────────────────────
190
- legal: {
191
- companyName: '__SITE_NAME__',
192
- email: '__SUPPORT_EMAIL__',
193
- lastUpdated: '1 de enero de 2024',
194
- },
195
- }
196
-
197
- export type SiteConfig = typeof siteConfig
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Archivo de configuración central del SaaS.
3
+ // Editá este archivo para personalizar textos, links y contenido de toda la app.
4
+ // Los valores con __PLACEHOLDER__ son reemplazados automáticamente por el CLI.
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ export const siteConfig = {
8
+ // ── Identidad ──────────────────────────────────────────────────────────────
9
+ name: '__SITE_NAME__',
10
+ tagline: '__SITE_TAGLINE__',
11
+ description: '__SITE_DESCRIPTION__',
12
+ url: 'https://tu-dominio.com',
13
+ supportEmail: '__SUPPORT_EMAIL__',
14
+
15
+ // ── Navegación pública (landing) ───────────────────────────────────────────
16
+ nav: {
17
+ links: [
18
+ { label: 'Funcionalidades', href: '#features' },
19
+ { label: 'Precios', href: '#pricing' },
20
+ { label: 'FAQ', href: '#faq' },
21
+ ],
22
+ },
23
+
24
+ // ── Hero ───────────────────────────────────────────────────────────────────
25
+ hero: {
26
+ headline: '__SITE_NAME__',
27
+ subheadline: '__SITE_TAGLINE__',
28
+ description: '__SITE_DESCRIPTION__',
29
+ cta: { label: 'Empezar gratis', href: '/register' },
30
+ ctaSecondary: { label: 'Ver funcionalidades', href: '#features' },
31
+ },
32
+
33
+ // ── Funcionalidades ────────────────────────────────────────────────────────
34
+ features: [
35
+ {
36
+ icon: 'Zap',
37
+ title: 'Rápido y confiable',
38
+ description: 'Infraestructura diseñada para escalar con tu negocio sin fricciones.',
39
+ },
40
+ {
41
+ icon: 'Shield',
42
+ title: 'Seguridad primero',
43
+ description: 'Autenticación robusta, tokens JWT y encriptación en toda la plataforma.',
44
+ },
45
+ {
46
+ icon: 'BarChart3',
47
+ title: 'Analytics en tiempo real',
48
+ description: 'Tomá decisiones basadas en datos con métricas actualizadas al instante.',
49
+ },
50
+ {
51
+ icon: 'Users',
52
+ title: 'Trabajo en equipo',
53
+ description: 'Invitá colaboradores y gestioná permisos de forma simple y segura.',
54
+ },
55
+ {
56
+ icon: 'Globe',
57
+ title: 'Multi-workspace',
58
+ description: 'Administrá múltiples proyectos o clientes desde una sola cuenta.',
59
+ },
60
+ {
61
+ icon: 'Headphones',
62
+ title: 'Soporte dedicado',
63
+ description: 'Estamos disponibles para ayudarte a resolver cualquier duda.',
64
+ },
65
+ ],
66
+
67
+ // ── Precios ────────────────────────────────────────────────────────────────
68
+ pricing: [
69
+ {
70
+ name: 'Free',
71
+ price: '$0',
72
+ period: '/mes',
73
+ description: 'Para empezar y explorar la plataforma.',
74
+ features: [
75
+ '1 workspace',
76
+ 'Hasta 3 usuarios',
77
+ '5 GB de almacenamiento',
78
+ 'Soporte por email',
79
+ ],
80
+ cta: 'Empezar gratis',
81
+ href: '/register',
82
+ highlight: false,
83
+ },
84
+ {
85
+ name: 'Pro',
86
+ price: '$29',
87
+ period: '/mes',
88
+ description: 'Para equipos que necesitan más poder.',
89
+ features: [
90
+ 'Workspaces ilimitados',
91
+ 'Usuarios ilimitados',
92
+ '50 GB de almacenamiento',
93
+ 'Analytics avanzados',
94
+ 'Soporte prioritario',
95
+ 'Integraciones premium',
96
+ ],
97
+ cta: 'Comenzar prueba gratis',
98
+ href: '/register',
99
+ highlight: true,
100
+ },
101
+ {
102
+ name: 'Enterprise',
103
+ price: 'A consultar',
104
+ period: '',
105
+ description: 'Soluciones a medida para grandes organizaciones.',
106
+ features: [
107
+ 'Todo lo de Pro',
108
+ 'SLA garantizado',
109
+ 'Onboarding dedicado',
110
+ 'SSO y SAML',
111
+ 'Auditoría y compliance',
112
+ 'Contrato personalizado',
113
+ ],
114
+ cta: 'Hablar con ventas',
115
+ href: 'mailto:__SUPPORT_EMAIL__',
116
+ highlight: false,
117
+ },
118
+ ],
119
+
120
+ // ── FAQ ────────────────────────────────────────────────────────────────────
121
+ faq: [
122
+ {
123
+ question: '¿Puedo cambiar de plan en cualquier momento?',
124
+ answer:
125
+ 'Sí, podés upgradear o downgradear tu plan cuando quieras. Los cambios se aplican al próximo ciclo de facturación.',
126
+ },
127
+ {
128
+ question: '¿Qué métodos de pago aceptan?',
129
+ answer:
130
+ 'Aceptamos tarjetas de crédito y débito (Visa, Mastercard, Amex) y transferencias bancarias para planes Enterprise.',
131
+ },
132
+ {
133
+ question: '¿Hay un período de prueba gratuito?',
134
+ answer:
135
+ 'El plan Free está disponible sin límite de tiempo. Los planes pagos incluyen 14 días de prueba sin necesidad de tarjeta.',
136
+ },
137
+ {
138
+ question: '¿Cómo es la seguridad de mis datos?',
139
+ answer:
140
+ 'Todos los datos se almacenan encriptados en reposo y en tránsito. Cumplimos con los estándares de seguridad más exigentes.',
141
+ },
142
+ {
143
+ question: '¿Puedo cancelar en cualquier momento?',
144
+ answer:
145
+ 'Sí, sin penalidades ni cargos adicionales. Si cancelás, seguís teniendo acceso hasta el fin del período pagado.',
146
+ },
147
+ {
148
+ question: '¿Tienen soporte en español?',
149
+ answer: 'Sí, todo nuestro soporte es en español. Respondemos dentro de las 24 horas hábiles.',
150
+ },
151
+ ],
152
+
153
+ // ── Footer ─────────────────────────────────────────────────────────────────
154
+ footer: {
155
+ columns: [
156
+ {
157
+ title: 'Producto',
158
+ links: [
159
+ { label: 'Funcionalidades', href: '#features' },
160
+ { label: 'Precios', href: '#pricing' },
161
+ { label: 'FAQ', href: '#faq' },
162
+ { label: 'Changelog', href: '#' },
163
+ ],
164
+ },
165
+ {
166
+ title: 'Legal',
167
+ links: [
168
+ { label: 'Términos y condiciones', href: '/terms' },
169
+ { label: 'Política de privacidad', href: '/privacy' },
170
+ ],
171
+ },
172
+ {
173
+ title: 'Soporte',
174
+ links: [
175
+ { label: 'Contacto', href: 'mailto:__SUPPORT_EMAIL__' },
176
+ { label: 'Documentación', href: '#' },
177
+ { label: 'Estado del servicio', href: '#' },
178
+ ],
179
+ },
180
+ ],
181
+ social: {
182
+ twitter: '',
183
+ github: '',
184
+ linkedin: '',
185
+ },
186
+ copyright: '© 2024 __SITE_NAME__. Todos los derechos reservados.',
187
+ },
188
+
189
+ // ── Legal ──────────────────────────────────────────────────────────────────
190
+ legal: {
191
+ companyName: '__SITE_NAME__',
192
+ email: '__SUPPORT_EMAIL__',
193
+ lastUpdated: '1 de enero de 2024',
194
+ },
195
+ }
196
+
197
+ export type SiteConfig = typeof siteConfig
@@ -1,116 +1,116 @@
1
- 'use client'
2
-
3
- import * as React from 'react'
4
- import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
5
-
6
- const TOAST_LIMIT = 3
7
- const TOAST_REMOVE_DELAY = 4000
8
-
9
- type ToasterToast = ToastProps & {
10
- id: string
11
- title?: React.ReactNode
12
- description?: React.ReactNode
13
- action?: ToastActionElement
14
- }
15
-
16
- type ActionType = {
17
- ADD_TOAST: 'ADD_TOAST'
18
- UPDATE_TOAST: 'UPDATE_TOAST'
19
- DISMISS_TOAST: 'DISMISS_TOAST'
20
- REMOVE_TOAST: 'REMOVE_TOAST'
21
- }
22
-
23
- type Action =
24
- | { type: ActionType['ADD_TOAST']; toast: ToasterToast }
25
- | { type: ActionType['UPDATE_TOAST']; toast: Partial<ToasterToast> }
26
- | { type: ActionType['DISMISS_TOAST']; toastId?: ToasterToast['id'] }
27
- | { type: ActionType['REMOVE_TOAST']; toastId?: ToasterToast['id'] }
28
-
29
- interface State {
30
- toasts: ToasterToast[]
31
- }
32
-
33
- const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
34
-
35
- const addToRemoveQueue = (toastId: string) => {
36
- if (toastTimeouts.has(toastId)) return
37
- const timeout = setTimeout(() => {
38
- toastTimeouts.delete(toastId)
39
- dispatch({ type: 'REMOVE_TOAST', toastId })
40
- }, TOAST_REMOVE_DELAY)
41
- toastTimeouts.set(toastId, timeout)
42
- }
43
-
44
- export const reducer = (state: State, action: Action): State => {
45
- switch (action.type) {
46
- case 'ADD_TOAST':
47
- return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) }
48
- case 'UPDATE_TOAST':
49
- return {
50
- ...state,
51
- toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
52
- }
53
- case 'DISMISS_TOAST': {
54
- const { toastId } = action
55
- if (toastId) {
56
- addToRemoveQueue(toastId)
57
- } else {
58
- state.toasts.forEach((toast) => addToRemoveQueue(toast.id))
59
- }
60
- return {
61
- ...state,
62
- toasts: state.toasts.map((t) =>
63
- t.id === toastId || toastId === undefined ? { ...t, open: false } : t,
64
- ),
65
- }
66
- }
67
- case 'REMOVE_TOAST':
68
- return {
69
- ...state,
70
- toasts: action.toastId === undefined
71
- ? []
72
- : state.toasts.filter((t) => t.id !== action.toastId),
73
- }
74
- }
75
- }
76
-
77
- const listeners: Array<(state: State) => void> = []
78
- let memoryState: State = { toasts: [] }
79
-
80
- function dispatch(action: Action) {
81
- memoryState = reducer(memoryState, action)
82
- listeners.forEach((listener) => listener(memoryState))
83
- }
84
-
85
- let count = 0
86
- function genId() {
87
- count = (count + 1) % Number.MAX_SAFE_INTEGER
88
- return count.toString()
89
- }
90
-
91
- type Toast = Omit<ToasterToast, 'id'>
92
-
93
- function toast({ ...props }: Toast) {
94
- const id = genId()
95
- const update = (props: ToasterToast) => dispatch({ type: 'UPDATE_TOAST', toast: { ...props, id } })
96
- const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
97
- dispatch({
98
- type: 'ADD_TOAST',
99
- toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() } },
100
- })
101
- return { id, dismiss, update }
102
- }
103
-
104
- function useToast() {
105
- const [state, setState] = React.useState<State>(memoryState)
106
- React.useEffect(() => {
107
- listeners.push(setState)
108
- return () => {
109
- const index = listeners.indexOf(setState)
110
- if (index > -1) listeners.splice(index, 1)
111
- }
112
- }, [])
113
- return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }) }
114
- }
115
-
116
- export { useToast, toast }
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
5
+
6
+ const TOAST_LIMIT = 3
7
+ const TOAST_REMOVE_DELAY = 4000
8
+
9
+ type ToasterToast = ToastProps & {
10
+ id: string
11
+ title?: React.ReactNode
12
+ description?: React.ReactNode
13
+ action?: ToastActionElement
14
+ }
15
+
16
+ type ActionType = {
17
+ ADD_TOAST: 'ADD_TOAST'
18
+ UPDATE_TOAST: 'UPDATE_TOAST'
19
+ DISMISS_TOAST: 'DISMISS_TOAST'
20
+ REMOVE_TOAST: 'REMOVE_TOAST'
21
+ }
22
+
23
+ type Action =
24
+ | { type: ActionType['ADD_TOAST']; toast: ToasterToast }
25
+ | { type: ActionType['UPDATE_TOAST']; toast: Partial<ToasterToast> }
26
+ | { type: ActionType['DISMISS_TOAST']; toastId?: ToasterToast['id'] }
27
+ | { type: ActionType['REMOVE_TOAST']; toastId?: ToasterToast['id'] }
28
+
29
+ interface State {
30
+ toasts: ToasterToast[]
31
+ }
32
+
33
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
34
+
35
+ const addToRemoveQueue = (toastId: string) => {
36
+ if (toastTimeouts.has(toastId)) return
37
+ const timeout = setTimeout(() => {
38
+ toastTimeouts.delete(toastId)
39
+ dispatch({ type: 'REMOVE_TOAST', toastId })
40
+ }, TOAST_REMOVE_DELAY)
41
+ toastTimeouts.set(toastId, timeout)
42
+ }
43
+
44
+ export const reducer = (state: State, action: Action): State => {
45
+ switch (action.type) {
46
+ case 'ADD_TOAST':
47
+ return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) }
48
+ case 'UPDATE_TOAST':
49
+ return {
50
+ ...state,
51
+ toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
52
+ }
53
+ case 'DISMISS_TOAST': {
54
+ const { toastId } = action
55
+ if (toastId) {
56
+ addToRemoveQueue(toastId)
57
+ } else {
58
+ state.toasts.forEach((toast) => addToRemoveQueue(toast.id))
59
+ }
60
+ return {
61
+ ...state,
62
+ toasts: state.toasts.map((t) =>
63
+ t.id === toastId || toastId === undefined ? { ...t, open: false } : t,
64
+ ),
65
+ }
66
+ }
67
+ case 'REMOVE_TOAST':
68
+ return {
69
+ ...state,
70
+ toasts: action.toastId === undefined
71
+ ? []
72
+ : state.toasts.filter((t) => t.id !== action.toastId),
73
+ }
74
+ }
75
+ }
76
+
77
+ const listeners: Array<(state: State) => void> = []
78
+ let memoryState: State = { toasts: [] }
79
+
80
+ function dispatch(action: Action) {
81
+ memoryState = reducer(memoryState, action)
82
+ listeners.forEach((listener) => listener(memoryState))
83
+ }
84
+
85
+ let count = 0
86
+ function genId() {
87
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
88
+ return count.toString()
89
+ }
90
+
91
+ type Toast = Omit<ToasterToast, 'id'>
92
+
93
+ function toast({ ...props }: Toast) {
94
+ const id = genId()
95
+ const update = (props: ToasterToast) => dispatch({ type: 'UPDATE_TOAST', toast: { ...props, id } })
96
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
97
+ dispatch({
98
+ type: 'ADD_TOAST',
99
+ toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() } },
100
+ })
101
+ return { id, dismiss, update }
102
+ }
103
+
104
+ function useToast() {
105
+ const [state, setState] = React.useState<State>(memoryState)
106
+ React.useEffect(() => {
107
+ listeners.push(setState)
108
+ return () => {
109
+ const index = listeners.indexOf(setState)
110
+ if (index > -1) listeners.splice(index, 1)
111
+ }
112
+ }, [])
113
+ return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }) }
114
+ }
115
+
116
+ export { useToast, toast }