@digilogiclabs/create-saas-app 1.4.0 → 1.5.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 (199) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/index.js +45 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/templates/web/base/template/package.json +20 -10
  6. package/dist/templates/web/base/template/src/app/error.tsx +97 -0
  7. package/dist/templates/web/base/template/src/app/layout.tsx +8 -2
  8. package/dist/templates/web/base/template/src/app/loading.tsx +34 -0
  9. package/dist/templates/web/base/template/src/components/__tests__/example.test.tsx +49 -0
  10. package/dist/templates/web/base/template/src/components/providers/app-providers.tsx +6 -2
  11. package/dist/templates/web/base/template/src/components/providers/theme-provider.tsx +94 -0
  12. package/dist/templates/web/base/template/src/components/shared/footer.tsx +36 -0
  13. package/dist/templates/web/base/template/src/components/shared/header.tsx +2 -0
  14. package/dist/templates/web/base/template/src/components/ui/theme-toggle.tsx +34 -0
  15. package/dist/templates/web/base/template/src/lib/auth-server.ts +177 -0
  16. package/dist/templates/web/base/template/src/lib/env.ts +46 -0
  17. package/dist/templates/web/base/template/src/lib/utils.ts +133 -0
  18. package/dist/templates/web/base/template/src/test/setup.ts +79 -0
  19. package/dist/templates/web/base/template/vitest.config.ts +17 -0
  20. package/dist/templates/web/ui-auth/template/package.json +14 -4
  21. package/dist/templates/web/ui-auth/template/src/app/error.tsx +67 -0
  22. package/dist/templates/web/ui-auth/template/src/app/layout.tsx +6 -2
  23. package/dist/templates/web/ui-auth/template/src/app/loading.tsx +20 -0
  24. package/dist/templates/web/ui-auth/template/src/components/__tests__/example.test.tsx +49 -0
  25. package/dist/templates/web/ui-auth/template/src/components/providers/app-providers.tsx +6 -2
  26. package/dist/templates/web/ui-auth/template/src/components/providers/theme-provider.tsx +94 -0
  27. package/dist/templates/web/ui-auth/template/src/components/shared/footer.tsx +36 -0
  28. package/dist/templates/web/ui-auth/template/src/components/shared/header.tsx +2 -0
  29. package/dist/templates/web/ui-auth/template/src/components/ui/theme-toggle.tsx +34 -0
  30. package/dist/templates/web/ui-auth/template/src/lib/env.ts +49 -0
  31. package/dist/templates/web/ui-auth/template/src/lib/utils.ts +133 -0
  32. package/dist/templates/web/ui-auth/template/src/test/setup.ts +79 -0
  33. package/dist/templates/web/ui-auth/template/vitest.config.ts +17 -0
  34. package/dist/templates/web/ui-auth-payments/template/middleware.ts +68 -0
  35. package/dist/templates/web/ui-auth-payments/template/package.json +14 -4
  36. package/dist/templates/web/ui-auth-payments/template/src/app/dashboard/layout.tsx +22 -0
  37. package/dist/templates/web/ui-auth-payments/template/src/app/dashboard/page.tsx +183 -0
  38. package/dist/templates/web/ui-auth-payments/template/src/app/error.tsx +67 -0
  39. package/dist/templates/web/ui-auth-payments/template/src/app/layout.tsx +6 -2
  40. package/dist/templates/web/ui-auth-payments/template/src/app/loading.tsx +20 -0
  41. package/dist/templates/web/ui-auth-payments/template/src/app/login/loading.tsx +38 -0
  42. package/dist/templates/web/ui-auth-payments/template/src/app/signup/loading.tsx +50 -0
  43. package/dist/templates/web/ui-auth-payments/template/src/components/__tests__/example.test.tsx +49 -0
  44. package/dist/templates/web/ui-auth-payments/template/src/components/client/auth-status.tsx +52 -0
  45. package/dist/templates/web/ui-auth-payments/template/src/components/client/login-form.tsx +144 -0
  46. package/dist/templates/web/ui-auth-payments/template/src/components/client/newsletter-signup.tsx +68 -0
  47. package/dist/templates/web/ui-auth-payments/template/src/components/client/signup-form.tsx +185 -0
  48. package/dist/templates/web/ui-auth-payments/template/src/components/providers/app-providers.tsx +6 -2
  49. package/dist/templates/web/ui-auth-payments/template/src/components/providers/theme-provider.tsx +94 -0
  50. package/dist/templates/web/ui-auth-payments/template/src/components/shared/footer.tsx +36 -0
  51. package/dist/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +2 -0
  52. package/dist/templates/web/ui-auth-payments/template/src/components/ui/theme-toggle.tsx +34 -0
  53. package/dist/templates/web/ui-auth-payments/template/src/lib/actions/auth.ts +246 -0
  54. package/dist/templates/web/ui-auth-payments/template/src/lib/actions/index.ts +340 -0
  55. package/dist/templates/web/ui-auth-payments/template/src/lib/auth-server.ts +177 -0
  56. package/dist/templates/web/ui-auth-payments/template/src/lib/env.ts +49 -0
  57. package/dist/templates/web/ui-auth-payments/template/src/lib/utils.ts +133 -0
  58. package/dist/templates/web/ui-auth-payments/template/src/test/setup.ts +79 -0
  59. package/dist/templates/web/ui-auth-payments/template/vitest.config.ts +17 -0
  60. package/dist/templates/web/ui-auth-payments-audio/template/package.json +14 -4
  61. package/dist/templates/web/ui-auth-payments-audio/template/src/app/error.tsx +67 -0
  62. package/dist/templates/web/ui-auth-payments-audio/template/src/app/layout.tsx +8 -2
  63. package/dist/templates/web/ui-auth-payments-audio/template/src/app/loading.tsx +20 -0
  64. package/dist/templates/web/ui-auth-payments-audio/template/src/components/__tests__/example.test.tsx +49 -0
  65. package/dist/templates/web/ui-auth-payments-audio/template/src/components/providers/app-providers.tsx +6 -2
  66. package/dist/templates/web/ui-auth-payments-audio/template/src/components/providers/theme-provider.tsx +94 -0
  67. package/dist/templates/web/ui-auth-payments-audio/template/src/components/shared/footer.tsx +36 -0
  68. package/dist/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +2 -0
  69. package/dist/templates/web/ui-auth-payments-audio/template/src/components/ui/theme-toggle.tsx +34 -0
  70. package/dist/templates/web/ui-auth-payments-audio/template/src/lib/env.ts +49 -0
  71. package/dist/templates/web/ui-auth-payments-audio/template/src/lib/utils.ts +133 -0
  72. package/dist/templates/web/ui-auth-payments-audio/template/src/test/setup.ts +79 -0
  73. package/dist/templates/web/ui-auth-payments-audio/template/vitest.config.ts +17 -0
  74. package/dist/templates/web/ui-auth-payments-video/template/package.json +14 -4
  75. package/dist/templates/web/ui-auth-payments-video/template/src/app/error.tsx +67 -0
  76. package/dist/templates/web/ui-auth-payments-video/template/src/app/layout.tsx +6 -2
  77. package/dist/templates/web/ui-auth-payments-video/template/src/app/loading.tsx +20 -0
  78. package/dist/templates/web/ui-auth-payments-video/template/src/components/__tests__/example.test.tsx +49 -0
  79. package/dist/templates/web/ui-auth-payments-video/template/src/components/providers/app-providers.tsx +6 -2
  80. package/dist/templates/web/ui-auth-payments-video/template/src/components/providers/theme-provider.tsx +94 -0
  81. package/dist/templates/web/ui-auth-payments-video/template/src/components/shared/footer.tsx +36 -0
  82. package/dist/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +2 -0
  83. package/dist/templates/web/ui-auth-payments-video/template/src/components/ui/theme-toggle.tsx +34 -0
  84. package/dist/templates/web/ui-auth-payments-video/template/src/lib/env.ts +49 -0
  85. package/dist/templates/web/ui-auth-payments-video/template/src/lib/utils.ts +133 -0
  86. package/dist/templates/web/ui-auth-payments-video/template/src/test/setup.ts +79 -0
  87. package/dist/templates/web/ui-auth-payments-video/template/vitest.config.ts +17 -0
  88. package/dist/templates/web/ui-only/template/package.json +14 -4
  89. package/dist/templates/web/ui-only/template/src/app/error.tsx +67 -0
  90. package/dist/templates/web/ui-only/template/src/app/layout.tsx +6 -2
  91. package/dist/templates/web/ui-only/template/src/app/loading.tsx +20 -0
  92. package/dist/templates/web/ui-only/template/src/components/__tests__/example.test.tsx +49 -0
  93. package/dist/templates/web/ui-only/template/src/components/providers/app-providers.tsx +6 -2
  94. package/dist/templates/web/ui-only/template/src/components/providers/theme-provider.tsx +94 -0
  95. package/dist/templates/web/ui-only/template/src/components/shared/footer.tsx +36 -0
  96. package/dist/templates/web/ui-only/template/src/components/shared/header.tsx +2 -0
  97. package/dist/templates/web/ui-only/template/src/components/ui/theme-toggle.tsx +34 -0
  98. package/dist/templates/web/ui-only/template/src/lib/env.ts +49 -0
  99. package/dist/templates/web/ui-only/template/src/lib/utils.ts +133 -0
  100. package/dist/templates/web/ui-only/template/src/test/setup.ts +79 -0
  101. package/dist/templates/web/ui-only/template/vitest.config.ts +17 -0
  102. package/package.json +1 -1
  103. package/src/templates/web/base/template/package.json +20 -10
  104. package/src/templates/web/base/template/src/app/error.tsx +97 -0
  105. package/src/templates/web/base/template/src/app/layout.tsx +8 -2
  106. package/src/templates/web/base/template/src/app/loading.tsx +34 -0
  107. package/src/templates/web/base/template/src/components/__tests__/example.test.tsx +49 -0
  108. package/src/templates/web/base/template/src/components/providers/app-providers.tsx +6 -2
  109. package/src/templates/web/base/template/src/components/providers/theme-provider.tsx +94 -0
  110. package/src/templates/web/base/template/src/components/shared/footer.tsx +36 -0
  111. package/src/templates/web/base/template/src/components/shared/header.tsx +2 -0
  112. package/src/templates/web/base/template/src/components/ui/theme-toggle.tsx +34 -0
  113. package/src/templates/web/base/template/src/lib/auth-server.ts +177 -0
  114. package/src/templates/web/base/template/src/lib/env.ts +46 -0
  115. package/src/templates/web/base/template/src/lib/utils.ts +133 -0
  116. package/src/templates/web/base/template/src/test/setup.ts +79 -0
  117. package/src/templates/web/base/template/vitest.config.ts +17 -0
  118. package/src/templates/web/ui-auth/template/package.json +14 -4
  119. package/src/templates/web/ui-auth/template/src/app/error.tsx +67 -0
  120. package/src/templates/web/ui-auth/template/src/app/layout.tsx +6 -2
  121. package/src/templates/web/ui-auth/template/src/app/loading.tsx +20 -0
  122. package/src/templates/web/ui-auth/template/src/components/__tests__/example.test.tsx +49 -0
  123. package/src/templates/web/ui-auth/template/src/components/providers/app-providers.tsx +6 -2
  124. package/src/templates/web/ui-auth/template/src/components/providers/theme-provider.tsx +94 -0
  125. package/src/templates/web/ui-auth/template/src/components/shared/footer.tsx +36 -0
  126. package/src/templates/web/ui-auth/template/src/components/shared/header.tsx +2 -0
  127. package/src/templates/web/ui-auth/template/src/components/ui/theme-toggle.tsx +34 -0
  128. package/src/templates/web/ui-auth/template/src/lib/env.ts +49 -0
  129. package/src/templates/web/ui-auth/template/src/lib/utils.ts +133 -0
  130. package/src/templates/web/ui-auth/template/src/test/setup.ts +79 -0
  131. package/src/templates/web/ui-auth/template/vitest.config.ts +17 -0
  132. package/src/templates/web/ui-auth-payments/template/middleware.ts +68 -0
  133. package/src/templates/web/ui-auth-payments/template/package.json +14 -4
  134. package/src/templates/web/ui-auth-payments/template/src/app/dashboard/layout.tsx +22 -0
  135. package/src/templates/web/ui-auth-payments/template/src/app/dashboard/page.tsx +183 -0
  136. package/src/templates/web/ui-auth-payments/template/src/app/error.tsx +67 -0
  137. package/src/templates/web/ui-auth-payments/template/src/app/layout.tsx +6 -2
  138. package/src/templates/web/ui-auth-payments/template/src/app/loading.tsx +20 -0
  139. package/src/templates/web/ui-auth-payments/template/src/app/login/loading.tsx +38 -0
  140. package/src/templates/web/ui-auth-payments/template/src/app/signup/loading.tsx +50 -0
  141. package/src/templates/web/ui-auth-payments/template/src/components/__tests__/example.test.tsx +49 -0
  142. package/src/templates/web/ui-auth-payments/template/src/components/client/auth-status.tsx +52 -0
  143. package/src/templates/web/ui-auth-payments/template/src/components/client/login-form.tsx +144 -0
  144. package/src/templates/web/ui-auth-payments/template/src/components/client/newsletter-signup.tsx +68 -0
  145. package/src/templates/web/ui-auth-payments/template/src/components/client/signup-form.tsx +185 -0
  146. package/src/templates/web/ui-auth-payments/template/src/components/providers/app-providers.tsx +6 -2
  147. package/src/templates/web/ui-auth-payments/template/src/components/providers/theme-provider.tsx +94 -0
  148. package/src/templates/web/ui-auth-payments/template/src/components/shared/footer.tsx +36 -0
  149. package/src/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +2 -0
  150. package/src/templates/web/ui-auth-payments/template/src/components/ui/theme-toggle.tsx +34 -0
  151. package/src/templates/web/ui-auth-payments/template/src/lib/actions/auth.ts +246 -0
  152. package/src/templates/web/ui-auth-payments/template/src/lib/actions/index.ts +340 -0
  153. package/src/templates/web/ui-auth-payments/template/src/lib/auth-server.ts +177 -0
  154. package/src/templates/web/ui-auth-payments/template/src/lib/env.ts +49 -0
  155. package/src/templates/web/ui-auth-payments/template/src/lib/utils.ts +133 -0
  156. package/src/templates/web/ui-auth-payments/template/src/test/setup.ts +79 -0
  157. package/src/templates/web/ui-auth-payments/template/vitest.config.ts +17 -0
  158. package/src/templates/web/ui-auth-payments-audio/template/package.json +14 -4
  159. package/src/templates/web/ui-auth-payments-audio/template/src/app/error.tsx +67 -0
  160. package/src/templates/web/ui-auth-payments-audio/template/src/app/layout.tsx +8 -2
  161. package/src/templates/web/ui-auth-payments-audio/template/src/app/loading.tsx +20 -0
  162. package/src/templates/web/ui-auth-payments-audio/template/src/components/__tests__/example.test.tsx +49 -0
  163. package/src/templates/web/ui-auth-payments-audio/template/src/components/providers/app-providers.tsx +6 -2
  164. package/src/templates/web/ui-auth-payments-audio/template/src/components/providers/theme-provider.tsx +94 -0
  165. package/src/templates/web/ui-auth-payments-audio/template/src/components/shared/footer.tsx +36 -0
  166. package/src/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +2 -0
  167. package/src/templates/web/ui-auth-payments-audio/template/src/components/ui/theme-toggle.tsx +34 -0
  168. package/src/templates/web/ui-auth-payments-audio/template/src/lib/env.ts +49 -0
  169. package/src/templates/web/ui-auth-payments-audio/template/src/lib/utils.ts +133 -0
  170. package/src/templates/web/ui-auth-payments-audio/template/src/test/setup.ts +79 -0
  171. package/src/templates/web/ui-auth-payments-audio/template/vitest.config.ts +17 -0
  172. package/src/templates/web/ui-auth-payments-video/template/package.json +14 -4
  173. package/src/templates/web/ui-auth-payments-video/template/src/app/error.tsx +67 -0
  174. package/src/templates/web/ui-auth-payments-video/template/src/app/layout.tsx +6 -2
  175. package/src/templates/web/ui-auth-payments-video/template/src/app/loading.tsx +20 -0
  176. package/src/templates/web/ui-auth-payments-video/template/src/components/__tests__/example.test.tsx +49 -0
  177. package/src/templates/web/ui-auth-payments-video/template/src/components/providers/app-providers.tsx +6 -2
  178. package/src/templates/web/ui-auth-payments-video/template/src/components/providers/theme-provider.tsx +94 -0
  179. package/src/templates/web/ui-auth-payments-video/template/src/components/shared/footer.tsx +36 -0
  180. package/src/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +2 -0
  181. package/src/templates/web/ui-auth-payments-video/template/src/components/ui/theme-toggle.tsx +34 -0
  182. package/src/templates/web/ui-auth-payments-video/template/src/lib/env.ts +49 -0
  183. package/src/templates/web/ui-auth-payments-video/template/src/lib/utils.ts +133 -0
  184. package/src/templates/web/ui-auth-payments-video/template/src/test/setup.ts +79 -0
  185. package/src/templates/web/ui-auth-payments-video/template/vitest.config.ts +17 -0
  186. package/src/templates/web/ui-only/template/package.json +14 -4
  187. package/src/templates/web/ui-only/template/src/app/error.tsx +67 -0
  188. package/src/templates/web/ui-only/template/src/app/layout.tsx +6 -2
  189. package/src/templates/web/ui-only/template/src/app/loading.tsx +20 -0
  190. package/src/templates/web/ui-only/template/src/components/__tests__/example.test.tsx +49 -0
  191. package/src/templates/web/ui-only/template/src/components/providers/app-providers.tsx +6 -2
  192. package/src/templates/web/ui-only/template/src/components/providers/theme-provider.tsx +94 -0
  193. package/src/templates/web/ui-only/template/src/components/shared/footer.tsx +36 -0
  194. package/src/templates/web/ui-only/template/src/components/shared/header.tsx +2 -0
  195. package/src/templates/web/ui-only/template/src/components/ui/theme-toggle.tsx +34 -0
  196. package/src/templates/web/ui-only/template/src/lib/env.ts +49 -0
  197. package/src/templates/web/ui-only/template/src/lib/utils.ts +133 -0
  198. package/src/templates/web/ui-only/template/src/test/setup.ts +79 -0
  199. package/src/templates/web/ui-only/template/vitest.config.ts +17 -0
@@ -0,0 +1,340 @@
1
+ 'use server'
2
+
3
+ import { after } from 'next/server'
4
+ import { revalidateTag, revalidatePath } from 'next/cache'
5
+ import { getCurrentUser } from '@/lib/auth-server'
6
+ import { z } from 'zod'
7
+
8
+ // Common response type
9
+ export type ActionResponse<T = any> = {
10
+ success: boolean
11
+ data?: T
12
+ error?: string
13
+ fieldErrors?: Record<string, string[]>
14
+ }
15
+
16
+ /**
17
+ * Example server action for contact form
18
+ */
19
+ export async function submitContactForm(prevState: any, formData: FormData): Promise<ActionResponse> {
20
+ try {
21
+ const contactSchema = z.object({
22
+ name: z.string().min(2, 'Name must be at least 2 characters'),
23
+ email: z.string().email('Invalid email address'),
24
+ subject: z.string().min(5, 'Subject must be at least 5 characters'),
25
+ message: z.string().min(10, 'Message must be at least 10 characters'),
26
+ })
27
+
28
+ const rawData = {
29
+ name: formData.get('name') as string,
30
+ email: formData.get('email') as string,
31
+ subject: formData.get('subject') as string,
32
+ message: formData.get('message') as string,
33
+ }
34
+
35
+ const validationResult = contactSchema.safeParse(rawData)
36
+
37
+ if (!validationResult.success) {
38
+ return {
39
+ success: false,
40
+ error: 'Please check your input',
41
+ fieldErrors: validationResult.error.formErrors.fieldErrors,
42
+ }
43
+ }
44
+
45
+ const { name, email, subject, message } = validationResult.data
46
+
47
+ // Process contact form submission
48
+ // This could involve:
49
+ // - Sending email to admin
50
+ // - Saving to database
51
+ // - Sending auto-reply to user
52
+
53
+ // Use after() for non-blocking operations (Next.js 15.3 feature)
54
+ after(async () => {
55
+ // Send notification email to admin (non-blocking)
56
+ console.log('Sending notification email for contact form submission')
57
+
58
+ // Log analytics event (non-blocking)
59
+ console.log('Logging contact form submission analytics')
60
+
61
+ // Update dashboard statistics (non-blocking)
62
+ console.log('Updating contact form statistics')
63
+ })
64
+
65
+ return {
66
+ success: true,
67
+ data: { message: 'Thank you for your message. We\'ll get back to you soon!' }
68
+ }
69
+ } catch (error) {
70
+ console.error('Contact form error:', error)
71
+ return {
72
+ success: false,
73
+ error: 'An unexpected error occurred. Please try again.',
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Example server action for newsletter signup
80
+ */
81
+ export async function subscribeToNewsletter(prevState: any, formData: FormData): Promise<ActionResponse> {
82
+ try {
83
+ const email = formData.get('email') as string
84
+
85
+ const emailSchema = z.string().email('Invalid email address')
86
+ const validationResult = emailSchema.safeParse(email)
87
+
88
+ if (!validationResult.success) {
89
+ return {
90
+ success: false,
91
+ error: 'Please enter a valid email address',
92
+ }
93
+ }
94
+
95
+ // Add to newsletter list
96
+ // This could involve:
97
+ // - Adding to email service provider (Mailchimp, ConvertKit, etc.)
98
+ // - Saving to database
99
+ // - Sending welcome email
100
+
101
+ after(async () => {
102
+ // Send welcome email (non-blocking)
103
+ console.log(`Sending welcome email to ${email}`)
104
+
105
+ // Add to email marketing platform (non-blocking)
106
+ console.log(`Adding ${email} to newsletter list`)
107
+
108
+ // Track conversion (non-blocking)
109
+ console.log('Tracking newsletter signup conversion')
110
+ })
111
+
112
+ return {
113
+ success: true,
114
+ data: { message: 'Successfully subscribed to newsletter!' }
115
+ }
116
+ } catch (error) {
117
+ console.error('Newsletter subscription error:', error)
118
+ return {
119
+ success: false,
120
+ error: 'An unexpected error occurred. Please try again.',
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Example authenticated server action
127
+ */
128
+ export async function createPost(prevState: any, formData: FormData): Promise<ActionResponse> {
129
+ try {
130
+ // Require authentication
131
+ const user = await getCurrentUser()
132
+ if (!user) {
133
+ return {
134
+ success: false,
135
+ error: 'Authentication required',
136
+ }
137
+ }
138
+
139
+ const postSchema = z.object({
140
+ title: z.string().min(3, 'Title must be at least 3 characters'),
141
+ content: z.string().min(10, 'Content must be at least 10 characters'),
142
+ published: z.boolean().default(false),
143
+ })
144
+
145
+ const rawData = {
146
+ title: formData.get('title') as string,
147
+ content: formData.get('content') as string,
148
+ published: formData.get('published') === 'true',
149
+ }
150
+
151
+ const validationResult = postSchema.safeParse(rawData)
152
+
153
+ if (!validationResult.success) {
154
+ return {
155
+ success: false,
156
+ error: 'Please check your input',
157
+ fieldErrors: validationResult.error.formErrors.fieldErrors,
158
+ }
159
+ }
160
+
161
+ const { title, content, published } = validationResult.data
162
+
163
+ // Create post in database
164
+ const post = {
165
+ id: Math.random().toString(36).substr(2, 9),
166
+ title,
167
+ content,
168
+ published,
169
+ authorId: user.id,
170
+ createdAt: new Date().toISOString(),
171
+ }
172
+
173
+ // Revalidate relevant paths and tags
174
+ revalidateTag('posts')
175
+ revalidatePath('/posts')
176
+ if (published) {
177
+ revalidatePath('/')
178
+ }
179
+
180
+ // Non-blocking operations
181
+ after(async () => {
182
+ // Send notifications to subscribers (non-blocking)
183
+ if (published) {
184
+ console.log('Notifying subscribers of new post')
185
+ }
186
+
187
+ // Update search index (non-blocking)
188
+ console.log('Updating search index')
189
+
190
+ // Track content creation analytics (non-blocking)
191
+ console.log('Tracking post creation analytics')
192
+ })
193
+
194
+ return {
195
+ success: true,
196
+ data: { post, message: published ? 'Post published successfully!' : 'Post saved as draft!' }
197
+ }
198
+ } catch (error) {
199
+ console.error('Create post error:', error)
200
+ return {
201
+ success: false,
202
+ error: 'An unexpected error occurred. Please try again.',
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Example action for updating user preferences
209
+ */
210
+ export async function updatePreferences(prevState: any, formData: FormData): Promise<ActionResponse> {
211
+ try {
212
+ const user = await getCurrentUser()
213
+ if (!user) {
214
+ return {
215
+ success: false,
216
+ error: 'Authentication required',
217
+ }
218
+ }
219
+
220
+ const preferencesSchema = z.object({
221
+ emailNotifications: z.boolean().default(true),
222
+ pushNotifications: z.boolean().default(false),
223
+ marketingEmails: z.boolean().default(false),
224
+ theme: z.enum(['light', 'dark', 'system']).default('system'),
225
+ })
226
+
227
+ const rawData = {
228
+ emailNotifications: formData.get('emailNotifications') === 'true',
229
+ pushNotifications: formData.get('pushNotifications') === 'true',
230
+ marketingEmails: formData.get('marketingEmails') === 'true',
231
+ theme: formData.get('theme') as 'light' | 'dark' | 'system' | null || 'system',
232
+ }
233
+
234
+ const validationResult = preferencesSchema.safeParse(rawData)
235
+
236
+ if (!validationResult.success) {
237
+ return {
238
+ success: false,
239
+ error: 'Please check your input',
240
+ fieldErrors: validationResult.error.formErrors.fieldErrors,
241
+ }
242
+ }
243
+
244
+ const preferences = validationResult.data
245
+
246
+ // Update user preferences in database
247
+ // This would typically involve a database update
248
+
249
+ after(async () => {
250
+ // Update notification service settings (non-blocking)
251
+ console.log('Updating notification service settings')
252
+
253
+ // Log preference changes for analytics (non-blocking)
254
+ console.log('Logging preference changes')
255
+ })
256
+
257
+ return {
258
+ success: true,
259
+ data: { preferences, message: 'Preferences updated successfully!' }
260
+ }
261
+ } catch (error) {
262
+ console.error('Update preferences error:', error)
263
+ return {
264
+ success: false,
265
+ error: 'An unexpected error occurred. Please try again.',
266
+ }
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Example action with file upload
272
+ */
273
+ export async function uploadAvatar(prevState: any, formData: FormData): Promise<ActionResponse> {
274
+ try {
275
+ const user = await getCurrentUser()
276
+ if (!user) {
277
+ return {
278
+ success: false,
279
+ error: 'Authentication required',
280
+ }
281
+ }
282
+
283
+ const file = formData.get('avatar') as File
284
+
285
+ if (!file || file.size === 0) {
286
+ return {
287
+ success: false,
288
+ error: 'Please select a file to upload',
289
+ }
290
+ }
291
+
292
+ // Validate file type and size
293
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
294
+ const maxSize = 5 * 1024 * 1024 // 5MB
295
+
296
+ if (!allowedTypes.includes(file.type)) {
297
+ return {
298
+ success: false,
299
+ error: 'Please upload a valid image file (JPEG, PNG, GIF, or WebP)',
300
+ }
301
+ }
302
+
303
+ if (file.size > maxSize) {
304
+ return {
305
+ success: false,
306
+ error: 'File size must be less than 5MB',
307
+ }
308
+ }
309
+
310
+ // Process file upload
311
+ // This would typically involve:
312
+ // - Uploading to cloud storage (AWS S3, Cloudinary, etc.)
313
+ // - Resizing/optimizing the image
314
+ // - Updating user record in database
315
+
316
+ const avatarUrl = `https://example.com/avatars/${user.id}-${Date.now()}.${file.type.split('/')[1]}`
317
+
318
+ after(async () => {
319
+ // Generate different image sizes (non-blocking)
320
+ console.log('Generating avatar thumbnails')
321
+
322
+ // Update CDN cache (non-blocking)
323
+ console.log('Updating CDN cache')
324
+
325
+ // Log upload event (non-blocking)
326
+ console.log('Logging avatar upload event')
327
+ })
328
+
329
+ return {
330
+ success: true,
331
+ data: { avatarUrl, message: 'Avatar uploaded successfully!' }
332
+ }
333
+ } catch (error) {
334
+ console.error('Upload avatar error:', error)
335
+ return {
336
+ success: false,
337
+ error: 'An unexpected error occurred. Please try again.',
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,177 @@
1
+ import { cookies } from 'next/headers'
2
+ import { redirect } from 'next/navigation'
3
+
4
+ // Server-side auth utilities that work with @digilogiclabs/saas-factory-auth
5
+ // These functions provide server-side access to auth state
6
+
7
+ export type User = {
8
+ id: string
9
+ email: string
10
+ name?: string
11
+ avatar?: string
12
+ [key: string]: any
13
+ }
14
+
15
+ /**
16
+ * Get the current user from server-side cookies
17
+ * This function should be called in Server Components, Server Actions, or Route Handlers
18
+ */
19
+ export async function getCurrentUser(): Promise<User | null> {
20
+ try {
21
+ const cookieStore = await cookies()
22
+
23
+ // Check for auth token in cookies set by @digilogiclabs/saas-factory-auth
24
+ const authToken = cookieStore.get('saas-factory-auth-token')?.value
25
+ const userCookie = cookieStore.get('saas-factory-auth-user')?.value
26
+
27
+ if (!authToken || !userCookie) {
28
+ return null
29
+ }
30
+
31
+ // Parse user data from cookie
32
+ try {
33
+ const userData = JSON.parse(userCookie)
34
+ return userData
35
+ } catch {
36
+ return null
37
+ }
38
+ } catch (error) {
39
+ console.error('Error getting current user:', error)
40
+ return null
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Require authentication - redirects to login if not authenticated
46
+ * Use this in Server Components that require authentication
47
+ */
48
+ export async function requireAuth(): Promise<User> {
49
+ const user = await getCurrentUser()
50
+
51
+ if (!user) {
52
+ redirect('/login')
53
+ }
54
+
55
+ return user
56
+ }
57
+
58
+ /**
59
+ * Check if user is authenticated (without redirecting)
60
+ * Returns boolean for conditional rendering
61
+ */
62
+ export async function isAuthenticated(): Promise<boolean> {
63
+ const user = await getCurrentUser()
64
+ return !!user
65
+ }
66
+
67
+ /**
68
+ * Redirect if already authenticated
69
+ * Use this on login/signup pages
70
+ */
71
+ export async function redirectIfAuthenticated(redirectTo: string = '/') {
72
+ const user = await getCurrentUser()
73
+
74
+ if (user) {
75
+ redirect(redirectTo)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get user session data for server components
81
+ * Returns both user and loading state info
82
+ */
83
+ export async function getServerSession() {
84
+ const user = await getCurrentUser()
85
+
86
+ return {
87
+ user,
88
+ isAuthenticated: !!user,
89
+ isLoading: false, // Server-side is never loading
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Server-side auth check for API routes
95
+ */
96
+ export async function getAuthFromRequest(request?: Request): Promise<User | null> {
97
+ try {
98
+ // If request is provided, extract cookies from it
99
+ if (request) {
100
+ const cookie = request.headers.get('cookie')
101
+ if (!cookie) return null
102
+
103
+ // Parse cookies manually from request
104
+ const cookies = cookie.split(';').reduce((acc, cookie) => {
105
+ const [key, value] = cookie.trim().split('=')
106
+ acc[key] = value
107
+ return acc
108
+ }, {} as Record<string, string>)
109
+
110
+ const authToken = cookies['saas-factory-auth-token']
111
+ const userCookie = cookies['saas-factory-auth-user']
112
+
113
+ if (!authToken || !userCookie) {
114
+ return null
115
+ }
116
+
117
+ try {
118
+ const userData = JSON.parse(decodeURIComponent(userCookie))
119
+ return userData
120
+ } catch {
121
+ return null
122
+ }
123
+ }
124
+
125
+ // Fallback to cookies() API
126
+ return await getCurrentUser()
127
+ } catch (error) {
128
+ console.error('Error getting auth from request:', error)
129
+ return null
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Create server action wrapper that requires authentication
135
+ */
136
+ export function withAuth<T extends any[], R>(
137
+ action: (user: User, ...args: T) => Promise<R>
138
+ ) {
139
+ return async (...args: T): Promise<R> => {
140
+ const user = await requireAuth()
141
+ return action(user, ...args)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Server-side utility to check specific roles/permissions
147
+ * Extend this based on your user data structure
148
+ */
149
+ export async function hasRole(role: string): Promise<boolean> {
150
+ const user = await getCurrentUser()
151
+
152
+ if (!user) return false
153
+
154
+ // Assuming role is stored in user.role or user.roles
155
+ if (Array.isArray(user.roles)) {
156
+ return user.roles.includes(role)
157
+ }
158
+
159
+ return user.role === role
160
+ }
161
+
162
+ /**
163
+ * Server-side utility to require specific roles
164
+ */
165
+ export async function requireRole(role: string): Promise<User> {
166
+ const user = await requireAuth()
167
+
168
+ const hasRequiredRole = Array.isArray(user.roles)
169
+ ? user.roles.includes(role)
170
+ : user.role === role
171
+
172
+ if (!hasRequiredRole) {
173
+ redirect('/unauthorized')
174
+ }
175
+
176
+ return user
177
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Optional Environment Configuration with Zod validation
3
+ *
4
+ * This provides type-safe environment variable validation.
5
+ * Usage is optional - existing templates will continue to work without this.
6
+ *
7
+ * To use: import { env } from '@/lib/env' instead of process.env
8
+ */
9
+
10
+ import { z } from 'zod'
11
+
12
+ const envSchema = z.object({
13
+ // Next.js built-in
14
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
15
+
16
+ // App configuration
17
+ NEXT_PUBLIC_APP_URL: z.string().url().optional(),
18
+ NEXT_PUBLIC_APP_NAME: z.string().default('{{titleCaseName}}'),
19
+
20
+ // Auth configuration (if using server-side auth)
21
+ SUPABASE_URL: z.string().url().optional(),
22
+ SUPABASE_ANON_KEY: z.string().optional(),
23
+ SUPABASE_SERVICE_ROLE_KEY: z.string().optional(),
24
+
25
+ // Payment configuration
26
+ STRIPE_SECRET_KEY: z.string().optional(),
27
+ STRIPE_WEBHOOK_SECRET: z.string().optional(),
28
+
29
+ // Optional analytics
30
+ ANALYTICS_ID: z.string().optional(),
31
+ })
32
+
33
+ // Parse with fallback to process.env for backward compatibility
34
+ function parseEnv() {
35
+ try {
36
+ return envSchema.parse(process.env)
37
+ } catch (error) {
38
+ if (process.env.NODE_ENV === 'development') {
39
+ console.warn('Environment validation failed. Using process.env fallback:', error)
40
+ }
41
+ // Fallback to process.env for backward compatibility
42
+ return process.env as any
43
+ }
44
+ }
45
+
46
+ export const env = parseEnv()
47
+
48
+ // Type helper for environment variables
49
+ export type Env = z.infer<typeof envSchema>
@@ -1,7 +1,140 @@
1
+ /**
2
+ * Enhanced utility functions for {{titleCaseName}}
3
+ *
4
+ * This extends the basic utils with additional functionality while
5
+ * maintaining compatibility with existing templates.
6
+ */
7
+
1
8
  import { type ClassValue, clsx } from "clsx"
2
9
  import { twMerge } from "tailwind-merge"
3
10
 
11
+ // Core utility function (keeps existing functionality)
4
12
  export function cn(...inputs: ClassValue[]) {
5
13
  return twMerge(clsx(inputs))
6
14
  }
7
15
 
16
+ // Additional utilities for enhanced templates
17
+
18
+ /**
19
+ * Format currency with proper locale support
20
+ */
21
+ export function formatCurrency(
22
+ amount: number,
23
+ currency: string = 'USD',
24
+ locale: string = 'en-US'
25
+ ): string {
26
+ return new Intl.NumberFormat(locale, {
27
+ style: 'currency',
28
+ currency,
29
+ }).format(amount)
30
+ }
31
+
32
+ /**
33
+ * Format date with relative time support
34
+ */
35
+ export function formatDate(
36
+ date: Date | string,
37
+ options?: Intl.DateTimeFormatOptions
38
+ ): string {
39
+ const dateObj = typeof date === 'string' ? new Date(date) : date
40
+
41
+ const defaultOptions: Intl.DateTimeFormatOptions = {
42
+ year: 'numeric',
43
+ month: 'short',
44
+ day: 'numeric',
45
+ }
46
+
47
+ return new Intl.DateTimeFormat('en-US', options || defaultOptions).format(dateObj)
48
+ }
49
+
50
+ /**
51
+ * Get relative time string (e.g., "2 hours ago")
52
+ */
53
+ export function getRelativeTime(date: Date | string): string {
54
+ const dateObj = typeof date === 'string' ? new Date(date) : date
55
+ const now = new Date()
56
+ const diffInMs = now.getTime() - dateObj.getTime()
57
+ const diffInSeconds = Math.floor(diffInMs / 1000)
58
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
59
+ const diffInHours = Math.floor(diffInMinutes / 60)
60
+ const diffInDays = Math.floor(diffInHours / 24)
61
+
62
+ if (diffInSeconds < 60) return 'just now'
63
+ if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes > 1 ? 's' : ''} ago`
64
+ if (diffInHours < 24) return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`
65
+ if (diffInDays < 7) return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`
66
+
67
+ return formatDate(dateObj)
68
+ }
69
+
70
+ /**
71
+ * Truncate text to specified length with ellipsis
72
+ */
73
+ export function truncate(text: string, length: number = 50): string {
74
+ if (text.length <= length) return text
75
+ return text.slice(0, length).trim() + '...'
76
+ }
77
+
78
+ /**
79
+ * Generate initials from a name
80
+ */
81
+ export function getInitials(name: string): string {
82
+ return name
83
+ .split(' ')
84
+ .map(word => word.charAt(0).toUpperCase())
85
+ .slice(0, 2)
86
+ .join('')
87
+ }
88
+
89
+ /**
90
+ * Validate email format
91
+ */
92
+ export function isValidEmail(email: string): boolean {
93
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
94
+ return emailRegex.test(email)
95
+ }
96
+
97
+ /**
98
+ * Generate a random ID
99
+ */
100
+ export function generateId(prefix: string = ''): string {
101
+ const timestamp = Date.now().toString(36)
102
+ const randomString = Math.random().toString(36).substring(2)
103
+ return prefix ? `${prefix}-${timestamp}-${randomString}` : `${timestamp}-${randomString}`
104
+ }
105
+
106
+ /**
107
+ * Debounce function
108
+ */
109
+ export function debounce<T extends (...args: any[]) => any>(
110
+ func: T,
111
+ delay: number
112
+ ): (...args: Parameters<T>) => void {
113
+ let timeoutId: NodeJS.Timeout | null = null
114
+
115
+ return (...args: Parameters<T>) => {
116
+ if (timeoutId !== null) {
117
+ clearTimeout(timeoutId)
118
+ }
119
+ timeoutId = setTimeout(() => func(...args), delay)
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Sleep/delay function for async operations
125
+ */
126
+ export function sleep(ms: number): Promise<void> {
127
+ return new Promise(resolve => setTimeout(resolve, ms))
128
+ }
129
+
130
+ /**
131
+ * Safe JSON parse with fallback
132
+ */
133
+ export function safeJsonParse<T>(json: string, fallback: T): T {
134
+ try {
135
+ return JSON.parse(json)
136
+ } catch {
137
+ return fallback
138
+ }
139
+ }
140
+