create-solostack 1.3.4 → 1.3.6

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.3.4",
3
+ "version": "1.3.6",
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": {
@@ -21,6 +21,7 @@ async function generateSupabaseAuth(projectPath) {
21
21
  // Create auth directories
22
22
  await ensureDir(path.join(projectPath, 'src/app/auth/callback'));
23
23
  await ensureDir(path.join(projectPath, 'src/app/login'));
24
+ await ensureDir(path.join(projectPath, 'src/app/signup'));
24
25
  await ensureDir(path.join(projectPath, 'src/app/private'));
25
26
 
26
27
  // 1. Client Utility
@@ -35,35 +36,29 @@ export function createClient() {
35
36
  `;
36
37
  await writeFile(path.join(projectPath, 'src/utils/supabase/client.ts'), clientUtil);
37
38
 
38
- // 2. Server Utility
39
- const serverUtil = `import { createServerClient, type CookieOptions } from '@supabase/ssr'
39
+ // 2. Server Utility (Next.js 15 compatible - async cookies)
40
+ const serverUtil = `import { createServerClient } from '@supabase/ssr'
40
41
  import { cookies } from 'next/headers'
41
42
 
42
- export function createClient(cookieStore: ReturnType<typeof cookies>) {
43
+ export async function createClient() {
44
+ const cookieStore = await cookies()
45
+
43
46
  return createServerClient(
44
47
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
45
48
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
46
49
  {
47
50
  cookies: {
48
- get(name: string) {
49
- return cookieStore.get(name)?.value
50
- },
51
- set(name: string, value: string, options: CookieOptions) {
52
- try {
53
- cookieStore.set({ name, value, ...options })
54
- } catch (error) {
55
- // The \`set\` method was called from a Server Component.
56
- // This can be ignored if you have middleware refreshing
57
- // user sessions.
58
- }
51
+ getAll() {
52
+ return cookieStore.getAll()
59
53
  },
60
- remove(name: string, options: CookieOptions) {
54
+ setAll(cookiesToSet) {
61
55
  try {
62
- cookieStore.set({ name, value: '', ...options })
63
- } catch (error) {
64
- // The \`delete\` method was called from a Server Component.
65
- // This can be ignored if you have middleware refreshing
66
- // user sessions.
56
+ cookiesToSet.forEach(({ name, value, options }) =>
57
+ cookieStore.set(name, value, options)
58
+ )
59
+ } catch {
60
+ // The \`setAll\` method was called from a Server Component.
61
+ // This can be ignored if you have middleware refreshing user sessions.
67
62
  }
68
63
  },
69
64
  },
@@ -73,7 +68,7 @@ export function createClient(cookieStore: ReturnType<typeof cookies>) {
73
68
  `;
74
69
  await writeFile(path.join(projectPath, 'src/utils/supabase/server.ts'), serverUtil);
75
70
 
76
- // 3. MacOS/Middleware Utility
71
+ // 3. Middleware Utility
77
72
  const middlewareUtil = `import { createServerClient, type CookieOptions } from '@supabase/ssr'
78
73
  import { NextResponse, type NextRequest } from 'next/server'
79
74
 
@@ -162,7 +157,6 @@ export const config = {
162
157
 
163
158
  // 5. Auth Callback Route
164
159
  const routeCallback = `import { NextResponse } from 'next/server'
165
- import { cookies } from 'next/headers'
166
160
  import { createClient } from '@/utils/supabase/server'
167
161
 
168
162
  export async function GET(request: Request) {
@@ -171,8 +165,7 @@ export async function GET(request: Request) {
171
165
  const next = searchParams.get('next') ?? '/'
172
166
 
173
167
  if (code) {
174
- const cookieStore = cookies()
175
- const supabase = createClient(cookieStore)
168
+ const supabase = await createClient()
176
169
  const { error } = await supabase.auth.exchangeCodeForSession(code)
177
170
  if (!error) {
178
171
  return NextResponse.redirect(new URL(next, request.url))
@@ -191,6 +184,7 @@ import { createClient } from '@/utils/supabase/client';
191
184
  import { useRouter } from 'next/navigation';
192
185
  import { useState } from 'react';
193
186
  import { Rocket } from 'lucide-react';
187
+ import Link from 'next/link';
194
188
 
195
189
  export default function LoginPage() {
196
190
  const router = useRouter();
@@ -301,6 +295,13 @@ export default function LoginPage() {
301
295
  {loading ? 'Signing in...' : 'Sign In'}
302
296
  </button>
303
297
  </form>
298
+
299
+ <div className="text-center text-sm">
300
+ <span className="text-zinc-500">Don't have an account? </span>
301
+ <Link href="/signup" className="font-medium text-indigo-400 hover:text-indigo-300">
302
+ Sign up
303
+ </Link>
304
+ </div>
304
305
  </div>
305
306
  </div>
306
307
  );
@@ -308,6 +309,215 @@ export default function LoginPage() {
308
309
  `;
309
310
 
310
311
  await writeFile(path.join(projectPath, 'src/app/login/page.tsx'), loginPage);
312
+
313
+ // 7. Signup Page (Supabase)
314
+ const signupPage = `'use client';
315
+ import { createClient } from '@/utils/supabase/client';
316
+ import { useRouter } from 'next/navigation';
317
+ import { useState } from 'react';
318
+ import { Rocket } from 'lucide-react';
319
+ import Link from 'next/link';
320
+
321
+ export default function SignupPage() {
322
+ const router = useRouter();
323
+ const [email, setEmail] = useState('');
324
+ const [password, setPassword] = useState('');
325
+ const [loading, setLoading] = useState(false);
326
+ const [message, setMessage] = useState('');
327
+
328
+ const supabase = createClient();
329
+
330
+ const handleEmailSignup = async (e: React.FormEvent) => {
331
+ e.preventDefault();
332
+ setLoading(true);
333
+ setMessage('');
334
+
335
+ const { error } = await supabase.auth.signUp({
336
+ email,
337
+ password,
338
+ options: {
339
+ emailRedirectTo: \`\${location.origin}/auth/callback\`,
340
+ },
341
+ });
342
+
343
+ if (error) {
344
+ setMessage(error.message);
345
+ } else {
346
+ setMessage('Check your email for the confirmation link!');
347
+ }
348
+ setLoading(false);
349
+ };
350
+
351
+ const handleGitHubLogin = async () => {
352
+ await supabase.auth.signInWithOAuth({
353
+ provider: 'github',
354
+ options: {
355
+ redirectTo: \`\${location.origin}/auth/callback\`,
356
+ },
357
+ });
358
+ };
359
+
360
+ const handleGoogleLogin = async () => {
361
+ await supabase.auth.signInWithOAuth({
362
+ provider: 'google',
363
+ options: {
364
+ redirectTo: \`\${location.origin}/auth/callback\`,
365
+ },
366
+ });
367
+ };
368
+
369
+ return (
370
+ <div className="flex min-h-screen flex-col items-center justify-center bg-zinc-950 p-4 text-white">
371
+ <div className="w-full max-w-sm space-y-8 rounded-xl border border-zinc-800 bg-zinc-900/50 p-8 shadow-xl backdrop-blur-xl">
372
+ <div className="text-center">
373
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-600">
374
+ <Rocket className="h-6 w-6 text-white" />
375
+ </div>
376
+ <h2 className="text-2xl font-bold tracking-tight">Create account</h2>
377
+ <p className="mt-2 text-sm text-zinc-400">Sign up to get started</p>
378
+ </div>
379
+
380
+ <div className="space-y-4">
381
+ <button
382
+ onClick={handleGitHubLogin}
383
+ className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
384
+ >
385
+ Continue with GitHub
386
+ </button>
387
+ <button
388
+ onClick={handleGoogleLogin}
389
+ className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
390
+ >
391
+ Continue with Google
392
+ </button>
393
+ </div>
394
+
395
+ <div className="relative">
396
+ <div className="absolute inset-0 flex items-center">
397
+ <div className="w-full border-t border-zinc-800" />
398
+ </div>
399
+ <div className="relative flex justify-center text-xs uppercase">
400
+ <span className="bg-zinc-900 px-2 text-zinc-500">Or continue with</span>
401
+ </div>
402
+ </div>
403
+
404
+ <form onSubmit={handleEmailSignup} className="space-y-4">
405
+ <div>
406
+ <label className="mb-2 block text-sm font-medium text-zinc-400">Email</label>
407
+ <input
408
+ type="email"
409
+ value={email}
410
+ onChange={(e) => setEmail(e.target.value)}
411
+ className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
412
+ required
413
+ />
414
+ </div>
415
+ <div>
416
+ <label className="mb-2 block text-sm font-medium text-zinc-400">Password</label>
417
+ <input
418
+ type="password"
419
+ value={password}
420
+ onChange={(e) => setPassword(e.target.value)}
421
+ className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
422
+ placeholder="Min 6 characters"
423
+ minLength={6}
424
+ required
425
+ />
426
+ </div>
427
+
428
+ {message && (
429
+ <p className={message.includes('Check') ? 'text-sm text-green-400' : 'text-sm text-red-400'}>
430
+ {message}
431
+ </p>
432
+ )}
433
+
434
+ <button
435
+ type="submit"
436
+ disabled={loading}
437
+ className="w-full rounded-md bg-indigo-600 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
438
+ >
439
+ {loading ? 'Creating account...' : 'Sign Up'}
440
+ </button>
441
+ </form>
442
+
443
+ <div className="text-center text-sm">
444
+ <span className="text-zinc-500">Already have an account? </span>
445
+ <Link href="/login" className="font-medium text-indigo-400 hover:text-indigo-300">
446
+ Sign in
447
+ </Link>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ );
452
+ }
453
+ `;
454
+
455
+ await writeFile(path.join(projectPath, 'src/app/signup/page.tsx'), signupPage);
456
+
457
+ // 8. Dashboard Layout (Supabase)
458
+ await ensureDir(path.join(projectPath, 'src/app/dashboard'));
459
+
460
+ const dashboardLayout = `import { redirect } from 'next/navigation';
461
+ import Link from 'next/link';
462
+ import { createClient } from '@/utils/supabase/server';
463
+
464
+ export default async function DashboardLayout({
465
+ children,
466
+ }: {
467
+ children: React.ReactNode;
468
+ }) {
469
+ const supabase = await createClient();
470
+ const { data: { user } } = await supabase.auth.getUser();
471
+
472
+ if (!user) {
473
+ redirect('/login');
474
+ }
475
+
476
+ return (
477
+ <div className="min-h-screen bg-gray-50">
478
+ <nav className="bg-white border-b">
479
+ <div className="container mx-auto px-4 py-4">
480
+ <div className="flex items-center justify-between">
481
+ <Link href="/dashboard" className="text-xl font-bold">
482
+ Dashboard
483
+ </Link>
484
+ <div className="flex items-center gap-6">
485
+ <Link href="/dashboard" className="text-sm hover:text-indigo-600">
486
+ Home
487
+ </Link>
488
+ <Link href="/dashboard/billing" className="text-sm hover:text-indigo-600">
489
+ Billing
490
+ </Link>
491
+ <Link href="/dashboard/settings" className="text-sm hover:text-indigo-600">
492
+ Settings
493
+ </Link>
494
+ <Link href="/api/auth/signout" className="text-sm text-red-600 hover:text-red-700">
495
+ Sign Out
496
+ </Link>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ </nav>
501
+ <main>{children}</main>
502
+ </div>
503
+ );
504
+ }
505
+ `;
506
+ await writeFile(path.join(projectPath, 'src/app/dashboard/layout.tsx'), dashboardLayout);
507
+
508
+ // 9. Signout Route (Supabase)
509
+ await ensureDir(path.join(projectPath, 'src/app/api/auth/signout'));
510
+
511
+ const signoutRoute = `import { NextResponse } from 'next/server';
512
+ import { createClient } from '@/utils/supabase/server';
513
+
514
+ export async function GET() {
515
+ const supabase = await createClient();
516
+ await supabase.auth.signOut();
517
+ return NextResponse.redirect(new URL('/login', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'));
518
+ }
519
+ `;
520
+ await writeFile(path.join(projectPath, 'src/app/api/auth/signout/route.ts'), signoutRoute);
311
521
  }
312
522
 
313
523
  async function generateNextAuth(projectPath) {
package/src/index.js CHANGED
@@ -53,7 +53,7 @@ export async function main() {
53
53
  ╚════════════════════════════════════════════════════════════════╝
54
54
  `));
55
55
 
56
- console.log(chalk.gray(` Version ${orangeBright('1.3.4')} • Built by ${orangeBright('Danish Akhtar')} • ${orangeBright('github.com/danish296')}\n`));
56
+ console.log(chalk.gray(` Version ${orangeBright('1.3.6')} • Built by ${orangeBright('Danish Akhtar')} • ${orangeBright('github.com/danish296')}\n`));
57
57
 
58
58
  // Parse command line arguments
59
59
  program