create-githat-app 1.7.0 → 1.8.1

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 (74) hide show
  1. package/dist/cli.js +4 -3
  2. package/package.json +6 -2
  3. package/templates/agent/app/(auth)/forgot-password/page.tsx.hbs +11 -0
  4. package/templates/agent/app/(auth)/reset-password/page.tsx.hbs +39 -0
  5. package/templates/agent/app/(auth)/verify-email/page.tsx.hbs +41 -0
  6. package/templates/agent/app/admin/agent/page.tsx.hbs +159 -62
  7. package/templates/agent/app/admin/layout.tsx.hbs +82 -0
  8. package/templates/agent/app/admin/mcp/page.tsx.hbs +156 -0
  9. package/templates/agent/app/dashboard/agents/page.tsx.hbs +9 -0
  10. package/templates/agent/app/dashboard/layout.tsx.hbs +9 -0
  11. package/templates/agent/app/dashboard/mcp/page.tsx.hbs +9 -0
  12. package/templates/agent/app/dashboard/page.tsx.hbs +128 -0
  13. package/templates/agent/app/globals.css.hbs +14 -9
  14. package/templates/agent/app/layout.tsx.hbs +7 -2
  15. package/templates/agent/app/page.tsx.hbs +127 -70
  16. package/templates/agent/app/verify/agent/page.tsx.hbs +124 -0
  17. package/templates/agent/next.config.ts.hbs +4 -5
  18. package/templates/agent/public/HERO_IMAGE.md +23 -0
  19. package/templates/base/README.md.hbs +2 -1
  20. package/templates/base/githat/api/agents.ts.hbs +6 -6
  21. package/templates/base/githat/auth/guard.tsx.hbs +6 -6
  22. package/templates/base/githat/config.ts.hbs +7 -9
  23. package/templates/base/githat/dashboard/layout.tsx.hbs +6 -6
  24. package/templates/base/githat/dashboard/overview.tsx.hbs +106 -16
  25. package/templates/classroom/app/layout.tsx.hbs +6 -1
  26. package/templates/classroom/app/projects/[id]/feedback/feedback-content.tsx.hbs +153 -0
  27. package/templates/classroom/app/projects/[id]/feedback/page.tsx.hbs +10 -149
  28. package/templates/classroom/app/projects/[id]/present/page.tsx.hbs +11 -104
  29. package/templates/classroom/app/projects/[id]/present/presenter-content.tsx.hbs +118 -0
  30. package/templates/classroom/next.config.ts.hbs +4 -5
  31. package/templates/content/app/layout.tsx.hbs +6 -1
  32. package/templates/content/app/posts/[slug]/page.tsx.hbs +10 -86
  33. package/templates/content/app/posts/[slug]/post-content.tsx.hbs +93 -0
  34. package/templates/content/next.config.ts.hbs +4 -5
  35. package/templates/dashboard/app/admin/data/[entity]/page.tsx.hbs +15 -2
  36. package/templates/dashboard/app/layout.tsx.hbs +6 -1
  37. package/templates/dashboard/next.config.ts.hbs +4 -5
  38. package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +6 -1
  39. package/templates/fullstack/apps-web-nextjs/next.config.ts.hbs +5 -5
  40. package/templates/marketplace/app/layout.tsx.hbs +6 -1
  41. package/templates/marketplace/next.config.ts.hbs +4 -5
  42. package/templates/nextjs/app/(auth)/forgot-password/page.tsx.hbs +2 -54
  43. package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +8 -75
  44. package/templates/nextjs/app/globals.css.hbs +71 -1
  45. package/templates/nextjs/app/layout.tsx.hbs +9 -3
  46. package/templates/nextjs/app/page.tsx.hbs +6 -3
  47. package/templates/nextjs/next.config.ts.hbs +4 -5
  48. package/templates/plain/app/layout.tsx.hbs +6 -1
  49. package/templates/plain/next.config.ts.hbs +4 -5
  50. package/templates/portfolio/app/layout.tsx.hbs +6 -1
  51. package/templates/portfolio/next.config.ts.hbs +4 -5
  52. package/templates/saas/app/layout.tsx.hbs +6 -1
  53. package/templates/saas/next.config.ts.hbs +4 -5
  54. package/templates/agent/app/api/githat/[...path]/route.ts.hbs +0 -21
  55. package/templates/agent/proxy.ts.hbs +0 -10
  56. package/templates/classroom/app/api/githat/[...path]/route.ts.hbs +0 -21
  57. package/templates/classroom/proxy.ts.hbs +0 -10
  58. package/templates/content/app/api/githat/[...path]/route.ts.hbs +0 -21
  59. package/templates/content/proxy.ts.hbs +0 -10
  60. package/templates/dashboard/app/api/githat/[...path]/route.ts.hbs +0 -21
  61. package/templates/dashboard/proxy.ts.hbs +0 -10
  62. package/templates/fullstack/apps-web-nextjs/app/api/githat/[...path]/route.ts.hbs +0 -21
  63. package/templates/marketplace/app/(shop)/[slug]/p/[productId]/page.tsx.hbs +0 -99
  64. package/templates/marketplace/app/(shop)/[slug]/page.tsx.hbs +0 -90
  65. package/templates/marketplace/app/api/githat/[...path]/route.ts.hbs +0 -21
  66. package/templates/marketplace/proxy.ts.hbs +0 -10
  67. package/templates/nextjs/app/api/githat/[...path]/route.ts.hbs +0 -21
  68. package/templates/nextjs/proxy.ts.hbs +0 -10
  69. package/templates/plain/app/api/githat/[...path]/route.ts.hbs +0 -21
  70. package/templates/plain/proxy.ts.hbs +0 -10
  71. package/templates/portfolio/app/api/githat/[...path]/route.ts.hbs +0 -21
  72. package/templates/portfolio/proxy.ts.hbs +0 -10
  73. package/templates/saas/app/api/githat/[...path]/route.ts.hbs +0 -21
  74. package/templates/saas/proxy.ts.hbs +0 -10
@@ -1,113 +1,20 @@
1
- 'use client';
2
-
3
- import { use, useEffect, useState } from 'react';
1
+ import PresenterContent from './presenter-content{{#unless typescript}}.jsx{{/unless}}';
4
2
 
5
3
  /**
6
4
  * Presenter view — `/projects/[id]/present`.
7
5
  *
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.
6
+ * generateStaticParams tells Next.js which ids to pre-render at build
7
+ * time (required for `output: "export"`). PresenterContent is a client
8
+ * component that renders the live feedback view with polling.
18
9
  */
19
10
 
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);
11
+ const SAMPLE_PROJECT_IDS = ['sustainable-bodega', 'after-school-tutor', 'community-fridge'];
36
12
 
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>
13
+ export function generateStaticParams() {
14
+ return SAMPLE_PROJECT_IDS.map((id) => ({ id }));
15
+ }
76
16
 
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
- );
17
+ export default async function PresentPage({ params }: { params: Promise<{ id: string }> }) {
18
+ const { id } = await params;
19
+ return <PresenterContent id={id} />;
113
20
  }
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ {{#if typescript}}
6
+ interface FeedbackEntry {
7
+ id: string;
8
+ oneThing: string;
9
+ stuck: string;
10
+ scale: number;
11
+ receivedAt: number;
12
+ }
13
+
14
+ interface PresenterContentProps {
15
+ id: string;
16
+ }
17
+ {{/if}}
18
+
19
+ /**
20
+ * Presenter view — `/projects/[id]/present`.
21
+ *
22
+ * The student opens this on their second screen (or a phone next
23
+ * to the laptop) and presents. As the audience submits feedback
24
+ * via /projects/[id]/feedback, responses fade in here in real
25
+ * time — newest first, color-coded by the scale rating.
26
+ *
27
+ * Real implementations:
28
+ * - Replace the mock interval with a polling fetch (every 3-5s)
29
+ * against your backend, OR a websocket / SSE stream.
30
+ * - Persist responses to your DB so the project can reiterate
31
+ * over multiple cohorts.
32
+ */
33
+
34
+ const MOCK_FEEDBACK{{#if typescript}}: FeedbackEntry[]{{/if}} = [
35
+ { id: 'a', oneThing: 'The slide on supply chain bottlenecks', stuck: 'How do you measure trust at scale?', scale: 9, receivedAt: Date.now() - 60_000 },
36
+ { id: 'b', oneThing: 'Loved the bilingual UX argument', stuck: 'What about Mexican misceláneas?', scale: 8, receivedAt: Date.now() - 120_000 },
37
+ ];
38
+
39
+ export default function PresenterContent({ id }{{#if typescript}}: PresenterContentProps{{/if}}) {
40
+ const [feedback, setFeedback] = useState{{#if typescript}}<FeedbackEntry[]>{{/if}}(MOCK_FEEDBACK);
41
+
42
+ // Simulated incoming feedback. Replace with a real fetch poll or
43
+ // websocket subscription. The shape is the same.
44
+ useEffect(() => {
45
+ const t = setInterval(() => {
46
+ // setFeedback((prev) => [{ ...newOne, id: crypto.randomUUID() }, ...prev]);
47
+ }, 5000);
48
+ return () => clearInterval(t);
49
+ }, []);
50
+
51
+ const audienceUrl = typeof window !== 'undefined'
52
+ ? `${window.location.origin}/projects/${id}/feedback`
53
+ : `/projects/${id}/feedback`;
54
+
55
+ return (
56
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: '100vh', padding: 'var(--space-6) var(--space-4)' }}>
57
+ <div style=\{{ maxWidth: '64rem', margin: '0 auto' }}>
58
+ <header style=\{{
59
+ padding: 'var(--space-5)',
60
+ borderRadius: 'var(--radius-md, 0.5rem)',
61
+ background: 'var(--surface-sub)',
62
+ marginBottom: 'var(--space-6)',
63
+ textAlign: 'center',
64
+ }}>
65
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', marginBottom: 'var(--space-2)' }}>
66
+ Audience submits feedback at:
67
+ </p>
68
+ <code style=\{{
69
+ fontSize: '1.25rem',
70
+ fontFamily: 'var(--font-mono, monospace)',
71
+ color: 'var(--primary)',
72
+ wordBreak: 'break-all',
73
+ }}>
74
+ {audienceUrl}
75
+ </code>
76
+ </header>
77
+
78
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.75rem', marginBottom: 'var(--space-4)' }}>
79
+ Live feedback ({feedback.length})
80
+ </h1>
81
+
82
+ <ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
83
+ {feedback.map((f) => (
84
+ <li key={f.id} style=\{{
85
+ padding: 'var(--space-5)',
86
+ borderRadius: 'var(--radius-md, 0.5rem)',
87
+ border: `2px solid ${f.scale >= 8 ? 'var(--success)' : f.scale >= 5 ? 'var(--info)' : 'var(--warn)'}`,
88
+ background: 'var(--surface)',
89
+ }}>
90
+ <div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-3)', marginBottom: 'var(--space-2)' }}>
91
+ <div style=\{{ flex: 1 }}>
92
+ <div style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-1)' }}>What landed</div>
93
+ <div style=\{{ fontWeight: 600 }}>{f.oneThing}</div>
94
+ </div>
95
+ <div style=\{{
96
+ padding: 'var(--space-1) var(--space-3)',
97
+ borderRadius: 'var(--radius-full, 9999px)',
98
+ background: f.scale >= 8 ? 'var(--success)' : f.scale >= 5 ? 'var(--info)' : 'var(--warn)',
99
+ color: 'var(--bg)',
100
+ fontSize: '0.75rem',
101
+ fontWeight: 700,
102
+ }}>
103
+ {f.scale}/10
104
+ </div>
105
+ </div>
106
+ {f.stuck && (
107
+ <div style=\{{ marginTop: 'var(--space-3)', paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border)' }}>
108
+ <div style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-1)' }}>Wants more on</div>
109
+ <div style=\{{ fontSize: '0.9375rem', color: 'var(--fg)' }}>{f.stuck}</div>
110
+ </div>
111
+ )}
112
+ </li>
113
+ ))}
114
+ </ul>
115
+ </div>
116
+ </div>
117
+ );
118
+ }
@@ -1,9 +1,8 @@
1
- {{#if typescript}}import type { NextConfig } from 'next';
2
- {{/if}}import { withGitHat } from '@githat/nextjs/server';
1
+ import { withGitHat } from '@githat/nextjs/server';
3
2
 
4
- {{#if typescript}}const nextConfig: NextConfig = {
5
- {{else}}const nextConfig = {
6
- {{/if}} output: 'standalone',
3
+ const nextConfig = {
4
+ output: 'export',
5
+ images: { unoptimized: true },
7
6
  };
8
7
 
9
8
  export default withGitHat(nextConfig);
@@ -19,7 +19,12 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
19
19
  */}
20
20
  <GitHatProvider config=\{{
21
21
  publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
22
- apiUrl: '/api/githat',
22
+ apiUrl: 'https://api.githat.io',
23
+ {{#if typescript}}
24
+ tokenStorage: 'localStorage' as const,
25
+ {{else}}
26
+ tokenStorage: 'localStorage',
27
+ {{/if}}
23
28
  signInUrl: '/sign-in',
24
29
  signUpUrl: '/sign-up',
25
30
  afterSignInUrl: '/',
@@ -1,19 +1,11 @@
1
- 'use client';
2
-
3
- import { use } from 'react';
4
- import Link from 'next/link';
5
- import { useAuth } from '@githat/nextjs';
1
+ import PostContent from './post-content{{#unless typescript}}.jsx{{/unless}}';
6
2
 
7
3
  /**
8
4
  * Post viewer with paywall.
9
5
  *
10
- * Three states a reader can be in:
11
- * 1. Post is free show full content
12
- * 2. Post is gated + reader is signed in + has active subscription → show full
13
- * 3. Post is gated + reader is anonymous OR not subscribed → show preview + CTA
14
- *
15
- * Subscription check is stubbed (`hasActiveSubscription` always
16
- * false). Wire to your backend that watches Sebastn webhooks.
6
+ * generateStaticParams tells Next.js which slugs to pre-render at
7
+ * build time (required for `output: "export"`). PostContent is a
8
+ * client component that handles the auth-gated paywall at runtime.
17
9
  */
18
10
 
19
11
  const SAMPLE_POSTS: Record<string, { title: string; gated: boolean; body: string }> = {
@@ -40,80 +32,12 @@ const SAMPLE_POSTS: Record<string, { title: string; gated: boolean; body: string
40
32
  },
41
33
  };
42
34
 
43
- export default function PostPage({ params }: { params: Promise<{ slug: string }> }) {
44
- const { slug } = use(params);
45
- const post = SAMPLE_POSTS[slug];
46
- const { isSignedIn } = useAuth();
47
-
48
- // Stub: real apps fetch the user's subscription state from your
49
- // backend (which watches Sebastn webhooks for the source of truth).
50
- const hasActiveSubscription = false;
51
-
52
- if (!post) {
53
- return (
54
- <div style=\{{ padding: 'var(--space-12)', textAlign: 'center' }}>
55
- <p>Post not found.</p>
56
- <Link href="/" style=\{{ color: 'var(--primary)' }}>← Home</Link>
57
- </div>
58
- );
59
- }
60
-
61
- const showPaywall = post.gated && (!isSignedIn || !hasActiveSubscription);
62
-
63
- return (
64
- <article style=\{{ maxWidth: '40rem', margin: '0 auto', padding: 'var(--space-8) var(--space-4)' }}>
65
- <Link href="/" style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', display: 'inline-block', marginBottom: 'var(--space-4)' }}>
66
- ← All posts
67
- </Link>
68
- <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2.25rem', lineHeight: 1.2, marginBottom: 'var(--space-4)' }}>
69
- {post.title}
70
- </h1>
71
-
72
- <div style=\{{ color: 'var(--fg)', lineHeight: 1.8, fontSize: '1.0625rem' }}>
73
- {showPaywall ? (
74
- <>
75
- <p style=\{{ marginBottom: 'var(--space-4)' }}>{post.body.slice(0, 200)}…</p>
76
- <Paywall />
77
- </>
78
- ) : (
79
- <p>{post.body}</p>
80
- )}
81
- </div>
82
- </article>
83
- );
35
+ export function generateStaticParams() {
36
+ return Object.keys(SAMPLE_POSTS).map((slug) => ({ slug }));
84
37
  }
85
38
 
86
- function Paywall() {
87
- return (
88
- <div style=\{{
89
- marginTop: 'var(--space-8)',
90
- padding: 'var(--space-6)',
91
- borderRadius: 'var(--radius-md, 0.5rem)',
92
- border: '2px solid var(--accent)',
93
- background: 'var(--surface-sub)',
94
- textAlign: 'center',
95
- }}>
96
- <h3 style=\{{ fontFamily: 'var(--font-wordmark)', marginBottom: 'var(--space-2)' }}>
97
- This post is for subscribers
98
- </h3>
99
- <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-4)' }}>
100
- Get access to every paid essay, plus the newsletter.
101
- </p>
102
- <button
103
- onClick={() => { /* TODO: open Sebastn checkout */ }}
104
- style=\{{
105
- padding: 'var(--space-3) var(--space-6)',
106
- borderRadius: 'var(--radius-md, 0.5rem)',
107
- border: 'none',
108
- background: 'var(--primary)',
109
- color: 'var(--bg)',
110
- fontWeight: 600,
111
- fontSize: '1rem',
112
- cursor: 'pointer',
113
- }}
114
- >
115
- Subscribe — $5/mo
116
- </button>
117
- </div>
118
- );
39
+ export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
40
+ const { slug } = await params;
41
+ const post = SAMPLE_POSTS[slug] ?? null;
42
+ return <PostContent slug={slug} post={post} />;
119
43
  }
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useAuth } from '@githat/nextjs';
5
+
6
+ {{#if typescript}}
7
+ interface Post {
8
+ title: string;
9
+ gated: boolean;
10
+ body: string;
11
+ }
12
+
13
+ interface PostContentProps {
14
+ slug: string;
15
+ post: Post | null;
16
+ }
17
+ {{/if}}
18
+
19
+ export default function PostContent({ slug, post }{{#if typescript}}: PostContentProps{{/if}}) {
20
+ const { isSignedIn } = useAuth();
21
+
22
+ // Stub: real apps fetch the user's subscription state from your
23
+ // backend (which watches Sebastn webhooks for the source of truth).
24
+ const hasActiveSubscription = false;
25
+
26
+ if (!post) {
27
+ return (
28
+ <div style=\{{ padding: 'var(--space-12)', textAlign: 'center' }}>
29
+ <p>Post not found.</p>
30
+ <Link href="/" style=\{{ color: 'var(--primary)' }}>← Home</Link>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const showPaywall = post.gated && (!isSignedIn || !hasActiveSubscription);
36
+
37
+ return (
38
+ <article style=\{{ maxWidth: '40rem', margin: '0 auto', padding: 'var(--space-8) var(--space-4)' }}>
39
+ <Link href="/" style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', display: 'inline-block', marginBottom: 'var(--space-4)' }}>
40
+ ← All posts
41
+ </Link>
42
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2.25rem', lineHeight: 1.2, marginBottom: 'var(--space-4)' }}>
43
+ {post.title}
44
+ </h1>
45
+
46
+ <div style=\{{ color: 'var(--fg)', lineHeight: 1.8, fontSize: '1.0625rem' }}>
47
+ {showPaywall ? (
48
+ <>
49
+ <p style=\{{ marginBottom: 'var(--space-4)' }}>{post.body.slice(0, 200)}…</p>
50
+ <Paywall />
51
+ </>
52
+ ) : (
53
+ <p>{post.body}</p>
54
+ )}
55
+ </div>
56
+ </article>
57
+ );
58
+ }
59
+
60
+ function Paywall() {
61
+ return (
62
+ <div style=\{{
63
+ marginTop: 'var(--space-8)',
64
+ padding: 'var(--space-6)',
65
+ borderRadius: 'var(--radius-md, 0.5rem)',
66
+ border: '2px solid var(--accent)',
67
+ background: 'var(--surface-sub)',
68
+ textAlign: 'center',
69
+ }}>
70
+ <h3 style=\{{ fontFamily: 'var(--font-wordmark)', marginBottom: 'var(--space-2)' }}>
71
+ This post is for subscribers
72
+ </h3>
73
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-4)' }}>
74
+ Get access to every paid essay, plus the newsletter.
75
+ </p>
76
+ <button
77
+ onClick={() => { /* TODO: open Sebastn checkout */ }}
78
+ style=\{{
79
+ padding: 'var(--space-3) var(--space-6)',
80
+ borderRadius: 'var(--radius-md, 0.5rem)',
81
+ border: 'none',
82
+ background: 'var(--primary)',
83
+ color: 'var(--bg)',
84
+ fontWeight: 600,
85
+ fontSize: '1rem',
86
+ cursor: 'pointer',
87
+ }}
88
+ >
89
+ Subscribe — $5/mo
90
+ </button>
91
+ </div>
92
+ );
93
+ }
@@ -1,9 +1,8 @@
1
- {{#if typescript}}import type { NextConfig } from 'next';
2
- {{/if}}import { withGitHat } from '@githat/nextjs/server';
1
+ import { withGitHat } from '@githat/nextjs/server';
3
2
 
4
- {{#if typescript}}const nextConfig: NextConfig = {
5
- {{else}}const nextConfig = {
6
- {{/if}} output: 'standalone',
3
+ const nextConfig = {
4
+ output: 'export',
5
+ images: { unoptimized: true },
7
6
  };
8
7
 
9
8
  export default withGitHat(nextConfig);
@@ -6,9 +6,22 @@ import { listEntities } from '../../../../src/lib/db';
6
6
  *
7
7
  * Server component — runs `listEntities(entityName)` (which you
8
8
  * implement in src/lib/db.ts) and renders rows as a basic table.
9
- * Auth-gating is enforced by the middleware in proxy.ts; if you
10
- * need entity-level RBAC, check `verifyToken` claims here.
9
+ * Auth-gating happens client-side via `useAuth` in the static export;
10
+ * if you need entity-level RBAC, check `verifyToken` claims here.
11
+ *
12
+ * generateStaticParams is required for `output: "export"`. Add the
13
+ * entity names your app actually uses. Unknown slugs are blocked by
14
+ * `dynamicParams = false`.
11
15
  */
16
+
17
+ const ENTITIES = ['users', 'products', 'orders'];
18
+
19
+ export function generateStaticParams() {
20
+ return ENTITIES.map((entity) => ({ entity }));
21
+ }
22
+
23
+ export const dynamicParams = false;
24
+
12
25
  export default async function EntityPage({
13
26
  params,
14
27
  }: {
@@ -19,7 +19,12 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
19
19
  */}
20
20
  <GitHatProvider config=\{{
21
21
  publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
22
- apiUrl: '/api/githat',
22
+ apiUrl: 'https://api.githat.io',
23
+ {{#if typescript}}
24
+ tokenStorage: 'localStorage' as const,
25
+ {{else}}
26
+ tokenStorage: 'localStorage',
27
+ {{/if}}
23
28
  signInUrl: '/sign-in',
24
29
  signUpUrl: '/sign-up',
25
30
  afterSignInUrl: '/',
@@ -1,9 +1,8 @@
1
- {{#if typescript}}import type { NextConfig } from 'next';
2
- {{/if}}import { withGitHat } from '@githat/nextjs/server';
1
+ import { withGitHat } from '@githat/nextjs/server';
3
2
 
4
- {{#if typescript}}const nextConfig: NextConfig = {
5
- {{else}}const nextConfig = {
6
- {{/if}} output: 'standalone',
3
+ const nextConfig = {
4
+ output: 'export',
5
+ images: { unoptimized: true },
7
6
  };
8
7
 
9
8
  export default withGitHat(nextConfig);
@@ -14,7 +14,12 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
14
14
  <body>
15
15
  <GitHatProvider config=\{{
16
16
  publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
17
- apiUrl: '/api/githat',
17
+ apiUrl: 'https://api.githat.io',
18
+ {{#if typescript}}
19
+ tokenStorage: 'localStorage' as const,
20
+ {{else}}
21
+ tokenStorage: 'localStorage',
22
+ {{/if}}
18
23
  signInUrl: '/sign-in',
19
24
  signUpUrl: '/sign-up',
20
25
  afterSignInUrl: '/dashboard',
@@ -1,9 +1,9 @@
1
- {{#if typescript}}import type { NextConfig } from 'next';
2
- {{/if}}import { withGitHat } from '@githat/nextjs/server';
1
+ import { withGitHat } from '@githat/nextjs/server';
3
2
 
4
- {{#if typescript}}const nextConfig: NextConfig = {
5
- {{else}}const nextConfig = {
6
- {{/if}} // Proxy API calls to the backend in development
3
+ const nextConfig = {
4
+ output: 'export',
5
+ images: { unoptimized: true },
6
+ // Proxy API calls to the backend in development
7
7
  async rewrites() {
8
8
  return [
9
9
  {
@@ -25,7 +25,12 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
25
25
  <body>
26
26
  <GitHatProvider config=\{{
27
27
  publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
28
- apiUrl: '/api/githat',
28
+ apiUrl: 'https://api.githat.io',
29
+ {{#if typescript}}
30
+ tokenStorage: 'localStorage' as const,
31
+ {{else}}
32
+ tokenStorage: 'localStorage',
33
+ {{/if}}
29
34
  signInUrl: '/sign-in',
30
35
  signUpUrl: '/sign-up',
31
36
  afterSignInUrl: '/',
@@ -1,9 +1,8 @@
1
- {{#if typescript}}import type { NextConfig } from 'next';
2
- {{/if}}import { withGitHat } from '@githat/nextjs/server';
1
+ import { withGitHat } from '@githat/nextjs/server';
3
2
 
4
- {{#if typescript}}const nextConfig: NextConfig = {
5
- {{else}}const nextConfig = {
6
- {{/if}} output: 'standalone',
3
+ const nextConfig = {
4
+ output: 'export',
5
+ images: { unoptimized: true },
7
6
  };
8
7
 
9
8
  export default withGitHat(nextConfig);