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
@@ -2,11 +2,11 @@
2
2
  'use client';
3
3
 
4
4
  import { useAuth } from '@githat/nextjs';
5
- {{#ifEquals framework "nextjs"}}
5
+ {{#ifNext framework}}
6
6
  import { useRouter } from 'next/navigation';
7
7
  {{else}}
8
8
  import { useNavigate } from 'react-router-dom';
9
- {{/ifEquals}}
9
+ {{/ifNext}}
10
10
  import { githatConfig } from '../config{{#unless typescript}}.js{{/unless}}';
11
11
 
12
12
  export function AuthGuard({ children, requiredRole }{{#if typescript}}: {
@@ -14,11 +14,11 @@ export function AuthGuard({ children, requiredRole }{{#if typescript}}: {
14
14
  requiredRole?: 'owner' | 'admin' | 'member';
15
15
  }{{/if}}) {
16
16
  const { isSignedIn, isLoading, org } = useAuth();
17
- {{#ifEquals framework "nextjs"}}
17
+ {{#ifNext framework}}
18
18
  const router = useRouter();
19
19
  {{else}}
20
20
  const navigate = useNavigate();
21
- {{/ifEquals}}
21
+ {{/ifNext}}
22
22
 
23
23
  if (isLoading) {
24
24
  return (
@@ -29,11 +29,11 @@ export function AuthGuard({ children, requiredRole }{{#if typescript}}: {
29
29
  }
30
30
 
31
31
  if (!isSignedIn) {
32
- {{#ifEquals framework "nextjs"}}
32
+ {{#ifNext framework}}
33
33
  router.push(githatConfig.signInUrl);
34
34
  {{else}}
35
35
  navigate(githatConfig.signInUrl);
36
- {{/ifEquals}}
36
+ {{/ifNext}}
37
37
  return null;
38
38
  }
39
39
 
@@ -1,15 +1,13 @@
1
1
  {{#if includeGithatFolder}}
2
2
  export const githatConfig = {
3
3
  appName: '{{businessName}}',
4
- publishableKey: {{#ifEquals framework "nextjs"}}process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY{{else}}import.meta.env.VITE_GITHAT_PUBLISHABLE_KEY{{/ifEquals}} || '',
5
- // For nextjs we point at the same-origin proxy mounted at
6
- // src/app/api/githat/[...path]/route.ts — that proxy forwards to the
7
- // real api.githat.io upstream and re-emits Set-Cookie on this app's
8
- // domain so cookies are visible to proxy.ts and getAuth(). For
9
- // react-vite (no Next.js API routes) we hit api.githat.io directly,
10
- // which only works for browser-token auth flows that don't depend on
11
- // cookies.
12
- apiUrl: {{#ifEquals framework "nextjs"}}process.env.NEXT_PUBLIC_GITHAT_API_URL || '/api/githat'{{else}}import.meta.env.VITE_GITHAT_API_URL || '{{apiUrl}}'{{/ifEquals}},
4
+ publishableKey: {{#ifNext framework}}process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY{{else}}import.meta.env.VITE_GITHAT_PUBLISHABLE_KEY{{/ifNext}} || '',
5
+ // For Next.js-shaped frameworks we point at the same-origin proxy
6
+ // mounted at app/api/githat/[...path]/route.ts — that proxy forwards
7
+ // to api.githat.io and re-emits Set-Cookie on this app's domain so
8
+ // cookies are visible to proxy.ts and getAuth(). For react-vite we
9
+ // hit api.githat.io directly (browser-token flows only).
10
+ apiUrl: {{#ifNext framework}}process.env.NEXT_PUBLIC_GITHAT_API_URL || '/api/githat'{{else}}import.meta.env.VITE_GITHAT_API_URL || '{{apiUrl}}'{{/ifNext}},
13
11
  signInUrl: '/sign-in',
14
12
  signUpUrl: '/sign-up',
15
13
  afterSignInUrl: '/dashboard',
@@ -4,12 +4,12 @@
4
4
  import { UserButton, OrgSwitcher } from '@githat/nextjs';
5
5
  import { AuthGuard } from '../auth/guard{{#unless typescript}}.jsx{{/unless}}';
6
6
  import { githatConfig } from '../config{{#unless typescript}}.js{{/unless}}';
7
- {{#ifEquals framework "nextjs"}}
7
+ {{#ifNext framework}}
8
8
  import Link from 'next/link';
9
9
  import { usePathname } from 'next/navigation';
10
10
  {{else}}
11
11
  import { Link, useLocation } from 'react-router-dom';
12
- {{/ifEquals}}
12
+ {{/ifNext}}
13
13
 
14
14
  const navItems = [
15
15
  { label: 'Overview', href: '/dashboard' },
@@ -27,11 +27,11 @@ const navItems = [
27
27
  ];
28
28
 
29
29
  export function DashboardLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
30
- {{#ifEquals framework "nextjs"}}
30
+ {{#ifNext framework}}
31
31
  const pathname = usePathname();
32
32
  {{else}}
33
33
  const { pathname } = useLocation();
34
- {{/ifEquals}}
34
+ {{/ifNext}}
35
35
 
36
36
  return (
37
37
  <AuthGuard>
@@ -45,11 +45,11 @@ export function DashboardLayout({ children }{{#if typescript}}: { children: Reac
45
45
  {navItems.map((item) => (
46
46
  <Link
47
47
  key={item.href}
48
- {{#ifEquals framework "nextjs"}}
48
+ {{#ifNext framework}}
49
49
  href={item.href}
50
50
  {{else}}
51
51
  to={item.href}
52
- {{/ifEquals}}
52
+ {{/ifNext}}
53
53
  style=\{{
54
54
  padding: '0.5rem 0.75rem',
55
55
  borderRadius: '0.375rem',
@@ -1,6 +1,7 @@
1
1
  {{#unless includeDashboard}}{{else}}
2
2
  'use client';
3
3
 
4
+ import Link from 'next/link';
4
5
  import { useAuth } from '@githat/nextjs';
5
6
 
6
7
  export function DashboardOverview() {
@@ -8,29 +9,118 @@ export function DashboardOverview() {
8
9
 
9
10
  return (
10
11
  <div>
11
- <h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>
12
- Welcome{user?.name ? `, ${user.name}` : ''}
13
- </h1>
14
- {org && (
15
- <p style=\{{ color: '#a1a1aa', marginBottom: '2rem' }}>
16
- Organization: <strong style=\{{ color: '#7c3aed' }}>{org.name}</strong> ({org.role})
17
- </p>
18
- )}
12
+ <div style=\{{ marginBottom: '2rem' }}>
13
+ <h1 style=\{{ fontSize: '1.5rem', fontWeight: 700, color: '#fafafa', marginBottom: '0.375rem' }}>
14
+ Welcome back{user?.name ? `, ${user.name}` : ''}
15
+ </h1>
16
+ {org && (
17
+ <p style=\{{ color: '#71717a', fontSize: '0.875rem' }}>
18
+ {org.name} · <span style=\{{ color: '#a1a1aa' }}>{org.role}</span>
19
+ </p>
20
+ )}
21
+ </div>
22
+
23
+ {/* Stats row */}
24
+ <div style=\{{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(14rem, 1fr))', gap: '1rem', marginBottom: '2.5rem' }}>
25
+ <StatCard label="Status" value="Active" sub="Account health" />
26
+ <StatCard label="Plan" value={org?.tier ?? 'Free'} sub="Current tier" />
27
+ <StatCard label="Identity" value="GitHat" sub="Auth provider" />
28
+ {{#if includeOrgManagement}}
29
+ <StatCard label="Members" value={String(org?.memberCount ?? 1)} sub="Team size" />
30
+ {{/if}}
31
+ </div>
19
32
 
20
- <div style=\{{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(16rem, 1fr))', gap: '1rem' }}>
21
- <StatCard title="Status" value="Active" />
22
- <StatCard title="Plan" value={org?.tier || 'Free'} />
23
- <StatCard title="Identity" value="GitHat" />
33
+ {/* Quick actions */}
34
+ <div style=\{{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(20rem, 1fr))', gap: '1rem' }}>
35
+ {{#if includeOrgManagement}}
36
+ <ActionCard
37
+ title="Invite team members"
38
+ desc="Add your business partner, accountant, or employee to your org."
39
+ cta="Invite"
40
+ href="/dashboard/members"
41
+ accent="#6366f1"
42
+ />
43
+ <ActionCard
44
+ title="Organization settings"
45
+ desc="Update your org name, billing plan, and member permissions."
46
+ cta="Settings"
47
+ href="/dashboard/settings"
48
+ accent="#8b5cf6"
49
+ />
50
+ {{/if}}
51
+ {{#if includeMcpModule}}
52
+ <ActionCard
53
+ title="Connect an MCP server"
54
+ desc="Hook your data into any AI assistant via the Model Context Protocol."
55
+ cta="Connect"
56
+ href="/dashboard/mcp"
57
+ accent="#0ea5e9"
58
+ />
59
+ {{/if}}
60
+ {{#if includeAgentModule}}
61
+ <ActionCard
62
+ title="Deploy an AI agent"
63
+ desc="Register a wallet-bound autonomous agent with capability scoping and a kill switch."
64
+ cta="Deploy"
65
+ href="/dashboard/agents"
66
+ accent="#10b981"
67
+ />
68
+ {{/if}}
69
+ <ActionCard
70
+ title="Manage apps"
71
+ desc="View and rotate your publishable keys, configure allowed origins."
72
+ cta="View apps"
73
+ href="/dashboard/apps"
74
+ accent="#f59e0b"
75
+ />
24
76
  </div>
25
77
  </div>
26
78
  );
27
79
  }
28
80
 
29
- function StatCard({ title, value }{{#if typescript}}: { title: string; value: string }{{/if}}) {
81
+ function StatCard({ label, value, sub }{{#if typescript}}: { label: string; value: string; sub: string }{{/if}}) {
82
+ return (
83
+ <div style=\{{
84
+ background: '#111113',
85
+ border: '1px solid #1e1e2e',
86
+ borderRadius: '0.75rem',
87
+ padding: '1.5rem',
88
+ }}>
89
+ <p style=\{{ fontSize: '0.8125rem', color: '#71717a', marginBottom: '0.375rem' }}>{label}</p>
90
+ <p style=\{{ fontSize: '1.5rem', fontWeight: 700, color: '#fafafa', marginBottom: '0.25rem' }}>{value}</p>
91
+ <p style=\{{ fontSize: '0.75rem', color: '#52525b' }}>{sub}</p>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function ActionCard({ title, desc, cta, href, accent }{{#if typescript}}: {
97
+ title: string; desc: string; cta: string; href: string; accent: string;
98
+ }{{/if}}) {
30
99
  return (
31
- <div style=\{{ padding: '1.5rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.5rem' }}>
32
- <p style=\{{ fontSize: '0.875rem', color: '#71717a', marginBottom: '0.25rem' }}>{title}</p>
33
- <p style=\{{ fontSize: '1.25rem', fontWeight: 600, color: '#fafafa' }}>{value}</p>
100
+ <div style=\{{
101
+ background: '#111113',
102
+ border: '1px solid #1e1e2e',
103
+ borderRadius: '0.75rem',
104
+ padding: '1.5rem',
105
+ display: 'flex',
106
+ flexDirection: 'column',
107
+ gap: '0.75rem',
108
+ }}>
109
+ <h3 style=\{{ fontSize: '0.9375rem', fontWeight: 600, color: '#fafafa' }}>{title}</h3>
110
+ <p style=\{{ fontSize: '0.875rem', color: '#71717a', lineHeight: 1.6, flex: 1 }}>{desc}</p>
111
+ <Link href={href} style=\{{
112
+ display: 'inline-block',
113
+ background: accent,
114
+ color: '#fff',
115
+ textDecoration: 'none',
116
+ padding: '0.5rem 1rem',
117
+ borderRadius: '0.375rem',
118
+ fontSize: '0.875rem',
119
+ fontWeight: 600,
120
+ alignSelf: 'flex-start',
121
+ }}>
122
+ {cta} →
123
+ </Link>
34
124
  </div>
35
125
  );
36
126
  }
@@ -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: '/',
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+
6
+ {{#if typescript}}
7
+ interface FeedbackContentProps {
8
+ id: string;
9
+ }
10
+ {{/if}}
11
+
12
+ export default function FeedbackContent({ id }{{#if typescript}}: FeedbackContentProps{{/if}}) {
13
+ const [submitted, setSubmitted] = useState(false);
14
+ const [oneThing, setOneThing] = useState('');
15
+ const [stuck, setStuck] = useState('');
16
+ const [scale, setScale] = useState(7);
17
+
18
+ if (submitted) {
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', textAlign: 'center' }}>
30
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-3)' }}>
31
+ ¡Gracias!
32
+ </h1>
33
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-4)' }}>
34
+ The presenter sees your feedback live on their screen.
35
+ You can submit again with a new thought any time.
36
+ </p>
37
+ <button
38
+ onClick={() => { setSubmitted(false); setOneThing(''); setStuck(''); setScale(7); }}
39
+ style=\{{
40
+ padding: 'var(--space-3) var(--space-5)',
41
+ borderRadius: 'var(--radius-md, 0.5rem)',
42
+ background: 'var(--primary)',
43
+ color: 'var(--bg)',
44
+ border: 'none',
45
+ fontWeight: 600,
46
+ cursor: 'pointer',
47
+ }}
48
+ >
49
+ Add another thought
50
+ </button>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)', padding: 'var(--space-8) var(--space-4)' }}>
58
+ <div style=\{{ maxWidth: '32rem', margin: '0 auto' }}>
59
+ <Link href={`/projects/${id}`} style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', display: 'inline-block', marginBottom: 'var(--space-4)' }}>
60
+ ← Project
61
+ </Link>
62
+ <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.75rem', marginBottom: 'var(--space-2)' }}>
63
+ Tell them what you think
64
+ </h1>
65
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-6)' }}>
66
+ No account, no email. Just three quick questions while
67
+ they're presenting.
68
+ </p>
69
+
70
+ <form
71
+ onSubmit={(e) => {
72
+ e.preventDefault();
73
+ /* TODO: POST /api/projects/[id]/feedback { oneThing, stuck, scale } */
74
+ setSubmitted(true);
75
+ }}
76
+ style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}
77
+ >
78
+ <Field label="One thing that landed for you">
79
+ <input
80
+ type="text"
81
+ required
82
+ value={oneThing}
83
+ onChange={(e) => setOneThing(e.target.value)}
84
+ placeholder="The thing you'll remember tomorrow"
85
+ style=\{{
86
+ width: '100%',
87
+ padding: 'var(--space-3)',
88
+ borderRadius: 'var(--radius-md, 0.5rem)',
89
+ border: '1px solid var(--border)',
90
+ background: 'var(--surface)',
91
+ color: 'var(--fg)',
92
+ fontSize: '1rem',
93
+ }}
94
+ />
95
+ </Field>
96
+
97
+ <Field label="One thing you'd want them to dig into more">
98
+ <textarea
99
+ value={stuck}
100
+ onChange={(e) => setStuck(e.target.value)}
101
+ rows={3}
102
+ placeholder="A question, a doubt, a what-if"
103
+ style=\{{
104
+ width: '100%',
105
+ padding: 'var(--space-3)',
106
+ borderRadius: 'var(--radius-md, 0.5rem)',
107
+ border: '1px solid var(--border)',
108
+ background: 'var(--surface)',
109
+ color: 'var(--fg)',
110
+ fontSize: '1rem',
111
+ fontFamily: 'inherit',
112
+ resize: 'vertical',
113
+ }}
114
+ />
115
+ </Field>
116
+
117
+ <Field label={`How likely are you to share this with someone? (${scale}/10)`}>
118
+ <input
119
+ type="range"
120
+ min={1}
121
+ max={10}
122
+ value={scale}
123
+ onChange={(e) => setScale(Number(e.target.value))}
124
+ style=\{{ width: '100%' }}
125
+ />
126
+ </Field>
127
+
128
+ <button type="submit" style=\{{
129
+ padding: 'var(--space-3) var(--space-5)',
130
+ borderRadius: 'var(--radius-md, 0.5rem)',
131
+ background: 'var(--primary)',
132
+ color: 'var(--bg)',
133
+ border: 'none',
134
+ fontWeight: 600,
135
+ fontSize: '1rem',
136
+ cursor: 'pointer',
137
+ }}>
138
+ Send feedback
139
+ </button>
140
+ </form>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ function Field({ label, children }{{#if typescript}}: { label: string; children: React.ReactNode }{{/if}}) {
147
+ return (
148
+ <label style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
149
+ <span style=\{{ fontSize: '0.875rem', fontWeight: 600 }}>{label}</span>
150
+ {children}
151
+ </label>
152
+ );
153
+ }
@@ -1,159 +1,20 @@
1
- 'use client';
2
-
3
- import { use, useState } from 'react';
4
- import Link from 'next/link';
1
+ import FeedbackContent from './feedback-content{{#unless typescript}}.jsx{{/unless}}';
5
2
 
6
3
  /**
7
4
  * Audience feedback form — `/projects/[id]/feedback`.
8
5
  *
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.
6
+ * generateStaticParams tells Next.js which ids to pre-render at build
7
+ * time (required for `output: "export"`). FeedbackContent is a client
8
+ * component that renders the form with React state.
16
9
  */
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
10
 
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>
11
+ const SAMPLE_PROJECT_IDS = ['sustainable-bodega', 'after-school-tutor', 'community-fridge'];
133
12
 
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
- );
13
+ export function generateStaticParams() {
14
+ return SAMPLE_PROJECT_IDS.map((id) => ({ id }));
150
15
  }
151
16
 
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
- );
17
+ export default async function FeedbackPage({ params }: { params: Promise<{ id: string }> }) {
18
+ const { id } = await params;
19
+ return <FeedbackContent id={id} />;
159
20
  }