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.
@@ -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
+ `