create-solostack 1.0.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/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/cli.js +13 -0
- package/package.json +45 -0
- package/src/constants.js +94 -0
- package/src/generators/auth.js +595 -0
- package/src/generators/base.js +592 -0
- package/src/generators/database.js +365 -0
- package/src/generators/emails.js +404 -0
- package/src/generators/payments.js +541 -0
- package/src/generators/ui.js +368 -0
- package/src/index.js +374 -0
- package/src/utils/files.js +81 -0
- package/src/utils/git.js +69 -0
- package/src/utils/logger.js +62 -0
- package/src/utils/packages.js +75 -0
- package/src/utils/validate.js +17 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { writeFile, renderTemplateToFile, getTemplatePath, ensureDir } from '../utils/files.js';
|
|
3
|
+
import { PACKAGE_VERSIONS } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates the base Next.js 15 project structure
|
|
7
|
+
* @param {string} projectPath - Path where the project should be generated
|
|
8
|
+
* @param {string} projectName - Name of the project
|
|
9
|
+
* @param {object} config - Project configuration
|
|
10
|
+
*/
|
|
11
|
+
export async function generateBase(projectPath, projectName, config) {
|
|
12
|
+
// Create directory structure
|
|
13
|
+
await ensureDir(path.join(projectPath, 'src/app'));
|
|
14
|
+
await ensureDir(path.join(projectPath, 'src/components'));
|
|
15
|
+
await ensureDir(path.join(projectPath, 'src/lib'));
|
|
16
|
+
await ensureDir(path.join(projectPath, 'public'));
|
|
17
|
+
|
|
18
|
+
//Generate package.json
|
|
19
|
+
const packageJson = {
|
|
20
|
+
name: projectName,
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
private: true,
|
|
23
|
+
scripts: {
|
|
24
|
+
dev: 'next dev',
|
|
25
|
+
build: 'next build',
|
|
26
|
+
start: 'next start',
|
|
27
|
+
lint: 'next lint',
|
|
28
|
+
'db:push': 'prisma db push',
|
|
29
|
+
'db:seed': 'tsx prisma/seed.ts',
|
|
30
|
+
'db:studio': 'prisma studio',
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
next: PACKAGE_VERSIONS.next,
|
|
34
|
+
react: PACKAGE_VERSIONS.react,
|
|
35
|
+
'react-dom': PACKAGE_VERSIONS.reactDom,
|
|
36
|
+
'@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
|
|
37
|
+
'next-auth': PACKAGE_VERSIONS['next-auth'],
|
|
38
|
+
bcryptjs: PACKAGE_VERSIONS.bcryptjs,
|
|
39
|
+
stripe: PACKAGE_VERSIONS.stripe,
|
|
40
|
+
resend: PACKAGE_VERSIONS.resend,
|
|
41
|
+
'@react-email/components': PACKAGE_VERSIONS['@react-email/components'],
|
|
42
|
+
zod: PACKAGE_VERSIONS.zod,
|
|
43
|
+
'react-hook-form': PACKAGE_VERSIONS['react-hook-form'],
|
|
44
|
+
'@hookform/resolvers': PACKAGE_VERSIONS['@hookform/resolvers'],
|
|
45
|
+
'class-variance-authority': PACKAGE_VERSIONS['class-variance-authority'],
|
|
46
|
+
clsx: PACKAGE_VERSIONS.clsx,
|
|
47
|
+
'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
|
|
48
|
+
'tailwindcss-animate': PACKAGE_VERSIONS['tailwindcss-animate'],
|
|
49
|
+
'lucide-react': PACKAGE_VERSIONS['lucide-react'],
|
|
50
|
+
'@radix-ui/react-dropdown-menu': PACKAGE_VERSIONS['@radix-ui/react-dropdown-menu'],
|
|
51
|
+
'@radix-ui/react-slot': PACKAGE_VERSIONS['@radix-ui/react-slot'],
|
|
52
|
+
'@radix-ui/react-toast': PACKAGE_VERSIONS['@radix-ui/react-toast'],
|
|
53
|
+
'@radix-ui/react-dialog': PACKAGE_VERSIONS['@radix-ui/react-dialog'],
|
|
54
|
+
'@radix-ui/react-label': PACKAGE_VERSIONS['@radix-ui/react-label'],
|
|
55
|
+
'@auth/prisma-adapter': PACKAGE_VERSIONS['@auth/prisma-adapter'],
|
|
56
|
+
},
|
|
57
|
+
devDependencies: {
|
|
58
|
+
typescript: PACKAGE_VERSIONS.typescript,
|
|
59
|
+
'@types/node': PACKAGE_VERSIONS['@types/node'],
|
|
60
|
+
'@types/react': PACKAGE_VERSIONS['@types/react'],
|
|
61
|
+
'@types/react-dom': PACKAGE_VERSIONS['@types/react-dom'],
|
|
62
|
+
'@types/bcryptjs': PACKAGE_VERSIONS['@types/bcryptjs'],
|
|
63
|
+
tailwindcss: PACKAGE_VERSIONS.tailwindcss,
|
|
64
|
+
postcss: PACKAGE_VERSIONS.postcss,
|
|
65
|
+
autoprefixer: PACKAGE_VERSIONS.autoprefixer,
|
|
66
|
+
prisma: PACKAGE_VERSIONS.prisma,
|
|
67
|
+
tsx: '^4.19.2',
|
|
68
|
+
eslint: '^9',
|
|
69
|
+
'eslint-config-next': '^15.1.6',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await writeFile(
|
|
74
|
+
path.join(projectPath, 'package.json'),
|
|
75
|
+
JSON.stringify(packageJson, null, 2)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Generate tsconfig.json
|
|
79
|
+
const tsConfig = {
|
|
80
|
+
compilerOptions: {
|
|
81
|
+
target: 'ES2017',
|
|
82
|
+
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
83
|
+
allowJs: true,
|
|
84
|
+
skipLibCheck: true,
|
|
85
|
+
strict: true,
|
|
86
|
+
noEmit: true,
|
|
87
|
+
esModuleInterop: true,
|
|
88
|
+
module: 'esnext',
|
|
89
|
+
moduleResolution: 'bundler',
|
|
90
|
+
resolveJsonModule: true,
|
|
91
|
+
isolatedModules: true,
|
|
92
|
+
jsx: 'preserve',
|
|
93
|
+
incremental: true,
|
|
94
|
+
plugins: [
|
|
95
|
+
{
|
|
96
|
+
name: 'next',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
paths: {
|
|
100
|
+
'@/*': ['./src/*'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
104
|
+
exclude: ['node_modules'],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
await writeFile(
|
|
108
|
+
path.join(projectPath, 'tsconfig.json'),
|
|
109
|
+
JSON.stringify(tsConfig, null, 2)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Generate next.config.js
|
|
113
|
+
const nextConfig = `/** @type {import('next').NextConfig} */
|
|
114
|
+
const nextConfig = {
|
|
115
|
+
experimental: {
|
|
116
|
+
serverActions: {
|
|
117
|
+
bodySizeLimit: '2mb',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default nextConfig;
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
await writeFile(path.join(projectPath, 'next.config.js'), nextConfig);
|
|
126
|
+
|
|
127
|
+
// Generate tailwind.config.ts
|
|
128
|
+
const tailwindConfig = `import type { Config } from "tailwindcss";
|
|
129
|
+
|
|
130
|
+
const config: Config = {
|
|
131
|
+
darkMode: ["class"],
|
|
132
|
+
content: [
|
|
133
|
+
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
134
|
+
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
135
|
+
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
136
|
+
],
|
|
137
|
+
theme: {
|
|
138
|
+
extend: {
|
|
139
|
+
colors: {
|
|
140
|
+
background: "hsl(var(--background))",
|
|
141
|
+
foreground: "hsl(var(--foreground))",
|
|
142
|
+
card: {
|
|
143
|
+
DEFAULT: "hsl(var(--card))",
|
|
144
|
+
foreground: "hsl(var(--card-foreground))",
|
|
145
|
+
},
|
|
146
|
+
popover: {
|
|
147
|
+
DEFAULT: "hsl(var(--popover))",
|
|
148
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
149
|
+
},
|
|
150
|
+
primary: {
|
|
151
|
+
DEFAULT: "hsl(var(--primary))",
|
|
152
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
153
|
+
},
|
|
154
|
+
secondary: {
|
|
155
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
156
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
157
|
+
},
|
|
158
|
+
muted: {
|
|
159
|
+
DEFAULT: "hsl(var(--muted))",
|
|
160
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
161
|
+
},
|
|
162
|
+
accent: {
|
|
163
|
+
DEFAULT: "hsl(var(--accent))",
|
|
164
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
165
|
+
},
|
|
166
|
+
destructive: {
|
|
167
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
168
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
169
|
+
},
|
|
170
|
+
border: "hsl(var(--border))",
|
|
171
|
+
input: "hsl(var(--input))",
|
|
172
|
+
ring: "hsl(var(--ring))",
|
|
173
|
+
chart: {
|
|
174
|
+
"1": "hsl(var(--chart-1))",
|
|
175
|
+
"2": "hsl(var(--chart-2))",
|
|
176
|
+
"3": "hsl(var(--chart-3))",
|
|
177
|
+
"4": "hsl(var(--chart-4))",
|
|
178
|
+
"5": "hsl(var(--chart-5))",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
borderRadius: {
|
|
182
|
+
lg: "var(--radius)",
|
|
183
|
+
md: "calc(var(--radius) - 2px)",
|
|
184
|
+
sm: "calc(var(--radius) - 4px)",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
plugins: [require("tailwindcss-animate")],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default config;
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
await writeFile(path.join(projectPath, 'tailwind.config.ts'), tailwindConfig);
|
|
195
|
+
|
|
196
|
+
// Generate postcss.config.mjs
|
|
197
|
+
const postcssConfig = `/** @type {import('postcss-load-config').Config} */
|
|
198
|
+
const config = {
|
|
199
|
+
plugins: {
|
|
200
|
+
tailwindcss: {},
|
|
201
|
+
autoprefixer: {},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default config;
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
await writeFile(path.join(projectPath, 'postcss.config.mjs'), postcssConfig);
|
|
209
|
+
|
|
210
|
+
// Generate app/globals.css
|
|
211
|
+
const globalsCss = `@tailwind base;
|
|
212
|
+
@tailwind components;
|
|
213
|
+
@tailwind utilities;
|
|
214
|
+
|
|
215
|
+
@layer base {
|
|
216
|
+
:root {
|
|
217
|
+
--background: 0 0% 100%;
|
|
218
|
+
--foreground: 222.2 84% 4.9%;
|
|
219
|
+
--card: 0 0% 100%;
|
|
220
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
221
|
+
--popover: 0 0% 100%;
|
|
222
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
223
|
+
--primary: 222.2 47.4% 11.2%;
|
|
224
|
+
--primary-foreground: 210 40% 98%;
|
|
225
|
+
--secondary: 210 40% 96.1%;
|
|
226
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
227
|
+
--muted: 210 40% 96.1%;
|
|
228
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
229
|
+
--accent: 210 40% 96.1%;
|
|
230
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
231
|
+
--destructive: 0 84.2% 60.2%;
|
|
232
|
+
--destructive-foreground: 210 40% 98%;
|
|
233
|
+
--border: 214.3 31.8% 91.4%;
|
|
234
|
+
--input: 214.3 31.8% 91.4%;
|
|
235
|
+
--ring: 222.2 84% 4.9%;
|
|
236
|
+
--radius: 0.5rem;
|
|
237
|
+
--chart-1: 12 76% 61%;
|
|
238
|
+
--chart-2: 173 58% 39%;
|
|
239
|
+
--chart-3: 197 37% 24%;
|
|
240
|
+
--chart-4: 43 74% 66%;
|
|
241
|
+
--chart-5: 27 87% 67%;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.dark {
|
|
245
|
+
--background: 222.2 84% 4.9%;
|
|
246
|
+
--foreground: 210 40% 98%;
|
|
247
|
+
--card: 222.2 84% 4.9%;
|
|
248
|
+
--card-foreground: 210 40% 98%;
|
|
249
|
+
--popover: 222.2 84% 4.9%;
|
|
250
|
+
--popover-foreground: 210 40% 98%;
|
|
251
|
+
--primary: 210 40% 98%;
|
|
252
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
253
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
254
|
+
--secondary-foreground: 210 40% 98%;
|
|
255
|
+
--muted: 217.2 32.6% 17.5%;
|
|
256
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
257
|
+
--accent: 217.2 32.6% 17.5%;
|
|
258
|
+
--accent-foreground: 210 40% 98%;
|
|
259
|
+
--destructive: 0 62.8% 30.6%;
|
|
260
|
+
--destructive-foreground: 210 40% 98%;
|
|
261
|
+
--border: 217.2 32.6% 17.5%;
|
|
262
|
+
--input: 217.2 32.6% 17.5%;
|
|
263
|
+
--ring: 212.7 26.8% 83.9%;
|
|
264
|
+
--chart-1: 220 70% 50%;
|
|
265
|
+
--chart-2: 160 60% 45%;
|
|
266
|
+
--chart-3: 30 80% 55%;
|
|
267
|
+
--chart-4: 280 65% 60%;
|
|
268
|
+
--chart-5: 340 75% 55%;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@layer base {
|
|
273
|
+
* {
|
|
274
|
+
@apply border-border;
|
|
275
|
+
}
|
|
276
|
+
body {
|
|
277
|
+
@apply bg-background text-foreground;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
await writeFile(path.join(projectPath, 'src/app/globals.css'), globalsCss);
|
|
283
|
+
|
|
284
|
+
// Generate app/layout.tsx
|
|
285
|
+
const layoutTsx = `import type { Metadata } from "next";
|
|
286
|
+
import { Inter } from "next/font/google";
|
|
287
|
+
import "./globals.css";
|
|
288
|
+
|
|
289
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
290
|
+
|
|
291
|
+
export const metadata: Metadata = {
|
|
292
|
+
title: "${projectName}",
|
|
293
|
+
description: "Built with SoloStack - The complete SaaS boilerplate",
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export default function RootLayout({
|
|
297
|
+
children,
|
|
298
|
+
}: Readonly<{
|
|
299
|
+
children: React.ReactNode;
|
|
300
|
+
}>) {
|
|
301
|
+
return (
|
|
302
|
+
<html lang="en">
|
|
303
|
+
<body className={inter.className}>{children}</body>
|
|
304
|
+
</html>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
await writeFile(path.join(projectPath, 'src/app/layout.tsx'), layoutTsx);
|
|
310
|
+
|
|
311
|
+
// Generate app/page.tsx
|
|
312
|
+
const pageTsx = `export default function Home() {
|
|
313
|
+
return (
|
|
314
|
+
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
|
315
|
+
<div className="z-10 max-w-5xl w-full items-center justify-center font-mono text-sm">
|
|
316
|
+
<h1 className="text-4xl font-bold text-center mb-4">
|
|
317
|
+
Welcome to ${projectName}
|
|
318
|
+
</h1>
|
|
319
|
+
<p className="text-center text-muted-foreground">
|
|
320
|
+
Built with SoloStack - Your Next.js SaaS boilerplate
|
|
321
|
+
</p>
|
|
322
|
+
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
323
|
+
<div className="p-6 border rounded-lg">
|
|
324
|
+
<h2 className="text-xl font-semibold mb-2">✅ Authentication</h2>
|
|
325
|
+
<p className="text-sm text-muted-foreground">
|
|
326
|
+
NextAuth.js configured with email and OAuth providers
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
<div className="p-6 border rounded-lg">
|
|
330
|
+
<h2 className="text-xl font-semibold mb-2">✅ Database</h2>
|
|
331
|
+
<p className="text-sm text-muted-foreground">
|
|
332
|
+
Prisma + PostgreSQL ready to use
|
|
333
|
+
</p>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="p-6 border rounded-lg">
|
|
336
|
+
<h2 className="text-xl font-semibold mb-2">✅ Payments</h2>
|
|
337
|
+
<p className="text-sm text-muted-foreground">
|
|
338
|
+
Stripe integration for subscriptions
|
|
339
|
+
</p>
|
|
340
|
+
</div>
|
|
341
|
+
<div className="p-6 border rounded-lg">
|
|
342
|
+
<h2 className="text-xl font-semibold mb-2">✅ Emails</h2>
|
|
343
|
+
<p className="text-sm text-muted-foreground">
|
|
344
|
+
Resend configured for transactional emails
|
|
345
|
+
</p>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="mt-8 text-center">
|
|
349
|
+
<p className="text-sm text-muted-foreground">
|
|
350
|
+
Get started by editing <code className="font-mono bg-muted px-2 py-1 rounded">src/app/page.tsx</code>
|
|
351
|
+
</p>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</main>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
|
|
359
|
+
await writeFile(path.join(projectPath, 'src/app/page.tsx'), pageTsx);
|
|
360
|
+
|
|
361
|
+
// Generate .env.example
|
|
362
|
+
const envExample = `# Database
|
|
363
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
364
|
+
# For cloud databases, add ?sslmode=require to the connection string
|
|
365
|
+
|
|
366
|
+
# NextAuth
|
|
367
|
+
NEXTAUTH_SECRET="" # Generate with: openssl rand -base64 32
|
|
368
|
+
NEXTAUTH_URL="http://localhost:3000"
|
|
369
|
+
# In production, set to your domain: https://yourdomain.com
|
|
370
|
+
|
|
371
|
+
# Stripe
|
|
372
|
+
STRIPE_SECRET_KEY="sk_test_..."
|
|
373
|
+
STRIPE_WEBHOOK_SECRET="whsec_..."
|
|
374
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
|
|
375
|
+
# Create price IDs in Stripe Dashboard -> Products
|
|
376
|
+
STRIPE_PRO_PRICE_ID="price_..." # Monthly Pro plan price ID
|
|
377
|
+
STRIPE_ENTERPRISE_PRICE_ID="price_..." # Monthly Enterprise plan price ID
|
|
378
|
+
|
|
379
|
+
# Resend
|
|
380
|
+
RESEND_API_KEY="re_..."
|
|
381
|
+
FROM_EMAIL="onboarding@resend.dev" # Use your verified domain
|
|
382
|
+
|
|
383
|
+
# OAuth Providers (Optional)
|
|
384
|
+
# Google: https://console.cloud.google.com/apis/credentials
|
|
385
|
+
GOOGLE_CLIENT_ID=""
|
|
386
|
+
GOOGLE_CLIENT_SECRET=""
|
|
387
|
+
# GitHub: https://github.com/settings/developers
|
|
388
|
+
GITHUB_CLIENT_ID=""
|
|
389
|
+
GITHUB_CLIENT_SECRET=""
|
|
390
|
+
`;
|
|
391
|
+
|
|
392
|
+
await writeFile(path.join(projectPath, '.env.example'), envExample);
|
|
393
|
+
|
|
394
|
+
// Generate README.md
|
|
395
|
+
const readme = `# ${projectName}
|
|
396
|
+
|
|
397
|
+
Built with [SoloStack](https://github.com/yourusername/create-solostack) - The complete SaaS boilerplate for indie hackers.
|
|
398
|
+
|
|
399
|
+
## Features
|
|
400
|
+
|
|
401
|
+
- ✅ **Next.js 15** with App Router
|
|
402
|
+
- ✅ **TypeScript** for type safety
|
|
403
|
+
- ✅ **Tailwind CSS** for styling
|
|
404
|
+
- ✅ **Prisma** + PostgreSQL for database
|
|
405
|
+
- ✅ **NextAuth.js** for authentication
|
|
406
|
+
- ✅ **Stripe** for payments
|
|
407
|
+
- ✅ **Resend** for emails
|
|
408
|
+
- ✅ **shadcn/ui** components
|
|
409
|
+
|
|
410
|
+
## Getting Started
|
|
411
|
+
|
|
412
|
+
### 1. Install dependencies
|
|
413
|
+
|
|
414
|
+
\`\`\`bash
|
|
415
|
+
npm install
|
|
416
|
+
\`\`\`
|
|
417
|
+
|
|
418
|
+
### 2. Set up environment variables
|
|
419
|
+
|
|
420
|
+
\`\`\`bash
|
|
421
|
+
cp .env.example .env
|
|
422
|
+
\`\`\`
|
|
423
|
+
|
|
424
|
+
Edit \`.env\` and add your own values.
|
|
425
|
+
|
|
426
|
+
### 3. Set up the database
|
|
427
|
+
|
|
428
|
+
\`\`\`bash
|
|
429
|
+
npm run db:push
|
|
430
|
+
npm run db:seed
|
|
431
|
+
\`\`\`
|
|
432
|
+
|
|
433
|
+
### 4. Run the development server
|
|
434
|
+
|
|
435
|
+
\`\`\`bash
|
|
436
|
+
npm run dev
|
|
437
|
+
\`\`\`
|
|
438
|
+
|
|
439
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser.
|
|
440
|
+
|
|
441
|
+
## Project Structure
|
|
442
|
+
|
|
443
|
+
\`\`\`
|
|
444
|
+
${projectName}/
|
|
445
|
+
├── prisma/ # Database schema and migrations
|
|
446
|
+
├── public/ # Static assets
|
|
447
|
+
├── src/
|
|
448
|
+
│ ├── app/ # Next.js app directory
|
|
449
|
+
│ │ ├── api/ # API routes
|
|
450
|
+
│ │ ├── (auth)/ # Auth pages
|
|
451
|
+
│ │ └── dashboard/ # Protected pages
|
|
452
|
+
│ ├── components/ # React components
|
|
453
|
+
│ └── lib/ # Utilities and configurations
|
|
454
|
+
└── package.json
|
|
455
|
+
\`\`\`
|
|
456
|
+
|
|
457
|
+
## Available Scripts
|
|
458
|
+
|
|
459
|
+
- \`npm run dev\` - Start development server
|
|
460
|
+
- \`npm run build\` - Build for production
|
|
461
|
+
- \`npm run start\` - Start production server
|
|
462
|
+
- \`npm run lint\` - Run ESLint
|
|
463
|
+
- \`npm run db:push\` - Push schema changes to database
|
|
464
|
+
- \`npm run db:seed\` - Seed database with sample data
|
|
465
|
+
- \`npm run db:studio\` - Open Prisma Studio
|
|
466
|
+
|
|
467
|
+
## Learn More
|
|
468
|
+
|
|
469
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
470
|
+
- [NextAuth.js Documentation](https://next-auth.js.org)
|
|
471
|
+
- [Prisma Documentation](https://www.prisma.io/docs)
|
|
472
|
+
- [Stripe Documentation](https://stripe.com/docs)
|
|
473
|
+
- [Resend Documentation](https://resend.com/docs)
|
|
474
|
+
|
|
475
|
+
## License
|
|
476
|
+
|
|
477
|
+
MIT
|
|
478
|
+
`;
|
|
479
|
+
|
|
480
|
+
await writeFile(path.join(projectPath, 'README.md'), readme);
|
|
481
|
+
|
|
482
|
+
// Create lib/utils.ts for cn helper
|
|
483
|
+
const utilsTs = `import { clsx, type ClassValue } from "clsx";
|
|
484
|
+
import { twMerge } from "tailwind-merge";
|
|
485
|
+
|
|
486
|
+
export function cn(...inputs: ClassValue[]) {
|
|
487
|
+
return twMerge(clsx(inputs));
|
|
488
|
+
}
|
|
489
|
+
`;
|
|
490
|
+
|
|
491
|
+
await writeFile(path.join(projectPath, 'src/lib/utils.ts'), utilsTs);
|
|
492
|
+
|
|
493
|
+
// Create next-auth.d.ts for type definitions
|
|
494
|
+
const nextAuthTypes = `import { DefaultSession } from "next-auth";
|
|
495
|
+
|
|
496
|
+
declare module "next-auth" {
|
|
497
|
+
interface Session {
|
|
498
|
+
user: {
|
|
499
|
+
id: string;
|
|
500
|
+
role: string;
|
|
501
|
+
} & DefaultSession["user"];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
declare module "next-auth/jwt" {
|
|
506
|
+
interface JWT {
|
|
507
|
+
id: string;
|
|
508
|
+
role: string;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
|
|
513
|
+
await writeFile(path.join(projectPath, 'next-auth.d.ts'), nextAuthTypes);
|
|
514
|
+
|
|
515
|
+
// Create global error boundary
|
|
516
|
+
const errorPage = `'use client';
|
|
517
|
+
|
|
518
|
+
import { useEffect } from 'react';
|
|
519
|
+
|
|
520
|
+
export default function Error({
|
|
521
|
+
error,
|
|
522
|
+
reset,
|
|
523
|
+
}: {
|
|
524
|
+
error: Error & { digest?: string };
|
|
525
|
+
reset: () => void;
|
|
526
|
+
}) {
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
console.error('Application error:', error);
|
|
529
|
+
}, [error]);
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4">
|
|
533
|
+
<div className="w-full max-w-md text-center">
|
|
534
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-4">Something went wrong!</h1>
|
|
535
|
+
<p className="text-gray-600 mb-8">
|
|
536
|
+
We're sorry, but something unexpected happened. Please try again.
|
|
537
|
+
</p>
|
|
538
|
+
<button
|
|
539
|
+
onClick={() => reset()}
|
|
540
|
+
className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500"
|
|
541
|
+
>
|
|
542
|
+
Try again
|
|
543
|
+
</button>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
`;
|
|
549
|
+
|
|
550
|
+
await writeFile(path.join(projectPath, 'src/app/error.tsx'), errorPage);
|
|
551
|
+
|
|
552
|
+
// Create not-found page
|
|
553
|
+
const notFoundPage = `import Link from 'next/link';
|
|
554
|
+
|
|
555
|
+
export default function NotFound() {
|
|
556
|
+
return (
|
|
557
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4">
|
|
558
|
+
<div className="w-full max-w-md text-center">
|
|
559
|
+
<h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
|
|
560
|
+
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Page Not Found</h2>
|
|
561
|
+
<p className="text-gray-600 mb-8">
|
|
562
|
+
The page you're looking for doesn't exist or has been moved.
|
|
563
|
+
</p>
|
|
564
|
+
<Link
|
|
565
|
+
href="/"
|
|
566
|
+
className="inline-block rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500"
|
|
567
|
+
>
|
|
568
|
+
Go Home
|
|
569
|
+
</Link>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
|
|
576
|
+
await writeFile(path.join(projectPath, 'src/app/not-found.tsx'), notFoundPage);
|
|
577
|
+
|
|
578
|
+
// Create root loading state
|
|
579
|
+
const loadingPage = `export default function Loading() {
|
|
580
|
+
return (
|
|
581
|
+
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
|
582
|
+
<div className="text-center">
|
|
583
|
+
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-600 border-r-transparent"></div>
|
|
584
|
+
<p className="mt-4 text-sm text-gray-600">Loading...</p>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
`;
|
|
590
|
+
|
|
591
|
+
await writeFile(path.join(projectPath, 'src/app/loading.tsx'), loadingPage);
|
|
592
|
+
}
|