create-nexora-next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/package.json +35 -0
- package/src/index.js +267 -0
- package/src/steps/01-create-next-app.js +19 -0
- package/src/steps/02-shadcn.js +39 -0
- package/src/steps/03-base-files.js +110 -0
- package/src/steps/04-providers.js +51 -0
- package/src/steps/05-i18n.js +42 -0
- package/src/steps/06-auth.js +31 -0
- package/src/steps/07-proxy.js +17 -0
- package/src/steps/08-install-deps.js +83 -0
- package/src/steps/09-husky.js +59 -0
- package/src/steps/10-axios.js +15 -0
- package/src/steps/11-patch-pkg.js +22 -0
- package/src/templates/files.js +908 -0
- package/src/utils/runner.js +50 -0
- package/src/utils/safe-step.js +43 -0
- package/src/utils/writer.js +38 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
// ─── globals.css append ───────────────────────────────────────────────────────
|
|
2
|
+
export const globalsCssAppend = `
|
|
3
|
+
@utility container {
|
|
4
|
+
padding-inline: 1.25rem;
|
|
5
|
+
margin-inline: auto;
|
|
6
|
+
@variant sm {
|
|
7
|
+
padding-inline: 2rem;
|
|
8
|
+
}
|
|
9
|
+
@variant lg {
|
|
10
|
+
padding-inline: 2rem;
|
|
11
|
+
max-width: 1024px;
|
|
12
|
+
}
|
|
13
|
+
@variant xl {
|
|
14
|
+
padding-inline: 2.5rem;
|
|
15
|
+
max-width: 1280px;
|
|
16
|
+
}
|
|
17
|
+
@variant 2xl {
|
|
18
|
+
padding-inline: 3rem;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
@layer theme {
|
|
22
|
+
@theme {
|
|
23
|
+
--breakpoint-dash-lg: 1010px;
|
|
24
|
+
--breakpoint-xs: 480px;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes skeleton {
|
|
29
|
+
0% { background-position: 200% 0; }
|
|
30
|
+
100% { background-position: -200% 0; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@utility corner-squircle { @apply corner-squircle-md; }
|
|
34
|
+
@utility corner-squircle-xs {
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
@supports (corner-shape: squircle) { border-radius: 8px !important; corner-shape: squircle; }
|
|
37
|
+
}
|
|
38
|
+
@utility corner-squircle-sm {
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
@supports (corner-shape: squircle) { border-radius: 12px !important; corner-shape: squircle; }
|
|
41
|
+
}
|
|
42
|
+
@utility corner-squircle-md {
|
|
43
|
+
border-radius: 12px;
|
|
44
|
+
@supports (corner-shape: squircle) { border-radius: 16px !important; corner-shape: squircle; }
|
|
45
|
+
}
|
|
46
|
+
@utility corner-squircle-lg {
|
|
47
|
+
border-radius: 16px;
|
|
48
|
+
@supports (corner-shape: squircle) { border-radius: 24px !important; corner-shape: squircle; }
|
|
49
|
+
}
|
|
50
|
+
@utility corner-squircle-xl {
|
|
51
|
+
border-radius: 24px;
|
|
52
|
+
@supports (corner-shape: squircle) { border-radius: 32px !important; corner-shape: squircle; }
|
|
53
|
+
}
|
|
54
|
+
@utility corner-squircle-2xl {
|
|
55
|
+
border-radius: 32px;
|
|
56
|
+
@supports (corner-shape: squircle) { border-radius: 48px !important; corner-shape: squircle; }
|
|
57
|
+
}
|
|
58
|
+
@utility corner-squircle-3xl {
|
|
59
|
+
border-radius: 48px;
|
|
60
|
+
@supports (corner-shape: squircle) { border-radius: 64px !important; corner-shape: squircle; }
|
|
61
|
+
}
|
|
62
|
+
@utility corner-squircle-l-lg {
|
|
63
|
+
border-top-left-radius: 16px; border-bottom-left-radius: 16px;
|
|
64
|
+
@supports (corner-shape: squircle) { border-top-left-radius: 24px !important; border-bottom-left-radius: 24px !important; corner-shape: squircle; }
|
|
65
|
+
}
|
|
66
|
+
@utility corner-squircle-t-lg {
|
|
67
|
+
border-top-left-radius: 16px; border-top-right-radius: 16px;
|
|
68
|
+
@supports (corner-shape: squircle) { border-top-left-radius: 24px !important; border-top-right-radius: 24px !important; corner-shape: squircle; }
|
|
69
|
+
}
|
|
70
|
+
@utility corner-squircle-tl-2xl {
|
|
71
|
+
border-top-left-radius: 32px;
|
|
72
|
+
@supports (corner-shape: squircle) { border-top-left-radius: 48px !important; corner-shape: squircle; }
|
|
73
|
+
}
|
|
74
|
+
@utility corner-squircle-r-lg {
|
|
75
|
+
border-top-right-radius: 16px; border-bottom-right-radius: 16px;
|
|
76
|
+
@supports (corner-shape: squircle) { border-top-right-radius: 24px !important; border-bottom-right-radius: 24px !important; corner-shape: squircle; }
|
|
77
|
+
}
|
|
78
|
+
@utility corner-squircle-full {
|
|
79
|
+
border-radius: 9999px !important;
|
|
80
|
+
@supports (corner-shape: squircle) { corner-shape: squircle; }
|
|
81
|
+
}
|
|
82
|
+
@utility corner-squircle-none {
|
|
83
|
+
border-radius: initial; corner-shape: initial;
|
|
84
|
+
@supports (corner-shape: squircle) { border-radius: initial; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (prefers-reduced-motion: reduce) {
|
|
88
|
+
*, ::before, ::after {
|
|
89
|
+
animation-delay: -1ms !important;
|
|
90
|
+
animation-duration: 1ms !important;
|
|
91
|
+
animation-iteration-count: 1 !important;
|
|
92
|
+
background-attachment: initial !important;
|
|
93
|
+
scroll-behavior: auto !important;
|
|
94
|
+
transition-duration: 1ms !important;
|
|
95
|
+
transition-delay: -1ms !important;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
// ─── .env.local ───────────────────────────────────────────────────────────────
|
|
101
|
+
export const envLocal = `NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
|
102
|
+
`
|
|
103
|
+
|
|
104
|
+
// ─── .prettierrc ──────────────────────────────────────────────────────────────
|
|
105
|
+
export const prettierRc = `{
|
|
106
|
+
"plugins": ["prettier-plugin-tailwindcss"],
|
|
107
|
+
"semi": false,
|
|
108
|
+
"singleQuote": true,
|
|
109
|
+
"bracketSameLine": false,
|
|
110
|
+
"bracketSpacing": true
|
|
111
|
+
}
|
|
112
|
+
`
|
|
113
|
+
|
|
114
|
+
// ─── src/lib/fonts.ts ─────────────────────────────────────────────────────────
|
|
115
|
+
export const fontsTs = `import { Poppins } from 'next/font/google'
|
|
116
|
+
|
|
117
|
+
export const PoppinsFont = Poppins({
|
|
118
|
+
subsets: ['latin'],
|
|
119
|
+
display: 'swap',
|
|
120
|
+
variable: '--font-poppins',
|
|
121
|
+
weight: ['200', '300', '400', '500', '600', '700', '800'],
|
|
122
|
+
})
|
|
123
|
+
`
|
|
124
|
+
|
|
125
|
+
// ─── src/lib/utils/index.ts (no axios) ───────────────────────────────────────
|
|
126
|
+
export const utilsTs = `import { clsx, type ClassValue } from 'clsx'
|
|
127
|
+
import { twMerge } from 'tailwind-merge'
|
|
128
|
+
|
|
129
|
+
export function cn(...inputs: ClassValue[]) {
|
|
130
|
+
return twMerge(clsx(inputs))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function toSlug(str?: string | null) {
|
|
134
|
+
if (!str) return ''
|
|
135
|
+
return str
|
|
136
|
+
.toString()
|
|
137
|
+
.trim()
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/\\s+/g, '-')
|
|
140
|
+
.replace(/[^\\w-]+/g, '')
|
|
141
|
+
.replace(/--+/g, '-')
|
|
142
|
+
.replace(/^-+|-+$/g, '')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const createInitials = (input?: string | null, count = 2): string => {
|
|
146
|
+
if (!input) return ''
|
|
147
|
+
try {
|
|
148
|
+
const words = input.split(/(?=[A-Z])|[\\W_]+/).filter(Boolean)
|
|
149
|
+
const abbreviation = words
|
|
150
|
+
.map((word) => word[0].toUpperCase())
|
|
151
|
+
.join('')
|
|
152
|
+
.slice(0, Math.min(count, 3))
|
|
153
|
+
return abbreviation.length < 2 && input.length > 1
|
|
154
|
+
? abbreviation + input[1].toUpperCase()
|
|
155
|
+
: abbreviation
|
|
156
|
+
} catch (_) {
|
|
157
|
+
return input.toUpperCase()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function normalizeEmail(email: string) {
|
|
162
|
+
const trimmed = email.trim().toLowerCase()
|
|
163
|
+
if (trimmed.endsWith('@gmail.com')) {
|
|
164
|
+
const [local, domain] = trimmed.split('@')
|
|
165
|
+
return \`\${local.replace(/\\./g, '')}@\${domain}\`
|
|
166
|
+
}
|
|
167
|
+
return trimmed
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function sanitizeName(name: string) {
|
|
171
|
+
return name.trim().replace(/<[^>]*>/g, '').replace(/\\0/g, '').substring(0, 255)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const trycatch = async <T, E = Error>(
|
|
175
|
+
promise: Promise<T>,
|
|
176
|
+
): Promise<[T, null] | [null, E]> => {
|
|
177
|
+
try {
|
|
178
|
+
return [await promise, null]
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return [null, err as E]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
`
|
|
184
|
+
|
|
185
|
+
// ─── src/lib/utils/index.ts (with axios) ─────────────────────────────────────
|
|
186
|
+
export const utilsWithAxiosTs = `import { AxiosError } from 'axios'
|
|
187
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
188
|
+
import { twMerge } from 'tailwind-merge'
|
|
189
|
+
|
|
190
|
+
export function cn(...inputs: ClassValue[]) {
|
|
191
|
+
return twMerge(clsx(inputs))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function toSlug(str?: string | null) {
|
|
195
|
+
if (!str) return ''
|
|
196
|
+
return str
|
|
197
|
+
.toString()
|
|
198
|
+
.trim()
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/\\s+/g, '-')
|
|
201
|
+
.replace(/[^\\w-]+/g, '')
|
|
202
|
+
.replace(/--+/g, '-')
|
|
203
|
+
.replace(/^-+|-+$/g, '')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const createInitials = (input?: string | null, count = 2): string => {
|
|
207
|
+
if (!input) return ''
|
|
208
|
+
try {
|
|
209
|
+
const words = input.split(/(?=[A-Z])|[\\W_]+/).filter(Boolean)
|
|
210
|
+
const abbreviation = words
|
|
211
|
+
.map((word) => word[0].toUpperCase())
|
|
212
|
+
.join('')
|
|
213
|
+
.slice(0, Math.min(count, 3))
|
|
214
|
+
return abbreviation.length < 2 && input.length > 1
|
|
215
|
+
? abbreviation + input[1].toUpperCase()
|
|
216
|
+
: abbreviation
|
|
217
|
+
} catch (_) {
|
|
218
|
+
return input.toUpperCase()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizeEmail(email: string) {
|
|
223
|
+
const trimmed = email.trim().toLowerCase()
|
|
224
|
+
if (trimmed.endsWith('@gmail.com')) {
|
|
225
|
+
const [local, domain] = trimmed.split('@')
|
|
226
|
+
return \`\${local.replace(/\\./g, '')}@\${domain}\`
|
|
227
|
+
}
|
|
228
|
+
return trimmed
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function sanitizeName(name: string) {
|
|
232
|
+
return name.trim().replace(/<[^>]*>/g, '').replace(/\\0/g, '').substring(0, 255)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const trycatch = async <T, E = AxiosError>(
|
|
236
|
+
promise: Promise<T>,
|
|
237
|
+
): Promise<[T, null] | [null, E]> => {
|
|
238
|
+
try {
|
|
239
|
+
return [await promise, null]
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return [null, err as E]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
`
|
|
245
|
+
|
|
246
|
+
// ─── src/constants/index.ts ───────────────────────────────────────────────────
|
|
247
|
+
export const constantsTs = (opts) => {
|
|
248
|
+
const lines = [
|
|
249
|
+
`export const TZ_COOKIE = 'app_tz'`,
|
|
250
|
+
`export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'`,
|
|
251
|
+
]
|
|
252
|
+
if (opts.i18n) {
|
|
253
|
+
lines.push(`export const LOCALE_COOKIE = 'app_locale'`)
|
|
254
|
+
lines.push(`export const APP_LOCALES = ['en'] as const`)
|
|
255
|
+
lines.push(`export type AppLocale = (typeof APP_LOCALES)[number]`)
|
|
256
|
+
}
|
|
257
|
+
return lines.join('\n') + '\n'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── src/lib/seo/index.ts ────────────────────────────────────────────────────
|
|
261
|
+
export const seoWithLocaleTs = `import { Metadata } from 'next'
|
|
262
|
+
import type { AppLocale } from '~/constants'
|
|
263
|
+
|
|
264
|
+
export type AppMetadata = Record<AppLocale, Metadata>
|
|
265
|
+
|
|
266
|
+
export default {
|
|
267
|
+
en: { title: 'App' },
|
|
268
|
+
} satisfies AppMetadata
|
|
269
|
+
`
|
|
270
|
+
|
|
271
|
+
export const seoTs = `import { Metadata } from 'next'
|
|
272
|
+
|
|
273
|
+
export default {
|
|
274
|
+
title: 'App',
|
|
275
|
+
} satisfies Metadata
|
|
276
|
+
`
|
|
277
|
+
|
|
278
|
+
// ─── src/lib/fonts.ts ─────────────────────────────────────────────────────────
|
|
279
|
+
// (already defined above as fontsTs)
|
|
280
|
+
|
|
281
|
+
// ─── src/components/ui/sileo.tsx ─────────────────────────────────────────────
|
|
282
|
+
export const sileoUiTsx = `import { Toaster as Sileo } from 'sileo'
|
|
283
|
+
|
|
284
|
+
const SileoToaster = () => {
|
|
285
|
+
return (
|
|
286
|
+
<div className="relative z-100!">
|
|
287
|
+
<Sileo
|
|
288
|
+
position="top-center"
|
|
289
|
+
options={{
|
|
290
|
+
duration: 7000,
|
|
291
|
+
fill: 'var(--foreground)',
|
|
292
|
+
styles: { description: 'text-background/80!' },
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export { SileoToaster }
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
// ─── src/components/providers/sileo.provider.tsx ─────────────────────────────
|
|
303
|
+
export const sileoProviderTsx = `// Sileo root provider — wrap app-level providers here as needed
|
|
304
|
+
import type { PropsWithChildren } from 'react'
|
|
305
|
+
|
|
306
|
+
export default ({ children }: PropsWithChildren) => {
|
|
307
|
+
return <>{children}</>
|
|
308
|
+
}
|
|
309
|
+
`
|
|
310
|
+
|
|
311
|
+
// ─── src/components/providers/index.tsx (dynamic) ─────────────────────────────
|
|
312
|
+
export const providersIndexTsx = (opts) => {
|
|
313
|
+
const imports = [
|
|
314
|
+
`import NextTopLoader from 'nextjs-toploader'`,
|
|
315
|
+
`import type { PropsWithChildren } from 'react'`,
|
|
316
|
+
`import NuqsAdapter from '../adapters/nuqs.adapter'`,
|
|
317
|
+
`import { SileoToaster } from '../ui/sileo'`,
|
|
318
|
+
]
|
|
319
|
+
if (opts.i18n) imports.push(`import LocaleProvider from './locale.provider'`)
|
|
320
|
+
if (opts.theming) imports.push(`import ThemeProvider from './theme.provider'`)
|
|
321
|
+
if (opts.query) imports.push(`import QueryClientProvider from './query-client.provider'`)
|
|
322
|
+
|
|
323
|
+
const open = []
|
|
324
|
+
const close = []
|
|
325
|
+
|
|
326
|
+
if (opts.i18n) { open.push(` <LocaleProvider>`); close.unshift(` </LocaleProvider>`) }
|
|
327
|
+
if (opts.theming) { open.push(` <ThemeProvider>`); close.unshift(` </ThemeProvider>`) }
|
|
328
|
+
if (opts.query) { open.push(` <QueryClientProvider>`); close.unshift(` </QueryClientProvider>`) }
|
|
329
|
+
|
|
330
|
+
const indent = ' '
|
|
331
|
+
const inner = [
|
|
332
|
+
`${indent}<SileoToaster />`,
|
|
333
|
+
`${indent}<NextTopLoader height={2.5} color="var(--primary-text)" showSpinner={false} />`,
|
|
334
|
+
`${indent}<NuqsAdapter>{children}</NuqsAdapter>`,
|
|
335
|
+
].join('\n')
|
|
336
|
+
|
|
337
|
+
return `${imports.join('\n')}
|
|
338
|
+
|
|
339
|
+
export default ({ children }: PropsWithChildren) => {
|
|
340
|
+
return (
|
|
341
|
+
${open.join('\n')}
|
|
342
|
+
${inner}
|
|
343
|
+
${close.join('\n')}
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
`
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── src/components/providers/theme.provider.tsx ─────────────────────────────
|
|
350
|
+
export const themeProviderTsx = `import { ThemeProvider } from 'next-themes'
|
|
351
|
+
import type { PropsWithChildren } from 'react'
|
|
352
|
+
|
|
353
|
+
export default ({ children }: PropsWithChildren) => (
|
|
354
|
+
<ThemeProvider
|
|
355
|
+
defaultTheme="system"
|
|
356
|
+
attribute={['class']}
|
|
357
|
+
disableTransitionOnChange
|
|
358
|
+
enableSystem
|
|
359
|
+
>
|
|
360
|
+
{children}
|
|
361
|
+
</ThemeProvider>
|
|
362
|
+
)
|
|
363
|
+
`
|
|
364
|
+
|
|
365
|
+
// ─── src/components/providers/locale.provider.tsx ────────────────────────────
|
|
366
|
+
export const localeProviderTsx = `import { NextIntlClientProvider } from 'next-intl'
|
|
367
|
+
import { Suspense, type PropsWithChildren } from 'react'
|
|
368
|
+
import TzProvider from './tz.provider'
|
|
369
|
+
|
|
370
|
+
export default ({ children }: PropsWithChildren) => {
|
|
371
|
+
return (
|
|
372
|
+
<Suspense fallback={null}>
|
|
373
|
+
<TzProvider />
|
|
374
|
+
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
|
375
|
+
</Suspense>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
`
|
|
379
|
+
|
|
380
|
+
// ─── src/components/providers/tz.provider.tsx ────────────────────────────────
|
|
381
|
+
export const tzProviderTsx = `'use client'
|
|
382
|
+
|
|
383
|
+
import { getCookie, setCookie } from 'cookies-next/client'
|
|
384
|
+
import { useEffect } from 'react'
|
|
385
|
+
import { TZ_COOKIE } from '~/constants'
|
|
386
|
+
|
|
387
|
+
export default () => {
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const existing = getCookie(TZ_COOKIE)
|
|
390
|
+
const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
391
|
+
if (!existing || existing !== deviceTimezone) {
|
|
392
|
+
setCookie(TZ_COOKIE, deviceTimezone, {
|
|
393
|
+
path: '/',
|
|
394
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
395
|
+
sameSite: 'lax',
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
}, [])
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
`
|
|
402
|
+
|
|
403
|
+
// ─── src/components/providers/query-client.provider.tsx ──────────────────────
|
|
404
|
+
export const queryClientProviderTsx = `'use client'
|
|
405
|
+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
|
|
406
|
+
import {
|
|
407
|
+
DefaultOptions,
|
|
408
|
+
MutationCache,
|
|
409
|
+
QueryClient,
|
|
410
|
+
QueryClientProvider,
|
|
411
|
+
QueryKey,
|
|
412
|
+
} from '@tanstack/react-query'
|
|
413
|
+
import {
|
|
414
|
+
PersistQueryClientProvider,
|
|
415
|
+
removeOldestQuery,
|
|
416
|
+
} from '@tanstack/react-query-persist-client'
|
|
417
|
+
import { PropsWithChildren, useState } from 'react'
|
|
418
|
+
|
|
419
|
+
const persister = createAsyncStoragePersister({
|
|
420
|
+
storage: typeof window !== 'undefined' ? localStorage : null,
|
|
421
|
+
key: 'rayan_app_persister_store',
|
|
422
|
+
retry: removeOldestQuery,
|
|
423
|
+
throttleTime: 3000,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const _5minutes = 5 * 60 * 1000
|
|
427
|
+
const _24hours = 1000 * 60 * 60 * 24
|
|
428
|
+
|
|
429
|
+
export const defaultOptions = {
|
|
430
|
+
queries: {
|
|
431
|
+
retry: false,
|
|
432
|
+
staleTime: _5minutes,
|
|
433
|
+
gcTime: _5minutes,
|
|
434
|
+
refetchOnWindowFocus: false,
|
|
435
|
+
refetchOnMount: 'always',
|
|
436
|
+
refetchOnReconnect: true,
|
|
437
|
+
refetchInterval: false,
|
|
438
|
+
},
|
|
439
|
+
} satisfies DefaultOptions
|
|
440
|
+
|
|
441
|
+
export default ({ children }: PropsWithChildren) => {
|
|
442
|
+
const mutationCache = new MutationCache({
|
|
443
|
+
async onSettled(_data, _error, _variables, _context, mutation) {
|
|
444
|
+
const meta = mutation.options.meta
|
|
445
|
+
const invalidateQueries = meta?.invalidateQueries as QueryKey | QueryKey[]
|
|
446
|
+
if (!invalidateQueries) return
|
|
447
|
+
const queryKeys = Array.isArray(invalidateQueries[0])
|
|
448
|
+
? (invalidateQueries as QueryKey[])
|
|
449
|
+
: [invalidateQueries]
|
|
450
|
+
await Promise.all(
|
|
451
|
+
queryKeys.map((queryKey) => client.invalidateQueries({ queryKey })),
|
|
452
|
+
)
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const [client] = useState(new QueryClient({ defaultOptions, mutationCache }))
|
|
457
|
+
const dehydrationKeys: string[] = []
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<QueryClientProvider client={client}>
|
|
461
|
+
<PersistQueryClientProvider
|
|
462
|
+
client={client}
|
|
463
|
+
persistOptions={{
|
|
464
|
+
persister,
|
|
465
|
+
maxAge: _24hours,
|
|
466
|
+
dehydrateOptions: {
|
|
467
|
+
shouldDehydrateQuery: (query) => {
|
|
468
|
+
const isSuccess =
|
|
469
|
+
query.state?.status === 'success' &&
|
|
470
|
+
typeof query.state?.data !== 'undefined'
|
|
471
|
+
return isSuccess && !dehydrationKeys.includes(query.queryKey[0] as string)
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
}}
|
|
475
|
+
>
|
|
476
|
+
{children}
|
|
477
|
+
</PersistQueryClientProvider>
|
|
478
|
+
</QueryClientProvider>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
`
|
|
482
|
+
|
|
483
|
+
// ─── src/components/adapters/nuqs.adapter.tsx ────────────────────────────────
|
|
484
|
+
export const nuqsAdapterTsx = `import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
|
485
|
+
import type { PropsWithChildren } from 'react'
|
|
486
|
+
|
|
487
|
+
export default ({ children }: PropsWithChildren) => {
|
|
488
|
+
return <NuqsAdapter>{children}</NuqsAdapter>
|
|
489
|
+
}
|
|
490
|
+
`
|
|
491
|
+
|
|
492
|
+
// ─── src/i18n/routing.ts ─────────────────────────────────────────────────────
|
|
493
|
+
export const i18nRoutingTs = `import { defineRouting } from 'next-intl/routing'
|
|
494
|
+
import { APP_LOCALES, LOCALE_COOKIE } from '../constants'
|
|
495
|
+
|
|
496
|
+
export const routing = defineRouting({
|
|
497
|
+
locales: APP_LOCALES,
|
|
498
|
+
localeCookie: { name: LOCALE_COOKIE, maxAge: 60 * 60 * 24 * 30 },
|
|
499
|
+
defaultLocale: 'en',
|
|
500
|
+
})
|
|
501
|
+
`
|
|
502
|
+
|
|
503
|
+
// ─── src/i18n/request.ts ─────────────────────────────────────────────────────
|
|
504
|
+
export const i18nRequestTs = `import { hasLocale } from 'next-intl'
|
|
505
|
+
import { getRequestConfig } from 'next-intl/server'
|
|
506
|
+
import { cookies } from 'next/headers'
|
|
507
|
+
import { localeFormats } from '~/lib/utils/locale-date-formats'
|
|
508
|
+
import { routing } from './routing'
|
|
509
|
+
|
|
510
|
+
export default getRequestConfig(async ({ requestLocale }) => {
|
|
511
|
+
const requested = await requestLocale
|
|
512
|
+
const locale = hasLocale(routing.locales, requested)
|
|
513
|
+
? requested
|
|
514
|
+
: routing.defaultLocale
|
|
515
|
+
const cookieStore = await cookies()
|
|
516
|
+
const timeZone =
|
|
517
|
+
cookieStore.get('app_tz')?.value ??
|
|
518
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
locale,
|
|
522
|
+
timeZone,
|
|
523
|
+
formats: localeFormats,
|
|
524
|
+
messages: (await import(\`../../locales/\${locale}.json\`)).default,
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
`
|
|
528
|
+
|
|
529
|
+
// ─── src/i18n/navigation.ts ──────────────────────────────────────────────────
|
|
530
|
+
export const i18nNavigationTs = `import { createNavigation } from 'next-intl/navigation'
|
|
531
|
+
import { routing } from './routing'
|
|
532
|
+
|
|
533
|
+
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
534
|
+
createNavigation(routing)
|
|
535
|
+
`
|
|
536
|
+
|
|
537
|
+
// ─── src/proxy.ts ─────────────────────────────────────────────────────────────
|
|
538
|
+
export const proxyTs = (opts) => {
|
|
539
|
+
const lines = []
|
|
540
|
+
if (opts.i18n) lines.push(`import createMiddleware from 'next-intl/middleware'`)
|
|
541
|
+
if (opts.i18n) lines.push(`import { routing } from './i18n/routing'`)
|
|
542
|
+
lines.push(`import { ContextualProxy, withProxyChain } from './lib/proxy'`)
|
|
543
|
+
if (opts.auth) lines.push(`import { authProxy } from './lib/proxy/auth'`)
|
|
544
|
+
|
|
545
|
+
const args = []
|
|
546
|
+
if (opts.i18n) args.push('createMiddleware(routing) as unknown as ContextualProxy')
|
|
547
|
+
if (opts.auth) args.push('authProxy')
|
|
548
|
+
|
|
549
|
+
lines.push('')
|
|
550
|
+
lines.push(`export default withProxyChain(${args.join(', ')})`)
|
|
551
|
+
lines.push('')
|
|
552
|
+
lines.push(`export const config = {`)
|
|
553
|
+
lines.push(` matcher: '/((?!api|trpc|_next|_vercel|.*\\\\..*).*)',`)
|
|
554
|
+
lines.push(`}`)
|
|
555
|
+
return lines.join('\n') + '\n'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ─── src/lib/proxy/index.ts ───────────────────────────────────────────────────
|
|
559
|
+
export const proxyLibTs = `import {
|
|
560
|
+
NextFetchEvent,
|
|
561
|
+
NextRequest,
|
|
562
|
+
NextResponse,
|
|
563
|
+
} from 'next/server'
|
|
564
|
+
|
|
565
|
+
// TODO: import UserRole from your types once defined
|
|
566
|
+
export interface ProxyContext {
|
|
567
|
+
role?: string
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export type ContextualProxy = (
|
|
571
|
+
req: NextRequest,
|
|
572
|
+
event: NextFetchEvent,
|
|
573
|
+
ctx: ProxyContext,
|
|
574
|
+
) => Promise<NextResponse | Response | undefined>
|
|
575
|
+
|
|
576
|
+
export function withProxyChain(...proxies: ContextualProxy[]) {
|
|
577
|
+
return async (req: NextRequest, event: NextFetchEvent) => {
|
|
578
|
+
const ctx: ProxyContext = {}
|
|
579
|
+
let response: NextResponse | Response = NextResponse.next()
|
|
580
|
+
|
|
581
|
+
for (const proxy of proxies) {
|
|
582
|
+
const result = await proxy(req, event, ctx)
|
|
583
|
+
if (result) {
|
|
584
|
+
response = result
|
|
585
|
+
if (
|
|
586
|
+
result instanceof NextResponse &&
|
|
587
|
+
result.status >= 300 &&
|
|
588
|
+
result.status < 400
|
|
589
|
+
) {
|
|
590
|
+
return response
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return response
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
`
|
|
598
|
+
|
|
599
|
+
// ─── src/lib/proxy/auth.ts ────────────────────────────────────────────────────
|
|
600
|
+
export const proxyAuthTs = `import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
|
|
601
|
+
import type { ContextualProxy, ProxyContext } from '.'
|
|
602
|
+
|
|
603
|
+
export const authProxy: ContextualProxy = async (
|
|
604
|
+
req: NextRequest,
|
|
605
|
+
_event: NextFetchEvent,
|
|
606
|
+
ctx: ProxyContext,
|
|
607
|
+
) => {
|
|
608
|
+
// TODO: implement auth checks — verify session/token, set ctx.role, redirect if needed
|
|
609
|
+
return NextResponse.next()
|
|
610
|
+
}
|
|
611
|
+
`
|
|
612
|
+
|
|
613
|
+
// ─── src/lib/validators/index.ts ─────────────────────────────────────────────
|
|
614
|
+
export const validatorsTs = `import * as z from 'zod'
|
|
615
|
+
import { normalizeEmail, sanitizeName } from '../utils'
|
|
616
|
+
|
|
617
|
+
const StrongPasswordRegex =
|
|
618
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z0-9]).{8,}$/
|
|
619
|
+
const OTPRegex = /^[a-zA-Z0-9]{6}$/
|
|
620
|
+
|
|
621
|
+
const StringValidator = z.string()
|
|
622
|
+
const EmailValidator = z
|
|
623
|
+
.string()
|
|
624
|
+
.email({ message: 'Invalid email' })
|
|
625
|
+
.transform((val) => normalizeEmail(val))
|
|
626
|
+
const NumberValidator = z.coerce.number()
|
|
627
|
+
const BooleanValidator = z.boolean().default(false)
|
|
628
|
+
const PasswordValidator = StringValidator.regex(StrongPasswordRegex, 'Password too weak')
|
|
629
|
+
const DateValidator = z.date()
|
|
630
|
+
const NameValidator = StringValidator.min(2, 'Name is required').transform(
|
|
631
|
+
(name) => sanitizeName(name),
|
|
632
|
+
)
|
|
633
|
+
const OTPValidator = StringValidator.regex(OTPRegex, 'Invalid OTP')
|
|
634
|
+
const IntValidator = NumberValidator.int()
|
|
635
|
+
const AgeValidator = IntValidator.min(0, 'Invalid age').max(150, 'Invalid age')
|
|
636
|
+
const FileValidator = z.instanceof(File)
|
|
637
|
+
|
|
638
|
+
export {
|
|
639
|
+
AgeValidator,
|
|
640
|
+
BooleanValidator,
|
|
641
|
+
DateValidator,
|
|
642
|
+
EmailValidator,
|
|
643
|
+
FileValidator,
|
|
644
|
+
IntValidator,
|
|
645
|
+
NameValidator,
|
|
646
|
+
NumberValidator,
|
|
647
|
+
OTPValidator,
|
|
648
|
+
PasswordValidator,
|
|
649
|
+
StringValidator,
|
|
650
|
+
}
|
|
651
|
+
export default z
|
|
652
|
+
`
|
|
653
|
+
|
|
654
|
+
// ─── src/lib/validators/auth.ts ───────────────────────────────────────────────
|
|
655
|
+
export const validatorsAuthTs = `// TODO: Define your auth-specific validators here
|
|
656
|
+
import z from '.'
|
|
657
|
+
import { EmailValidator, PasswordValidator, NameValidator } from '.'
|
|
658
|
+
|
|
659
|
+
export const LoginSchema = z.object({
|
|
660
|
+
email: EmailValidator,
|
|
661
|
+
password: PasswordValidator,
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
export const RegisterSchema = z.object({
|
|
665
|
+
name: NameValidator,
|
|
666
|
+
email: EmailValidator,
|
|
667
|
+
password: PasswordValidator,
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
export type LoginInput = z.infer<typeof LoginSchema>
|
|
671
|
+
export type RegisterInput = z.infer<typeof RegisterSchema>
|
|
672
|
+
`
|
|
673
|
+
|
|
674
|
+
// ─── src/lib/utils/locale-date-formats.ts ────────────────────────────────────
|
|
675
|
+
export const localeDateFormatsTs = `import type { Formats } from 'next-intl'
|
|
676
|
+
|
|
677
|
+
export const localeFormats = {
|
|
678
|
+
dateTime: {},
|
|
679
|
+
number: {
|
|
680
|
+
precise: { maximumFractionDigits: 5 },
|
|
681
|
+
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
|
682
|
+
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 1 },
|
|
683
|
+
currency: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
|
|
684
|
+
compact: { notation: 'compact', maximumFractionDigits: 1 },
|
|
685
|
+
},
|
|
686
|
+
list: {
|
|
687
|
+
enumeration: { style: 'long', type: 'conjunction' },
|
|
688
|
+
disjunction: { style: 'long', type: 'disjunction' },
|
|
689
|
+
unit: { style: 'narrow', type: 'unit' },
|
|
690
|
+
},
|
|
691
|
+
} satisfies Formats
|
|
692
|
+
`
|
|
693
|
+
|
|
694
|
+
// ─── next-intl.ts ─────────────────────────────────────────────────────────────
|
|
695
|
+
export const nextIntlTs = `import { localeFormats } from './src/lib/utils/locale-date-formats'
|
|
696
|
+
import { routing } from './src/i18n/routing'
|
|
697
|
+
import locales from './locales/en.json'
|
|
698
|
+
|
|
699
|
+
declare module 'next-intl' {
|
|
700
|
+
interface AppConfig {
|
|
701
|
+
Locale: (typeof routing.locales)[number]
|
|
702
|
+
Messages: typeof locales
|
|
703
|
+
Formats: typeof localeFormats
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
`
|
|
707
|
+
|
|
708
|
+
// ─── locales/en.json ──────────────────────────────────────────────────────────
|
|
709
|
+
export const localesEnJson = `{
|
|
710
|
+
"Locale": {
|
|
711
|
+
"en": "English"
|
|
712
|
+
},
|
|
713
|
+
"Form": {
|
|
714
|
+
"Validators": {}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
`
|
|
718
|
+
|
|
719
|
+
// ─── src/app/[locale]/page.tsx ────────────────────────────────────────────────
|
|
720
|
+
export const localPageTsx = `// TODO: This is the localized home page entry point.
|
|
721
|
+
// Import and render your home page content here.
|
|
722
|
+
// You have access to locale via: import { useLocale } from 'next-intl'
|
|
723
|
+
// And to translations via: import { useTranslations } from 'next-intl'
|
|
724
|
+
|
|
725
|
+
export default function LocalePage() {
|
|
726
|
+
return (
|
|
727
|
+
<main>
|
|
728
|
+
{/* TODO: Add your localized page content */}
|
|
729
|
+
</main>
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
`
|
|
733
|
+
|
|
734
|
+
// ─── src/lib/axios/axios.client.ts ────────────────────────────────────────────
|
|
735
|
+
export const axiosClientTs = `// TODO: configure your client-side axios instance
|
|
736
|
+
import axios from 'axios'
|
|
737
|
+
import { SITE_URL } from '~/constants'
|
|
738
|
+
|
|
739
|
+
const client = axios.create({
|
|
740
|
+
baseURL: SITE_URL,
|
|
741
|
+
withCredentials: true,
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
export default client
|
|
745
|
+
`
|
|
746
|
+
|
|
747
|
+
// ─── src/lib/axios/axios.server.ts ───────────────────────────────────────────
|
|
748
|
+
export const axiosServerTs = `// TODO: configure your server-side axios instance
|
|
749
|
+
import axios from 'axios'
|
|
750
|
+
import { SITE_URL } from '~/constants'
|
|
751
|
+
|
|
752
|
+
const server = axios.create({
|
|
753
|
+
baseURL: SITE_URL,
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
export default server
|
|
757
|
+
`
|
|
758
|
+
|
|
759
|
+
// ─── src/apis/client/index.ts ────────────────────────────────────────────────
|
|
760
|
+
export const apisClientTs = `// TODO: add your client-side API functions here
|
|
761
|
+
`
|
|
762
|
+
|
|
763
|
+
// ─── src/apis/server/index.ts ────────────────────────────────────────────────
|
|
764
|
+
export const apisServerTs = `// TODO: add your server-side API functions here
|
|
765
|
+
`
|
|
766
|
+
|
|
767
|
+
// ─── src/app/layout.tsx (i18n — passthrough shell) ───────────────────────────
|
|
768
|
+
export const rootLayoutI18nTs = `
|
|
769
|
+
|
|
770
|
+
export default ({ children }: LayoutProps<'/'>) => children
|
|
771
|
+
`
|
|
772
|
+
|
|
773
|
+
// ─── src/app/[locale]/layout.tsx ─────────────────────────────────────────────
|
|
774
|
+
export const localeLayoutTs = `import Providers from '../../components/providers'
|
|
775
|
+
import seo from '../../lib/seo'
|
|
776
|
+
import '../globals.css'
|
|
777
|
+
import { hasLocale } from 'next-intl'
|
|
778
|
+
import { notFound } from 'next/navigation'
|
|
779
|
+
import { routing } from '../../i18n/routing'
|
|
780
|
+
import { APP_LOCALES, type AppLocale } from '../../constants'
|
|
781
|
+
import { setRequestLocale } from 'next-intl/server'
|
|
782
|
+
import type { Metadata } from 'next'
|
|
783
|
+
import { PoppinsFont } from '../../lib/fonts'
|
|
784
|
+
|
|
785
|
+
export const generateStaticParams = () =>
|
|
786
|
+
APP_LOCALES.map((locale) => ({ locale }))
|
|
787
|
+
|
|
788
|
+
export const generateMetadata = async ({
|
|
789
|
+
params,
|
|
790
|
+
}: LayoutProps<'/[locale]'>): Promise<Metadata> => {
|
|
791
|
+
const locale = (await params).locale as AppLocale
|
|
792
|
+
return seo[locale]
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export default async function RootLayout({
|
|
796
|
+
params,
|
|
797
|
+
children,
|
|
798
|
+
}: LayoutProps<'/[locale]'>) {
|
|
799
|
+
const { locale } = await params
|
|
800
|
+
if (!hasLocale(routing.locales, locale)) {
|
|
801
|
+
notFound()
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
setRequestLocale(locale)
|
|
805
|
+
|
|
806
|
+
return (
|
|
807
|
+
<html
|
|
808
|
+
lang={locale}
|
|
809
|
+
className="scroll-smooth"
|
|
810
|
+
suppressHydrationWarning
|
|
811
|
+
data-scroll-behavior="smooth"
|
|
812
|
+
>
|
|
813
|
+
<body className={\`antialiased \${PoppinsFont.className}\`}>
|
|
814
|
+
<Providers>{children}</Providers>
|
|
815
|
+
</body>
|
|
816
|
+
</html>
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
`
|
|
820
|
+
|
|
821
|
+
// ─── src/app/layout.tsx (no i18n — standard root layout) ─────────────────────
|
|
822
|
+
export const rootLayoutTs = `import Providers from '../components/providers'
|
|
823
|
+
import { PoppinsFont } from '../lib/fonts'
|
|
824
|
+
import './globals.css'
|
|
825
|
+
import type { Metadata } from 'next'
|
|
826
|
+
import seo from '../lib/seo'
|
|
827
|
+
|
|
828
|
+
export const metadata: Metadata = seo
|
|
829
|
+
|
|
830
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
831
|
+
return (
|
|
832
|
+
<html lang="en" className="scroll-smooth" suppressHydrationWarning>
|
|
833
|
+
<body className={\`antialiased \${PoppinsFont.className}\`}>
|
|
834
|
+
<Providers>{children}</Providers>
|
|
835
|
+
</body>
|
|
836
|
+
</html>
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
`
|
|
840
|
+
|
|
841
|
+
// ─── eslint.config.mjs ────────────────────────────────────────────────────────
|
|
842
|
+
export const eslintConfigMjs = (opts) => `import { defineConfig, globalIgnores } from 'eslint/config'
|
|
843
|
+
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
844
|
+
import nextTs from 'eslint-config-next/typescript'
|
|
845
|
+
${opts.query ? `import tanstackPluginQuery from '@tanstack/eslint-plugin-query'` : ''}
|
|
846
|
+
|
|
847
|
+
const eslintConfig = defineConfig([
|
|
848
|
+
...nextVitals,
|
|
849
|
+
...nextTs,
|
|
850
|
+
${opts.query ? `...tanstackPluginQuery.configs['flat/recommended'],` : ''}
|
|
851
|
+
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
|
|
852
|
+
{
|
|
853
|
+
rules: {
|
|
854
|
+
'@next/next/no-html-link-for-pages': ['error'],
|
|
855
|
+
'react/display-name': 'off',
|
|
856
|
+
'import/no-anonymous-default-export': [
|
|
857
|
+
'error',
|
|
858
|
+
{
|
|
859
|
+
allowArray: true,
|
|
860
|
+
allowArrowFunction: true,
|
|
861
|
+
allowObject: true,
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
'@typescript-eslint/no-unused-vars': [
|
|
865
|
+
'error',
|
|
866
|
+
{
|
|
867
|
+
args: 'all',
|
|
868
|
+
caughtErrors: 'all',
|
|
869
|
+
argsIgnorePattern: '^_',
|
|
870
|
+
varsIgnorePattern: '^_',
|
|
871
|
+
ignoreRestSiblings: true,
|
|
872
|
+
caughtErrorsIgnorePattern: '^_',
|
|
873
|
+
destructuredArrayIgnorePattern: '^_',
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
])
|
|
879
|
+
|
|
880
|
+
export default eslintConfig
|
|
881
|
+
`
|
|
882
|
+
|
|
883
|
+
// ─── src/app/sitemap.ts ───────────────────────────────────────────────────────
|
|
884
|
+
export const sitemapTs = `import type { MetadataRoute } from 'next'
|
|
885
|
+
import { SITE_URL } from '../constants'
|
|
886
|
+
|
|
887
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
888
|
+
return [
|
|
889
|
+
{
|
|
890
|
+
url: SITE_URL,
|
|
891
|
+
lastModified: new Date(),
|
|
892
|
+
changeFrequency: 'weekly',
|
|
893
|
+
priority: 1,
|
|
894
|
+
},
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
`
|
|
898
|
+
// ─── next.config.ts (i18n) ────────────────────────────────────────────────────
|
|
899
|
+
export const nextConfigI18nTs = (opts) => `import { NextConfig } from 'next'
|
|
900
|
+
import createNextIntlPlugin from 'next-intl/plugin'
|
|
901
|
+
|
|
902
|
+
const nextConfig: NextConfig = {${opts.reactCompiler ? `
|
|
903
|
+
reactCompiler: true,` : ''}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const withNextIntl = createNextIntlPlugin()
|
|
907
|
+
export default withNextIntl(nextConfig)
|
|
908
|
+
`
|