@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.
- package/CHANGELOG.md +42 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +45 -3
- package/dist/index.js.map +1 -1
- package/dist/templates/web/base/template/package.json +20 -10
- package/dist/templates/web/base/template/src/app/error.tsx +97 -0
- package/dist/templates/web/base/template/src/app/layout.tsx +8 -2
- package/dist/templates/web/base/template/src/app/loading.tsx +34 -0
- package/dist/templates/web/base/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/base/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/base/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/base/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/base/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/base/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/base/template/src/lib/auth-server.ts +177 -0
- package/dist/templates/web/base/template/src/lib/env.ts +46 -0
- package/dist/templates/web/base/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/base/template/src/test/setup.ts +79 -0
- package/dist/templates/web/base/template/vitest.config.ts +17 -0
- package/dist/templates/web/ui-auth/template/package.json +14 -4
- package/dist/templates/web/ui-auth/template/src/app/error.tsx +67 -0
- package/dist/templates/web/ui-auth/template/src/app/layout.tsx +6 -2
- package/dist/templates/web/ui-auth/template/src/app/loading.tsx +20 -0
- package/dist/templates/web/ui-auth/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/ui-auth/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/ui-auth/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/ui-auth/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/ui-auth/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/ui-auth/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/ui-auth/template/src/lib/env.ts +49 -0
- package/dist/templates/web/ui-auth/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/ui-auth/template/src/test/setup.ts +79 -0
- package/dist/templates/web/ui-auth/template/vitest.config.ts +17 -0
- package/dist/templates/web/ui-auth-payments/template/middleware.ts +68 -0
- package/dist/templates/web/ui-auth-payments/template/package.json +14 -4
- package/dist/templates/web/ui-auth-payments/template/src/app/dashboard/layout.tsx +22 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/dashboard/page.tsx +183 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/error.tsx +67 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/layout.tsx +6 -2
- package/dist/templates/web/ui-auth-payments/template/src/app/loading.tsx +20 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/login/loading.tsx +38 -0
- package/dist/templates/web/ui-auth-payments/template/src/app/signup/loading.tsx +50 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/client/auth-status.tsx +52 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/client/login-form.tsx +144 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/client/newsletter-signup.tsx +68 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/client/signup-form.tsx +185 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/ui-auth-payments/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/ui-auth-payments/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/ui-auth-payments/template/src/lib/actions/auth.ts +246 -0
- package/dist/templates/web/ui-auth-payments/template/src/lib/actions/index.ts +340 -0
- package/dist/templates/web/ui-auth-payments/template/src/lib/auth-server.ts +177 -0
- package/dist/templates/web/ui-auth-payments/template/src/lib/env.ts +49 -0
- package/dist/templates/web/ui-auth-payments/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/ui-auth-payments/template/src/test/setup.ts +79 -0
- package/dist/templates/web/ui-auth-payments/template/vitest.config.ts +17 -0
- package/dist/templates/web/ui-auth-payments-audio/template/package.json +14 -4
- package/dist/templates/web/ui-auth-payments-audio/template/src/app/error.tsx +67 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/app/layout.tsx +8 -2
- package/dist/templates/web/ui-auth-payments-audio/template/src/app/loading.tsx +20 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/lib/env.ts +49 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/test/setup.ts +79 -0
- package/dist/templates/web/ui-auth-payments-audio/template/vitest.config.ts +17 -0
- package/dist/templates/web/ui-auth-payments-video/template/package.json +14 -4
- package/dist/templates/web/ui-auth-payments-video/template/src/app/error.tsx +67 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/app/layout.tsx +6 -2
- package/dist/templates/web/ui-auth-payments-video/template/src/app/loading.tsx +20 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/ui-auth-payments-video/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/lib/env.ts +49 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/ui-auth-payments-video/template/src/test/setup.ts +79 -0
- package/dist/templates/web/ui-auth-payments-video/template/vitest.config.ts +17 -0
- package/dist/templates/web/ui-only/template/package.json +14 -4
- package/dist/templates/web/ui-only/template/src/app/error.tsx +67 -0
- package/dist/templates/web/ui-only/template/src/app/layout.tsx +6 -2
- package/dist/templates/web/ui-only/template/src/app/loading.tsx +20 -0
- package/dist/templates/web/ui-only/template/src/components/__tests__/example.test.tsx +49 -0
- package/dist/templates/web/ui-only/template/src/components/providers/app-providers.tsx +6 -2
- package/dist/templates/web/ui-only/template/src/components/providers/theme-provider.tsx +94 -0
- package/dist/templates/web/ui-only/template/src/components/shared/footer.tsx +36 -0
- package/dist/templates/web/ui-only/template/src/components/shared/header.tsx +2 -0
- package/dist/templates/web/ui-only/template/src/components/ui/theme-toggle.tsx +34 -0
- package/dist/templates/web/ui-only/template/src/lib/env.ts +49 -0
- package/dist/templates/web/ui-only/template/src/lib/utils.ts +133 -0
- package/dist/templates/web/ui-only/template/src/test/setup.ts +79 -0
- package/dist/templates/web/ui-only/template/vitest.config.ts +17 -0
- package/package.json +1 -1
- package/src/templates/web/base/template/package.json +20 -10
- package/src/templates/web/base/template/src/app/error.tsx +97 -0
- package/src/templates/web/base/template/src/app/layout.tsx +8 -2
- package/src/templates/web/base/template/src/app/loading.tsx +34 -0
- package/src/templates/web/base/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/base/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/base/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/base/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/base/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/base/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/base/template/src/lib/auth-server.ts +177 -0
- package/src/templates/web/base/template/src/lib/env.ts +46 -0
- package/src/templates/web/base/template/src/lib/utils.ts +133 -0
- package/src/templates/web/base/template/src/test/setup.ts +79 -0
- package/src/templates/web/base/template/vitest.config.ts +17 -0
- package/src/templates/web/ui-auth/template/package.json +14 -4
- package/src/templates/web/ui-auth/template/src/app/error.tsx +67 -0
- package/src/templates/web/ui-auth/template/src/app/layout.tsx +6 -2
- package/src/templates/web/ui-auth/template/src/app/loading.tsx +20 -0
- package/src/templates/web/ui-auth/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/ui-auth/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/ui-auth/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/ui-auth/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/ui-auth/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/ui-auth/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/ui-auth/template/src/lib/env.ts +49 -0
- package/src/templates/web/ui-auth/template/src/lib/utils.ts +133 -0
- package/src/templates/web/ui-auth/template/src/test/setup.ts +79 -0
- package/src/templates/web/ui-auth/template/vitest.config.ts +17 -0
- package/src/templates/web/ui-auth-payments/template/middleware.ts +68 -0
- package/src/templates/web/ui-auth-payments/template/package.json +14 -4
- package/src/templates/web/ui-auth-payments/template/src/app/dashboard/layout.tsx +22 -0
- package/src/templates/web/ui-auth-payments/template/src/app/dashboard/page.tsx +183 -0
- package/src/templates/web/ui-auth-payments/template/src/app/error.tsx +67 -0
- package/src/templates/web/ui-auth-payments/template/src/app/layout.tsx +6 -2
- package/src/templates/web/ui-auth-payments/template/src/app/loading.tsx +20 -0
- package/src/templates/web/ui-auth-payments/template/src/app/login/loading.tsx +38 -0
- package/src/templates/web/ui-auth-payments/template/src/app/signup/loading.tsx +50 -0
- package/src/templates/web/ui-auth-payments/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/ui-auth-payments/template/src/components/client/auth-status.tsx +52 -0
- package/src/templates/web/ui-auth-payments/template/src/components/client/login-form.tsx +144 -0
- package/src/templates/web/ui-auth-payments/template/src/components/client/newsletter-signup.tsx +68 -0
- package/src/templates/web/ui-auth-payments/template/src/components/client/signup-form.tsx +185 -0
- package/src/templates/web/ui-auth-payments/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/ui-auth-payments/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/ui-auth-payments/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/ui-auth-payments/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/ui-auth-payments/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/ui-auth-payments/template/src/lib/actions/auth.ts +246 -0
- package/src/templates/web/ui-auth-payments/template/src/lib/actions/index.ts +340 -0
- package/src/templates/web/ui-auth-payments/template/src/lib/auth-server.ts +177 -0
- package/src/templates/web/ui-auth-payments/template/src/lib/env.ts +49 -0
- package/src/templates/web/ui-auth-payments/template/src/lib/utils.ts +133 -0
- package/src/templates/web/ui-auth-payments/template/src/test/setup.ts +79 -0
- package/src/templates/web/ui-auth-payments/template/vitest.config.ts +17 -0
- package/src/templates/web/ui-auth-payments-audio/template/package.json +14 -4
- package/src/templates/web/ui-auth-payments-audio/template/src/app/error.tsx +67 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/app/layout.tsx +8 -2
- package/src/templates/web/ui-auth-payments-audio/template/src/app/loading.tsx +20 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/ui-auth-payments-audio/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/lib/env.ts +49 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/lib/utils.ts +133 -0
- package/src/templates/web/ui-auth-payments-audio/template/src/test/setup.ts +79 -0
- package/src/templates/web/ui-auth-payments-audio/template/vitest.config.ts +17 -0
- package/src/templates/web/ui-auth-payments-video/template/package.json +14 -4
- package/src/templates/web/ui-auth-payments-video/template/src/app/error.tsx +67 -0
- package/src/templates/web/ui-auth-payments-video/template/src/app/layout.tsx +6 -2
- package/src/templates/web/ui-auth-payments-video/template/src/app/loading.tsx +20 -0
- package/src/templates/web/ui-auth-payments-video/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/ui-auth-payments-video/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/ui-auth-payments-video/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/ui-auth-payments-video/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/ui-auth-payments-video/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/ui-auth-payments-video/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/ui-auth-payments-video/template/src/lib/env.ts +49 -0
- package/src/templates/web/ui-auth-payments-video/template/src/lib/utils.ts +133 -0
- package/src/templates/web/ui-auth-payments-video/template/src/test/setup.ts +79 -0
- package/src/templates/web/ui-auth-payments-video/template/vitest.config.ts +17 -0
- package/src/templates/web/ui-only/template/package.json +14 -4
- package/src/templates/web/ui-only/template/src/app/error.tsx +67 -0
- package/src/templates/web/ui-only/template/src/app/layout.tsx +6 -2
- package/src/templates/web/ui-only/template/src/app/loading.tsx +20 -0
- package/src/templates/web/ui-only/template/src/components/__tests__/example.test.tsx +49 -0
- package/src/templates/web/ui-only/template/src/components/providers/app-providers.tsx +6 -2
- package/src/templates/web/ui-only/template/src/components/providers/theme-provider.tsx +94 -0
- package/src/templates/web/ui-only/template/src/components/shared/footer.tsx +36 -0
- package/src/templates/web/ui-only/template/src/components/shared/header.tsx +2 -0
- package/src/templates/web/ui-only/template/src/components/ui/theme-toggle.tsx +34 -0
- package/src/templates/web/ui-only/template/src/lib/env.ts +49 -0
- package/src/templates/web/ui-only/template/src/lib/utils.ts +133 -0
- package/src/templates/web/ui-only/template/src/test/setup.ts +79 -0
- 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
|
+
|