create-githat-app 0.1.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.
- package/README.md +223 -0
- package/bin/index.js +7 -0
- package/package.json +25 -0
- package/src/cli.js +131 -0
- package/src/scaffold.js +529 -0
- package/src/templates/components.js +97 -0
- package/src/templates/index.js +172 -0
- package/src/templates/pages.js +471 -0
- package/src/templates/react.js +191 -0
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const nextjsTemplates = require('./templates');
|
|
6
|
+
const reactTemplates = require('./templates/react');
|
|
7
|
+
const pageTemplates = require('./templates/pages');
|
|
8
|
+
const componentTemplates = require('./templates/components');
|
|
9
|
+
|
|
10
|
+
// ANSI helpers
|
|
11
|
+
const c = {
|
|
12
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
13
|
+
green: '\x1b[32m', cyan: '\x1b[36m', magenta: '\x1b[35m', red: '\x1b[31m',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ok = (msg) => console.log(` ${c.green}✓${c.reset} ${msg}`);
|
|
17
|
+
const info = (msg) => console.log(` ${c.cyan}◐${c.reset} ${msg}`);
|
|
18
|
+
|
|
19
|
+
async function scaffold(options) {
|
|
20
|
+
const { projectName, framework, typescript, packageManager, publishableKey, features } = options;
|
|
21
|
+
const root = path.resolve(process.cwd(), projectName);
|
|
22
|
+
|
|
23
|
+
if (fs.existsSync(root)) {
|
|
24
|
+
console.error(`\n ${c.red}✗${c.reset} Directory "${projectName}" already exists.\n`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
fs.mkdirSync(root, { recursive: true });
|
|
30
|
+
|
|
31
|
+
if (framework === 'react') {
|
|
32
|
+
scaffoldReact(root, options);
|
|
33
|
+
} else {
|
|
34
|
+
scaffoldNextjs(root, options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Install dependencies
|
|
38
|
+
const installCmd = { npm: 'npm install', pnpm: 'pnpm install', yarn: 'yarn' }[packageManager] || 'npm install';
|
|
39
|
+
info(`Installing dependencies with ${packageManager}...`);
|
|
40
|
+
try {
|
|
41
|
+
execSync(installCmd, { cwd: root, stdio: 'ignore' });
|
|
42
|
+
ok('Installed dependencies');
|
|
43
|
+
} catch {
|
|
44
|
+
console.log(` ${c.red}⚠${c.reset} Could not auto-install. Run ${c.cyan}${installCmd}${c.reset} manually.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const devCmd = packageManager === 'npm' ? 'npm run dev' : `${packageManager} dev`;
|
|
48
|
+
const port = framework === 'react' ? '5173' : '3000';
|
|
49
|
+
|
|
50
|
+
// Build routes list for the banner
|
|
51
|
+
const routes = [
|
|
52
|
+
' /sign-up Create account ',
|
|
53
|
+
' /sign-in Sign in ',
|
|
54
|
+
];
|
|
55
|
+
if (features.forgotPassword) routes.push(' /forgot-pw Reset password ');
|
|
56
|
+
if (features.dashboard) routes.push(' /dashboard Protected page ');
|
|
57
|
+
if (features.settings) routes.push(' /settings Profile & prefs ');
|
|
58
|
+
if (features.team) routes.push(' /team Org members ');
|
|
59
|
+
if (features.apps) routes.push(' /apps API keys ');
|
|
60
|
+
|
|
61
|
+
const routeLines = routes.map((r) =>
|
|
62
|
+
` ${c.magenta}│${c.reset} ${c.dim}${r}${c.reset} ${c.magenta}│${c.reset}`
|
|
63
|
+
).join('\n');
|
|
64
|
+
|
|
65
|
+
console.log(`
|
|
66
|
+
${c.magenta}┌─────────────────────────────────────┐${c.reset}
|
|
67
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
68
|
+
${c.magenta}│${c.reset} ${c.green}${c.bold}✓ Ready!${c.reset} ${c.magenta}│${c.reset}
|
|
69
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
70
|
+
${c.magenta}│${c.reset} ${c.cyan}cd ${projectName}${c.reset}${pad(25 - projectName.length)}${c.magenta}│${c.reset}
|
|
71
|
+
${c.magenta}│${c.reset} ${c.cyan}${devCmd}${c.reset}${pad(33 - devCmd.length)}${c.magenta}│${c.reset}
|
|
72
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
73
|
+
${c.magenta}│${c.reset} ${c.dim}→ http://localhost:${port}${c.reset}${pad(port.length === 4 ? 8 : 9)}${c.magenta}│${c.reset}
|
|
74
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
75
|
+
${routeLines}
|
|
76
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
77
|
+
${c.magenta}│${c.reset} ${c.dim}Docs: https://githat.io/docs/sdk${c.reset} ${c.magenta}│${c.reset}
|
|
78
|
+
${c.magenta}│${c.reset} ${c.magenta}│${c.reset}
|
|
79
|
+
${c.magenta}└─────────────────────────────────────┘${c.reset}
|
|
80
|
+
|
|
81
|
+
${c.bold}Next steps:${c.reset}
|
|
82
|
+
${c.dim}1.${c.reset} Get your publishable key at ${c.cyan}https://githat.io/dashboard/apps${c.reset}
|
|
83
|
+
${c.dim}2.${c.reset} Add it to ${c.cyan}.env.local${c.reset} (or ${c.cyan}.env${c.reset} for Vite)
|
|
84
|
+
${c.dim}3.${c.reset} Run ${c.cyan}${devCmd}${c.reset} and open ${c.cyan}http://localhost:${port}${c.reset}
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pad(n) { return ' '.repeat(Math.max(0, n)); }
|
|
89
|
+
|
|
90
|
+
// ─── Next.js (App Router) ────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function scaffoldNextjs(root, options) {
|
|
93
|
+
const { projectName, typescript, publishableKey, features } = options;
|
|
94
|
+
const ext = typescript ? 'tsx' : 'jsx';
|
|
95
|
+
const hasSidebar = features.settings || features.team || features.apps;
|
|
96
|
+
|
|
97
|
+
const pkg = {
|
|
98
|
+
name: projectName, version: '0.1.0', private: true,
|
|
99
|
+
scripts: { dev: 'next dev', build: 'next build', start: 'next start', lint: 'next lint' },
|
|
100
|
+
dependencies: { next: '^14.0.0', react: '^18.0.0', 'react-dom': '^18.0.0', '@githat/nextjs': '^0.2.0' },
|
|
101
|
+
devDependencies: {},
|
|
102
|
+
};
|
|
103
|
+
if (typescript) {
|
|
104
|
+
Object.assign(pkg.devDependencies, {
|
|
105
|
+
typescript: '^5.0.0', '@types/react': '^18.0.0', '@types/react-dom': '^18.0.0', '@types/node': '^20.0.0',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
writeFile(root, 'package.json', JSON.stringify(pkg, null, 2));
|
|
109
|
+
|
|
110
|
+
writeFile(root, 'next.config.mjs',
|
|
111
|
+
`/** @type {import('next').NextConfig} */\nconst nextConfig = {};\nexport default nextConfig;\n`);
|
|
112
|
+
|
|
113
|
+
if (typescript) {
|
|
114
|
+
writeFile(root, 'tsconfig.json', JSON.stringify({
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], allowJs: true, skipLibCheck: true,
|
|
117
|
+
strict: true, noEmit: true, esModuleInterop: true, module: 'esnext', moduleResolution: 'bundler',
|
|
118
|
+
resolveJsonModule: true, isolatedModules: true, jsx: 'preserve', incremental: true,
|
|
119
|
+
plugins: [{ name: 'next' }], paths: { '@/*': ['./*'] },
|
|
120
|
+
},
|
|
121
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
122
|
+
exclude: ['node_modules'],
|
|
123
|
+
}, null, 2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Public routes for middleware
|
|
127
|
+
const publicRoutes = ['/', '/sign-in', '/sign-up'];
|
|
128
|
+
if (features.forgotPassword) publicRoutes.push('/forgot-password', '/reset-password');
|
|
129
|
+
|
|
130
|
+
writeFile(root, '.env.local', `NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=${publishableKey}\n`);
|
|
131
|
+
writeFile(root, '.env.example', `# Get your publishable key at https://githat.io/dashboard/apps\nNEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here\n`);
|
|
132
|
+
writeFile(root, '.gitignore', 'node_modules\n.next\n.env*.local\nout\n');
|
|
133
|
+
writeFile(root, `middleware.${typescript ? 'ts' : 'js'}`, nextjsMiddleware(publicRoutes));
|
|
134
|
+
|
|
135
|
+
fs.mkdirSync(path.join(root, 'app'), { recursive: true });
|
|
136
|
+
writeFile(root, `app/layout.${ext}`, nextjsTemplates.layout(typescript));
|
|
137
|
+
writeFile(root, `app/page.${ext}`, nextjsTemplates.page());
|
|
138
|
+
writeFile(root, 'app/globals.css', nextjsTemplates.globalsCss());
|
|
139
|
+
ok('Created project structure');
|
|
140
|
+
|
|
141
|
+
// Auth pages
|
|
142
|
+
fs.mkdirSync(path.join(root, 'app', '(auth)', 'sign-in'), { recursive: true });
|
|
143
|
+
fs.mkdirSync(path.join(root, 'app', '(auth)', 'sign-up'), { recursive: true });
|
|
144
|
+
writeFile(root, `app/(auth)/sign-in/page.${ext}`, nextjsTemplates.signInPage());
|
|
145
|
+
writeFile(root, `app/(auth)/sign-up/page.${ext}`, nextjsTemplates.signUpPage());
|
|
146
|
+
ok('Generated auth pages (sign-in, sign-up)');
|
|
147
|
+
|
|
148
|
+
// Forgot / Reset password
|
|
149
|
+
if (features.forgotPassword) {
|
|
150
|
+
fs.mkdirSync(path.join(root, 'app', '(auth)', 'forgot-password'), { recursive: true });
|
|
151
|
+
fs.mkdirSync(path.join(root, 'app', '(auth)', 'reset-password'), { recursive: true });
|
|
152
|
+
writeFile(root, `app/(auth)/forgot-password/page.${ext}`, pageTemplates.forgotPasswordPage());
|
|
153
|
+
writeFile(root, `app/(auth)/reset-password/page.${ext}`, pageTemplates.resetPasswordPage());
|
|
154
|
+
ok('Added forgot/reset password flow');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Dashboard + sub-pages
|
|
158
|
+
if (features.dashboard) {
|
|
159
|
+
fs.mkdirSync(path.join(root, 'app', 'dashboard'), { recursive: true });
|
|
160
|
+
writeFile(root, `app/dashboard/layout.${ext}`, nextjsDashboardLayout(features));
|
|
161
|
+
writeFile(root, `app/dashboard/page.${ext}`, nextjsTemplates.dashboardPage());
|
|
162
|
+
|
|
163
|
+
// Verify email banner component
|
|
164
|
+
fs.mkdirSync(path.join(root, 'app', 'components'), { recursive: true });
|
|
165
|
+
writeFile(root, `app/components/VerifyEmailBanner.${ext}`, componentTemplates.verifyEmailBanner());
|
|
166
|
+
|
|
167
|
+
if (hasSidebar) {
|
|
168
|
+
writeFile(root, `app/components/Sidebar.${ext}`, componentTemplates.sidebar(features));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ok('Added protected dashboard');
|
|
172
|
+
|
|
173
|
+
if (features.settings) {
|
|
174
|
+
fs.mkdirSync(path.join(root, 'app', 'dashboard', 'settings'), { recursive: true });
|
|
175
|
+
writeFile(root, `app/dashboard/settings/page.${ext}`, pageTemplates.settingsPage());
|
|
176
|
+
ok('Added settings page');
|
|
177
|
+
}
|
|
178
|
+
if (features.team) {
|
|
179
|
+
fs.mkdirSync(path.join(root, 'app', 'dashboard', 'team'), { recursive: true });
|
|
180
|
+
writeFile(root, `app/dashboard/team/page.${ext}`, pageTemplates.teamPage());
|
|
181
|
+
ok('Added team management page');
|
|
182
|
+
}
|
|
183
|
+
if (features.apps) {
|
|
184
|
+
fs.mkdirSync(path.join(root, 'app', 'dashboard', 'apps'), { recursive: true });
|
|
185
|
+
writeFile(root, `app/dashboard/apps/page.${ext}`, pageTemplates.appsPage());
|
|
186
|
+
ok('Added API keys page');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// README
|
|
191
|
+
writeFile(root, 'README.md', generateReadme(projectName, 'nextjs', features));
|
|
192
|
+
ok('Generated README.md');
|
|
193
|
+
|
|
194
|
+
ok('Configured middleware');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function nextjsMiddleware(publicRoutes) {
|
|
198
|
+
const routesStr = publicRoutes.map((r) => `'${r}'`).join(', ');
|
|
199
|
+
return `import { authMiddleware } from '@githat/nextjs/middleware';
|
|
200
|
+
|
|
201
|
+
export default authMiddleware({
|
|
202
|
+
publicRoutes: [${routesStr}],
|
|
203
|
+
signInUrl: '/sign-in',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export const config = {
|
|
207
|
+
matcher: ['/((?!_next|api|.*\\\\..*).*)'],
|
|
208
|
+
};
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function nextjsDashboardLayout(features) {
|
|
213
|
+
const hasSidebar = features.settings || features.team || features.apps;
|
|
214
|
+
const sidebarImport = hasSidebar ? `\nimport Sidebar from '../components/Sidebar';` : '';
|
|
215
|
+
const bannerImport = `\nimport VerifyEmailBanner from '../components/VerifyEmailBanner';`;
|
|
216
|
+
const sidebarJsx = hasSidebar
|
|
217
|
+
? `\n <Sidebar />\n <main style={{ flex: 1, padding: '2rem', overflow: 'auto' }}>`
|
|
218
|
+
: `\n <main style={{ padding: '2rem' }}>`;
|
|
219
|
+
|
|
220
|
+
return `import { ProtectedRoute, UserButton, OrgSwitcher } from '@githat/nextjs';${sidebarImport}${bannerImport}
|
|
221
|
+
|
|
222
|
+
export default function DashboardLayout({ children }) {
|
|
223
|
+
return (
|
|
224
|
+
<ProtectedRoute>
|
|
225
|
+
<div style={{ minHeight: '100vh', background: '#09090b', display: 'flex', flexDirection: 'column' }}>
|
|
226
|
+
<VerifyEmailBanner />
|
|
227
|
+
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 2rem', borderBottom: '1px solid #1e1e2e' }}>
|
|
228
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
229
|
+
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: '#fafafa' }}>Dashboard</h2>
|
|
230
|
+
<OrgSwitcher />
|
|
231
|
+
</div>
|
|
232
|
+
<UserButton />
|
|
233
|
+
</header>
|
|
234
|
+
<div style={{ display: 'flex', flex: 1 }}>${sidebarJsx}
|
|
235
|
+
{children}
|
|
236
|
+
</main>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</ProtectedRoute>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── React (Vite) ────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function scaffoldReact(root, options) {
|
|
248
|
+
const { projectName, typescript, publishableKey, features } = options;
|
|
249
|
+
const ext = typescript ? 'tsx' : 'jsx';
|
|
250
|
+
const hasSidebar = features.settings || features.team || features.apps;
|
|
251
|
+
|
|
252
|
+
const pkg = {
|
|
253
|
+
name: projectName, version: '0.1.0', private: true, type: 'module',
|
|
254
|
+
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
|
|
255
|
+
dependencies: {
|
|
256
|
+
react: '^18.0.0', 'react-dom': '^18.0.0', 'react-router-dom': '^6.0.0', '@githat/nextjs': '^0.2.0',
|
|
257
|
+
},
|
|
258
|
+
devDependencies: { vite: '^5.0.0', '@vitejs/plugin-react': '^4.0.0' },
|
|
259
|
+
};
|
|
260
|
+
if (typescript) {
|
|
261
|
+
Object.assign(pkg.devDependencies, {
|
|
262
|
+
typescript: '^5.0.0', '@types/react': '^18.0.0', '@types/react-dom': '^18.0.0',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
writeFile(root, 'package.json', JSON.stringify(pkg, null, 2));
|
|
266
|
+
|
|
267
|
+
writeFile(root, typescript ? 'vite.config.ts' : 'vite.config.js', reactTemplates.viteConfig());
|
|
268
|
+
|
|
269
|
+
if (typescript) {
|
|
270
|
+
writeFile(root, 'tsconfig.json', JSON.stringify({
|
|
271
|
+
compilerOptions: {
|
|
272
|
+
target: 'ES2020', lib: ['ES2020', 'DOM', 'DOM.Iterable'], module: 'ESNext', skipLibCheck: true,
|
|
273
|
+
moduleResolution: 'bundler', allowImportingTsExtensions: true, isolatedModules: true,
|
|
274
|
+
moduleDetection: 'force', noEmit: true, jsx: 'react-jsx', strict: true,
|
|
275
|
+
},
|
|
276
|
+
include: ['src'],
|
|
277
|
+
}, null, 2));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
writeFile(root, '.env', `VITE_GITHAT_PUBLISHABLE_KEY=${publishableKey}\n`);
|
|
281
|
+
writeFile(root, '.env.example', `# Get your publishable key at https://githat.io/dashboard/apps\nVITE_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here\n`);
|
|
282
|
+
writeFile(root, '.gitignore', 'node_modules\ndist\n.env*.local\n');
|
|
283
|
+
writeFile(root, 'index.html', reactTemplates.indexHtml(projectName, typescript));
|
|
284
|
+
ok('Created project structure');
|
|
285
|
+
|
|
286
|
+
fs.mkdirSync(path.join(root, 'src', 'pages'), { recursive: true });
|
|
287
|
+
fs.mkdirSync(path.join(root, 'src', 'components'), { recursive: true });
|
|
288
|
+
writeFile(root, `src/main.${ext}`, reactTemplates.mainEntry());
|
|
289
|
+
writeFile(root, `src/App.${ext}`, reactAppRoutes(features));
|
|
290
|
+
writeFile(root, 'src/index.css', reactTemplates.indexCss());
|
|
291
|
+
|
|
292
|
+
// Core pages
|
|
293
|
+
writeFile(root, `src/pages/Home.${ext}`, reactTemplates.homePage());
|
|
294
|
+
writeFile(root, `src/pages/SignIn.${ext}`, reactTemplates.signInPage());
|
|
295
|
+
writeFile(root, `src/pages/SignUp.${ext}`, reactTemplates.signUpPage());
|
|
296
|
+
ok('Generated auth pages (sign-in, sign-up)');
|
|
297
|
+
|
|
298
|
+
// Forgot / Reset password
|
|
299
|
+
if (features.forgotPassword) {
|
|
300
|
+
writeFile(root, `src/pages/ForgotPassword.${ext}`, pageTemplates.forgotPasswordPage());
|
|
301
|
+
writeFile(root, `src/pages/ResetPassword.${ext}`, pageTemplates.resetPasswordPage());
|
|
302
|
+
ok('Added forgot/reset password flow');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Dashboard + sub-pages
|
|
306
|
+
if (features.dashboard) {
|
|
307
|
+
writeFile(root, `src/pages/Dashboard.${ext}`, reactTemplates.dashboardPage());
|
|
308
|
+
writeFile(root, `src/components/VerifyEmailBanner.${ext}`, componentTemplates.verifyEmailBanner());
|
|
309
|
+
|
|
310
|
+
if (hasSidebar) {
|
|
311
|
+
writeFile(root, `src/components/Sidebar.${ext}`, componentTemplates.sidebar(features));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
writeFile(root, `src/pages/DashboardLayout.${ext}`, reactDashboardLayout(features));
|
|
315
|
+
ok('Added protected dashboard');
|
|
316
|
+
|
|
317
|
+
if (features.settings) {
|
|
318
|
+
writeFile(root, `src/pages/Settings.${ext}`, pageTemplates.settingsPage());
|
|
319
|
+
ok('Added settings page');
|
|
320
|
+
}
|
|
321
|
+
if (features.team) {
|
|
322
|
+
writeFile(root, `src/pages/Team.${ext}`, pageTemplates.teamPage());
|
|
323
|
+
ok('Added team management page');
|
|
324
|
+
}
|
|
325
|
+
if (features.apps) {
|
|
326
|
+
writeFile(root, `src/pages/Apps.${ext}`, pageTemplates.appsPage());
|
|
327
|
+
ok('Added API keys page');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// README
|
|
332
|
+
writeFile(root, 'README.md', generateReadme(projectName, 'react', features));
|
|
333
|
+
ok('Generated README.md');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function reactAppRoutes(features) {
|
|
337
|
+
const imports = [
|
|
338
|
+
`import { Routes, Route } from 'react-router-dom';`,
|
|
339
|
+
`import Home from './pages/Home';`,
|
|
340
|
+
`import SignIn from './pages/SignIn';`,
|
|
341
|
+
`import SignUp from './pages/SignUp';`,
|
|
342
|
+
];
|
|
343
|
+
const routes = [
|
|
344
|
+
` <Route path="/" element={<Home />} />`,
|
|
345
|
+
` <Route path="/sign-in" element={<SignIn />} />`,
|
|
346
|
+
` <Route path="/sign-up" element={<SignUp />} />`,
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
if (features.forgotPassword) {
|
|
350
|
+
imports.push(`import ForgotPassword from './pages/ForgotPassword';`);
|
|
351
|
+
imports.push(`import ResetPassword from './pages/ResetPassword';`);
|
|
352
|
+
routes.push(` <Route path="/forgot-password" element={<ForgotPassword />} />`);
|
|
353
|
+
routes.push(` <Route path="/reset-password" element={<ResetPassword />} />`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (features.dashboard) {
|
|
357
|
+
imports.push(`import DashboardLayout from './pages/DashboardLayout';`);
|
|
358
|
+
imports.push(`import Dashboard from './pages/Dashboard';`);
|
|
359
|
+
|
|
360
|
+
const subRoutes = [` <Route index element={<Dashboard />} />`];
|
|
361
|
+
if (features.settings) {
|
|
362
|
+
imports.push(`import Settings from './pages/Settings';`);
|
|
363
|
+
subRoutes.push(` <Route path="settings" element={<Settings />} />`);
|
|
364
|
+
}
|
|
365
|
+
if (features.team) {
|
|
366
|
+
imports.push(`import Team from './pages/Team';`);
|
|
367
|
+
subRoutes.push(` <Route path="team" element={<Team />} />`);
|
|
368
|
+
}
|
|
369
|
+
if (features.apps) {
|
|
370
|
+
imports.push(`import Apps from './pages/Apps';`);
|
|
371
|
+
subRoutes.push(` <Route path="apps" element={<Apps />} />`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
routes.push(` <Route path="/dashboard" element={<DashboardLayout />}>`);
|
|
375
|
+
routes.push(...subRoutes);
|
|
376
|
+
routes.push(` </Route>`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return `${imports.join('\n')}
|
|
380
|
+
|
|
381
|
+
export default function App() {
|
|
382
|
+
return (
|
|
383
|
+
<Routes>
|
|
384
|
+
${routes.join('\n')}
|
|
385
|
+
</Routes>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function reactDashboardLayout(features) {
|
|
392
|
+
const hasSidebar = features.settings || features.team || features.apps;
|
|
393
|
+
const sidebarImport = hasSidebar ? `\nimport Sidebar from '../components/Sidebar';` : '';
|
|
394
|
+
const sidebarJsx = hasSidebar
|
|
395
|
+
? `\n <Sidebar />\n <main style={{ flex: 1, padding: '2rem', overflow: 'auto' }}>`
|
|
396
|
+
: `\n <main style={{ padding: '2rem' }}>`;
|
|
397
|
+
|
|
398
|
+
return `import { Outlet } from 'react-router-dom';
|
|
399
|
+
import { ProtectedRoute, UserButton, OrgSwitcher } from '@githat/nextjs';
|
|
400
|
+
import VerifyEmailBanner from '../components/VerifyEmailBanner';${sidebarImport}
|
|
401
|
+
|
|
402
|
+
export default function DashboardLayout() {
|
|
403
|
+
return (
|
|
404
|
+
<ProtectedRoute>
|
|
405
|
+
<div style={{ minHeight: '100vh', background: '#09090b', display: 'flex', flexDirection: 'column' }}>
|
|
406
|
+
<VerifyEmailBanner />
|
|
407
|
+
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 2rem', borderBottom: '1px solid #1e1e2e' }}>
|
|
408
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
409
|
+
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: '#fafafa' }}>Dashboard</h2>
|
|
410
|
+
<OrgSwitcher />
|
|
411
|
+
</div>
|
|
412
|
+
<UserButton />
|
|
413
|
+
</header>
|
|
414
|
+
<div style={{ display: 'flex', flex: 1 }}>${sidebarJsx}
|
|
415
|
+
<Outlet />
|
|
416
|
+
</main>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</ProtectedRoute>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── README Generator ────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
function generateReadme(projectName, framework, features) {
|
|
428
|
+
const isNext = framework !== 'react';
|
|
429
|
+
const port = isNext ? '3000' : '5173';
|
|
430
|
+
const devCmd = isNext ? 'npm run dev' : 'npm run dev';
|
|
431
|
+
const envFile = isNext ? '.env.local' : '.env';
|
|
432
|
+
const envVar = isNext ? 'NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY' : 'VITE_GITHAT_PUBLISHABLE_KEY';
|
|
433
|
+
|
|
434
|
+
const routes = ['- `/sign-in` — Sign in', '- `/sign-up` — Create account'];
|
|
435
|
+
if (features.forgotPassword) routes.push('- `/forgot-password` — Password recovery');
|
|
436
|
+
if (features.dashboard) routes.push('- `/dashboard` — Protected dashboard');
|
|
437
|
+
if (features.settings) routes.push('- `/dashboard/settings` — Profile, avatar, password');
|
|
438
|
+
if (features.team) routes.push('- `/dashboard/team` — Invite members, manage roles');
|
|
439
|
+
if (features.apps) routes.push('- `/dashboard/apps` — Create apps, manage API keys');
|
|
440
|
+
|
|
441
|
+
return `# ${projectName}
|
|
442
|
+
|
|
443
|
+
Built with [GitHat](https://githat.io) — authentication, teams, and API keys out of the box.
|
|
444
|
+
|
|
445
|
+
## Getting Started
|
|
446
|
+
|
|
447
|
+
1. Install dependencies:
|
|
448
|
+
|
|
449
|
+
\`\`\`bash
|
|
450
|
+
npm install
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
2. Add your GitHat publishable key to \`${envFile}\`:
|
|
454
|
+
|
|
455
|
+
\`\`\`env
|
|
456
|
+
${envVar}=pk_live_your_key_here
|
|
457
|
+
\`\`\`
|
|
458
|
+
|
|
459
|
+
Get your key at [https://githat.io/dashboard/apps](https://githat.io/dashboard/apps).
|
|
460
|
+
|
|
461
|
+
3. Start the dev server:
|
|
462
|
+
|
|
463
|
+
\`\`\`bash
|
|
464
|
+
${devCmd}
|
|
465
|
+
\`\`\`
|
|
466
|
+
|
|
467
|
+
4. Open [http://localhost:${port}](http://localhost:${port})
|
|
468
|
+
|
|
469
|
+
## Pages
|
|
470
|
+
|
|
471
|
+
${routes.join('\n')}
|
|
472
|
+
|
|
473
|
+
## GitHat SDK
|
|
474
|
+
|
|
475
|
+
This project uses [\`@githat/nextjs\`](https://www.npmjs.com/package/@githat/nextjs) for authentication. Key imports:
|
|
476
|
+
|
|
477
|
+
\`\`\`tsx
|
|
478
|
+
import { useAuth, UserButton, OrgSwitcher } from '@githat/nextjs';
|
|
479
|
+
|
|
480
|
+
const { user, org, signIn, signUp, signOut } = useAuth();
|
|
481
|
+
\`\`\`
|
|
482
|
+
|
|
483
|
+
See the [SDK docs](https://www.npmjs.com/package/@githat/nextjs) for the full API.
|
|
484
|
+
|
|
485
|
+
## Deploy
|
|
486
|
+
|
|
487
|
+
${isNext ? `### Vercel (recommended)
|
|
488
|
+
|
|
489
|
+
\`\`\`bash
|
|
490
|
+
npx vercel
|
|
491
|
+
\`\`\`
|
|
492
|
+
|
|
493
|
+
Add \`${envVar}\` to your Vercel project environment variables.` : `### Vercel
|
|
494
|
+
|
|
495
|
+
\`\`\`bash
|
|
496
|
+
npx vercel
|
|
497
|
+
\`\`\`
|
|
498
|
+
|
|
499
|
+
### Netlify
|
|
500
|
+
|
|
501
|
+
\`\`\`bash
|
|
502
|
+
npx netlify deploy
|
|
503
|
+
\`\`\`
|
|
504
|
+
|
|
505
|
+
Add \`${envVar}\` to your hosting provider's environment variables.`}
|
|
506
|
+
|
|
507
|
+
## Learn More
|
|
508
|
+
|
|
509
|
+
- [GitHat Documentation](https://githat.io/docs)
|
|
510
|
+
- [SDK Reference](https://www.npmjs.com/package/@githat/nextjs)
|
|
511
|
+
- [API Reference](https://githat.io/docs/api)
|
|
512
|
+
`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
function writeFile(root, relativePath, content) {
|
|
518
|
+
const fullPath = path.join(root, relativePath);
|
|
519
|
+
const dir = path.dirname(fullPath);
|
|
520
|
+
try {
|
|
521
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
522
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.error(`\n ${c.red}✗${c.reset} Failed to write ${relativePath}: ${err.message}\n`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
module.exports = { scaffold };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Shared component templates used by both Next.js and React scaffolds.
|
|
2
|
+
|
|
3
|
+
function verifyEmailBanner() {
|
|
4
|
+
return `'use client';
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { useAuth, useGitHat } from '@githat/nextjs';
|
|
8
|
+
|
|
9
|
+
export default function VerifyEmailBanner() {
|
|
10
|
+
const { user } = useAuth();
|
|
11
|
+
const { fetch: apiFetch } = useGitHat();
|
|
12
|
+
const [sending, setSending] = useState(false);
|
|
13
|
+
const [sent, setSent] = useState(false);
|
|
14
|
+
const [dismissed, setDismissed] = useState(false);
|
|
15
|
+
|
|
16
|
+
if (!user || user.emailVerified || dismissed) return null;
|
|
17
|
+
|
|
18
|
+
const resend = async () => {
|
|
19
|
+
setSending(true);
|
|
20
|
+
try {
|
|
21
|
+
await apiFetch('/auth/resend-verification', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: JSON.stringify({ email: user.email }),
|
|
24
|
+
});
|
|
25
|
+
setSent(true);
|
|
26
|
+
} catch {}
|
|
27
|
+
setSending(false);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{
|
|
32
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
33
|
+
padding: '0.75rem 2rem', background: '#1e1b4b', borderBottom: '1px solid #312e81',
|
|
34
|
+
fontSize: '0.875rem', color: '#c7d2fe',
|
|
35
|
+
}}>
|
|
36
|
+
<span>
|
|
37
|
+
Please verify your email <strong style={{ color: '#fafafa' }}>{user.email}</strong>
|
|
38
|
+
{sent
|
|
39
|
+
? <span style={{ marginLeft: '0.5rem', color: '#34d399' }}>— Check your inbox!</span>
|
|
40
|
+
: <button type="button" onClick={resend} disabled={sending} style={{
|
|
41
|
+
marginLeft: '0.5rem', background: 'none', border: 'none', color: '#818cf8',
|
|
42
|
+
cursor: 'pointer', textDecoration: 'underline', fontSize: '0.875rem',
|
|
43
|
+
}}>{sending ? 'Sending...' : 'Resend'}</button>
|
|
44
|
+
}
|
|
45
|
+
</span>
|
|
46
|
+
<button type="button" onClick={() => setDismissed(true)} style={{
|
|
47
|
+
background: 'none', border: 'none', color: '#6366f1', cursor: 'pointer', fontSize: '1rem',
|
|
48
|
+
}}>×</button>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sidebar(features) {
|
|
56
|
+
const links = [{ href: '/dashboard', label: 'Dashboard', icon: 'home' }];
|
|
57
|
+
if (features.settings) links.push({ href: '/dashboard/settings', label: 'Settings', icon: 'settings' });
|
|
58
|
+
if (features.team) links.push({ href: '/dashboard/team', label: 'Team', icon: 'team' });
|
|
59
|
+
if (features.apps) links.push({ href: '/dashboard/apps', label: 'Apps', icon: 'apps' });
|
|
60
|
+
|
|
61
|
+
const icons = {
|
|
62
|
+
home: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
|
|
63
|
+
settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
|
|
64
|
+
team: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
|
|
65
|
+
apps: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const linkItems = links.map((l) => {
|
|
69
|
+
return ` <a href="${l.href}" style={{
|
|
70
|
+
display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem',
|
|
71
|
+
borderRadius: '0.375rem', color: '#a1a1aa', fontSize: '0.875rem', textDecoration: 'none',
|
|
72
|
+
transition: 'background 0.15s',
|
|
73
|
+
}} onMouseEnter={(e) => { e.currentTarget.style.background = '#27272a'; e.currentTarget.style.color = '#fafafa'; }}
|
|
74
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#a1a1aa'; }}>
|
|
75
|
+
${icons[l.icon]}
|
|
76
|
+
${l.label}
|
|
77
|
+
</a>`;
|
|
78
|
+
}).join('\n');
|
|
79
|
+
|
|
80
|
+
return `export default function Sidebar() {
|
|
81
|
+
return (
|
|
82
|
+
<nav style={{
|
|
83
|
+
width: '14rem', padding: '1rem 0.75rem',
|
|
84
|
+
borderRight: '1px solid #27272a', background: '#0f0f14',
|
|
85
|
+
display: 'flex', flexDirection: 'column', gap: '0.25rem',
|
|
86
|
+
}}>
|
|
87
|
+
${linkItems}
|
|
88
|
+
</nav>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
verifyEmailBanner,
|
|
96
|
+
sidebar,
|
|
97
|
+
};
|