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,368 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates UI components and pages
|
|
6
|
+
* @param {string} projectPath - Path where the project is located
|
|
7
|
+
*/
|
|
8
|
+
export async function generateUI(projectPath) {
|
|
9
|
+
// Create dashboard page
|
|
10
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard'));
|
|
11
|
+
|
|
12
|
+
const dashboardPage = `import { auth } from '@/lib/auth';
|
|
13
|
+
import { redirect } from 'next/navigation';
|
|
14
|
+
import { db } from '@/lib/db';
|
|
15
|
+
|
|
16
|
+
export default async function DashboardPage() {
|
|
17
|
+
const session = await auth();
|
|
18
|
+
|
|
19
|
+
if (!session?.user) {
|
|
20
|
+
redirect('/login');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const user = await db.user.findUnique({
|
|
24
|
+
where: { id: session.user.id },
|
|
25
|
+
include: { subscription: true },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="container mx-auto px-4 py-8">
|
|
30
|
+
<div className="mb-8">
|
|
31
|
+
<h1 className="text-3xl font-bold">Dashboard</h1>
|
|
32
|
+
<p className="text-muted-foreground">
|
|
33
|
+
Welcome back, {session.user.name || session.user.email}!
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
38
|
+
<div className="rounded-lg border p-6">
|
|
39
|
+
<h3 className="text-sm font-medium text-muted-foreground">Status</h3>
|
|
40
|
+
<p className="mt-2 text-3xl font-bold">
|
|
41
|
+
{user?.subscription?.status === 'ACTIVE' ? 'Active' : 'Free'}
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="rounded-lg border p-6">
|
|
46
|
+
<h3 className="text-sm font-medium text-muted-foreground">Account Type</h3>
|
|
47
|
+
<p className="mt-2 text-3xl font-bold capitalize">{user?.role.toLowerCase()}</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="rounded-lg border p-6">
|
|
51
|
+
<h3 className="text-sm font-medium text-muted-foreground">Member Since</h3>
|
|
52
|
+
<p className="mt-2 text-xl font-bold">
|
|
53
|
+
{new Date(user?.createdAt || '').toLocaleDateString()}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="mt-8">
|
|
59
|
+
<h2 className="text-2xl font-bold mb-4">Quick Actions</h2>
|
|
60
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
61
|
+
<a
|
|
62
|
+
href="/dashboard/billing"
|
|
63
|
+
className="rounded-lg border p-6 hover:border-indigo-500 transition-colors"
|
|
64
|
+
>
|
|
65
|
+
<h3 className="text-lg font-semibold mb-2">Manage Subscription</h3>
|
|
66
|
+
<p className="text-sm text-muted-foreground">
|
|
67
|
+
View and manage your billing and subscription
|
|
68
|
+
</p>
|
|
69
|
+
</a>
|
|
70
|
+
<a
|
|
71
|
+
href="/dashboard/settings"
|
|
72
|
+
className="rounded-lg border p-6 hover:border-indigo-500 transition-colors"
|
|
73
|
+
>
|
|
74
|
+
<h3 className="text-lg font-semibold mb-2">Account Settings</h3>
|
|
75
|
+
<p className="text-sm text-muted-foreground">
|
|
76
|
+
Update your profile and preferences
|
|
77
|
+
</p>
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
await writeFile(
|
|
87
|
+
path.join(projectPath, 'src/app/dashboard/page.tsx'),
|
|
88
|
+
dashboardPage
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Create dashboard layout with navigation
|
|
92
|
+
const dashboardLayout = `import { auth } from '@/lib/auth';
|
|
93
|
+
import { redirect } from 'next/navigation';
|
|
94
|
+
import Link from 'next/link';
|
|
95
|
+
import { signOut } from '@/lib/auth';
|
|
96
|
+
|
|
97
|
+
export default async function DashboardLayout({
|
|
98
|
+
children,
|
|
99
|
+
}: {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
}) {
|
|
102
|
+
const session = await auth();
|
|
103
|
+
|
|
104
|
+
if (!session?.user) {
|
|
105
|
+
redirect('/login');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="min-h-screen bg-gray-50">
|
|
110
|
+
<nav className="bg-white border-b">
|
|
111
|
+
<div className="container mx-auto px-4 py-4">
|
|
112
|
+
<div className="flex items-center justify-between">
|
|
113
|
+
<Link href="/dashboard" className="text-xl font-bold">
|
|
114
|
+
Dashboard
|
|
115
|
+
</Link>
|
|
116
|
+
<div className="flex items-center gap-6">
|
|
117
|
+
<Link href="/dashboard" className="text-sm hover:text-indigo-600">
|
|
118
|
+
Home
|
|
119
|
+
</Link>
|
|
120
|
+
<Link href="/dashboard/billing" className="text-sm hover:text-indigo-600">
|
|
121
|
+
Billing
|
|
122
|
+
</Link>
|
|
123
|
+
<Link href="/dashboard/settings" className="text-sm hover:text-indigo-600">
|
|
124
|
+
Settings
|
|
125
|
+
</Link>
|
|
126
|
+
<form
|
|
127
|
+
action={async () => {
|
|
128
|
+
'use server';
|
|
129
|
+
await signOut({ redirectTo: '/login' });
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<button
|
|
133
|
+
type="submit"
|
|
134
|
+
className="text-sm text-red-600 hover:text-red-700"
|
|
135
|
+
>
|
|
136
|
+
Sign Out
|
|
137
|
+
</button>
|
|
138
|
+
</form>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</nav>
|
|
143
|
+
<main>{children}</main>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
await writeFile(
|
|
150
|
+
path.join(projectPath, 'src/app/dashboard/layout.tsx'),
|
|
151
|
+
dashboardLayout
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Create settings page
|
|
155
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard/settings'));
|
|
156
|
+
|
|
157
|
+
const settingsPage = `import { auth } from '@/lib/auth';
|
|
158
|
+
import { redirect } from 'next/navigation';
|
|
159
|
+
import { db } from '@/lib/db';
|
|
160
|
+
import { SettingsForm } from '@/components/settings-form';
|
|
161
|
+
|
|
162
|
+
export default async function SettingsPage() {
|
|
163
|
+
const session = await auth();
|
|
164
|
+
|
|
165
|
+
if (!session?.user) {
|
|
166
|
+
redirect('/login');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const user = await db.user.findUnique({
|
|
170
|
+
where: { id: session.user.id },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!user) {
|
|
174
|
+
redirect('/login');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="container mx-auto px-4 py-8">
|
|
179
|
+
<h1 className="text-3xl font-bold mb-8">Settings</h1>
|
|
180
|
+
|
|
181
|
+
<div className="max-w-2xl">
|
|
182
|
+
<div className="rounded-lg border p-6 mb-6">
|
|
183
|
+
<h2 className="text-xl font-semibold mb-4">Profile Information</h2>
|
|
184
|
+
<SettingsForm user={user} />
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="rounded-lg border p-6">
|
|
188
|
+
<h2 className="text-xl font-semibold mb-4">Account Information</h2>
|
|
189
|
+
<dl className="space-y-4">
|
|
190
|
+
<div>
|
|
191
|
+
<dt className="text-sm font-medium text-muted-foreground">Email</dt>
|
|
192
|
+
<dd className="mt-1 text-sm">{user.email}</dd>
|
|
193
|
+
</div>
|
|
194
|
+
<div>
|
|
195
|
+
<dt className="text-sm font-medium text-muted-foreground">Account Created</dt>
|
|
196
|
+
<dd className="mt-1 text-sm">{new Date(user.createdAt).toLocaleDateString()}</dd>
|
|
197
|
+
</div>
|
|
198
|
+
<div>
|
|
199
|
+
<dt className="text-sm font-medium text-muted-foreground">Last Updated</dt>
|
|
200
|
+
<dd className="mt-1 text-sm">{new Date(user.updatedAt).toLocaleDateString()}</dd>
|
|
201
|
+
</div>
|
|
202
|
+
</dl>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
await writeFile(
|
|
211
|
+
path.join(projectPath, 'src/app/dashboard/settings/page.tsx'),
|
|
212
|
+
settingsPage
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Create settings form component
|
|
216
|
+
const settingsForm = `'use client';
|
|
217
|
+
|
|
218
|
+
import { useState } from 'react';
|
|
219
|
+
import { useRouter } from 'next/navigation';
|
|
220
|
+
|
|
221
|
+
interface SettingsFormProps {
|
|
222
|
+
user: {
|
|
223
|
+
id: string;
|
|
224
|
+
name: string | null;
|
|
225
|
+
email: string;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function SettingsForm({ user }: SettingsFormProps) {
|
|
230
|
+
const router = useRouter();
|
|
231
|
+
const [name, setName] = useState(user.name || '');
|
|
232
|
+
const [loading, setLoading] = useState(false);
|
|
233
|
+
const [message, setMessage] = useState('');
|
|
234
|
+
|
|
235
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
setLoading(true);
|
|
238
|
+
setMessage('');
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch('/api/user/update', {
|
|
242
|
+
method: 'PATCH',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({ name }),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
throw new Error('Failed to update profile');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setMessage('Profile updated successfully!');
|
|
252
|
+
router.refresh();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
setMessage('Failed to update profile. Please try again.');
|
|
255
|
+
} finally {
|
|
256
|
+
setLoading(false);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
262
|
+
{message && (
|
|
263
|
+
<div
|
|
264
|
+
className={\`rounded-md p-4 \${
|
|
265
|
+
message.includes('success') ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
|
|
266
|
+
}\`}
|
|
267
|
+
>
|
|
268
|
+
<p className="text-sm">{message}</p>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
<div>
|
|
273
|
+
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
|
274
|
+
Name
|
|
275
|
+
</label>
|
|
276
|
+
<input
|
|
277
|
+
type="text"
|
|
278
|
+
id="name"
|
|
279
|
+
value={name}
|
|
280
|
+
onChange={(e) => setName(e.target.value)}
|
|
281
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<button
|
|
286
|
+
type="submit"
|
|
287
|
+
disabled={loading}
|
|
288
|
+
className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
289
|
+
>
|
|
290
|
+
{loading ? 'Saving...' : 'Save Changes'}
|
|
291
|
+
</button>
|
|
292
|
+
</form>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
await writeFile(
|
|
298
|
+
path.join(projectPath, 'src/components/settings-form.tsx'),
|
|
299
|
+
settingsForm
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Create user update API route
|
|
303
|
+
await ensureDir(path.join(projectPath, 'src/app/api/user/update'));
|
|
304
|
+
|
|
305
|
+
const updateUserApi = `import { NextRequest, NextResponse } from 'next/server';
|
|
306
|
+
import { auth } from '@/lib/auth';
|
|
307
|
+
import { db } from '@/lib/db';
|
|
308
|
+
|
|
309
|
+
export async function PATCH(req: NextRequest) {
|
|
310
|
+
try {
|
|
311
|
+
const session = await auth();
|
|
312
|
+
|
|
313
|
+
if (!session?.user) {
|
|
314
|
+
return NextResponse.json(
|
|
315
|
+
{ error: 'Unauthorized' },
|
|
316
|
+
{ status: 401 }
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const { name } = await req.json();
|
|
321
|
+
|
|
322
|
+
const user = await db.user.update({
|
|
323
|
+
where: { id: session.user.id },
|
|
324
|
+
data: { name },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return NextResponse.json({ user });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('Update user error:', error);
|
|
330
|
+
return NextResponse.json(
|
|
331
|
+
{ error: 'Failed to update user' },
|
|
332
|
+
{ status: 500 }
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
await writeFile(
|
|
339
|
+
path.join(projectPath, 'src/app/api/user/update/route.ts'),
|
|
340
|
+
updateUserApi
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Create dashboard loading state
|
|
344
|
+
const dashboardLoading = `export default function DashboardLoading() {
|
|
345
|
+
return (
|
|
346
|
+
<div className="container mx-auto px-4 py-8">
|
|
347
|
+
<div className="mb-8">
|
|
348
|
+
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse"></div>
|
|
349
|
+
<div className="h-4 w-32 bg-gray-200 rounded animate-pulse mt-2"></div>
|
|
350
|
+
</div>
|
|
351
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
352
|
+
{[1, 2, 3].map((i) => (
|
|
353
|
+
<div key={i} className="rounded-lg border p-6">
|
|
354
|
+
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse"></div>
|
|
355
|
+
<div className="h-8 w-16 bg-gray-200 rounded animate-pulse mt-2"></div>
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
await writeFile(
|
|
365
|
+
path.join(projectPath, 'src/app/dashboard/loading.tsx'),
|
|
366
|
+
dashboardLoading
|
|
367
|
+
);
|
|
368
|
+
}
|