@victusvinceere/saas-cli 0.1.0 → 0.1.1
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 +209 -0
- package/dist/index.mjs +916 -124
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -32,59 +32,114 @@ import chalk2 from "chalk";
|
|
|
32
32
|
import ora from "ora";
|
|
33
33
|
import inquirer from "inquirer";
|
|
34
34
|
async function init(projectName, options = {}) {
|
|
35
|
-
logger.log(chalk2.bold("\
|
|
35
|
+
logger.log(chalk2.bold("\n\u{1F680} Let's create your new SaaS project!\n"));
|
|
36
|
+
const useDefaults = options.yes;
|
|
36
37
|
let name = projectName;
|
|
37
38
|
if (!name) {
|
|
38
|
-
|
|
39
|
+
if (useDefaults) {
|
|
40
|
+
name = "my-saas-app";
|
|
41
|
+
} else {
|
|
42
|
+
const answers = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: "input",
|
|
45
|
+
name: "projectName",
|
|
46
|
+
message: "What is your project name?",
|
|
47
|
+
default: "my-saas-app",
|
|
48
|
+
validate: (input) => {
|
|
49
|
+
if (/^[a-z0-9-]+$/.test(input)) return true;
|
|
50
|
+
return "Project name can only contain lowercase letters, numbers, and hyphens";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
name = answers.projectName;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let branding;
|
|
58
|
+
if (useDefaults) {
|
|
59
|
+
branding = {
|
|
60
|
+
productName: options.product || "MySaaS",
|
|
61
|
+
description: options.description || "The best SaaS for your needs",
|
|
62
|
+
primaryColor: options.color || "green"
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
branding = await inquirer.prompt([
|
|
39
66
|
{
|
|
40
67
|
type: "input",
|
|
41
|
-
name: "
|
|
42
|
-
message: "What is your
|
|
43
|
-
default: "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
name: "productName",
|
|
69
|
+
message: "What is your product name? (e.g., MySaaS)",
|
|
70
|
+
default: options.product || "MySaaS"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "input",
|
|
74
|
+
name: "description",
|
|
75
|
+
message: "Short description of your product:",
|
|
76
|
+
default: options.description || "The best SaaS for your needs"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "list",
|
|
80
|
+
name: "primaryColor",
|
|
81
|
+
message: "Choose your primary color:",
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: "Green", value: "green" },
|
|
84
|
+
{ name: "Blue", value: "blue" },
|
|
85
|
+
{ name: "Purple", value: "purple" },
|
|
86
|
+
{ name: "Orange", value: "orange" },
|
|
87
|
+
{ name: "Red", value: "red" },
|
|
88
|
+
{ name: "Indigo", value: "indigo" }
|
|
89
|
+
],
|
|
90
|
+
default: options.color || "green"
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
let config;
|
|
95
|
+
if (useDefaults) {
|
|
96
|
+
config = {
|
|
97
|
+
modules: ["landing", "payments", "admin"],
|
|
98
|
+
providers: ["google", "email"],
|
|
99
|
+
database: "postgresql"
|
|
100
|
+
};
|
|
101
|
+
} else {
|
|
102
|
+
config = await inquirer.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: "checkbox",
|
|
105
|
+
name: "modules",
|
|
106
|
+
message: "Which modules would you like to include?",
|
|
107
|
+
choices: [
|
|
108
|
+
{ name: "Landing Pages", value: "landing", checked: true },
|
|
109
|
+
{ name: "Payments (Lemon Squeezy)", value: "payments", checked: true },
|
|
110
|
+
{ name: "Admin Panel", value: "admin", checked: true },
|
|
111
|
+
{ name: "Blog (MDX)", value: "blog", checked: false }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "checkbox",
|
|
116
|
+
name: "providers",
|
|
117
|
+
message: "Which auth providers do you want?",
|
|
118
|
+
choices: [
|
|
119
|
+
{ name: "Google", value: "google", checked: true },
|
|
120
|
+
{ name: "GitHub", value: "github", checked: false },
|
|
121
|
+
{ name: "Email (Magic Links)", value: "email", checked: true }
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: "list",
|
|
126
|
+
name: "database",
|
|
127
|
+
message: "Which database will you use?",
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: "PostgreSQL", value: "postgresql" },
|
|
130
|
+
{ name: "MySQL", value: "mysql" },
|
|
131
|
+
{ name: "SQLite", value: "sqlite" }
|
|
132
|
+
],
|
|
133
|
+
default: "postgresql"
|
|
48
134
|
}
|
|
49
135
|
]);
|
|
50
|
-
name = answers.projectName;
|
|
51
136
|
}
|
|
52
|
-
const config = await inquirer.prompt([
|
|
53
|
-
{
|
|
54
|
-
type: "checkbox",
|
|
55
|
-
name: "modules",
|
|
56
|
-
message: "Which modules would you like to include?",
|
|
57
|
-
choices: [
|
|
58
|
-
{ name: "Payments (Lemon Squeezy)", value: "payments", checked: true },
|
|
59
|
-
{ name: "Admin Panel", value: "admin", checked: true },
|
|
60
|
-
{ name: "Blog (MDX)", value: "blog", checked: false },
|
|
61
|
-
{ name: "Landing Pages", value: "landing", checked: true }
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: "checkbox",
|
|
66
|
-
name: "providers",
|
|
67
|
-
message: "Which auth providers do you want?",
|
|
68
|
-
choices: [
|
|
69
|
-
{ name: "Google", value: "google", checked: true },
|
|
70
|
-
{ name: "GitHub", value: "github", checked: false },
|
|
71
|
-
{ name: "Email (Magic Links)", value: "email", checked: true }
|
|
72
|
-
]
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
type: "list",
|
|
76
|
-
name: "database",
|
|
77
|
-
message: "Which database will you use?",
|
|
78
|
-
choices: [
|
|
79
|
-
{ name: "PostgreSQL", value: "postgresql" },
|
|
80
|
-
{ name: "MySQL", value: "mysql" },
|
|
81
|
-
{ name: "SQLite", value: "sqlite" }
|
|
82
|
-
],
|
|
83
|
-
default: "postgresql"
|
|
84
|
-
}
|
|
85
|
-
]);
|
|
86
137
|
const projectDir = path.resolve(process.cwd(), name);
|
|
87
138
|
if (await fs.pathExists(projectDir)) {
|
|
139
|
+
if (useDefaults) {
|
|
140
|
+
logger.error(`Directory ${name} already exists. Use a different name or remove the directory.`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
88
143
|
const { overwrite } = await inquirer.prompt([
|
|
89
144
|
{
|
|
90
145
|
type: "confirm",
|
|
@@ -116,28 +171,43 @@ async function init(projectName, options = {}) {
|
|
|
116
171
|
"db:studio": "prisma studio"
|
|
117
172
|
},
|
|
118
173
|
dependencies: {
|
|
119
|
-
"@saas-
|
|
174
|
+
"@victusvinceere/saas-core": "latest",
|
|
120
175
|
...config.modules.includes("payments") && {
|
|
121
|
-
"@saas-
|
|
176
|
+
"@victusvinceere/saas-payments": "latest"
|
|
122
177
|
},
|
|
123
178
|
...config.modules.includes("admin") && {
|
|
124
|
-
"@saas-
|
|
179
|
+
"@victusvinceere/saas-admin": "latest"
|
|
180
|
+
},
|
|
181
|
+
...config.modules.includes("blog") && {
|
|
182
|
+
"@victusvinceere/saas-blog": "latest"
|
|
125
183
|
},
|
|
126
|
-
...config.modules.includes("blog") && { "@saas-kit/blog": "^0.1.0" },
|
|
127
184
|
...config.modules.includes("landing") && {
|
|
128
|
-
"@saas-
|
|
185
|
+
"@victusvinceere/saas-landing": "latest"
|
|
129
186
|
},
|
|
130
|
-
next: "^
|
|
131
|
-
react: "^
|
|
132
|
-
"react-dom": "^
|
|
133
|
-
"next-auth": "^5.0.0-beta.
|
|
187
|
+
next: "^15.0.0",
|
|
188
|
+
react: "^19.0.0",
|
|
189
|
+
"react-dom": "^19.0.0",
|
|
190
|
+
"next-auth": "^5.0.0-beta.30",
|
|
191
|
+
"@auth/prisma-adapter": "^2.0.0",
|
|
192
|
+
"@prisma/client": "^6.0.0",
|
|
193
|
+
"lucide-react": "^0.460.0",
|
|
194
|
+
"@radix-ui/react-accordion": "^1.2.0",
|
|
195
|
+
"@radix-ui/react-avatar": "^1.1.0",
|
|
196
|
+
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
|
197
|
+
"@radix-ui/react-label": "^2.1.0",
|
|
198
|
+
"@radix-ui/react-select": "^2.1.0",
|
|
199
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
200
|
+
"class-variance-authority": "^0.7.0",
|
|
201
|
+
clsx: "^2.1.0",
|
|
202
|
+
"tailwind-merge": "^2.5.0",
|
|
203
|
+
"tailwindcss-animate": "^1.0.7"
|
|
134
204
|
},
|
|
135
205
|
devDependencies: {
|
|
136
|
-
"@types/node": "^
|
|
137
|
-
"@types/react": "^
|
|
138
|
-
"@types/react-dom": "^
|
|
206
|
+
"@types/node": "^22",
|
|
207
|
+
"@types/react": "^19",
|
|
208
|
+
"@types/react-dom": "^19",
|
|
139
209
|
typescript: "^5",
|
|
140
|
-
prisma: "^
|
|
210
|
+
prisma: "^6.0.0",
|
|
141
211
|
tailwindcss: "^3.4.0",
|
|
142
212
|
postcss: "^8.4.0",
|
|
143
213
|
autoprefixer: "^10.4.0"
|
|
@@ -146,10 +216,188 @@ async function init(projectName, options = {}) {
|
|
|
146
216
|
await fs.writeJSON(path.join(projectDir, "package.json"), packageJson, {
|
|
147
217
|
spaces: 2
|
|
148
218
|
});
|
|
149
|
-
await fs.ensureDir(path.join(projectDir, "src/app"));
|
|
150
|
-
await fs.ensureDir(path.join(projectDir, "src/
|
|
219
|
+
await fs.ensureDir(path.join(projectDir, "src/app/(auth)/login"));
|
|
220
|
+
await fs.ensureDir(path.join(projectDir, "src/app/(auth)/signup"));
|
|
221
|
+
await fs.ensureDir(path.join(projectDir, "src/app/(auth)/signout"));
|
|
222
|
+
await fs.ensureDir(path.join(projectDir, "src/app/api/waitlist"));
|
|
223
|
+
await fs.ensureDir(path.join(projectDir, "src/app/dashboard"));
|
|
224
|
+
await fs.ensureDir(path.join(projectDir, "src/components/ui"));
|
|
225
|
+
await fs.ensureDir(path.join(projectDir, "src/config"));
|
|
151
226
|
await fs.ensureDir(path.join(projectDir, "src/lib"));
|
|
152
227
|
await fs.ensureDir(path.join(projectDir, "prisma"));
|
|
228
|
+
await fs.ensureDir(path.join(projectDir, "public"));
|
|
229
|
+
const siteConfig = `export const siteConfig = {
|
|
230
|
+
// Branding
|
|
231
|
+
name: "${branding.productName}",
|
|
232
|
+
description: "${branding.description}",
|
|
233
|
+
|
|
234
|
+
// Theme
|
|
235
|
+
theme: {
|
|
236
|
+
primary: "${branding.primaryColor}",
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// Navigation
|
|
240
|
+
navigation: [
|
|
241
|
+
{ label: "Features", href: "#features" },
|
|
242
|
+
{ label: "Pricing", href: "#pricing" },
|
|
243
|
+
{ label: "FAQ", href: "#faq" },
|
|
244
|
+
],
|
|
245
|
+
|
|
246
|
+
// Hero Section
|
|
247
|
+
hero: {
|
|
248
|
+
badge: "Now in Beta \xB7 Early Access",
|
|
249
|
+
title: "Your Amazing Product",
|
|
250
|
+
highlight: "Headline",
|
|
251
|
+
description: "${branding.description}",
|
|
252
|
+
cta: {
|
|
253
|
+
primary: { label: "Get Started", href: "#waitlist" },
|
|
254
|
+
secondary: { label: "Learn More", href: "#features" },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// Features
|
|
259
|
+
features: [
|
|
260
|
+
{
|
|
261
|
+
icon: "Zap",
|
|
262
|
+
title: "Lightning Fast",
|
|
263
|
+
description: "Built for speed and performance.",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
icon: "Shield",
|
|
267
|
+
title: "Secure by Default",
|
|
268
|
+
description: "Enterprise-grade security out of the box.",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
icon: "Clock",
|
|
272
|
+
title: "Save Time",
|
|
273
|
+
description: "Automate repetitive tasks and focus on what matters.",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
icon: "CheckCircle",
|
|
277
|
+
title: "Easy to Use",
|
|
278
|
+
description: "Intuitive interface that anyone can master.",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
|
|
282
|
+
// Pricing
|
|
283
|
+
pricing: {
|
|
284
|
+
title: "Simple, Transparent Pricing",
|
|
285
|
+
subtitle: "Choose the plan that fits your needs",
|
|
286
|
+
plans: [
|
|
287
|
+
{
|
|
288
|
+
name: "Starter",
|
|
289
|
+
price: 29,
|
|
290
|
+
period: "mo",
|
|
291
|
+
description: "Perfect for individuals",
|
|
292
|
+
features: ["Feature 1", "Feature 2", "Feature 3", "Email support"],
|
|
293
|
+
cta: { label: "Get Started", href: "#waitlist" },
|
|
294
|
+
highlighted: false,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "Pro",
|
|
298
|
+
price: 79,
|
|
299
|
+
period: "mo",
|
|
300
|
+
description: "For growing teams",
|
|
301
|
+
features: ["Everything in Starter", "Feature 4", "Feature 5", "Priority support"],
|
|
302
|
+
cta: { label: "Get Started", href: "#waitlist" },
|
|
303
|
+
highlighted: true,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// FAQ
|
|
309
|
+
faq: [
|
|
310
|
+
{
|
|
311
|
+
question: "What is ${branding.productName}?",
|
|
312
|
+
answer: "${branding.description}",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
question: "How does it work?",
|
|
316
|
+
answer: "Sign up, configure your settings, and start using the platform immediately.",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
question: "Can I cancel anytime?",
|
|
320
|
+
answer: "Yes, you can cancel your subscription at any time with no questions asked.",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
question: "Do you offer refunds?",
|
|
324
|
+
answer: "Yes, we offer a 30-day money-back guarantee.",
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
|
|
328
|
+
// Testimonials
|
|
329
|
+
testimonials: [
|
|
330
|
+
{
|
|
331
|
+
quote: "This product has completely transformed how we work!",
|
|
332
|
+
author: "Jane Smith",
|
|
333
|
+
role: "CEO",
|
|
334
|
+
company: "TechCorp",
|
|
335
|
+
initial: "J",
|
|
336
|
+
color: "from-${branding.primaryColor}-400 to-${branding.primaryColor}-600",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
quote: "The best investment we've made this year.",
|
|
340
|
+
author: "John Doe",
|
|
341
|
+
role: "Product Manager",
|
|
342
|
+
company: "StartupXYZ",
|
|
343
|
+
initial: "J",
|
|
344
|
+
color: "from-blue-400 to-blue-600",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
quote: "Simple, intuitive, and powerful.",
|
|
348
|
+
author: "Sarah Johnson",
|
|
349
|
+
role: "Founder",
|
|
350
|
+
company: "GrowthCo",
|
|
351
|
+
initial: "S",
|
|
352
|
+
color: "from-purple-400 to-purple-600",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
|
|
356
|
+
// Waitlist
|
|
357
|
+
waitlist: {
|
|
358
|
+
title: "Join the Waitlist",
|
|
359
|
+
description: "Be the first to know when we launch. Early adopters get exclusive pricing.",
|
|
360
|
+
badge: "Early Access",
|
|
361
|
+
submitText: "Join Waitlist",
|
|
362
|
+
successMessage: "You're on the list! We'll notify you when we launch.",
|
|
363
|
+
businessTypes: [
|
|
364
|
+
{ value: "startup", label: "Startup" },
|
|
365
|
+
{ value: "small_business", label: "Small Business" },
|
|
366
|
+
{ value: "enterprise", label: "Enterprise" },
|
|
367
|
+
{ value: "agency", label: "Agency" },
|
|
368
|
+
{ value: "freelancer", label: "Freelancer" },
|
|
369
|
+
{ value: "other", label: "Other" },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
// Footer
|
|
374
|
+
footer: {
|
|
375
|
+
copyright: "${branding.productName}",
|
|
376
|
+
links: [
|
|
377
|
+
{ label: "Privacy Policy", href: "/privacy" },
|
|
378
|
+
{ label: "Terms of Service", href: "/terms" },
|
|
379
|
+
{ label: "Contact", href: "mailto:hello@example.com" },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// Auth
|
|
384
|
+
auth: {
|
|
385
|
+
providers: [${config.providers.map((p) => `"${p}"`).join(", ")}],
|
|
386
|
+
redirectAfterLogin: "/dashboard",
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// Links
|
|
390
|
+
links: {
|
|
391
|
+
demo: "/login",
|
|
392
|
+
login: "/login",
|
|
393
|
+
signup: "/signup",
|
|
394
|
+
dashboard: "/dashboard",
|
|
395
|
+
},
|
|
396
|
+
} as const;
|
|
397
|
+
|
|
398
|
+
export type SiteConfig = typeof siteConfig;
|
|
399
|
+
`;
|
|
400
|
+
await fs.writeFile(path.join(projectDir, "src/config/site.ts"), siteConfig);
|
|
153
401
|
const tsconfig = {
|
|
154
402
|
compilerOptions: {
|
|
155
403
|
target: "ES2022",
|
|
@@ -175,7 +423,7 @@ async function init(projectName, options = {}) {
|
|
|
175
423
|
spaces: 2
|
|
176
424
|
});
|
|
177
425
|
const envExample = `# Database
|
|
178
|
-
DATABASE_URL="
|
|
426
|
+
DATABASE_URL="${config.database === "sqlite" ? "file:./dev.db" : `${config.database}://user:password@localhost:5432/mydb`}"
|
|
179
427
|
|
|
180
428
|
# NextAuth
|
|
181
429
|
AUTH_SECRET="your-secret-here"
|
|
@@ -202,36 +450,17 @@ LEMONSQUEEZY_WEBHOOK_SECRET=""
|
|
|
202
450
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
203
451
|
`;
|
|
204
452
|
await fs.writeFile(path.join(projectDir, ".env.example"), envExample);
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
export default defineConfig({
|
|
208
|
-
app: {
|
|
209
|
-
name: "${name}",
|
|
210
|
-
url: process.env.NEXT_PUBLIC_APP_URL,
|
|
211
|
-
description: "Your SaaS application",
|
|
212
|
-
},
|
|
213
|
-
auth: {
|
|
214
|
-
providers: [${config.providers.map((p) => `"${p}"`).join(", ")}],
|
|
215
|
-
pages: {
|
|
216
|
-
signIn: "/login",
|
|
217
|
-
error: "/auth-error",
|
|
218
|
-
verifyRequest: "/verify-request",
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
features: {
|
|
222
|
-
payments: ${config.modules.includes("payments")},
|
|
223
|
-
admin: ${config.modules.includes("admin")},
|
|
224
|
-
blog: ${config.modules.includes("blog")},
|
|
225
|
-
},
|
|
226
|
-
theme: {
|
|
227
|
-
defaultTheme: "system",
|
|
228
|
-
enableSystem: true,
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
`;
|
|
232
|
-
await fs.writeFile(path.join(projectDir, "saas-kit.config.ts"), configFile);
|
|
233
|
-
const layoutFile = `import { SaasKitProvider } from "@saas-kit/core/providers";
|
|
453
|
+
const layoutFile = `import type { Metadata } from "next";
|
|
454
|
+
import { Inter } from "next/font/google";
|
|
234
455
|
import "./globals.css";
|
|
456
|
+
import { siteConfig } from "@/config/site";
|
|
457
|
+
|
|
458
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
459
|
+
|
|
460
|
+
export const metadata: Metadata = {
|
|
461
|
+
title: siteConfig.name,
|
|
462
|
+
description: siteConfig.description,
|
|
463
|
+
};
|
|
235
464
|
|
|
236
465
|
export default function RootLayout({
|
|
237
466
|
children,
|
|
@@ -240,42 +469,148 @@ export default function RootLayout({
|
|
|
240
469
|
}) {
|
|
241
470
|
return (
|
|
242
471
|
<html lang="en" suppressHydrationWarning>
|
|
243
|
-
<body>
|
|
244
|
-
|
|
245
|
-
{children}
|
|
246
|
-
</SaasKitProvider>
|
|
472
|
+
<body className={inter.className}>
|
|
473
|
+
{children}
|
|
247
474
|
</body>
|
|
248
475
|
</html>
|
|
249
476
|
);
|
|
250
477
|
}
|
|
251
478
|
`;
|
|
252
|
-
await fs.writeFile(
|
|
253
|
-
path.join(projectDir, "src/app/layout.tsx"),
|
|
254
|
-
layoutFile
|
|
255
|
-
);
|
|
479
|
+
await fs.writeFile(path.join(projectDir, "src/app/layout.tsx"), layoutFile);
|
|
256
480
|
const globalsCss = `@tailwind base;
|
|
257
481
|
@tailwind components;
|
|
258
482
|
@tailwind utilities;
|
|
259
483
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
484
|
+
@layer base {
|
|
485
|
+
:root {
|
|
486
|
+
--background: 0 0% 100%;
|
|
487
|
+
--foreground: 222.2 84% 4.9%;
|
|
488
|
+
--card: 0 0% 100%;
|
|
489
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
490
|
+
--popover: 0 0% 100%;
|
|
491
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
492
|
+
--primary: 142.1 76.2% 36.3%;
|
|
493
|
+
--primary-foreground: 355.7 100% 97.3%;
|
|
494
|
+
--secondary: 210 40% 96.1%;
|
|
495
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
496
|
+
--muted: 210 40% 96.1%;
|
|
497
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
498
|
+
--accent: 210 40% 96.1%;
|
|
499
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
500
|
+
--destructive: 0 84.2% 60.2%;
|
|
501
|
+
--destructive-foreground: 210 40% 98%;
|
|
502
|
+
--border: 214.3 31.8% 91.4%;
|
|
503
|
+
--input: 214.3 31.8% 91.4%;
|
|
504
|
+
--ring: 142.1 76.2% 36.3%;
|
|
505
|
+
--radius: 0.5rem;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.dark {
|
|
509
|
+
--background: 222.2 84% 4.9%;
|
|
510
|
+
--foreground: 210 40% 98%;
|
|
511
|
+
--card: 222.2 84% 4.9%;
|
|
512
|
+
--card-foreground: 210 40% 98%;
|
|
513
|
+
--popover: 222.2 84% 4.9%;
|
|
514
|
+
--popover-foreground: 210 40% 98%;
|
|
515
|
+
--primary: 142.1 76.2% 36.3%;
|
|
516
|
+
--primary-foreground: 355.7 100% 97.3%;
|
|
517
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
518
|
+
--secondary-foreground: 210 40% 98%;
|
|
519
|
+
--muted: 217.2 32.6% 17.5%;
|
|
520
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
521
|
+
--accent: 217.2 32.6% 17.5%;
|
|
522
|
+
--accent-foreground: 210 40% 98%;
|
|
523
|
+
--destructive: 0 62.8% 30.6%;
|
|
524
|
+
--destructive-foreground: 210 40% 98%;
|
|
525
|
+
--border: 217.2 32.6% 17.5%;
|
|
526
|
+
--input: 217.2 32.6% 17.5%;
|
|
527
|
+
--ring: 142.1 76.2% 36.3%;
|
|
528
|
+
}
|
|
265
529
|
}
|
|
266
530
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
531
|
+
@layer base {
|
|
532
|
+
* {
|
|
533
|
+
@apply border-border;
|
|
534
|
+
}
|
|
535
|
+
body {
|
|
536
|
+
@apply bg-background text-foreground;
|
|
537
|
+
}
|
|
272
538
|
}
|
|
273
539
|
`;
|
|
274
|
-
await fs.writeFile(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
540
|
+
await fs.writeFile(path.join(projectDir, "src/app/globals.css"), globalsCss);
|
|
541
|
+
const hasLanding = config.modules.includes("landing");
|
|
542
|
+
const pageTsx = hasLanding ? `"use client";
|
|
543
|
+
|
|
544
|
+
import { siteConfig } from "@/config/site";
|
|
545
|
+
import {
|
|
546
|
+
Navbar,
|
|
547
|
+
Hero,
|
|
548
|
+
Features,
|
|
549
|
+
Pricing,
|
|
550
|
+
Faq,
|
|
551
|
+
Testimonials,
|
|
552
|
+
Waitlist,
|
|
553
|
+
Footer,
|
|
554
|
+
} from "@victusvinceere/saas-landing/components";
|
|
555
|
+
|
|
556
|
+
export default function Home() {
|
|
557
|
+
return (
|
|
558
|
+
<div className="min-h-screen">
|
|
559
|
+
<Navbar
|
|
560
|
+
brand={siteConfig.name}
|
|
561
|
+
links={siteConfig.navigation}
|
|
562
|
+
cta={{ label: "Get Started", href: "#waitlist" }}
|
|
563
|
+
/>
|
|
564
|
+
|
|
565
|
+
<Hero
|
|
566
|
+
badge={siteConfig.hero.badge}
|
|
567
|
+
title={siteConfig.hero.title}
|
|
568
|
+
highlight={siteConfig.hero.highlight}
|
|
569
|
+
description={siteConfig.hero.description}
|
|
570
|
+
primaryCta={siteConfig.hero.cta.primary}
|
|
571
|
+
secondaryCta={siteConfig.hero.cta.secondary}
|
|
572
|
+
/>
|
|
573
|
+
|
|
574
|
+
<Features
|
|
575
|
+
title="Features"
|
|
576
|
+
subtitle="Everything you need to succeed"
|
|
577
|
+
features={siteConfig.features}
|
|
578
|
+
/>
|
|
579
|
+
|
|
580
|
+
<Pricing
|
|
581
|
+
title={siteConfig.pricing.title}
|
|
582
|
+
subtitle={siteConfig.pricing.subtitle}
|
|
583
|
+
plans={siteConfig.pricing.plans}
|
|
584
|
+
/>
|
|
585
|
+
|
|
586
|
+
<Testimonials
|
|
587
|
+
title="What Our Customers Say"
|
|
588
|
+
testimonials={siteConfig.testimonials}
|
|
589
|
+
/>
|
|
590
|
+
|
|
591
|
+
<Faq
|
|
592
|
+
title="Frequently Asked Questions"
|
|
593
|
+
items={siteConfig.faq}
|
|
594
|
+
/>
|
|
595
|
+
|
|
596
|
+
<Waitlist
|
|
597
|
+
title={siteConfig.waitlist.title}
|
|
598
|
+
description={siteConfig.waitlist.description}
|
|
599
|
+
badge={siteConfig.waitlist.badge}
|
|
600
|
+
submitText={siteConfig.waitlist.submitText}
|
|
601
|
+
successMessage={siteConfig.waitlist.successMessage}
|
|
602
|
+
businessTypes={siteConfig.waitlist.businessTypes}
|
|
603
|
+
/>
|
|
604
|
+
|
|
605
|
+
<Footer
|
|
606
|
+
brand={siteConfig.name}
|
|
607
|
+
links={siteConfig.footer.links}
|
|
608
|
+
copyright={\`\xA9 \${new Date().getFullYear()} \${siteConfig.footer.copyright}. All rights reserved.\`}
|
|
609
|
+
/>
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
` : `export default function Home() {
|
|
279
614
|
return (
|
|
280
615
|
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
|
281
616
|
<h1 className="text-4xl font-bold">Welcome to ${name}</h1>
|
|
@@ -287,17 +622,390 @@ export default function RootLayout({
|
|
|
287
622
|
}
|
|
288
623
|
`;
|
|
289
624
|
await fs.writeFile(path.join(projectDir, "src/app/page.tsx"), pageTsx);
|
|
625
|
+
const loginPage = `"use client";
|
|
626
|
+
|
|
627
|
+
import { signIn } from "next-auth/react";
|
|
628
|
+
import { useState } from "react";
|
|
629
|
+
import Link from "next/link";
|
|
630
|
+
import { siteConfig } from "@/config/site";
|
|
631
|
+
|
|
632
|
+
export default function LoginPage() {
|
|
633
|
+
const [email, setEmail] = useState("");
|
|
634
|
+
const [loading, setLoading] = useState(false);
|
|
635
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
636
|
+
|
|
637
|
+
const handleMagicLink = async (e: React.FormEvent) => {
|
|
638
|
+
e.preventDefault();
|
|
639
|
+
setLoading(true);
|
|
640
|
+
await signIn("resend", { email, callbackUrl: siteConfig.auth.redirectAfterLogin });
|
|
641
|
+
setEmailSent(true);
|
|
642
|
+
setLoading(false);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
if (emailSent) {
|
|
646
|
+
return (
|
|
647
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
648
|
+
<div className="w-full max-w-md text-center">
|
|
649
|
+
<h1 className="text-2xl font-bold mb-4">Check your email</h1>
|
|
650
|
+
<p className="text-muted-foreground">
|
|
651
|
+
We sent a magic link to <strong>{email}</strong>
|
|
652
|
+
</p>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return (
|
|
659
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
660
|
+
<div className="w-full max-w-md space-y-6">
|
|
661
|
+
<div className="text-center">
|
|
662
|
+
<h1 className="text-2xl font-bold">Welcome back</h1>
|
|
663
|
+
<p className="text-muted-foreground">Sign in to your account</p>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
<div className="space-y-4">
|
|
667
|
+
<button
|
|
668
|
+
onClick={() => signIn("google", { callbackUrl: siteConfig.auth.redirectAfterLogin })}
|
|
669
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg border px-4 py-3 hover:bg-muted"
|
|
670
|
+
>
|
|
671
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
672
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
673
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
674
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
675
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
676
|
+
</svg>
|
|
677
|
+
Continue with Google
|
|
678
|
+
</button>
|
|
679
|
+
|
|
680
|
+
<div className="relative">
|
|
681
|
+
<div className="absolute inset-0 flex items-center">
|
|
682
|
+
<span className="w-full border-t" />
|
|
683
|
+
</div>
|
|
684
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
685
|
+
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<form onSubmit={handleMagicLink} className="space-y-4">
|
|
690
|
+
<input
|
|
691
|
+
type="email"
|
|
692
|
+
placeholder="email@example.com"
|
|
693
|
+
value={email}
|
|
694
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
695
|
+
required
|
|
696
|
+
className="w-full rounded-lg border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
697
|
+
/>
|
|
698
|
+
<button
|
|
699
|
+
type="submit"
|
|
700
|
+
disabled={loading}
|
|
701
|
+
className="w-full rounded-lg bg-primary px-4 py-3 text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
702
|
+
>
|
|
703
|
+
{loading ? "Sending..." : "Send Magic Link"}
|
|
704
|
+
</button>
|
|
705
|
+
</form>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
709
|
+
Don't have an account?{" "}
|
|
710
|
+
<Link href="/signup" className="text-primary hover:underline">
|
|
711
|
+
Sign up
|
|
712
|
+
</Link>
|
|
713
|
+
</p>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
`;
|
|
719
|
+
await fs.writeFile(
|
|
720
|
+
path.join(projectDir, "src/app/(auth)/login/page.tsx"),
|
|
721
|
+
loginPage
|
|
722
|
+
);
|
|
723
|
+
const signupPage = `"use client";
|
|
724
|
+
|
|
725
|
+
import { signIn } from "next-auth/react";
|
|
726
|
+
import { useState } from "react";
|
|
727
|
+
import Link from "next/link";
|
|
728
|
+
import { siteConfig } from "@/config/site";
|
|
729
|
+
|
|
730
|
+
export default function SignupPage() {
|
|
731
|
+
const [email, setEmail] = useState("");
|
|
732
|
+
const [loading, setLoading] = useState(false);
|
|
733
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
734
|
+
|
|
735
|
+
const handleMagicLink = async (e: React.FormEvent) => {
|
|
736
|
+
e.preventDefault();
|
|
737
|
+
setLoading(true);
|
|
738
|
+
await signIn("resend", { email, callbackUrl: siteConfig.auth.redirectAfterLogin });
|
|
739
|
+
setEmailSent(true);
|
|
740
|
+
setLoading(false);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
if (emailSent) {
|
|
744
|
+
return (
|
|
745
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
746
|
+
<div className="w-full max-w-md text-center">
|
|
747
|
+
<h1 className="text-2xl font-bold mb-4">Check your email</h1>
|
|
748
|
+
<p className="text-muted-foreground">
|
|
749
|
+
We sent a magic link to <strong>{email}</strong>
|
|
750
|
+
</p>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return (
|
|
757
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
758
|
+
<div className="w-full max-w-md space-y-6">
|
|
759
|
+
<div className="text-center">
|
|
760
|
+
<h1 className="text-2xl font-bold">Create an account</h1>
|
|
761
|
+
<p className="text-muted-foreground">Get started with {siteConfig.name}</p>
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
<div className="space-y-4">
|
|
765
|
+
<button
|
|
766
|
+
onClick={() => signIn("google", { callbackUrl: siteConfig.auth.redirectAfterLogin })}
|
|
767
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg border px-4 py-3 hover:bg-muted"
|
|
768
|
+
>
|
|
769
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
770
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
771
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
772
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
773
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
774
|
+
</svg>
|
|
775
|
+
Continue with Google
|
|
776
|
+
</button>
|
|
777
|
+
|
|
778
|
+
<div className="relative">
|
|
779
|
+
<div className="absolute inset-0 flex items-center">
|
|
780
|
+
<span className="w-full border-t" />
|
|
781
|
+
</div>
|
|
782
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
783
|
+
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
<form onSubmit={handleMagicLink} className="space-y-4">
|
|
788
|
+
<input
|
|
789
|
+
type="email"
|
|
790
|
+
placeholder="email@example.com"
|
|
791
|
+
value={email}
|
|
792
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
793
|
+
required
|
|
794
|
+
className="w-full rounded-lg border px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
795
|
+
/>
|
|
796
|
+
<button
|
|
797
|
+
type="submit"
|
|
798
|
+
disabled={loading}
|
|
799
|
+
className="w-full rounded-lg bg-primary px-4 py-3 text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
800
|
+
>
|
|
801
|
+
{loading ? "Sending..." : "Send Magic Link"}
|
|
802
|
+
</button>
|
|
803
|
+
</form>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
807
|
+
Already have an account?{" "}
|
|
808
|
+
<Link href="/login" className="text-primary hover:underline">
|
|
809
|
+
Sign in
|
|
810
|
+
</Link>
|
|
811
|
+
</p>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
`;
|
|
817
|
+
await fs.writeFile(
|
|
818
|
+
path.join(projectDir, "src/app/(auth)/signup/page.tsx"),
|
|
819
|
+
signupPage
|
|
820
|
+
);
|
|
821
|
+
const signoutPage = `"use client";
|
|
822
|
+
|
|
823
|
+
import { signOut } from "next-auth/react";
|
|
824
|
+
import Link from "next/link";
|
|
825
|
+
import { siteConfig } from "@/config/site";
|
|
826
|
+
|
|
827
|
+
export default function SignOutPage() {
|
|
828
|
+
return (
|
|
829
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
830
|
+
<div className="w-full max-w-md space-y-6 text-center">
|
|
831
|
+
<div>
|
|
832
|
+
<h1 className="text-2xl font-bold">Sign out</h1>
|
|
833
|
+
<p className="text-muted-foreground mt-2">
|
|
834
|
+
Are you sure you want to sign out?
|
|
835
|
+
</p>
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<div className="space-y-3">
|
|
839
|
+
<button
|
|
840
|
+
onClick={() => signOut({ callbackUrl: "/" })}
|
|
841
|
+
className="w-full rounded-lg bg-destructive px-4 py-3 text-destructive-foreground hover:bg-destructive/90"
|
|
842
|
+
>
|
|
843
|
+
Yes, sign me out
|
|
844
|
+
</button>
|
|
845
|
+
<Link
|
|
846
|
+
href={siteConfig.links.dashboard}
|
|
847
|
+
className="block w-full rounded-lg border px-4 py-3 hover:bg-muted"
|
|
848
|
+
>
|
|
849
|
+
Cancel, go back
|
|
850
|
+
</Link>
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
`;
|
|
857
|
+
await fs.writeFile(
|
|
858
|
+
path.join(projectDir, "src/app/(auth)/signout/page.tsx"),
|
|
859
|
+
signoutPage
|
|
860
|
+
);
|
|
861
|
+
const dashboardPage = `export default function DashboardPage() {
|
|
862
|
+
return (
|
|
863
|
+
<div className="p-8">
|
|
864
|
+
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
|
865
|
+
<p className="text-muted-foreground">
|
|
866
|
+
Welcome to your dashboard. Start building your product here.
|
|
867
|
+
</p>
|
|
868
|
+
</div>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
`;
|
|
872
|
+
await fs.writeFile(
|
|
873
|
+
path.join(projectDir, "src/app/dashboard/page.tsx"),
|
|
874
|
+
dashboardPage
|
|
875
|
+
);
|
|
876
|
+
const dashboardLayout = `import Link from "next/link";
|
|
877
|
+
|
|
878
|
+
export default function DashboardLayout({
|
|
879
|
+
children,
|
|
880
|
+
}: {
|
|
881
|
+
children: React.ReactNode;
|
|
882
|
+
}) {
|
|
883
|
+
return (
|
|
884
|
+
<div className="min-h-screen bg-muted/30">
|
|
885
|
+
<nav className="border-b bg-background px-6 py-4">
|
|
886
|
+
<div className="flex items-center justify-between">
|
|
887
|
+
<span className="text-xl font-bold">Dashboard</span>
|
|
888
|
+
<Link
|
|
889
|
+
href="/signout"
|
|
890
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
891
|
+
>
|
|
892
|
+
Sign out
|
|
893
|
+
</Link>
|
|
894
|
+
</div>
|
|
895
|
+
</nav>
|
|
896
|
+
<main>{children}</main>
|
|
897
|
+
</div>
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
`;
|
|
901
|
+
await fs.writeFile(
|
|
902
|
+
path.join(projectDir, "src/app/dashboard/layout.tsx"),
|
|
903
|
+
dashboardLayout
|
|
904
|
+
);
|
|
905
|
+
const waitlistRoute = `import { NextResponse } from "next/server";
|
|
906
|
+
|
|
907
|
+
export async function POST(request: Request) {
|
|
908
|
+
try {
|
|
909
|
+
const body = await request.json();
|
|
910
|
+
const { email, name, businessType } = body;
|
|
911
|
+
|
|
912
|
+
if (!email || !name) {
|
|
913
|
+
return NextResponse.json(
|
|
914
|
+
{ error: "Email and name are required" },
|
|
915
|
+
{ status: 400 }
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// TODO: Save to database
|
|
920
|
+
// await prisma.waitlistEntry.create({
|
|
921
|
+
// data: { email, name, businessType },
|
|
922
|
+
// });
|
|
923
|
+
|
|
924
|
+
console.log("Waitlist signup:", { email, name, businessType });
|
|
925
|
+
|
|
926
|
+
return NextResponse.json({
|
|
927
|
+
message: "Successfully joined the waitlist!",
|
|
928
|
+
});
|
|
929
|
+
} catch (error) {
|
|
930
|
+
console.error("Waitlist signup error:", error);
|
|
931
|
+
return NextResponse.json(
|
|
932
|
+
{ error: "Something went wrong" },
|
|
933
|
+
{ status: 500 }
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
`;
|
|
938
|
+
await fs.writeFile(
|
|
939
|
+
path.join(projectDir, "src/app/api/waitlist/route.ts"),
|
|
940
|
+
waitlistRoute
|
|
941
|
+
);
|
|
942
|
+
const utilsTs = `import { type ClassValue, clsx } from "clsx";
|
|
943
|
+
import { twMerge } from "tailwind-merge";
|
|
944
|
+
|
|
945
|
+
export function cn(...inputs: ClassValue[]) {
|
|
946
|
+
return twMerge(clsx(inputs));
|
|
947
|
+
}
|
|
948
|
+
`;
|
|
949
|
+
await fs.writeFile(path.join(projectDir, "src/lib/utils.ts"), utilsTs);
|
|
290
950
|
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
291
951
|
module.exports = {
|
|
292
952
|
darkMode: ["class"],
|
|
293
953
|
content: [
|
|
294
954
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
|
295
|
-
"./node_modules/@saas
|
|
955
|
+
"./node_modules/@victusvinceere/saas-*/dist/**/*.{js,ts,jsx,tsx}",
|
|
296
956
|
],
|
|
297
957
|
theme: {
|
|
298
|
-
|
|
958
|
+
container: {
|
|
959
|
+
center: true,
|
|
960
|
+
padding: "2rem",
|
|
961
|
+
screens: {
|
|
962
|
+
"2xl": "1400px",
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
extend: {
|
|
966
|
+
colors: {
|
|
967
|
+
border: "hsl(var(--border))",
|
|
968
|
+
input: "hsl(var(--input))",
|
|
969
|
+
ring: "hsl(var(--ring))",
|
|
970
|
+
background: "hsl(var(--background))",
|
|
971
|
+
foreground: "hsl(var(--foreground))",
|
|
972
|
+
primary: {
|
|
973
|
+
DEFAULT: "hsl(var(--primary))",
|
|
974
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
975
|
+
},
|
|
976
|
+
secondary: {
|
|
977
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
978
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
979
|
+
},
|
|
980
|
+
destructive: {
|
|
981
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
982
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
983
|
+
},
|
|
984
|
+
muted: {
|
|
985
|
+
DEFAULT: "hsl(var(--muted))",
|
|
986
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
987
|
+
},
|
|
988
|
+
accent: {
|
|
989
|
+
DEFAULT: "hsl(var(--accent))",
|
|
990
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
991
|
+
},
|
|
992
|
+
popover: {
|
|
993
|
+
DEFAULT: "hsl(var(--popover))",
|
|
994
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
995
|
+
},
|
|
996
|
+
card: {
|
|
997
|
+
DEFAULT: "hsl(var(--card))",
|
|
998
|
+
foreground: "hsl(var(--card-foreground))",
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
borderRadius: {
|
|
1002
|
+
lg: "var(--radius)",
|
|
1003
|
+
md: "calc(var(--radius) - 2px)",
|
|
1004
|
+
sm: "calc(var(--radius) - 4px)",
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
299
1007
|
},
|
|
300
|
-
plugins: [],
|
|
1008
|
+
plugins: [require("tailwindcss-animate")],
|
|
301
1009
|
};
|
|
302
1010
|
`;
|
|
303
1011
|
await fs.writeFile(
|
|
@@ -317,9 +1025,13 @@ module.exports = {
|
|
|
317
1025
|
);
|
|
318
1026
|
const nextConfig = `/** @type {import('next').NextConfig} */
|
|
319
1027
|
const nextConfig = {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1028
|
+
transpilePackages: [
|
|
1029
|
+
"@victusvinceere/saas-core",
|
|
1030
|
+
"@victusvinceere/saas-landing",
|
|
1031
|
+
"@victusvinceere/saas-admin",
|
|
1032
|
+
"@victusvinceere/saas-payments",
|
|
1033
|
+
"@victusvinceere/saas-blog",
|
|
1034
|
+
],
|
|
323
1035
|
};
|
|
324
1036
|
|
|
325
1037
|
module.exports = nextConfig;
|
|
@@ -361,17 +1073,97 @@ yarn-error.log*
|
|
|
361
1073
|
next-env.d.ts
|
|
362
1074
|
`;
|
|
363
1075
|
await fs.writeFile(path.join(projectDir, ".gitignore"), gitignore);
|
|
1076
|
+
const prismaSchema = `generator client {
|
|
1077
|
+
provider = "prisma-client-js"
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
datasource db {
|
|
1081
|
+
provider = "${config.database}"
|
|
1082
|
+
url = env("DATABASE_URL")
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
model Account {
|
|
1086
|
+
id String @id @default(cuid())
|
|
1087
|
+
userId String
|
|
1088
|
+
type String
|
|
1089
|
+
provider String
|
|
1090
|
+
providerAccountId String
|
|
1091
|
+
refresh_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
|
|
1092
|
+
access_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
|
|
1093
|
+
expires_at Int?
|
|
1094
|
+
token_type String?
|
|
1095
|
+
scope String?
|
|
1096
|
+
id_token String? ${config.database === "mysql" ? "@db.Text" : config.database === "postgresql" ? "@db.Text" : ""}
|
|
1097
|
+
session_state String?
|
|
1098
|
+
|
|
1099
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
1100
|
+
|
|
1101
|
+
@@unique([provider, providerAccountId])
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
model Session {
|
|
1105
|
+
id String @id @default(cuid())
|
|
1106
|
+
sessionToken String @unique
|
|
1107
|
+
userId String
|
|
1108
|
+
expires DateTime
|
|
1109
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
model User {
|
|
1113
|
+
id String @id @default(cuid())
|
|
1114
|
+
name String?
|
|
1115
|
+
email String? @unique
|
|
1116
|
+
emailVerified DateTime?
|
|
1117
|
+
image String?
|
|
1118
|
+
role String @default("USER")
|
|
1119
|
+
accounts Account[]
|
|
1120
|
+
sessions Session[]
|
|
1121
|
+
createdAt DateTime @default(now())
|
|
1122
|
+
updatedAt DateTime @updatedAt
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
model VerificationToken {
|
|
1126
|
+
identifier String
|
|
1127
|
+
token String @unique
|
|
1128
|
+
expires DateTime
|
|
1129
|
+
|
|
1130
|
+
@@unique([identifier, token])
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
model WaitlistEntry {
|
|
1134
|
+
id String @id @default(cuid())
|
|
1135
|
+
email String @unique
|
|
1136
|
+
name String
|
|
1137
|
+
businessType String?
|
|
1138
|
+
createdAt DateTime @default(now())
|
|
1139
|
+
}
|
|
1140
|
+
`;
|
|
1141
|
+
await fs.writeFile(
|
|
1142
|
+
path.join(projectDir, "prisma/schema.prisma"),
|
|
1143
|
+
prismaSchema
|
|
1144
|
+
);
|
|
364
1145
|
spinner.succeed("Project created successfully!");
|
|
365
1146
|
logger.log("");
|
|
366
|
-
logger.log(chalk2.bold("
|
|
1147
|
+
logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
1148
|
+
logger.log(chalk2.bold(" Next steps:"));
|
|
1149
|
+
logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
367
1150
|
logger.log("");
|
|
368
1151
|
logger.log(chalk2.cyan(` cd ${name}`));
|
|
369
|
-
logger.log(chalk2.cyan("
|
|
1152
|
+
logger.log(chalk2.cyan(" pnpm install"));
|
|
370
1153
|
logger.log(chalk2.cyan(" cp .env.example .env.local"));
|
|
371
|
-
logger.log(chalk2.
|
|
372
|
-
logger.log(chalk2.cyan("
|
|
1154
|
+
logger.log(chalk2.gray(" # Edit .env.local with your credentials"));
|
|
1155
|
+
logger.log(chalk2.cyan(" pnpm db:push"));
|
|
1156
|
+
logger.log(chalk2.cyan(" pnpm dev"));
|
|
1157
|
+
logger.log("");
|
|
1158
|
+
logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
1159
|
+
logger.log(chalk2.bold(" Customization:"));
|
|
1160
|
+
logger.log(chalk2.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
1161
|
+
logger.log("");
|
|
1162
|
+
logger.log(chalk2.gray(" \u2022 Edit src/config/site.ts to customize content"));
|
|
1163
|
+
logger.log(chalk2.gray(" \u2022 Update src/app/page.tsx to modify landing page"));
|
|
1164
|
+
logger.log(chalk2.gray(" \u2022 Build features in src/app/dashboard/"));
|
|
373
1165
|
logger.log("");
|
|
374
|
-
logger.success("Happy building!");
|
|
1166
|
+
logger.success("Happy building! \u{1F680}");
|
|
375
1167
|
} catch (error) {
|
|
376
1168
|
spinner.fail("Failed to create project");
|
|
377
1169
|
logger.error(String(error));
|
|
@@ -747,7 +1539,7 @@ export const POST = createWebhookHandler({
|
|
|
747
1539
|
// src/index.ts
|
|
748
1540
|
var program = new Command();
|
|
749
1541
|
program.name("saas-kit").description("CLI tool for scaffolding SaaS Kit projects").version("0.1.0");
|
|
750
|
-
program.command("init [name]").description("Create a new SaaS Kit project").option("-t, --template <template>", "Use a specific template").action((name, options) => {
|
|
1542
|
+
program.command("init [name]").description("Create a new SaaS Kit project").option("-t, --template <template>", "Use a specific template").option("-y, --yes", "Use defaults without prompting").option("--product <name>", "Product name").option("--description <desc>", "Product description").option("--color <color>", "Primary color (green, blue, purple, orange, red, indigo)").action((name, options) => {
|
|
751
1543
|
printBanner();
|
|
752
1544
|
init(name, options);
|
|
753
1545
|
});
|