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.
- package/README.md +36 -0
- package/bin/create-skit.mjs +1064 -0
- package/lib/module-application.mjs +281 -0
- package/lib/module-resolver.mjs +179 -0
- package/modules/README.md +22 -0
- package/modules/ai-dx/files/AGENTS.md +116 -0
- package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
- package/modules/ai-dx/module.json +14 -0
- package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
- package/modules/ai-dx-claude/module.json +13 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
- package/modules/ai-dx-cursor/module.json +18 -0
- package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
- package/modules/ai-dx-gemini/module.json +13 -0
- package/modules/auth-core/module.json +8 -0
- package/modules/auth-github/module.json +20 -0
- package/modules/billing-polar/module.json +20 -0
- package/modules/billing-stripe/module.json +23 -0
- package/modules/dashboard-shell/files/src/app/globals.css +756 -0
- package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
- package/modules/dashboard-shell/module.json +11 -0
- package/modules/db-pg/module.json +21 -0
- package/modules/db-postgresjs/module.json +21 -0
- package/modules/deploy-docker/files/.dockerignore +19 -0
- package/modules/deploy-docker/files/Dockerfile +25 -0
- package/modules/deploy-docker/module.json +11 -0
- package/modules/email-resend/module.json +21 -0
- package/modules/quality-baseline/module.json +8 -0
- package/modules/testing-baseline/module.json +8 -0
- package/package.json +40 -0
- package/presets/README.md +12 -0
- package/presets/blank.json +67 -0
- package/presets/dashboard.json +67 -0
- package/templates/base-web/.env.example +17 -0
- package/templates/base-web/.github/workflows/ci.yml +34 -0
- package/templates/base-web/.husky/pre-commit +3 -0
- package/templates/base-web/.husky/pre-push +3 -0
- package/templates/base-web/.prettierignore +3 -0
- package/templates/base-web/README.md +42 -0
- package/templates/base-web/drizzle.config.ts +16 -0
- package/templates/base-web/eslint.config.mjs +127 -0
- package/templates/base-web/manifest.json +5 -0
- package/templates/base-web/next-env.d.ts +4 -0
- package/templates/base-web/next.config.ts +5 -0
- package/templates/base-web/package.json +75 -0
- package/templates/base-web/playwright.config.ts +21 -0
- package/templates/base-web/prettier.config.mjs +9 -0
- package/templates/base-web/proxy.ts +23 -0
- package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
- package/templates/base-web/src/app/api/email/test/route.ts +28 -0
- package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/base-web/src/app/billing/page.tsx +55 -0
- package/templates/base-web/src/app/dashboard/page.tsx +15 -0
- package/templates/base-web/src/app/email/page.tsx +46 -0
- package/templates/base-web/src/app/error.tsx +27 -0
- package/templates/base-web/src/app/globals.css +534 -0
- package/templates/base-web/src/app/layout.tsx +19 -0
- package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
- package/templates/base-web/src/app/llms.txt/route.ts +59 -0
- package/templates/base-web/src/app/loading.tsx +24 -0
- package/templates/base-web/src/app/not-found.tsx +16 -0
- package/templates/base-web/src/app/page.tsx +5 -0
- package/templates/base-web/src/app/sign-in/page.tsx +14 -0
- package/templates/base-web/src/app/sign-up/page.tsx +14 -0
- package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
- package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
- package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/base-web/src/db/index.ts +16 -0
- package/templates/base-web/src/db/schema/auth.ts +4 -0
- package/templates/base-web/src/db/schema/index.ts +2 -0
- package/templates/base-web/src/db/schema/projects.ts +17 -0
- package/templates/base-web/src/db/seeds/index.ts +32 -0
- package/templates/base-web/src/lib/auth-client.ts +5 -0
- package/templates/base-web/src/lib/auth-session.ts +21 -0
- package/templates/base-web/src/lib/auth.ts +23 -0
- package/templates/base-web/src/lib/billing/index.ts +37 -0
- package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
- package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/base-web/src/lib/billing/types.ts +25 -0
- package/templates/base-web/src/lib/email/index.ts +19 -0
- package/templates/base-web/src/lib/email/templates.test.ts +12 -0
- package/templates/base-web/src/lib/email/templates.ts +40 -0
- package/templates/base-web/src/lib/env.ts +83 -0
- package/templates/base-web/tests/e2e/home.spec.ts +8 -0
- package/templates/base-web/tsconfig.json +34 -0
- package/templates/base-web/vitest.config.ts +19 -0
- package/templates/blank/.env.example +16 -0
- package/templates/blank/.github/workflows/ci.yml +34 -0
- package/templates/blank/.husky/pre-commit +3 -0
- package/templates/blank/.husky/pre-push +3 -0
- package/templates/blank/.prettierignore +3 -0
- package/templates/blank/drizzle.config.ts +16 -0
- package/templates/blank/eslint.config.mjs +127 -0
- package/templates/blank/next-env.d.ts +4 -0
- package/templates/blank/next.config.ts +5 -0
- package/templates/blank/package.json +75 -0
- package/templates/blank/playwright.config.ts +21 -0
- package/templates/blank/prettier.config.mjs +9 -0
- package/templates/blank/proxy.ts +28 -0
- package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
- package/templates/blank/src/app/api/email/test/route.ts +28 -0
- package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/blank/src/app/billing/page.tsx +70 -0
- package/templates/blank/src/app/email/page.tsx +46 -0
- package/templates/blank/src/app/globals.css +394 -0
- package/templates/blank/src/app/layout.tsx +19 -0
- package/templates/blank/src/app/page.tsx +23 -0
- package/templates/blank/src/app/sign-in/page.tsx +18 -0
- package/templates/blank/src/app/sign-up/page.tsx +18 -0
- package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
- package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/blank/src/db/index.ts +16 -0
- package/templates/blank/src/db/schema/auth.ts +4 -0
- package/templates/blank/src/db/schema/index.ts +2 -0
- package/templates/blank/src/db/schema/projects.ts +17 -0
- package/templates/blank/src/db/seeds/index.ts +28 -0
- package/templates/blank/src/lib/auth-client.ts +5 -0
- package/templates/blank/src/lib/auth-session.ts +11 -0
- package/templates/blank/src/lib/auth.ts +23 -0
- package/templates/blank/src/lib/billing/index.ts +37 -0
- package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
- package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/blank/src/lib/billing/types.ts +25 -0
- package/templates/blank/src/lib/email/index.ts +19 -0
- package/templates/blank/src/lib/email/templates.test.ts +15 -0
- package/templates/blank/src/lib/email/templates.ts +40 -0
- package/templates/blank/src/lib/env.ts +80 -0
- package/templates/blank/tsconfig.json +34 -0
- package/templates/blank/vitest.config.ts +19 -0
- package/templates/dashboard/.env.example +16 -0
- package/templates/dashboard/.github/workflows/ci.yml +34 -0
- package/templates/dashboard/.husky/pre-commit +3 -0
- package/templates/dashboard/.husky/pre-push +3 -0
- package/templates/dashboard/.prettierignore +3 -0
- package/templates/dashboard/drizzle.config.ts +16 -0
- package/templates/dashboard/eslint.config.mjs +127 -0
- package/templates/dashboard/next-env.d.ts +4 -0
- package/templates/dashboard/next.config.ts +5 -0
- package/templates/dashboard/package.json +75 -0
- package/templates/dashboard/playwright.config.ts +21 -0
- package/templates/dashboard/prettier.config.mjs +9 -0
- package/templates/dashboard/proxy.ts +36 -0
- package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
- package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
- package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/dashboard/src/app/billing/layout.tsx +22 -0
- package/templates/dashboard/src/app/billing/page.tsx +73 -0
- package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
- package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
- package/templates/dashboard/src/app/email/layout.tsx +22 -0
- package/templates/dashboard/src/app/email/page.tsx +54 -0
- package/templates/dashboard/src/app/globals.css +1357 -0
- package/templates/dashboard/src/app/layout.tsx +25 -0
- package/templates/dashboard/src/app/page.tsx +154 -0
- package/templates/dashboard/src/app/settings/layout.tsx +22 -0
- package/templates/dashboard/src/app/settings/page.tsx +85 -0
- package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
- package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
- package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
- package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
- package/templates/dashboard/src/db/index.ts +16 -0
- package/templates/dashboard/src/db/schema/auth.ts +4 -0
- package/templates/dashboard/src/db/schema/index.ts +2 -0
- package/templates/dashboard/src/db/schema/projects.ts +17 -0
- package/templates/dashboard/src/db/seeds/index.ts +28 -0
- package/templates/dashboard/src/lib/auth-client.ts +5 -0
- package/templates/dashboard/src/lib/auth-session.ts +11 -0
- package/templates/dashboard/src/lib/auth.ts +41 -0
- package/templates/dashboard/src/lib/billing/index.ts +37 -0
- package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
- package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/dashboard/src/lib/billing/types.ts +25 -0
- package/templates/dashboard/src/lib/email/index.ts +19 -0
- package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
- package/templates/dashboard/src/lib/email/templates.ts +40 -0
- package/templates/dashboard/src/lib/env.ts +88 -0
- package/templates/dashboard/tsconfig.json +34 -0
- 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
|
+
}
|