create-githat-app 1.0.15 → 1.0.17

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 (42) hide show
  1. package/dist/cli.js +42 -28
  2. package/package.json +1 -1
  3. package/templates/agent/app/admin/agent/page.tsx.hbs +127 -0
  4. package/templates/agent/app/page.tsx.hbs +85 -108
  5. package/templates/base/.env.local.hbs +11 -0
  6. package/templates/base/.gitignore.hbs +17 -2
  7. package/templates/classroom/app/(auth)/sign-in/page.tsx.hbs +9 -0
  8. package/templates/classroom/app/(auth)/sign-up/page.tsx.hbs +9 -0
  9. package/templates/classroom/app/globals.css.hbs +87 -0
  10. package/templates/classroom/app/layout.tsx.hbs +41 -0
  11. package/templates/classroom/app/page.tsx.hbs +103 -0
  12. package/templates/classroom/app/projects/[id]/feedback/page.tsx.hbs +159 -0
  13. package/templates/classroom/app/projects/[id]/present/page.tsx.hbs +113 -0
  14. package/templates/classroom/next.config.ts.hbs +7 -0
  15. package/templates/classroom/postcss.config.mjs.hbs +14 -0
  16. package/templates/classroom/proxy.ts.hbs +10 -0
  17. package/templates/classroom/tsconfig.json.hbs +21 -0
  18. package/templates/content/app/newsletter/page.tsx.hbs +90 -0
  19. package/templates/content/app/page.tsx.hbs +93 -111
  20. package/templates/content/app/posts/[slug]/page.tsx.hbs +119 -0
  21. package/templates/dashboard/app/admin/data/[entity]/page.tsx.hbs +68 -0
  22. package/templates/dashboard/app/admin/page.tsx.hbs +59 -0
  23. package/templates/dashboard/app/page.tsx.hbs +42 -108
  24. package/templates/dashboard/src/lib/db.ts.hbs +39 -0
  25. package/templates/portfolio/app/(auth)/sign-in/page.tsx.hbs +9 -0
  26. package/templates/portfolio/app/(auth)/sign-up/page.tsx.hbs +9 -0
  27. package/templates/portfolio/app/globals.css.hbs +87 -0
  28. package/templates/portfolio/app/layout.tsx.hbs +41 -0
  29. package/templates/portfolio/app/page.tsx.hbs +86 -0
  30. package/templates/portfolio/next.config.ts.hbs +7 -0
  31. package/templates/portfolio/postcss.config.mjs.hbs +14 -0
  32. package/templates/portfolio/proxy.ts.hbs +10 -0
  33. package/templates/portfolio/tsconfig.json.hbs +21 -0
  34. package/templates/saas/app/admin/billing/page.tsx.hbs +145 -0
  35. package/templates/saas/app/admin/page.tsx.hbs +106 -0
  36. package/templates/saas/app/admin/team/page.tsx.hbs +134 -0
  37. package/templates/saas/app/page.tsx.hbs +95 -110
  38. package/templates/saas/app/pricing/page.tsx.hbs +131 -0
  39. package/templates/agent/TODO.md +0 -9
  40. package/templates/content/TODO.md +0 -9
  41. package/templates/dashboard/TODO.md +0 -9
  42. package/templates/saas/TODO.md +0 -9
@@ -0,0 +1,41 @@
1
+ import { GitHatProvider } from '@githat/nextjs';
2
+ import '@githat/nextjs/styles';
3
+ import './globals.css';
4
+
5
+ export const metadata = {
6
+ title: '{{businessName}}',
7
+ description: '{{description}}',
8
+ };
9
+
10
+ export default function RootLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
11
+ return (
12
+ <html lang="en">
13
+ <body>
14
+ {/*
15
+ Plain template: no @githat/ui dep, no Wordmark. The
16
+ full-kit (`nextjs`) template uses @githat/ui for the
17
+ shared design system; the plain scaffold is the smallest
18
+ working app, so we avoid extra deps.
19
+ */}
20
+ <GitHatProvider config=\{{
21
+ publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
22
+ signInUrl: '/sign-in',
23
+ signUpUrl: '/sign-up',
24
+ afterSignInUrl: '/',
25
+ afterSignOutUrl: '/',
26
+ }}>
27
+ <header style=\{{
28
+ padding: '1rem 1.5rem',
29
+ borderBottom: '1px solid var(--border, #e5e7eb)',
30
+ fontFamily: 'system-ui, -apple-system, sans-serif',
31
+ }}>
32
+ <a href="/" style=\{{ textDecoration: 'none', color: 'inherit', fontWeight: 600 }}>
33
+ {{businessName}}
34
+ </a>
35
+ </header>
36
+ <main>{children}</main>
37
+ </GitHatProvider>
38
+ </body>
39
+ </html>
40
+ );
41
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useAuth, SignInButton } from '@githat/nextjs';
5
+
6
+ /**
7
+ * Classroom — projects with real-world feedback.
8
+ *
9
+ * Built for students (and anyone presenting work-in-progress) to
10
+ * replace the dead-end PDF. You upload a project, share a link,
11
+ * and audience feedback streams in as you present. The work
12
+ * iterates — every cohort that hits the same project sees the
13
+ * latest version and adds to it.
14
+ *
15
+ * The flagship feature is /projects/[id]/present + /projects/[id]/feedback —
16
+ * presenter sees responses live, audience submits without an account.
17
+ *
18
+ * Why this matters: a portfolio of dead PDFs is worth less than
19
+ * a single project with a year of audience feedback layered on top.
20
+ */
21
+ const SAMPLE_PROJECTS = [
22
+ { id: 'sustainable-bodega', title: 'Sustainable bodega supply chain', author: 'Maria L.', cohort: 'Spring 2026', responses: 47 },
23
+ { id: 'after-school-tutor', title: 'After-school tutor matching app', author: 'Devon R.', cohort: 'Spring 2026', responses: 12 },
24
+ { id: 'community-fridge', title: 'Community fridge tracker', author: 'Aisha M.', cohort: 'Fall 2025', responses: 89 },
25
+ ];
26
+
27
+ export default function Home() {
28
+ const { isSignedIn } = useAuth();
29
+
30
+ return (
31
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)' }}>
32
+ <section style=\{{
33
+ padding: 'var(--space-12) var(--space-4)',
34
+ textAlign: 'center',
35
+ maxWidth: '40rem',
36
+ margin: '0 auto',
37
+ }}>
38
+ <h1 style=\{{
39
+ fontFamily: 'var(--font-wordmark, Georgia, serif)',
40
+ fontSize: 'clamp(2rem, 5vw, 3rem)',
41
+ lineHeight: 1.1,
42
+ marginBottom: 'var(--space-3)',
43
+ }}>
44
+ {{businessName}}
45
+ </h1>
46
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '1.125rem', lineHeight: 1.6, marginBottom: 'var(--space-6)' }}>
47
+ Show your work. Get real feedback as you present. Watch
48
+ your project iterate over time — each cohort that visits
49
+ adds to it.
50
+ <br />
51
+ <em style=\{{ fontSize: '0.875rem' }}>The PDF is dead. Long live the living project.</em>
52
+ </p>
53
+ {!isSignedIn ? (
54
+ <SignInButton />
55
+ ) : (
56
+ <Link href="/admin" style=\{{
57
+ display: 'inline-block',
58
+ padding: 'var(--space-3) var(--space-6)',
59
+ borderRadius: 'var(--radius-md, 0.5rem)',
60
+ background: 'var(--primary)',
61
+ color: 'var(--bg)',
62
+ fontWeight: 600,
63
+ textDecoration: 'none',
64
+ }}>
65
+ New project →
66
+ </Link>
67
+ )}
68
+ </section>
69
+
70
+ <section style=\{{ padding: 'var(--space-8) var(--space-4)', maxWidth: '48rem', margin: '0 auto' }}>
71
+ <h2 style=\{{ fontSize: '1.5rem', marginBottom: 'var(--space-4)' }}>Live projects</h2>
72
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', marginBottom: 'var(--space-4)' }}>
73
+ Anyone can submit feedback — no account required. Authors
74
+ see responses streaming in as they present.
75
+ </p>
76
+ <ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
77
+ {SAMPLE_PROJECTS.map((p) => (
78
+ <li key={p.id}>
79
+ <Link href={`/projects/${p.id}`} style=\{{
80
+ display: 'block',
81
+ padding: 'var(--space-5)',
82
+ borderRadius: 'var(--radius-md, 0.5rem)',
83
+ border: '1px solid var(--border)',
84
+ background: 'var(--surface)',
85
+ color: 'var(--fg)',
86
+ textDecoration: 'none',
87
+ }}>
88
+ <h3 style=\{{ fontSize: '1.125rem', fontWeight: 600, marginBottom: 'var(--space-1)' }}>{p.title}</h3>
89
+ <div style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)', display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
90
+ <span>{p.author}</span>
91
+ <span>·</span>
92
+ <span>{p.cohort}</span>
93
+ <span>·</span>
94
+ <span><strong>{p.responses}</strong> responses</span>
95
+ </div>
96
+ </Link>
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </section>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,159 @@
1
+ 'use client';
2
+
3
+ import { use, useState } from 'react';
4
+ import Link from 'next/link';
5
+
6
+ /**
7
+ * Audience feedback form — `/projects/[id]/feedback`.
8
+ *
9
+ * Anyone with the link can fill this out. NO sign-in required —
10
+ * that's the whole point. Anonymous responses go to a queue the
11
+ * presenter watches in real time on /present.
12
+ *
13
+ * The form questions are deliberately short — long forms kill
14
+ * response rates and the value of this template is volume of
15
+ * feedback over time, not depth per response.
16
+ */
17
+ export default function FeedbackPage({ params }: { params: Promise<{ id: string }> }) {
18
+ const { id } = use(params);
19
+ const [submitted, setSubmitted] = useState(false);
20
+ const [oneThing, setOneThing] = useState('');
21
+ const [stuck, setStuck] = useState('');
22
+ const [scale, setScale] = useState(7);
23
+
24
+ if (submitted) {
25
+ return (
26
+ <div style=\{{
27
+ background: 'var(--bg)',
28
+ color: 'var(--fg)',
29
+ minHeight: 'calc(100vh - 64px)',
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ padding: 'var(--space-4)',
34
+ }}>
35
+ <div style=\{{ maxWidth: '32rem', textAlign: 'center' }}>
36
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-3)' }}>
37
+ ¡Gracias!
38
+ </h1>
39
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-4)' }}>
40
+ The presenter sees your feedback live on their screen.
41
+ You can submit again with a new thought any time.
42
+ </p>
43
+ <button
44
+ onClick={() => { setSubmitted(false); setOneThing(''); setStuck(''); setScale(7); }}
45
+ style=\{{
46
+ padding: 'var(--space-3) var(--space-5)',
47
+ borderRadius: 'var(--radius-md, 0.5rem)',
48
+ background: 'var(--primary)',
49
+ color: 'var(--bg)',
50
+ border: 'none',
51
+ fontWeight: 600,
52
+ cursor: 'pointer',
53
+ }}
54
+ >
55
+ Add another thought
56
+ </button>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)', padding: 'var(--space-8) var(--space-4)' }}>
64
+ <div style=\{{ maxWidth: '32rem', margin: '0 auto' }}>
65
+ <Link href={`/projects/${id}`} style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', display: 'inline-block', marginBottom: 'var(--space-4)' }}>
66
+ ← Project
67
+ </Link>
68
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.75rem', marginBottom: 'var(--space-2)' }}>
69
+ Tell them what you think
70
+ </h1>
71
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-6)' }}>
72
+ No account, no email. Just three quick questions while
73
+ they're presenting.
74
+ </p>
75
+
76
+ <form
77
+ onSubmit={(e) => {
78
+ e.preventDefault();
79
+ /* TODO: POST /api/projects/[id]/feedback { oneThing, stuck, scale } */
80
+ setSubmitted(true);
81
+ }}
82
+ style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}
83
+ >
84
+ <Field label="One thing that landed for you">
85
+ <input
86
+ type="text"
87
+ required
88
+ value={oneThing}
89
+ onChange={(e) => setOneThing(e.target.value)}
90
+ placeholder="The thing you'll remember tomorrow"
91
+ style=\{{
92
+ width: '100%',
93
+ padding: 'var(--space-3)',
94
+ borderRadius: 'var(--radius-md, 0.5rem)',
95
+ border: '1px solid var(--border)',
96
+ background: 'var(--surface)',
97
+ color: 'var(--fg)',
98
+ fontSize: '1rem',
99
+ }}
100
+ />
101
+ </Field>
102
+
103
+ <Field label="One thing you'd want them to dig into more">
104
+ <textarea
105
+ value={stuck}
106
+ onChange={(e) => setStuck(e.target.value)}
107
+ rows={3}
108
+ placeholder="A question, a doubt, a what-if"
109
+ style=\{{
110
+ width: '100%',
111
+ padding: 'var(--space-3)',
112
+ borderRadius: 'var(--radius-md, 0.5rem)',
113
+ border: '1px solid var(--border)',
114
+ background: 'var(--surface)',
115
+ color: 'var(--fg)',
116
+ fontSize: '1rem',
117
+ fontFamily: 'inherit',
118
+ resize: 'vertical',
119
+ }}
120
+ />
121
+ </Field>
122
+
123
+ <Field label={`How likely are you to share this with someone? (${scale}/10)`}>
124
+ <input
125
+ type="range"
126
+ min={1}
127
+ max={10}
128
+ value={scale}
129
+ onChange={(e) => setScale(Number(e.target.value))}
130
+ style=\{{ width: '100%' }}
131
+ />
132
+ </Field>
133
+
134
+ <button type="submit" style=\{{
135
+ padding: 'var(--space-3) var(--space-5)',
136
+ borderRadius: 'var(--radius-md, 0.5rem)',
137
+ background: 'var(--primary)',
138
+ color: 'var(--bg)',
139
+ border: 'none',
140
+ fontWeight: 600,
141
+ fontSize: '1rem',
142
+ cursor: 'pointer',
143
+ }}>
144
+ Send feedback
145
+ </button>
146
+ </form>
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ function Field({ label, children }: { label: string; children: React.ReactNode }) {
153
+ return (
154
+ <label style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
155
+ <span style=\{{ fontSize: '0.875rem', fontWeight: 600 }}>{label}</span>
156
+ {children}
157
+ </label>
158
+ );
159
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { use, useEffect, useState } from 'react';
4
+
5
+ /**
6
+ * Presenter view — `/projects/[id]/present`.
7
+ *
8
+ * The student opens this on their second screen (or a phone next
9
+ * to the laptop) and presents. As the audience submits feedback
10
+ * via /projects/[id]/feedback, responses fade in here in real
11
+ * time — newest first, color-coded by the scale rating.
12
+ *
13
+ * Real implementations:
14
+ * - Replace the mock interval with a polling fetch (every 3-5s)
15
+ * against your backend, OR a websocket / SSE stream.
16
+ * - Persist responses to your DB so the project can reiterate
17
+ * over multiple cohorts.
18
+ */
19
+
20
+ interface FeedbackEntry {
21
+ id: string;
22
+ oneThing: string;
23
+ stuck: string;
24
+ scale: number;
25
+ receivedAt: number;
26
+ }
27
+
28
+ const MOCK_FEEDBACK: FeedbackEntry[] = [
29
+ { id: 'a', oneThing: 'The slide on supply chain bottlenecks', stuck: 'How do you measure trust at scale?', scale: 9, receivedAt: Date.now() - 60_000 },
30
+ { id: 'b', oneThing: 'Loved the bilingual UX argument', stuck: 'What about Mexican misceláneas?', scale: 8, receivedAt: Date.now() - 120_000 },
31
+ ];
32
+
33
+ export default function PresenterPage({ params }: { params: Promise<{ id: string }> }) {
34
+ const { id } = use(params);
35
+ const [feedback, setFeedback] = useState<FeedbackEntry[]>(MOCK_FEEDBACK);
36
+
37
+ // Simulated incoming feedback. Replace with a real fetch poll or
38
+ // websocket subscription. The shape is the same.
39
+ useEffect(() => {
40
+ const t = setInterval(() => {
41
+ // setFeedback((prev) => [{ ...newOne, id: crypto.randomUUID() }, ...prev]);
42
+ }, 5000);
43
+ return () => clearInterval(t);
44
+ }, []);
45
+
46
+ const audienceUrl = typeof window !== 'undefined'
47
+ ? `${window.location.origin}/projects/${id}/feedback`
48
+ : `/projects/${id}/feedback`;
49
+
50
+ return (
51
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: '100vh', padding: 'var(--space-6) var(--space-4)' }}>
52
+ <div style=\{{ maxWidth: '64rem', margin: '0 auto' }}>
53
+ <header style=\{{
54
+ padding: 'var(--space-5)',
55
+ borderRadius: 'var(--radius-md, 0.5rem)',
56
+ background: 'var(--surface-sub)',
57
+ marginBottom: 'var(--space-6)',
58
+ textAlign: 'center',
59
+ }}>
60
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', marginBottom: 'var(--space-2)' }}>
61
+ Audience submits feedback at:
62
+ </p>
63
+ <code style=\{{
64
+ fontSize: '1.25rem',
65
+ fontFamily: 'var(--font-mono, monospace)',
66
+ color: 'var(--primary)',
67
+ wordBreak: 'break-all',
68
+ }}>
69
+ {audienceUrl}
70
+ </code>
71
+ </header>
72
+
73
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.75rem', marginBottom: 'var(--space-4)' }}>
74
+ Live feedback ({feedback.length})
75
+ </h1>
76
+
77
+ <ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
78
+ {feedback.map((f) => (
79
+ <li key={f.id} style=\{{
80
+ padding: 'var(--space-5)',
81
+ borderRadius: 'var(--radius-md, 0.5rem)',
82
+ border: `2px solid ${f.scale >= 8 ? 'var(--success)' : f.scale >= 5 ? 'var(--info)' : 'var(--warn)'}`,
83
+ background: 'var(--surface)',
84
+ }}>
85
+ <div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-3)', marginBottom: 'var(--space-2)' }}>
86
+ <div style=\{{ flex: 1 }}>
87
+ <div style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-1)' }}>What landed</div>
88
+ <div style=\{{ fontWeight: 600 }}>{f.oneThing}</div>
89
+ </div>
90
+ <div style=\{{
91
+ padding: 'var(--space-1) var(--space-3)',
92
+ borderRadius: 'var(--radius-full, 9999px)',
93
+ background: f.scale >= 8 ? 'var(--success)' : f.scale >= 5 ? 'var(--info)' : 'var(--warn)',
94
+ color: 'var(--bg)',
95
+ fontSize: '0.75rem',
96
+ fontWeight: 700,
97
+ }}>
98
+ {f.scale}/10
99
+ </div>
100
+ </div>
101
+ {f.stuck && (
102
+ <div style=\{{ marginTop: 'var(--space-3)', paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border)' }}>
103
+ <div style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-1)' }}>Wants more on</div>
104
+ <div style=\{{ fontSize: '0.9375rem', color: 'var(--fg)' }}>{f.stuck}</div>
105
+ </div>
106
+ )}
107
+ </li>
108
+ ))}
109
+ </ul>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: 'standalone',
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Plain template — Tailwind v4 PostCSS plugin is required even though
3
+ * the plain scaffold doesn't use Tailwind utility classes. The auth
4
+ * page CSS shipped by `@githat/nextjs/styles` is processed through
5
+ * @tailwindcss/postcss at build time. Drop this config and the
6
+ * auth pages render unstyled.
7
+ */
8
+ const config = {
9
+ plugins: {
10
+ '@tailwindcss/postcss': {},
11
+ },
12
+ };
13
+
14
+ export default config;
@@ -0,0 +1,10 @@
1
+ import { authProxy } from '@githat/nextjs/proxy';
2
+
3
+ export const proxy = authProxy({
4
+ publicRoutes: ['/', '/sign-in', '/sign-up'{{#if includeForgotPassword}}, '/forgot-password', '/reset-password'{{/if}}{{#if includeEmailVerification}}, '/verify-email'{{/if}}],
5
+ signInUrl: '/sign-in',
6
+ });
7
+
8
+ export const config = {
9
+ matcher: ['/((?!_next|api|.*\\..*).*)'],
10
+ };
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ /**
6
+ * Free newsletter sign-up.
7
+ *
8
+ * Newsletters work without a GitHat account — readers just give an
9
+ * email. Real apps POST to /api/newsletter/subscribe which calls
10
+ * GitHat's POST /email/send to confirm + your audience-of-record
11
+ * (Postgres `subscribers` table, MailerLite, etc.).
12
+ *
13
+ * Keep this page simple — adding fields kills conversion.
14
+ */
15
+ export default function NewsletterPage() {
16
+ const [email, setEmail] = useState('');
17
+ const [submitted, setSubmitted] = useState(false);
18
+
19
+ return (
20
+ <div style=\{{
21
+ background: 'var(--bg)',
22
+ color: 'var(--fg)',
23
+ minHeight: 'calc(100vh - 64px)',
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ padding: 'var(--space-4)',
28
+ }}>
29
+ <div style=\{{ maxWidth: '32rem', width: '100%', textAlign: 'center' }}>
30
+ {submitted ? (
31
+ <>
32
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-3)' }}>
33
+ Check your inbox
34
+ </h1>
35
+ <p style=\{{ color: 'var(--fg-muted)' }}>
36
+ We sent a confirmation to <strong>{email}</strong>. Click the link
37
+ and you'll get the next post when it ships.
38
+ </p>
39
+ </>
40
+ ) : (
41
+ <>
42
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-3)' }}>
43
+ Get the newsletter
44
+ </h1>
45
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-6)' }}>
46
+ Free posts, occasional extras. Unsubscribe any time.
47
+ </p>
48
+ <form
49
+ onSubmit={(e) => {
50
+ e.preventDefault();
51
+ /* TODO: POST /api/newsletter/subscribe { email } */
52
+ setSubmitted(true);
53
+ }}
54
+ style=\{{ display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap', justifyContent: 'center' }}
55
+ >
56
+ <input
57
+ type="email"
58
+ required
59
+ value={email}
60
+ onChange={(e) => setEmail(e.target.value)}
61
+ placeholder="you@example.com"
62
+ style=\{{
63
+ flex: '1 1 240px',
64
+ padding: 'var(--space-3) var(--space-4)',
65
+ borderRadius: 'var(--radius-md, 0.5rem)',
66
+ border: '1px solid var(--border)',
67
+ background: 'var(--surface)',
68
+ color: 'var(--fg)',
69
+ fontSize: '1rem',
70
+ }}
71
+ />
72
+ <button type="submit" style=\{{
73
+ padding: 'var(--space-3) var(--space-6)',
74
+ borderRadius: 'var(--radius-md, 0.5rem)',
75
+ border: 'none',
76
+ background: 'var(--primary)',
77
+ color: 'var(--bg)',
78
+ fontWeight: 600,
79
+ fontSize: '1rem',
80
+ cursor: 'pointer',
81
+ }}>
82
+ Subscribe
83
+ </button>
84
+ </form>
85
+ </>
86
+ )}
87
+ </div>
88
+ </div>
89
+ );
90
+ }