create-skit 0.1.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.
Files changed (195) hide show
  1. package/README.md +36 -0
  2. package/bin/create-skit.mjs +1064 -0
  3. package/lib/module-application.mjs +281 -0
  4. package/lib/module-resolver.mjs +179 -0
  5. package/modules/README.md +22 -0
  6. package/modules/ai-dx/files/AGENTS.md +116 -0
  7. package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
  8. package/modules/ai-dx/module.json +14 -0
  9. package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
  10. package/modules/ai-dx-claude/module.json +13 -0
  11. package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
  12. package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
  13. package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
  14. package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
  15. package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
  16. package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
  17. package/modules/ai-dx-cursor/module.json +18 -0
  18. package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
  19. package/modules/ai-dx-gemini/module.json +13 -0
  20. package/modules/auth-core/module.json +8 -0
  21. package/modules/auth-github/module.json +20 -0
  22. package/modules/billing-polar/module.json +20 -0
  23. package/modules/billing-stripe/module.json +23 -0
  24. package/modules/dashboard-shell/files/src/app/globals.css +756 -0
  25. package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
  26. package/modules/dashboard-shell/module.json +11 -0
  27. package/modules/db-pg/module.json +21 -0
  28. package/modules/db-postgresjs/module.json +21 -0
  29. package/modules/deploy-docker/files/.dockerignore +19 -0
  30. package/modules/deploy-docker/files/Dockerfile +25 -0
  31. package/modules/deploy-docker/module.json +11 -0
  32. package/modules/email-resend/module.json +21 -0
  33. package/modules/quality-baseline/module.json +8 -0
  34. package/modules/testing-baseline/module.json +8 -0
  35. package/package.json +40 -0
  36. package/presets/README.md +12 -0
  37. package/presets/blank.json +67 -0
  38. package/presets/dashboard.json +67 -0
  39. package/templates/base-web/.env.example +17 -0
  40. package/templates/base-web/.github/workflows/ci.yml +34 -0
  41. package/templates/base-web/.husky/pre-commit +3 -0
  42. package/templates/base-web/.husky/pre-push +3 -0
  43. package/templates/base-web/.prettierignore +3 -0
  44. package/templates/base-web/README.md +42 -0
  45. package/templates/base-web/drizzle.config.ts +16 -0
  46. package/templates/base-web/eslint.config.mjs +127 -0
  47. package/templates/base-web/manifest.json +5 -0
  48. package/templates/base-web/next-env.d.ts +4 -0
  49. package/templates/base-web/next.config.ts +5 -0
  50. package/templates/base-web/package.json +75 -0
  51. package/templates/base-web/playwright.config.ts +21 -0
  52. package/templates/base-web/prettier.config.mjs +9 -0
  53. package/templates/base-web/proxy.ts +23 -0
  54. package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
  55. package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
  56. package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
  57. package/templates/base-web/src/app/api/email/test/route.ts +28 -0
  58. package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
  59. package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
  60. package/templates/base-web/src/app/billing/page.tsx +55 -0
  61. package/templates/base-web/src/app/dashboard/page.tsx +15 -0
  62. package/templates/base-web/src/app/email/page.tsx +46 -0
  63. package/templates/base-web/src/app/error.tsx +27 -0
  64. package/templates/base-web/src/app/globals.css +534 -0
  65. package/templates/base-web/src/app/layout.tsx +19 -0
  66. package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
  67. package/templates/base-web/src/app/llms.txt/route.ts +59 -0
  68. package/templates/base-web/src/app/loading.tsx +24 -0
  69. package/templates/base-web/src/app/not-found.tsx +16 -0
  70. package/templates/base-web/src/app/page.tsx +5 -0
  71. package/templates/base-web/src/app/sign-in/page.tsx +14 -0
  72. package/templates/base-web/src/app/sign-up/page.tsx +14 -0
  73. package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
  74. package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
  75. package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
  76. package/templates/base-web/src/db/index.ts +16 -0
  77. package/templates/base-web/src/db/schema/auth.ts +4 -0
  78. package/templates/base-web/src/db/schema/index.ts +2 -0
  79. package/templates/base-web/src/db/schema/projects.ts +17 -0
  80. package/templates/base-web/src/db/seeds/index.ts +32 -0
  81. package/templates/base-web/src/lib/auth-client.ts +5 -0
  82. package/templates/base-web/src/lib/auth-session.ts +21 -0
  83. package/templates/base-web/src/lib/auth.ts +23 -0
  84. package/templates/base-web/src/lib/billing/index.ts +37 -0
  85. package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
  86. package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
  87. package/templates/base-web/src/lib/billing/types.ts +25 -0
  88. package/templates/base-web/src/lib/email/index.ts +19 -0
  89. package/templates/base-web/src/lib/email/templates.test.ts +12 -0
  90. package/templates/base-web/src/lib/email/templates.ts +40 -0
  91. package/templates/base-web/src/lib/env.ts +83 -0
  92. package/templates/base-web/tests/e2e/home.spec.ts +8 -0
  93. package/templates/base-web/tsconfig.json +34 -0
  94. package/templates/base-web/vitest.config.ts +19 -0
  95. package/templates/blank/.env.example +16 -0
  96. package/templates/blank/.github/workflows/ci.yml +34 -0
  97. package/templates/blank/.husky/pre-commit +3 -0
  98. package/templates/blank/.husky/pre-push +3 -0
  99. package/templates/blank/.prettierignore +3 -0
  100. package/templates/blank/drizzle.config.ts +16 -0
  101. package/templates/blank/eslint.config.mjs +127 -0
  102. package/templates/blank/next-env.d.ts +4 -0
  103. package/templates/blank/next.config.ts +5 -0
  104. package/templates/blank/package.json +75 -0
  105. package/templates/blank/playwright.config.ts +21 -0
  106. package/templates/blank/prettier.config.mjs +9 -0
  107. package/templates/blank/proxy.ts +28 -0
  108. package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
  109. package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
  110. package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
  111. package/templates/blank/src/app/api/email/test/route.ts +28 -0
  112. package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
  113. package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
  114. package/templates/blank/src/app/billing/page.tsx +70 -0
  115. package/templates/blank/src/app/email/page.tsx +46 -0
  116. package/templates/blank/src/app/globals.css +394 -0
  117. package/templates/blank/src/app/layout.tsx +19 -0
  118. package/templates/blank/src/app/page.tsx +23 -0
  119. package/templates/blank/src/app/sign-in/page.tsx +18 -0
  120. package/templates/blank/src/app/sign-up/page.tsx +18 -0
  121. package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
  122. package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
  123. package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
  124. package/templates/blank/src/db/index.ts +16 -0
  125. package/templates/blank/src/db/schema/auth.ts +4 -0
  126. package/templates/blank/src/db/schema/index.ts +2 -0
  127. package/templates/blank/src/db/schema/projects.ts +17 -0
  128. package/templates/blank/src/db/seeds/index.ts +28 -0
  129. package/templates/blank/src/lib/auth-client.ts +5 -0
  130. package/templates/blank/src/lib/auth-session.ts +11 -0
  131. package/templates/blank/src/lib/auth.ts +23 -0
  132. package/templates/blank/src/lib/billing/index.ts +37 -0
  133. package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
  134. package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
  135. package/templates/blank/src/lib/billing/types.ts +25 -0
  136. package/templates/blank/src/lib/email/index.ts +19 -0
  137. package/templates/blank/src/lib/email/templates.test.ts +15 -0
  138. package/templates/blank/src/lib/email/templates.ts +40 -0
  139. package/templates/blank/src/lib/env.ts +80 -0
  140. package/templates/blank/tsconfig.json +34 -0
  141. package/templates/blank/vitest.config.ts +19 -0
  142. package/templates/dashboard/.env.example +16 -0
  143. package/templates/dashboard/.github/workflows/ci.yml +34 -0
  144. package/templates/dashboard/.husky/pre-commit +3 -0
  145. package/templates/dashboard/.husky/pre-push +3 -0
  146. package/templates/dashboard/.prettierignore +3 -0
  147. package/templates/dashboard/drizzle.config.ts +16 -0
  148. package/templates/dashboard/eslint.config.mjs +127 -0
  149. package/templates/dashboard/next-env.d.ts +4 -0
  150. package/templates/dashboard/next.config.ts +5 -0
  151. package/templates/dashboard/package.json +75 -0
  152. package/templates/dashboard/playwright.config.ts +21 -0
  153. package/templates/dashboard/prettier.config.mjs +9 -0
  154. package/templates/dashboard/proxy.ts +36 -0
  155. package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
  156. package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
  157. package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
  158. package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
  159. package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
  160. package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
  161. package/templates/dashboard/src/app/billing/layout.tsx +22 -0
  162. package/templates/dashboard/src/app/billing/page.tsx +73 -0
  163. package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
  164. package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
  165. package/templates/dashboard/src/app/email/layout.tsx +22 -0
  166. package/templates/dashboard/src/app/email/page.tsx +54 -0
  167. package/templates/dashboard/src/app/globals.css +1357 -0
  168. package/templates/dashboard/src/app/layout.tsx +25 -0
  169. package/templates/dashboard/src/app/page.tsx +154 -0
  170. package/templates/dashboard/src/app/settings/layout.tsx +22 -0
  171. package/templates/dashboard/src/app/settings/page.tsx +85 -0
  172. package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
  173. package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
  174. package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
  175. package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
  176. package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
  177. package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
  178. package/templates/dashboard/src/db/index.ts +16 -0
  179. package/templates/dashboard/src/db/schema/auth.ts +4 -0
  180. package/templates/dashboard/src/db/schema/index.ts +2 -0
  181. package/templates/dashboard/src/db/schema/projects.ts +17 -0
  182. package/templates/dashboard/src/db/seeds/index.ts +28 -0
  183. package/templates/dashboard/src/lib/auth-client.ts +5 -0
  184. package/templates/dashboard/src/lib/auth-session.ts +11 -0
  185. package/templates/dashboard/src/lib/auth.ts +41 -0
  186. package/templates/dashboard/src/lib/billing/index.ts +37 -0
  187. package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
  188. package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
  189. package/templates/dashboard/src/lib/billing/types.ts +25 -0
  190. package/templates/dashboard/src/lib/email/index.ts +19 -0
  191. package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
  192. package/templates/dashboard/src/lib/email/templates.ts +40 -0
  193. package/templates/dashboard/src/lib/env.ts +88 -0
  194. package/templates/dashboard/tsconfig.json +34 -0
  195. package/templates/dashboard/vitest.config.ts +19 -0
@@ -0,0 +1,25 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "__PROJECT_NAME__",
6
+ description: "Generated by Skit",
7
+ metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"),
8
+ openGraph: {
9
+ title: "__PROJECT_NAME__",
10
+ description: "Generated by Skit",
11
+ type: "website"
12
+ }
13
+ };
14
+
15
+ export default function RootLayout({
16
+ children
17
+ }: Readonly<{
18
+ children: React.ReactNode;
19
+ }>) {
20
+ return (
21
+ <html lang="en">
22
+ <body>{children}</body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,154 @@
1
+ import Link from "next/link";
2
+
3
+ export default function HomePage() {
4
+ return (
5
+ <main className="landing">
6
+ <nav className="landing-nav">
7
+ <div className="logo">
8
+ launch<span>frame</span>
9
+ </div>
10
+ <div className="landing-nav-links">
11
+ <Link href="/sign-in">Sign in</Link>
12
+ <Link href="/sign-up" className="nav-cta">Get started</Link>
13
+ </div>
14
+ </nav>
15
+
16
+ <section className="landing-hero">
17
+ <p className="eyebrow">Ship faster in 2026</p>
18
+ <h1>
19
+ Build your SaaS <em>without the boilerplate.</em>
20
+ </h1>
21
+ <p className="lede">
22
+ Auth, billing, email, database — all wired up and ready.
23
+ Stop rebuilding infrastructure and start shipping your product.
24
+ </p>
25
+ <div className="hero-actions">
26
+ <Link href="/sign-up" className="btn-primary">Start building free</Link>
27
+ <Link href="/dashboard" className="btn-secondary">Open dashboard</Link>
28
+ </div>
29
+ </section>
30
+
31
+ <section className="social-proof">
32
+ <p>Trusted by teams at</p>
33
+ <div className="social-proof-logos">
34
+ <span>Vercel</span>
35
+ <span>Supabase</span>
36
+ <span>Linear</span>
37
+ <span>Resend</span>
38
+ </div>
39
+ </section>
40
+
41
+ <section className="bento-section">
42
+ <p className="eyebrow">Everything included</p>
43
+ <h2>Production-ready from day one</h2>
44
+
45
+ <div className="bento-grid">
46
+ <div className="bento-card span-2">
47
+ <div className="bento-card-icon">🔐</div>
48
+ <h3>Authentication</h3>
49
+ <p>
50
+ Email and password auth with Better Auth. Session management, protected routes,
51
+ and middleware — all pre-configured and type-safe.
52
+ </p>
53
+ <div className="tag-row">
54
+ <span className="bento-tag">Better Auth</span>
55
+ <span className="bento-tag">Server sessions</span>
56
+ <span className="bento-tag">Edge middleware</span>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="bento-card">
61
+ <div className="bento-card-icon">💳</div>
62
+ <h3>Billing</h3>
63
+ <p>
64
+ Stripe and Polar checkout flows with a provider abstraction you can extend.
65
+ </p>
66
+ <div className="tag-row">
67
+ <span className="bento-tag">Stripe</span>
68
+ <span className="bento-tag">Polar</span>
69
+ </div>
70
+ </div>
71
+
72
+ <div className="bento-card">
73
+ <div className="bento-card-icon">🗄️</div>
74
+ <h3>Database</h3>
75
+ <p>
76
+ Drizzle ORM with migrations, seeds, and a typed schema ready for your models.
77
+ </p>
78
+ <div className="tag-row">
79
+ <span className="bento-tag">Drizzle</span>
80
+ <span className="bento-tag">PostgreSQL</span>
81
+ </div>
82
+ </div>
83
+
84
+ <div className="bento-card span-2">
85
+ <div className="bento-card-icon">📧</div>
86
+ <h3>Transactional email</h3>
87
+ <p>
88
+ Resend integration with HTML templates for welcome emails, password resets,
89
+ and billing notifications. Test from the dashboard to verify wiring.
90
+ </p>
91
+ <div className="tag-row">
92
+ <span className="bento-tag">Resend</span>
93
+ <span className="bento-tag">HTML templates</span>
94
+ <span className="bento-tag">Test harness</span>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="bento-card">
99
+ <div className="bento-card-icon">🧪</div>
100
+ <h3>Testing</h3>
101
+ <p>
102
+ Vitest for unit tests, Playwright for E2E. Pre-commit hooks enforce quality.
103
+ </p>
104
+ <div className="tag-row">
105
+ <span className="bento-tag">Vitest</span>
106
+ <span className="bento-tag">Playwright</span>
107
+ <span className="bento-tag">Husky</span>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="bento-card">
112
+ <div className="bento-card-icon">⚡</div>
113
+ <h3>Next.js 16</h3>
114
+ <p>
115
+ App Router, React 19, server components, and the latest compiler out of the box.
116
+ </p>
117
+ <div className="tag-row">
118
+ <span className="bento-tag">App Router</span>
119
+ <span className="bento-tag">React 19</span>
120
+ </div>
121
+ </div>
122
+
123
+ <div className="bento-card">
124
+ <div className="bento-card-icon">🤖</div>
125
+ <h3>Agent-friendly</h3>
126
+ <p>
127
+ AGENTS.md, explicit placeholders, and a flat file tree that AI coding assistants love.
128
+ </p>
129
+ <div className="tag-row">
130
+ <span className="bento-tag">Cursor</span>
131
+ <span className="bento-tag">Copilot</span>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ <section className="cta-section">
138
+ <h2>Ready to ship?</h2>
139
+ <p>
140
+ Generate your project in seconds. Everything is yours to customize —
141
+ no vendor lock-in, no hidden abstractions.
142
+ </p>
143
+ <div className="hero-actions">
144
+ <Link href="/sign-up" className="btn-primary">Create your account</Link>
145
+ <Link href="/sign-in" className="btn-secondary">Sign in</Link>
146
+ </div>
147
+ </section>
148
+
149
+ <footer className="landing-footer">
150
+ <p>Built with Skit — open source SaaS starter for Next.js</p>
151
+ </footer>
152
+ </main>
153
+ );
154
+ }
@@ -0,0 +1,22 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { getSession } from "@/lib/auth-session";
4
+ import { DashboardShell } from "@/components/dashboard/shell";
5
+
6
+ export default async function SettingsLayout({
7
+ children
8
+ }: {
9
+ children: React.ReactNode;
10
+ }) {
11
+ const session = await getSession();
12
+
13
+ if (!session) {
14
+ redirect("/sign-in");
15
+ }
16
+
17
+ return (
18
+ <DashboardShell user={session.user}>
19
+ {children}
20
+ </DashboardShell>
21
+ );
22
+ }
@@ -0,0 +1,85 @@
1
+ import { getSession } from "@/lib/auth-session";
2
+
3
+ export default async function SettingsPage() {
4
+ const session = await getSession();
5
+
6
+ return (
7
+ <>
8
+ <header className="dashboard-header">
9
+ <div>
10
+ <h1>Settings</h1>
11
+ <p className="lede" style={{ marginTop: 4, fontSize: "0.9375rem" }}>
12
+ Manage your account and preferences.
13
+ </p>
14
+ </div>
15
+ </header>
16
+
17
+ <div className="settings-grid">
18
+ <section className="settings-section">
19
+ <h3>Profile</h3>
20
+ <p>Update your personal information.</p>
21
+ <div className="settings-fields">
22
+ <div className="settings-row">
23
+ <label className="field">
24
+ <span>Name</span>
25
+ <input
26
+ type="text"
27
+ defaultValue={session?.user.name || ""}
28
+ placeholder="Your name"
29
+ />
30
+ </label>
31
+ </div>
32
+ <div className="settings-row">
33
+ <label className="field">
34
+ <span>Email</span>
35
+ <input
36
+ type="email"
37
+ defaultValue={session?.user.email || ""}
38
+ placeholder="you@example.com"
39
+ disabled
40
+ />
41
+ </label>
42
+ </div>
43
+ <div className="settings-actions">
44
+ <button className="btn-primary" disabled>Save changes</button>
45
+ </div>
46
+ </div>
47
+ </section>
48
+
49
+ <section className="settings-section">
50
+ <h3>Notifications</h3>
51
+ <p>Choose what you want to be notified about.</p>
52
+ <div className="toggle-row">
53
+ <div className="toggle-label">
54
+ <strong>Product updates</strong>
55
+ <p>Get notified about new features and improvements.</p>
56
+ </div>
57
+ <div className="toggle-switch on" />
58
+ </div>
59
+ <div className="toggle-row">
60
+ <div className="toggle-label">
61
+ <strong>Billing alerts</strong>
62
+ <p>Receive alerts about billing events and invoices.</p>
63
+ </div>
64
+ <div className="toggle-switch on" />
65
+ </div>
66
+ <div className="toggle-row">
67
+ <div className="toggle-label">
68
+ <strong>Marketing emails</strong>
69
+ <p>Occasional updates about tips and best practices.</p>
70
+ </div>
71
+ <div className="toggle-switch" />
72
+ </div>
73
+ </section>
74
+
75
+ <section className="settings-section danger-zone">
76
+ <h3>Danger zone</h3>
77
+ <p>Irreversible actions for your account.</p>
78
+ <button className="btn-ghost" style={{ color: "var(--color-danger)" }}>
79
+ Delete account
80
+ </button>
81
+ </section>
82
+ </div>
83
+ </>
84
+ );
85
+ }
@@ -0,0 +1,47 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignInPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return (
14
+ <main className="auth-screen">
15
+ <div className="auth-brand">
16
+ <div className="logo">
17
+ launch<span>frame</span>
18
+ </div>
19
+ <blockquote>
20
+ Stop rebuilding auth, billing, and email for every new project.
21
+ <cite>— Ship what matters, not infrastructure</cite>
22
+ </blockquote>
23
+ <div className="auth-features">
24
+ <div className="auth-feature-item">
25
+ <div className="auth-feature-check">✓</div>
26
+ <span>Authentication with session management</span>
27
+ </div>
28
+ <div className="auth-feature-item">
29
+ <div className="auth-feature-check">✓</div>
30
+ <span>Stripe & Polar billing ready</span>
31
+ </div>
32
+ <div className="auth-feature-item">
33
+ <div className="auth-feature-check">✓</div>
34
+ <span>Transactional email wired</span>
35
+ </div>
36
+ <div className="auth-feature-item">
37
+ <div className="auth-feature-check">✓</div>
38
+ <span>Database with migrations & seeds</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div className="auth-form-side">
43
+ <EmailAuthForm mode="sign-in" />
44
+ </div>
45
+ </main>
46
+ );
47
+ }
@@ -0,0 +1,47 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
4
+ import { getSession } from "@/lib/auth-session";
5
+
6
+ export default async function SignUpPage() {
7
+ const session = await getSession();
8
+
9
+ if (session) {
10
+ redirect("/dashboard");
11
+ }
12
+
13
+ return (
14
+ <main className="auth-screen">
15
+ <div className="auth-brand">
16
+ <div className="logo">
17
+ launch<span>frame</span>
18
+ </div>
19
+ <blockquote>
20
+ From zero to production SaaS in minutes, not months.
21
+ <cite>— Your foundation, your rules</cite>
22
+ </blockquote>
23
+ <div className="auth-features">
24
+ <div className="auth-feature-item">
25
+ <div className="auth-feature-check">✓</div>
26
+ <span>Next.js 16 with App Router</span>
27
+ </div>
28
+ <div className="auth-feature-item">
29
+ <div className="auth-feature-check">✓</div>
30
+ <span>TypeScript strict mode</span>
31
+ </div>
32
+ <div className="auth-feature-item">
33
+ <div className="auth-feature-check">✓</div>
34
+ <span>Testing with Vitest & Playwright</span>
35
+ </div>
36
+ <div className="auth-feature-item">
37
+ <div className="auth-feature-check">✓</div>
38
+ <span>CI/CD with GitHub Actions</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div className="auth-form-side">
43
+ <EmailAuthForm mode="sign-up" />
44
+ </div>
45
+ </main>
46
+ );
47
+ }
@@ -0,0 +1,39 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { EmailAuthForm } from "@/components/auth/email-auth-form";
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ vi.mock("next/link", () => ({
9
+ default: ({ children, href }: { children: ReactNode; href: string }) => (
10
+ <a href={href}>{children}</a>
11
+ )
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ useRouter: () => ({
16
+ push: vi.fn(),
17
+ refresh: vi.fn()
18
+ })
19
+ }));
20
+
21
+ vi.mock("@/lib/auth-client", () => ({
22
+ authClient: {
23
+ signIn: {
24
+ email: vi.fn()
25
+ },
26
+ signUp: {
27
+ email: vi.fn()
28
+ }
29
+ }
30
+ }));
31
+
32
+ describe("EmailAuthForm", () => {
33
+ it("renders sign-up mode copy", () => {
34
+ render(<EmailAuthForm mode="sign-up" />);
35
+
36
+ expect(screen.getByRole("heading", { name: /create your operator account/i })).toBeTruthy();
37
+ expect(screen.getByLabelText(/password/i)).toBeTruthy();
38
+ });
39
+ });
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useRouter } from "next/navigation";
5
+ import { useState } from "react";
6
+
7
+ import { authClient } from "@/lib/auth-client";
8
+
9
+ type EmailAuthFormProps = {
10
+ mode: "sign-in" | "sign-up";
11
+ };
12
+
13
+ export function EmailAuthForm({ mode }: EmailAuthFormProps) {
14
+ const isSignUp = mode === "sign-up";
15
+ const router = useRouter();
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [isPending, setIsPending] = useState(false);
18
+
19
+ async function handleSocialSignIn(provider: "github" | "google") {
20
+ setIsPending(true);
21
+ setError(null);
22
+ try {
23
+ const result = await authClient.signIn.social({
24
+ provider,
25
+ callbackURL: "/dashboard"
26
+ });
27
+ if (result.error) {
28
+ setError(result.error.message ?? `${provider} sign-in failed.`);
29
+ }
30
+ } catch {
31
+ setError(`${provider} sign-in failed.`);
32
+ } finally {
33
+ setIsPending(false);
34
+ }
35
+ }
36
+
37
+ async function handleSubmit(formData: FormData) {
38
+ const email = String(formData.get("email") ?? "");
39
+ const password = String(formData.get("password") ?? "");
40
+ const name = String(formData.get("name") ?? "");
41
+
42
+ setIsPending(true);
43
+ setError(null);
44
+
45
+ try {
46
+ const result = isSignUp
47
+ ? await authClient.signUp.email({
48
+ email,
49
+ password,
50
+ name
51
+ })
52
+ : await authClient.signIn.email({
53
+ email,
54
+ password
55
+ });
56
+
57
+ if (result.error) {
58
+ setError(result.error.message ?? "Authentication failed.");
59
+ return;
60
+ }
61
+
62
+ router.push("/dashboard");
63
+ router.refresh();
64
+ } catch {
65
+ setError("Authentication failed.");
66
+ } finally {
67
+ setIsPending(false);
68
+ }
69
+ }
70
+
71
+ return (
72
+ <form
73
+ action={(formData) => {
74
+ void handleSubmit(formData);
75
+ }}
76
+ className="auth-panel"
77
+ >
78
+ <div>
79
+ <p className="eyebrow">{isSignUp ? "Create account" : "Access dashboard"}</p>
80
+ <h1>{isSignUp ? "Create your operator account" : "Sign in"}</h1>
81
+ <p className="lede">
82
+ {isSignUp
83
+ ? "Create an account to access the protected dashboard shell."
84
+ : "Use your email and password to continue into the app."}
85
+ </p>
86
+ </div>
87
+
88
+ <div className="auth-social-buttons">
89
+ <button
90
+ type="button"
91
+ className="auth-social-btn"
92
+ onClick={() => { void handleSocialSignIn("google"); }}
93
+ disabled={isPending}
94
+ >
95
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
96
+ <path 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" fill="#4285F4" />
97
+ <path 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" fill="#34A853" />
98
+ <path 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" fill="#FBBC05" />
99
+ <path 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" fill="#EA4335" />
100
+ </svg>
101
+ Continue with Google
102
+ </button>
103
+
104
+ <button
105
+ type="button"
106
+ className="auth-social-btn"
107
+ onClick={() => { void handleSocialSignIn("github"); }}
108
+ disabled={isPending}
109
+ >
110
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
111
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
112
+ </svg>
113
+ Continue with GitHub
114
+ </button>
115
+ </div>
116
+
117
+ <div className="auth-divider">
118
+ <span>or continue with email</span>
119
+ </div>
120
+
121
+ <div className="auth-grid">
122
+ {isSignUp ? (
123
+ <label className="field">
124
+ <span>Name</span>
125
+ <input name="name" type="text" placeholder="Ada Lovelace" required />
126
+ </label>
127
+ ) : null}
128
+
129
+ <label className="field">
130
+ <span>Email</span>
131
+ <input name="email" type="email" placeholder="you@example.com" required />
132
+ </label>
133
+
134
+ <label className="field">
135
+ <span>Password</span>
136
+ <input
137
+ name="password"
138
+ type="password"
139
+ placeholder="At least 8 characters"
140
+ minLength={8}
141
+ required
142
+ />
143
+ </label>
144
+ </div>
145
+
146
+ {error ? <p className="form-error">{error}</p> : null}
147
+
148
+ <button type="submit" className="auth-button" disabled={isPending}>
149
+ {isPending ? "Working..." : isSignUp ? "Create account" : "Sign in"}
150
+ </button>
151
+
152
+ <p className="auth-switch">
153
+ {isSignUp ? "Already have an account?" : "Need an account?"}{" "}
154
+ <Link href={isSignUp ? "/sign-in" : "/sign-up"}>
155
+ {isSignUp ? "Sign in" : "Create one"}
156
+ </Link>
157
+ </p>
158
+ </form>
159
+ );
160
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { useState } from "react";
5
+
6
+ import { authClient } from "@/lib/auth-client";
7
+
8
+ export function SignOutButton() {
9
+ const router = useRouter();
10
+ const [isPending, setIsPending] = useState(false);
11
+
12
+ async function handleSignOut() {
13
+ setIsPending(true);
14
+
15
+ try {
16
+ await authClient.signOut();
17
+ router.push("/sign-in");
18
+ router.refresh();
19
+ } finally {
20
+ setIsPending(false);
21
+ }
22
+ }
23
+
24
+ return (
25
+ <button type="button" className="btn-ghost" onClick={() => void handleSignOut()}>
26
+ {isPending ? "Signing out..." : "Sign out"}
27
+ </button>
28
+ );
29
+ }