create-solostack 1.2.2 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-solostack",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "The complete SaaS boilerplate for indie hackers - Next.js 15 with auth, payments, database, and email",
5
5
  "type": "module",
6
6
  "bin": {
@@ -398,20 +398,21 @@ export default function Home() {
398
398
  icon={<Database className="w-5 h-5 text-emerald-400" />}
399
399
  status={status?.database?.connected}
400
400
  loading={loading}
401
- description="PostgreSQL + Prisma"
401
+ description="${config?.database === 'Supabase' ? 'Supabase' : 'PostgreSQL + Prisma'}"
402
402
  details={status?.database?.connected ? \`User count: \${status.database.userCount}\` : status?.database?.error || "Not connected"}
403
- actionLabel="Open Studio"
404
- actionCommand="npm run db:studio"
403
+ actionLabel="${config?.database === 'Supabase' ? 'Dashboard' : 'Open Studio'}"
404
+ actionCommand="${config?.database === 'Supabase' ? '' : 'npm run db:studio'}"
405
+ actionUrl="${config?.database === 'Supabase' ? 'https://supabase.com/dashboard' : ''}"
405
406
  />
406
407
  <StatusCard
407
408
  title="Authentication"
408
409
  icon={<Shield className="w-5 h-5 text-blue-400" />}
409
410
  status={status?.auth?.configured}
410
411
  loading={loading}
411
- description={status?.auth?.providers?.map((p: any) => p.name).join(', ') || "No providers"}
412
+ description={status?.auth?.type || status?.auth?.providers?.map((p: any) => p.name).join(', ') || "Not configured"}
412
413
  details={status?.auth?.configured ? "Ready to authenticate users" : "Missing secrets"}
413
414
  actionLabel="Read Docs"
414
- actionUrl="https://next-auth.js.org"
415
+ actionUrl="${config?.auth === 'Supabase Auth' ? 'https://supabase.com/docs/guides/auth' : 'https://next-auth.js.org'}"
415
416
  />
416
417
  <StatusCard
417
418
  title="Payments"
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Admin Panel Generator - Pro Feature
3
+ * Generates admin dashboard with user management and analytics
4
+ */
5
+
6
+ import path from 'path';
7
+ import { writeFile, ensureDir } from '../../utils/files.js';
8
+
9
+ export async function generateAdmin(projectPath) {
10
+ await ensureDir(path.join(projectPath, 'src/app/admin'));
11
+ await ensureDir(path.join(projectPath, 'src/app/admin/users'));
12
+ await ensureDir(path.join(projectPath, 'src/app/admin/subscriptions'));
13
+ await ensureDir(path.join(projectPath, 'src/components/admin'));
14
+
15
+ // Generate admin layout
16
+ const adminLayout = `import { redirect } from 'next/navigation';
17
+ import { auth } from '@/lib/auth';
18
+ import Link from 'next/link';
19
+ import { LayoutDashboard, Users, CreditCard, BarChart3, ArrowLeft } from 'lucide-react';
20
+
21
+ export default async function AdminLayout({
22
+ children,
23
+ }: {
24
+ children: React.ReactNode;
25
+ }) {
26
+ const session = await auth();
27
+
28
+ if (!session || session.user.role !== 'ADMIN') {
29
+ redirect('/login');
30
+ }
31
+
32
+ const navItems = [
33
+ { href: '/admin', label: 'Dashboard', icon: LayoutDashboard },
34
+ { href: '/admin/users', label: 'Users', icon: Users },
35
+ { href: '/admin/subscriptions', label: 'Subscriptions', icon: CreditCard },
36
+ ];
37
+
38
+ return (
39
+ <div className="min-h-screen bg-gray-50">
40
+ {/* Sidebar */}
41
+ <aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r bg-white">
42
+ <div className="flex h-full flex-col">
43
+ <div className="flex h-16 items-center border-b px-6">
44
+ <span className="text-lg font-bold text-gray-900">Admin Panel</span>
45
+ </div>
46
+
47
+ <nav className="flex-1 space-y-1 px-3 py-4">
48
+ {navItems.map((item) => (
49
+ <Link
50
+ key={item.href}
51
+ href={item.href}
52
+ className="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
53
+ >
54
+ <item.icon className="h-5 w-5" />
55
+ {item.label}
56
+ </Link>
57
+ ))}
58
+ </nav>
59
+
60
+ <div className="border-t p-4">
61
+ <Link
62
+ href="/dashboard"
63
+ className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-900"
64
+ >
65
+ <ArrowLeft className="h-4 w-4" />
66
+ Back to App
67
+ </Link>
68
+ </div>
69
+ </div>
70
+ </aside>
71
+
72
+ {/* Main content */}
73
+ <main className="ml-64 min-h-screen p-8">
74
+ {children}
75
+ </main>
76
+ </div>
77
+ );
78
+ }
79
+ `;
80
+
81
+ await writeFile(
82
+ path.join(projectPath, 'src/app/admin/layout.tsx'),
83
+ adminLayout
84
+ );
85
+
86
+ // Generate admin dashboard
87
+ const adminDashboard = `import { db } from '@/lib/db';
88
+ import { Users, CreditCard, DollarSign, TrendingUp } from 'lucide-react';
89
+
90
+ async function getStats() {
91
+ const [totalUsers, activeSubscriptions, totalRevenue] = await Promise.all([
92
+ db.user.count(),
93
+ db.subscription.count({ where: { status: 'ACTIVE' } }),
94
+ db.payment.aggregate({
95
+ _sum: { amount: true },
96
+ where: { status: 'succeeded' },
97
+ }),
98
+ ]);
99
+
100
+ return {
101
+ totalUsers,
102
+ activeSubscriptions,
103
+ totalRevenue: (totalRevenue._sum.amount || 0) / 100, // Convert cents to dollars
104
+ mrr: activeSubscriptions * 29, // Simplified MRR calculation
105
+ };
106
+ }
107
+
108
+ export default async function AdminDashboard() {
109
+ const stats = await getStats();
110
+
111
+ const cards = [
112
+ {
113
+ title: 'Total Users',
114
+ value: stats.totalUsers.toLocaleString(),
115
+ icon: Users,
116
+ color: 'bg-blue-500',
117
+ },
118
+ {
119
+ title: 'Active Subscriptions',
120
+ value: stats.activeSubscriptions.toLocaleString(),
121
+ icon: CreditCard,
122
+ color: 'bg-green-500',
123
+ },
124
+ {
125
+ title: 'Total Revenue',
126
+ value: \`$\${stats.totalRevenue.toLocaleString()}\`,
127
+ icon: DollarSign,
128
+ color: 'bg-purple-500',
129
+ },
130
+ {
131
+ title: 'Monthly Recurring Revenue',
132
+ value: \`$\${stats.mrr.toLocaleString()}\`,
133
+ icon: TrendingUp,
134
+ color: 'bg-amber-500',
135
+ },
136
+ ];
137
+
138
+ return (
139
+ <div>
140
+ <h1 className="text-3xl font-bold text-gray-900 mb-8">Dashboard</h1>
141
+
142
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
143
+ {cards.map((card) => (
144
+ <div
145
+ key={card.title}
146
+ className="bg-white rounded-xl border p-6 shadow-sm"
147
+ >
148
+ <div className="flex items-center justify-between mb-4">
149
+ <span className="text-sm font-medium text-gray-500">{card.title}</span>
150
+ <div className={\`p-2 rounded-lg \${card.color}\`}>
151
+ <card.icon className="h-5 w-5 text-white" />
152
+ </div>
153
+ </div>
154
+ <p className="text-3xl font-bold text-gray-900">{card.value}</p>
155
+ </div>
156
+ ))}
157
+ </div>
158
+
159
+ <div className="bg-white rounded-xl border p-6 shadow-sm">
160
+ <h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
161
+ <div className="grid grid-cols-2 gap-4">
162
+ <a
163
+ href="/admin/users"
164
+ className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
165
+ >
166
+ <Users className="h-6 w-6 mb-2 text-gray-600" />
167
+ <span className="font-medium">Manage Users</span>
168
+ </a>
169
+ <a
170
+ href="/admin/subscriptions"
171
+ className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
172
+ >
173
+ <CreditCard className="h-6 w-6 mb-2 text-gray-600" />
174
+ <span className="font-medium">View Subscriptions</span>
175
+ </a>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ );
180
+ }
181
+ `;
182
+
183
+ await writeFile(
184
+ path.join(projectPath, 'src/app/admin/page.tsx'),
185
+ adminDashboard
186
+ );
187
+
188
+ // Generate users management page
189
+ const usersPage = `import { db } from '@/lib/db';
190
+ import { formatDistanceToNow } from 'date-fns';
191
+
192
+ async function getUsers() {
193
+ return db.user.findMany({
194
+ include: {
195
+ subscription: true,
196
+ },
197
+ orderBy: { createdAt: 'desc' },
198
+ take: 100,
199
+ });
200
+ }
201
+
202
+ export default async function UsersPage() {
203
+ const users = await getUsers();
204
+
205
+ return (
206
+ <div>
207
+ <div className="flex items-center justify-between mb-8">
208
+ <h1 className="text-3xl font-bold text-gray-900">Users</h1>
209
+ <span className="text-sm text-gray-500">{users.length} total users</span>
210
+ </div>
211
+
212
+ <div className="bg-white rounded-xl border shadow-sm overflow-hidden">
213
+ <table className="w-full">
214
+ <thead>
215
+ <tr className="border-b bg-gray-50">
216
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
217
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Role</th>
218
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Subscription</th>
219
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Joined</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody className="divide-y">
223
+ {users.map((user) => (
224
+ <tr key={user.id} className="hover:bg-gray-50">
225
+ <td className="px-6 py-4">
226
+ <div>
227
+ <p className="font-medium text-gray-900">{user.name || 'No name'}</p>
228
+ <p className="text-sm text-gray-500">{user.email}</p>
229
+ </div>
230
+ </td>
231
+ <td className="px-6 py-4">
232
+ <span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
233
+ user.role === 'ADMIN' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'
234
+ }\`}>
235
+ {user.role}
236
+ </span>
237
+ </td>
238
+ <td className="px-6 py-4">
239
+ {user.subscription ? (
240
+ <span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
241
+ user.subscription.status === 'ACTIVE' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
242
+ }\`}>
243
+ {user.subscription.status}
244
+ </span>
245
+ ) : (
246
+ <span className="text-sm text-gray-400">Free</span>
247
+ )}
248
+ </td>
249
+ <td className="px-6 py-4 text-sm text-gray-500">
250
+ {formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
251
+ </td>
252
+ </tr>
253
+ ))}
254
+ </tbody>
255
+ </table>
256
+ </div>
257
+ </div>
258
+ );
259
+ }
260
+ `;
261
+
262
+ await writeFile(
263
+ path.join(projectPath, 'src/app/admin/users/page.tsx'),
264
+ usersPage
265
+ );
266
+
267
+ // Generate subscriptions page
268
+ const subscriptionsPage = `import { db } from '@/lib/db';
269
+ import { formatDistanceToNow } from 'date-fns';
270
+
271
+ async function getSubscriptions() {
272
+ return db.subscription.findMany({
273
+ include: {
274
+ user: {
275
+ select: { email: true, name: true },
276
+ },
277
+ },
278
+ orderBy: { createdAt: 'desc' },
279
+ });
280
+ }
281
+
282
+ export default async function SubscriptionsPage() {
283
+ const subscriptions = await getSubscriptions();
284
+
285
+ return (
286
+ <div>
287
+ <div className="flex items-center justify-between mb-8">
288
+ <h1 className="text-3xl font-bold text-gray-900">Subscriptions</h1>
289
+ <span className="text-sm text-gray-500">{subscriptions.length} total</span>
290
+ </div>
291
+
292
+ <div className="bg-white rounded-xl border shadow-sm overflow-hidden">
293
+ <table className="w-full">
294
+ <thead>
295
+ <tr className="border-b bg-gray-50">
296
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
297
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
298
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Plan</th>
299
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Renews</th>
300
+ </tr>
301
+ </thead>
302
+ <tbody className="divide-y">
303
+ {subscriptions.map((sub) => (
304
+ <tr key={sub.id} className="hover:bg-gray-50">
305
+ <td className="px-6 py-4">
306
+ <div>
307
+ <p className="font-medium text-gray-900">{sub.user.name || 'No name'}</p>
308
+ <p className="text-sm text-gray-500">{sub.user.email}</p>
309
+ </div>
310
+ </td>
311
+ <td className="px-6 py-4">
312
+ <span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
313
+ sub.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
314
+ sub.status === 'CANCELED' ? 'bg-red-100 text-red-800' :
315
+ 'bg-gray-100 text-gray-800'
316
+ }\`}>
317
+ {sub.status}
318
+ </span>
319
+ </td>
320
+ <td className="px-6 py-4 text-sm text-gray-600">
321
+ {sub.stripePriceId.includes('enterprise') ? 'Enterprise' : 'Pro'}
322
+ </td>
323
+ <td className="px-6 py-4 text-sm text-gray-500">
324
+ {sub.cancelAtPeriodEnd ? (
325
+ <span className="text-amber-600">Canceling</span>
326
+ ) : (
327
+ formatDistanceToNow(new Date(sub.currentPeriodEnd), { addSuffix: true })
328
+ )}
329
+ </td>
330
+ </tr>
331
+ ))}
332
+ </tbody>
333
+ </table>
334
+ </div>
335
+ </div>
336
+ );
337
+ }
338
+ `;
339
+
340
+ await writeFile(
341
+ path.join(projectPath, 'src/app/admin/subscriptions/page.tsx'),
342
+ subscriptionsPage
343
+ );
344
+ }