create-githat-app 1.3.0 → 1.4.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 +48 -18
- package/dist/cli.js +1161 -114
- package/package.json +34 -9
- package/templates/agent/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/agent/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/agent/app/admin/agent/page.tsx.hbs +127 -0
- package/templates/agent/app/globals.css.hbs +87 -0
- package/templates/agent/app/layout.tsx.hbs +41 -0
- package/templates/agent/app/page.tsx.hbs +100 -0
- package/templates/agent/next.config.ts.hbs +8 -0
- package/templates/agent/postcss.config.mjs.hbs +14 -0
- package/templates/agent/proxy.ts.hbs +10 -0
- package/templates/agent/tsconfig.json.hbs +21 -0
- package/templates/base/.env.example.hbs +2 -2
- package/templates/base/.env.local.example.hbs +20 -0
- package/templates/base/.env.local.hbs +13 -2
- package/templates/base/.github/CODEOWNERS.hbs +1 -0
- package/templates/base/.github/SECURITY.md +10 -0
- package/templates/base/.github/dependabot.yml +19 -0
- package/templates/base/.github/workflows/ci.yml.hbs +77 -0
- package/templates/base/.github/workflows/githat-policy.yml +51 -0
- package/templates/base/.gitignore.hbs +17 -2
- package/templates/base/README.md.hbs +31 -52
- package/templates/classroom/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/classroom/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/classroom/app/globals.css.hbs +87 -0
- package/templates/classroom/app/layout.tsx.hbs +41 -0
- package/templates/classroom/app/page.tsx.hbs +103 -0
- package/templates/classroom/app/projects/[id]/feedback/page.tsx.hbs +159 -0
- package/templates/classroom/app/projects/[id]/present/page.tsx.hbs +113 -0
- package/templates/classroom/next.config.ts.hbs +8 -0
- package/templates/classroom/postcss.config.mjs.hbs +14 -0
- package/templates/classroom/proxy.ts.hbs +10 -0
- package/templates/classroom/tsconfig.json.hbs +21 -0
- package/templates/content/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/content/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/content/app/globals.css.hbs +87 -0
- package/templates/content/app/layout.tsx.hbs +41 -0
- package/templates/content/app/newsletter/page.tsx.hbs +90 -0
- package/templates/content/app/page.tsx.hbs +105 -0
- package/templates/content/app/posts/[slug]/page.tsx.hbs +119 -0
- package/templates/content/next.config.ts.hbs +8 -0
- package/templates/content/postcss.config.mjs.hbs +14 -0
- package/templates/content/proxy.ts.hbs +10 -0
- package/templates/content/tsconfig.json.hbs +21 -0
- package/templates/dashboard/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/dashboard/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/dashboard/app/admin/data/[entity]/page.tsx.hbs +68 -0
- package/templates/dashboard/app/admin/page.tsx.hbs +59 -0
- package/templates/dashboard/app/globals.css.hbs +87 -0
- package/templates/dashboard/app/layout.tsx.hbs +41 -0
- package/templates/dashboard/app/page.tsx.hbs +57 -0
- package/templates/dashboard/next.config.ts.hbs +8 -0
- package/templates/dashboard/postcss.config.mjs.hbs +14 -0
- package/templates/dashboard/proxy.ts.hbs +10 -0
- package/templates/dashboard/src/lib/db.ts.hbs +39 -0
- package/templates/dashboard/tsconfig.json.hbs +21 -0
- package/templates/fullstack/apps-api-express/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-express/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-express/package.json.hbs +24 -0
- package/templates/fullstack/apps-api-express/src/index.ts.hbs +41 -0
- package/templates/fullstack/apps-api-express/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-express/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-express/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-api-fastify/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-fastify/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-fastify/package.json.hbs +22 -0
- package/templates/fullstack/apps-api-fastify/src/index.ts.hbs +28 -0
- package/templates/fullstack/apps-api-fastify/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-fastify/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-fastify/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-api-hono/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-hono/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-hono/package.json.hbs +22 -0
- package/templates/fullstack/apps-api-hono/src/index.ts.hbs +35 -0
- package/templates/fullstack/apps-api-hono/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-hono/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-hono/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-web-nextjs/.env.example.hbs +5 -0
- package/templates/fullstack/apps-web-nextjs/.env.local.hbs +5 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/forgot-password/page.tsx.hbs +11 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/reset-password/page.tsx.hbs +39 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/verify-email/page.tsx.hbs +11 -0
- package/templates/fullstack/apps-web-nextjs/app/dashboard/layout.tsx.hbs +15 -0
- package/templates/fullstack/apps-web-nextjs/app/dashboard/page.tsx.hbs +27 -0
- package/templates/fullstack/apps-web-nextjs/app/globals.css.hbs +21 -0
- package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +30 -0
- package/templates/fullstack/apps-web-nextjs/app/page.tsx.hbs +17 -0
- package/templates/fullstack/apps-web-nextjs/next.config.ts.hbs +16 -0
- package/templates/fullstack/apps-web-nextjs/package.json.hbs +34 -0
- package/templates/fullstack/apps-web-nextjs/postcss.config.mjs.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/tsconfig.json.hbs +21 -0
- package/templates/fullstack/root/.gitignore.hbs +42 -0
- package/templates/fullstack/root/githat.yaml.hbs +17 -0
- package/templates/fullstack/root/package.json.hbs +15 -0
- package/templates/fullstack/root/turbo.json.hbs +20 -0
- package/templates/marketplace/CULTURE.md +74 -0
- package/templates/marketplace/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/marketplace/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/marketplace/app/(shop)/[slug]/p/[productId]/page.tsx.hbs +99 -0
- package/templates/marketplace/app/(shop)/[slug]/page.tsx.hbs +90 -0
- package/templates/marketplace/app/admin/page.tsx.hbs +95 -0
- package/templates/marketplace/app/cart/page.tsx.hbs +157 -0
- package/templates/marketplace/app/globals.css.hbs +87 -0
- package/templates/marketplace/app/layout.tsx.hbs +77 -0
- package/templates/marketplace/app/page.tsx.hbs +178 -0
- package/templates/marketplace/app/sell/page.tsx.hbs +78 -0
- package/templates/marketplace/next.config.ts.hbs +8 -0
- package/templates/marketplace/postcss.config.mjs.hbs +14 -0
- package/templates/marketplace/proxy.ts.hbs +10 -0
- package/templates/marketplace/src/lib/anon-session.ts.hbs +117 -0
- package/templates/marketplace/src/lib/categories.ts.hbs +35 -0
- package/templates/marketplace/tsconfig.json.hbs +21 -0
- package/templates/nextjs/.github/workflows/deploy.yml.hbs +107 -0
- package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +106 -0
- package/templates/nextjs/app/globals.css.hbs +4 -3
- package/templates/nextjs/app/layout.tsx.hbs +5 -1
- package/templates/nextjs/app/page.tsx.hbs +3 -6
- package/templates/nextjs/next.config.ts.hbs +5 -2
- package/templates/nextjs/proxy.ts.hbs +1 -1
- package/templates/plain/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/plain/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/plain/app/globals.css.hbs +87 -0
- package/templates/plain/app/layout.tsx.hbs +41 -0
- package/templates/plain/app/page.tsx.hbs +123 -0
- package/templates/plain/next.config.ts.hbs +8 -0
- package/templates/plain/postcss.config.mjs.hbs +14 -0
- package/templates/plain/proxy.ts.hbs +10 -0
- package/templates/plain/tsconfig.json.hbs +21 -0
- package/templates/portfolio/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/portfolio/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/portfolio/app/globals.css.hbs +87 -0
- package/templates/portfolio/app/layout.tsx.hbs +41 -0
- package/templates/portfolio/app/page.tsx.hbs +86 -0
- package/templates/portfolio/next.config.ts.hbs +8 -0
- package/templates/portfolio/postcss.config.mjs.hbs +14 -0
- package/templates/portfolio/proxy.ts.hbs +10 -0
- package/templates/portfolio/tsconfig.json.hbs +21 -0
- package/templates/react-vite/src/App.tsx.hbs +11 -9
- package/templates/react-vite/src/index.css.hbs +4 -3
- package/templates/react-vite/src/pages/Home.tsx.hbs +3 -6
- package/templates/saas/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/saas/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/saas/app/admin/billing/page.tsx.hbs +145 -0
- package/templates/saas/app/admin/page.tsx.hbs +106 -0
- package/templates/saas/app/admin/team/page.tsx.hbs +134 -0
- package/templates/saas/app/globals.css.hbs +87 -0
- package/templates/saas/app/layout.tsx.hbs +41 -0
- package/templates/saas/app/page.tsx.hbs +108 -0
- package/templates/saas/app/pricing/page.tsx.hbs +131 -0
- package/templates/saas/next.config.ts.hbs +8 -0
- package/templates/saas/postcss.config.mjs.hbs +14 -0
- package/templates/saas/proxy.ts.hbs +10 -0
- package/templates/saas/tsconfig.json.hbs +21 -0
|
@@ -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,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,9 @@
|
|
|
1
|
+
import { SignInForm } from '@githat/nextjs';
|
|
2
|
+
|
|
3
|
+
export default function SignInPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
|
|
6
|
+
<SignInForm signUpUrl="/sign-up" {{#if includeForgotPassword}}forgotPasswordUrl="/forgot-password"{{/if}} />
|
|
7
|
+
</main>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SignUpForm } from '@githat/nextjs';
|
|
2
|
+
|
|
3
|
+
export default function SignUpPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
|
|
6
|
+
<SignUpForm signInUrl="/sign-in" />
|
|
7
|
+
</main>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind v4 — required because @githat/nextjs/styles is processed
|
|
3
|
+
* through @tailwindcss/postcss. Plain doesn't ship utility classes,
|
|
4
|
+
* but the import is needed for the auth pages to render styled.
|
|
5
|
+
*/
|
|
6
|
+
@import "tailwindcss";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Plain template — self-contained globals.
|
|
10
|
+
*
|
|
11
|
+
* Defines the minimum CSS variables a GitHat app uses for layout and
|
|
12
|
+
* the auth-page styling that ships with @githat/nextjs/styles.
|
|
13
|
+
* Override these in your own files when you want a real theme.
|
|
14
|
+
*
|
|
15
|
+
* Light theme by default; flip --bg/--fg for dark.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
/* Surface */
|
|
20
|
+
--bg: #ffffff;
|
|
21
|
+
--surface: #fafafa;
|
|
22
|
+
--surface-sub: #f4f4f5;
|
|
23
|
+
|
|
24
|
+
/* Borders */
|
|
25
|
+
--border: #e5e7eb;
|
|
26
|
+
|
|
27
|
+
/* Foreground */
|
|
28
|
+
--fg: #0a0a0a;
|
|
29
|
+
--fg-muted: #525252;
|
|
30
|
+
--fg-subtle: #737373;
|
|
31
|
+
|
|
32
|
+
/* Brand — change these two to re-skin the whole auth flow */
|
|
33
|
+
--primary: #6366f1;
|
|
34
|
+
--accent: #f59e0b;
|
|
35
|
+
|
|
36
|
+
/* Semantic */
|
|
37
|
+
--success: #16a34a;
|
|
38
|
+
--warn: #d97706;
|
|
39
|
+
--danger: #dc2626;
|
|
40
|
+
|
|
41
|
+
/* Spacing — used by @githat/nextjs/styles */
|
|
42
|
+
--space-1: 0.25rem;
|
|
43
|
+
--space-2: 0.5rem;
|
|
44
|
+
--space-3: 0.75rem;
|
|
45
|
+
--space-4: 1rem;
|
|
46
|
+
--space-6: 1.5rem;
|
|
47
|
+
--space-8: 2rem;
|
|
48
|
+
|
|
49
|
+
/* Radius */
|
|
50
|
+
--radius: 0.5rem;
|
|
51
|
+
--radius-md: 0.5rem;
|
|
52
|
+
--radius-lg: 0.75rem;
|
|
53
|
+
|
|
54
|
+
/* Fonts */
|
|
55
|
+
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
56
|
+
--font-wordmark: 'Instrument Serif', Georgia, serif;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
:root {
|
|
61
|
+
--bg: #0a0a0a;
|
|
62
|
+
--surface: #18181b;
|
|
63
|
+
--surface-sub: #27272a;
|
|
64
|
+
--border: #3f3f46;
|
|
65
|
+
--fg: #fafafa;
|
|
66
|
+
--fg-muted: #a1a1aa;
|
|
67
|
+
--fg-subtle: #71717a;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
* {
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
body {
|
|
78
|
+
font-family: var(--font-sans);
|
|
79
|
+
background: var(--bg);
|
|
80
|
+
color: var(--fg);
|
|
81
|
+
line-height: 1.5;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
a {
|
|
85
|
+
color: inherit;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useAuth } from '@githat/nextjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Content site homepage.
|
|
8
|
+
*
|
|
9
|
+
* Replaces Substack + Stripe + a paywall plugin. Demonstrates:
|
|
10
|
+
* - Free posts, paid posts, subscriber-only posts
|
|
11
|
+
* - Sebastn for one-time tips and recurring subscriptions
|
|
12
|
+
* - GitHat email for the newsletter
|
|
13
|
+
*
|
|
14
|
+
* Sample posts are hardcoded — replace with a fetch from your CMS,
|
|
15
|
+
* Markdown files, or a Postgres `posts` table.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const SAMPLE_POSTS = [
|
|
19
|
+
{ slug: 'welcome', title: 'Welcome to {{businessName}}', excerpt: 'What this newsletter is about.', gated: false },
|
|
20
|
+
{ slug: 'first-deep-dive', title: 'Why I started this', excerpt: 'The one thing nobody told me.', gated: false },
|
|
21
|
+
{ slug: 'paid-essay-1', title: 'Paid essay: the long version', excerpt: 'For paid subscribers — the unfiltered take.', gated: true },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export default function Home() {
|
|
25
|
+
const { isSignedIn } = useAuth();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div style=\{{ background: 'var(--bg)', color: 'var(--fg)' }}>
|
|
29
|
+
<section style=\{{
|
|
30
|
+
padding: 'var(--space-12) var(--space-4)',
|
|
31
|
+
textAlign: 'center',
|
|
32
|
+
maxWidth: '40rem',
|
|
33
|
+
margin: '0 auto',
|
|
34
|
+
}}>
|
|
35
|
+
<h1 style=\{{
|
|
36
|
+
fontFamily: 'var(--font-wordmark, Georgia, serif)',
|
|
37
|
+
fontSize: 'clamp(2rem, 5vw, 3rem)',
|
|
38
|
+
lineHeight: 1.1,
|
|
39
|
+
marginBottom: 'var(--space-3)',
|
|
40
|
+
}}>
|
|
41
|
+
{{businessName}}
|
|
42
|
+
</h1>
|
|
43
|
+
<p style=\{{ color: 'var(--fg-muted)', fontSize: '1.125rem', marginBottom: 'var(--space-6)' }}>
|
|
44
|
+
{{description}}
|
|
45
|
+
</p>
|
|
46
|
+
<Link href="/newsletter" style=\{{
|
|
47
|
+
display: 'inline-block',
|
|
48
|
+
padding: 'var(--space-3) var(--space-6)',
|
|
49
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
50
|
+
background: 'var(--primary)',
|
|
51
|
+
color: 'var(--bg)',
|
|
52
|
+
fontWeight: 600,
|
|
53
|
+
textDecoration: 'none',
|
|
54
|
+
}}>
|
|
55
|
+
Subscribe to the newsletter →
|
|
56
|
+
</Link>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section style=\{{ padding: 'var(--space-8) var(--space-4)', maxWidth: '48rem', margin: '0 auto' }}>
|
|
60
|
+
<h2 style=\{{ fontSize: '1.5rem', marginBottom: 'var(--space-4)' }}>Recent posts</h2>
|
|
61
|
+
<ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
|
|
62
|
+
{SAMPLE_POSTS.map((post) => (
|
|
63
|
+
<li key={post.slug}>
|
|
64
|
+
<Link href={`/posts/${post.slug}`} style=\{{
|
|
65
|
+
display: 'block',
|
|
66
|
+
padding: 'var(--space-5)',
|
|
67
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
68
|
+
border: '1px solid var(--border)',
|
|
69
|
+
background: 'var(--surface)',
|
|
70
|
+
color: 'var(--fg)',
|
|
71
|
+
textDecoration: 'none',
|
|
72
|
+
}}>
|
|
73
|
+
<div style=\{{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', marginBottom: 'var(--space-2)' }}>
|
|
74
|
+
<h3 style=\{{ fontSize: '1.25rem', fontWeight: 600, margin: 0 }}>{post.title}</h3>
|
|
75
|
+
{post.gated && (
|
|
76
|
+
<span style=\{{
|
|
77
|
+
padding: '2px var(--space-2)',
|
|
78
|
+
borderRadius: 'var(--radius-full, 9999px)',
|
|
79
|
+
background: 'var(--accent)',
|
|
80
|
+
color: 'var(--bg)',
|
|
81
|
+
fontSize: '0.625rem',
|
|
82
|
+
fontWeight: 700,
|
|
83
|
+
}}>
|
|
84
|
+
PAID
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
<p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>{post.excerpt}</p>
|
|
89
|
+
</Link>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
{!isSignedIn && (
|
|
96
|
+
<section style=\{{ padding: 'var(--space-8) var(--space-4)', background: 'var(--surface-sub)', textAlign: 'center' }}>
|
|
97
|
+
<p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-3)' }}>
|
|
98
|
+
Already a subscriber?
|
|
99
|
+
</p>
|
|
100
|
+
<Link href="/sign-in" style=\{{ color: 'var(--primary)' }}>Sign in →</Link>
|
|
101
|
+
</section>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|