create-githat-app 1.0.15 → 1.0.16
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/dist/cli.js +20 -10
- package/package.json +1 -1
- package/templates/agent/app/admin/agent/page.tsx.hbs +127 -0
- package/templates/agent/app/page.tsx.hbs +85 -108
- 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 +7 -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/newsletter/page.tsx.hbs +90 -0
- package/templates/content/app/page.tsx.hbs +93 -111
- package/templates/content/app/posts/[slug]/page.tsx.hbs +119 -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/page.tsx.hbs +42 -108
- package/templates/dashboard/src/lib/db.ts.hbs +39 -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 +7 -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/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/page.tsx.hbs +95 -110
- package/templates/saas/app/pricing/page.tsx.hbs +131 -0
- package/templates/agent/TODO.md +0 -9
- package/templates/content/TODO.md +0 -9
- package/templates/dashboard/TODO.md +0 -9
- package/templates/saas/TODO.md +0 -9
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database adapter — bring your own.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard template stays database-agnostic on purpose. Pick
|
|
5
|
+
* whatever fits and replace the stubs below:
|
|
6
|
+
*
|
|
7
|
+
* Postgres: `import { Pool } from 'pg';`
|
|
8
|
+
* DynamoDB: `import { DynamoDBClient } from '@aws-sdk/client-dynamodb';`
|
|
9
|
+
* MySQL: `import mysql from 'mysql2/promise';`
|
|
10
|
+
* Drizzle/Prisma: drop in your existing client
|
|
11
|
+
*
|
|
12
|
+
* Set DATABASE_URL in .env.local.
|
|
13
|
+
*
|
|
14
|
+
* The `listEntities` and `getEntity` shapes are what the routes
|
|
15
|
+
* under /admin/data expect — keep the function names so the UI
|
|
16
|
+
* keeps working when you swap the body.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface EntityRow {
|
|
20
|
+
id: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function listEntities(entity: string): Promise<EntityRow[]> {
|
|
25
|
+
// TODO: replace with your real query, e.g.
|
|
26
|
+
// const { rows } = await pool.query('SELECT * FROM ' + entity + ' LIMIT 100');
|
|
27
|
+
// return rows;
|
|
28
|
+
void entity;
|
|
29
|
+
return [
|
|
30
|
+
{ id: '1', name: 'Sample row 1', createdAt: new Date().toISOString() },
|
|
31
|
+
{ id: '2', name: 'Sample row 2', createdAt: new Date().toISOString() },
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getEntity(entity: string, id: string): Promise<EntityRow | null> {
|
|
36
|
+
// TODO: replace with your real query
|
|
37
|
+
void entity;
|
|
38
|
+
return { id, name: 'Sample row', createdAt: new Date().toISOString() };
|
|
39
|
+
}
|
|
@@ -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,86 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portfolio homepage.
|
|
5
|
+
*
|
|
6
|
+
* Personal/professional site for showing projects you've shipped.
|
|
7
|
+
* Public reads, auth-gated edits — sign in only when you want to
|
|
8
|
+
* add or edit a project. Replaces Linktree + Cargo + a static
|
|
9
|
+
* site generator + Vercel.
|
|
10
|
+
*
|
|
11
|
+
* Project data is hardcoded for the starter. Real apps store
|
|
12
|
+
* projects in a Postgres / DynamoDB table or in Markdown files,
|
|
13
|
+
* and fetch in this server component.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const PROJECTS = [
|
|
17
|
+
{
|
|
18
|
+
slug: 'project-one',
|
|
19
|
+
title: 'A thing I shipped',
|
|
20
|
+
summary: 'Two-line description of the project, what it does, what was hard.',
|
|
21
|
+
year: 2026,
|
|
22
|
+
role: 'Designer + Engineer',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
slug: 'project-two',
|
|
26
|
+
title: 'Another thing',
|
|
27
|
+
summary: 'A research project that turned into a product. Worked for two years.',
|
|
28
|
+
year: 2025,
|
|
29
|
+
role: 'Lead',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: 'project-three',
|
|
33
|
+
title: 'A side project',
|
|
34
|
+
summary: 'Built in a weekend, used by 12 people, learned three things.',
|
|
35
|
+
year: 2025,
|
|
36
|
+
role: 'Solo',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export default function Home() {
|
|
41
|
+
return (
|
|
42
|
+
<div style=\{{ background: 'var(--bg)', color: 'var(--fg)' }}>
|
|
43
|
+
<section style=\{{ padding: 'var(--space-12) var(--space-4)', maxWidth: '40rem', margin: '0 auto' }}>
|
|
44
|
+
<h1 style=\{{
|
|
45
|
+
fontFamily: 'var(--font-wordmark, Georgia, serif)',
|
|
46
|
+
fontSize: 'clamp(2rem, 5vw, 3rem)',
|
|
47
|
+
lineHeight: 1.1,
|
|
48
|
+
marginBottom: 'var(--space-3)',
|
|
49
|
+
}}>
|
|
50
|
+
{{businessName}}
|
|
51
|
+
</h1>
|
|
52
|
+
<p style=\{{ color: 'var(--fg-muted)', fontSize: '1.125rem', lineHeight: 1.7 }}>
|
|
53
|
+
One-paragraph intro. What you do, what you care about, where to find
|
|
54
|
+
the rest of you. Edit this in <code>app/page.tsx</code>.
|
|
55
|
+
<br /><br />
|
|
56
|
+
<Link href="/about" style=\{{ color: 'var(--primary)' }}>More about me →</Link>
|
|
57
|
+
</p>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section style=\{{ padding: 'var(--space-8) var(--space-4)', maxWidth: '48rem', margin: '0 auto' }}>
|
|
61
|
+
<h2 style=\{{ fontSize: '1.5rem', marginBottom: 'var(--space-6)' }}>Projects</h2>
|
|
62
|
+
<ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
|
|
63
|
+
{PROJECTS.map((p) => (
|
|
64
|
+
<li key={p.slug}>
|
|
65
|
+
<Link href={`/${p.slug}`} style=\{{
|
|
66
|
+
display: 'block',
|
|
67
|
+
padding: 'var(--space-5)',
|
|
68
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
69
|
+
border: '1px solid var(--border)',
|
|
70
|
+
background: 'var(--surface)',
|
|
71
|
+
color: 'var(--fg)',
|
|
72
|
+
textDecoration: 'none',
|
|
73
|
+
}}>
|
|
74
|
+
<div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--space-3)', flexWrap: 'wrap', marginBottom: 'var(--space-2)' }}>
|
|
75
|
+
<h3 style=\{{ fontSize: '1.25rem', fontWeight: 600, margin: 0 }}>{p.title}</h3>
|
|
76
|
+
<span style=\{{ fontSize: '0.75rem', color: 'var(--fg-subtle)' }}>{p.role} · {p.year}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>{p.summary}</p>
|
|
79
|
+
</Link>
|
|
80
|
+
</li>
|
|
81
|
+
))}
|
|
82
|
+
</ul>
|
|
83
|
+
</section>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -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,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useAuth } from '@githat/nextjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Billing — Sebastn-powered subscription management.
|
|
8
|
+
*
|
|
9
|
+
* The starter shows the data shape; a real app fetches the
|
|
10
|
+
* org's subscription state from your backend (which subscribed
|
|
11
|
+
* to Sebastn webhooks) and renders accordingly.
|
|
12
|
+
*
|
|
13
|
+
* Key actions:
|
|
14
|
+
* - Change plan → opens Sebastn checkout via /api/billing/checkout
|
|
15
|
+
* - Cancel → calls /api/billing/cancel which calls Sebastn
|
|
16
|
+
* - Update payment method → opens Sebastn customer portal
|
|
17
|
+
*
|
|
18
|
+
* All three are stubs — wire them when you connect Sebastn.
|
|
19
|
+
*/
|
|
20
|
+
export default function BillingPage() {
|
|
21
|
+
const { isSignedIn, isLoading } = useAuth();
|
|
22
|
+
|
|
23
|
+
if (isLoading) return <div style=\{{ padding: 'var(--space-8)', color: 'var(--fg-muted)' }}>Loading…</div>;
|
|
24
|
+
if (!isSignedIn) {
|
|
25
|
+
return (
|
|
26
|
+
<div style=\{{ padding: 'var(--space-8)', textAlign: 'center' }}>
|
|
27
|
+
<p>Sign in to manage billing.</p>
|
|
28
|
+
<Link href="/sign-in" style=\{{ color: 'var(--primary)' }}>Sign in →</Link>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const subscription = {
|
|
34
|
+
status: 'trialing' as 'trialing' | 'active' | 'past_due' | 'canceled',
|
|
35
|
+
plan: 'Team',
|
|
36
|
+
price: 29,
|
|
37
|
+
renews: '2026-05-14',
|
|
38
|
+
seats: 5,
|
|
39
|
+
seatsUsed: 3,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)' }}>
|
|
44
|
+
<div style=\{{ maxWidth: '48rem', margin: '0 auto', padding: 'var(--space-8) var(--space-4)' }}>
|
|
45
|
+
<Link href="/admin" style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', display: 'inline-block', marginBottom: 'var(--space-4)' }}>
|
|
46
|
+
← Dashboard
|
|
47
|
+
</Link>
|
|
48
|
+
<h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-6)' }}>
|
|
49
|
+
Billing
|
|
50
|
+
</h1>
|
|
51
|
+
|
|
52
|
+
<section style=\{{
|
|
53
|
+
padding: 'var(--space-6)',
|
|
54
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
55
|
+
border: '1px solid var(--border)',
|
|
56
|
+
background: 'var(--surface)',
|
|
57
|
+
marginBottom: 'var(--space-6)',
|
|
58
|
+
}}>
|
|
59
|
+
<div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 'var(--space-4)' }}>
|
|
60
|
+
<div>
|
|
61
|
+
<h2 style=\{{ fontSize: '1.5rem', marginBottom: 'var(--space-1)' }}>{subscription.plan}</h2>
|
|
62
|
+
<p style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>${subscription.price}/mo · renews {subscription.renews}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<Badge status={subscription.status} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div style=\{{ display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
|
|
68
|
+
<button onClick={() => { /* TODO: Sebastn checkout */ }} style=\{{
|
|
69
|
+
padding: 'var(--space-3) var(--space-4)',
|
|
70
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
71
|
+
border: 'none',
|
|
72
|
+
background: 'var(--primary)',
|
|
73
|
+
color: 'var(--bg)',
|
|
74
|
+
fontWeight: 600,
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
}}>
|
|
77
|
+
Change plan
|
|
78
|
+
</button>
|
|
79
|
+
<button onClick={() => { /* TODO: Sebastn customer portal */ }} style=\{{
|
|
80
|
+
padding: 'var(--space-3) var(--space-4)',
|
|
81
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
82
|
+
border: '1px solid var(--border)',
|
|
83
|
+
background: 'transparent',
|
|
84
|
+
color: 'var(--fg)',
|
|
85
|
+
cursor: 'pointer',
|
|
86
|
+
}}>
|
|
87
|
+
Update payment method
|
|
88
|
+
</button>
|
|
89
|
+
<button onClick={() => { /* TODO: cancel via /api/billing/cancel */ }} style=\{{
|
|
90
|
+
padding: 'var(--space-3) var(--space-4)',
|
|
91
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
92
|
+
border: '1px solid var(--border)',
|
|
93
|
+
background: 'transparent',
|
|
94
|
+
color: 'var(--danger)',
|
|
95
|
+
cursor: 'pointer',
|
|
96
|
+
}}>
|
|
97
|
+
Cancel
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section style=\{{
|
|
103
|
+
padding: 'var(--space-5)',
|
|
104
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
105
|
+
border: '1px solid var(--border)',
|
|
106
|
+
background: 'var(--surface)',
|
|
107
|
+
}}>
|
|
108
|
+
<h3 style=\{{ fontSize: '1rem', marginBottom: 'var(--space-2)' }}>Seats used</h3>
|
|
109
|
+
<div style=\{{ fontSize: '1.5rem', fontWeight: 600, marginBottom: 'var(--space-3)' }}>
|
|
110
|
+
{subscription.seatsUsed} of {subscription.seats}
|
|
111
|
+
</div>
|
|
112
|
+
<div style=\{{ height: '8px', borderRadius: '4px', background: 'var(--surface-sub)', overflow: 'hidden' }}>
|
|
113
|
+
<div style=\{{
|
|
114
|
+
width: `${(subscription.seatsUsed / subscription.seats) * 100}%`,
|
|
115
|
+
height: '100%',
|
|
116
|
+
background: 'var(--primary)',
|
|
117
|
+
}} />
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Badge({ status }: { status: 'trialing' | 'active' | 'past_due' | 'canceled' }) {
|
|
126
|
+
const styles: Record<typeof status, { bg: string; fg: string; label: string }> = {
|
|
127
|
+
trialing: { bg: 'var(--info)', fg: 'var(--bg)', label: 'TRIAL' },
|
|
128
|
+
active: { bg: 'var(--success)', fg: 'var(--bg)', label: 'ACTIVE' },
|
|
129
|
+
past_due: { bg: 'var(--warn)', fg: 'var(--bg)', label: 'PAST DUE' },
|
|
130
|
+
canceled: { bg: 'var(--danger)', fg: 'var(--bg)', label: 'CANCELED' },
|
|
131
|
+
};
|
|
132
|
+
const s = styles[status];
|
|
133
|
+
return (
|
|
134
|
+
<span style=\{{
|
|
135
|
+
padding: 'var(--space-1) var(--space-3)',
|
|
136
|
+
borderRadius: 'var(--radius-full, 9999px)',
|
|
137
|
+
background: s.bg,
|
|
138
|
+
color: s.fg,
|
|
139
|
+
fontSize: '0.75rem',
|
|
140
|
+
fontWeight: 700,
|
|
141
|
+
}}>
|
|
142
|
+
{s.label}
|
|
143
|
+
</span>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useAuth } from '@githat/nextjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Admin dashboard — auth-gated.
|
|
8
|
+
*
|
|
9
|
+
* Three tiles a B2B operator looks at on Monday morning:
|
|
10
|
+
* 1. Active members on the org
|
|
11
|
+
* 2. Subscription status (trial / active / past-due / canceled)
|
|
12
|
+
* 3. Usage against plan limits (seats, storage, API calls)
|
|
13
|
+
*
|
|
14
|
+
* Tile data is hardcoded in the starter — wire to your data layer.
|
|
15
|
+
* Real org info comes from useGitHat().org once the user has joined
|
|
16
|
+
* or created an org.
|
|
17
|
+
*/
|
|
18
|
+
export default function AdminPage() {
|
|
19
|
+
const { isSignedIn, user, isLoading } = useAuth();
|
|
20
|
+
|
|
21
|
+
if (isLoading) return <Loading />;
|
|
22
|
+
if (!isSignedIn) return <SignInPrompt />;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)' }}>
|
|
26
|
+
<div style=\{{ maxWidth: '64rem', margin: '0 auto', padding: 'var(--space-8) var(--space-4)' }}>
|
|
27
|
+
<header style=\{{ marginBottom: 'var(--space-8)' }}>
|
|
28
|
+
<h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-2)' }}>
|
|
29
|
+
Welcome back{user?.name ? `, ${user.name}` : ''}
|
|
30
|
+
</h1>
|
|
31
|
+
<p style=\{{ color: 'var(--fg-muted)' }}>
|
|
32
|
+
Your org dashboard. Wire each tile to real data.
|
|
33
|
+
</p>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<div style=\{{
|
|
37
|
+
display: 'grid',
|
|
38
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
|
39
|
+
gap: 'var(--space-4)',
|
|
40
|
+
marginBottom: 'var(--space-8)',
|
|
41
|
+
}}>
|
|
42
|
+
<Tile label="Members" value="—" hint="Active org members" />
|
|
43
|
+
<Tile label="Plan" value="Trial" hint="14 days remaining" />
|
|
44
|
+
<Tile label="Usage" value="—" hint="API calls this month" />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<nav style=\{{
|
|
48
|
+
display: 'grid',
|
|
49
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
|
50
|
+
gap: 'var(--space-3)',
|
|
51
|
+
}}>
|
|
52
|
+
<AdminLink href="/admin/team" title="Team" hint="Invite members, set roles" />
|
|
53
|
+
<AdminLink href="/admin/billing" title="Billing" hint="Subscription, invoices, payment method" />
|
|
54
|
+
<AdminLink href="/admin/settings" title="Settings" hint="Org name, custom domain, branding" />
|
|
55
|
+
</nav>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Tile({ label, value, hint }: { label: string; value: string; hint: string }) {
|
|
62
|
+
return (
|
|
63
|
+
<div style=\{{
|
|
64
|
+
padding: 'var(--space-5)',
|
|
65
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
66
|
+
border: '1px solid var(--border)',
|
|
67
|
+
background: 'var(--surface)',
|
|
68
|
+
}}>
|
|
69
|
+
<div style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-1)' }}>{label}</div>
|
|
70
|
+
<div style=\{{ fontSize: '2rem', fontWeight: 600 }}>{value}</div>
|
|
71
|
+
<div style=\{{ fontSize: '0.75rem', color: 'var(--fg-subtle)', marginTop: 'var(--space-1)' }}>{hint}</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AdminLink({ href, title, hint }: { href: string; title: string; hint: string }) {
|
|
77
|
+
return (
|
|
78
|
+
<Link href={href} style=\{{
|
|
79
|
+
display: 'block',
|
|
80
|
+
padding: 'var(--space-5)',
|
|
81
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
82
|
+
border: '1px solid var(--border)',
|
|
83
|
+
background: 'var(--surface)',
|
|
84
|
+
color: 'var(--fg)',
|
|
85
|
+
textDecoration: 'none',
|
|
86
|
+
}}>
|
|
87
|
+
<div style=\{{ fontWeight: 600, marginBottom: 'var(--space-1)' }}>{title} →</div>
|
|
88
|
+
<div style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)' }}>{hint}</div>
|
|
89
|
+
</Link>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Loading() {
|
|
94
|
+
return <div style=\{{ display: 'flex', minHeight: 'calc(100vh - 64px)', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-muted)' }}>Loading…</div>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function SignInPrompt() {
|
|
98
|
+
return (
|
|
99
|
+
<div style=\{{ display: 'flex', minHeight: 'calc(100vh - 64px)', alignItems: 'center', justifyContent: 'center' }}>
|
|
100
|
+
<div style=\{{ textAlign: 'center' }}>
|
|
101
|
+
<h2 style=\{{ marginBottom: 'var(--space-3)' }}>Sign in to continue</h2>
|
|
102
|
+
<Link href="/sign-in" style=\{{ color: 'var(--primary)' }}>Sign in →</Link>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|