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