create-githat-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +259 -0
- package/bin/index.js +6 -0
- package/dist/cli.js +623 -0
- package/package.json +42 -0
- package/templates/base/.env.example.hbs +13 -0
- package/templates/base/.env.local.hbs +10 -0
- package/templates/base/.gitignore.hbs +11 -0
- package/templates/base/githat/api/agents.ts.hbs +17 -0
- package/templates/base/githat/api/auth.ts.hbs +38 -0
- package/templates/base/githat/api/client.ts.hbs +92 -0
- package/templates/base/githat/api/mcp.ts.hbs +21 -0
- package/templates/base/githat/api/orgs.ts.hbs +34 -0
- package/templates/base/githat/api/types.ts.hbs +70 -0
- package/templates/base/githat/api/users.ts.hbs +14 -0
- package/templates/base/githat/auth/guard.tsx.hbs +53 -0
- package/templates/base/githat/auth/index.ts.hbs +3 -0
- package/templates/base/githat/config.ts.hbs +18 -0
- package/templates/base/githat/dashboard/agents.tsx.hbs +121 -0
- package/templates/base/githat/dashboard/apps.tsx.hbs +53 -0
- package/templates/base/githat/dashboard/layout.tsx.hbs +83 -0
- package/templates/base/githat/dashboard/mcp-servers.tsx.hbs +111 -0
- package/templates/base/githat/dashboard/members.tsx.hbs +123 -0
- package/templates/base/githat/dashboard/overview.tsx.hbs +37 -0
- package/templates/base/githat/dashboard/settings.tsx.hbs +87 -0
- package/templates/nextjs/app/(auth)/forgot-password/page.tsx.hbs +63 -0
- package/templates/nextjs/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/nextjs/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/nextjs/app/(auth)/verify-email/page.tsx.hbs +58 -0
- package/templates/nextjs/app/dashboard/agents/page.tsx.hbs +9 -0
- package/templates/nextjs/app/dashboard/apps/page.tsx.hbs +9 -0
- package/templates/nextjs/app/dashboard/layout.tsx.hbs +28 -0
- package/templates/nextjs/app/dashboard/mcp/page.tsx.hbs +9 -0
- package/templates/nextjs/app/dashboard/members/page.tsx.hbs +9 -0
- package/templates/nextjs/app/dashboard/page.tsx.hbs +30 -0
- package/templates/nextjs/app/dashboard/settings/page.tsx.hbs +9 -0
- package/templates/nextjs/app/globals.css.hbs +20 -0
- package/templates/nextjs/app/layout.tsx.hbs +33 -0
- package/templates/nextjs/app/page.tsx.hbs +18 -0
- package/templates/nextjs/middleware.ts.hbs +10 -0
- package/templates/nextjs/next.config.ts.hbs +5 -0
- package/templates/nextjs/postcss.config.mjs.hbs +9 -0
- package/templates/nextjs/tsconfig.json.hbs +21 -0
- package/templates/react-vite/index.html.hbs +12 -0
- package/templates/react-vite/src/App.tsx.hbs +46 -0
- package/templates/react-vite/src/index.css.hbs +20 -0
- package/templates/react-vite/src/main.tsx.hbs +30 -0
- package/templates/react-vite/src/pages/Dashboard.tsx.hbs +68 -0
- package/templates/react-vite/src/pages/ForgotPassword.tsx.hbs +58 -0
- package/templates/react-vite/src/pages/Home.tsx.hbs +18 -0
- package/templates/react-vite/src/pages/SignIn.tsx.hbs +9 -0
- package/templates/react-vite/src/pages/SignUp.tsx.hbs +9 -0
- package/templates/react-vite/src/pages/VerifyEmail.tsx.hbs +51 -0
- package/templates/react-vite/tsconfig.json.hbs +16 -0
- package/templates/react-vite/vite.config.ts.hbs +14 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{{#if includeMcpModule}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { githatApi } from '../api/client{{#unless typescript}}.js{{/unless}}';
|
|
6
|
+
{{#if typescript}}
|
|
7
|
+
import type { McpServer } from '../api/types';
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
export function DashboardMcpServers() {
|
|
11
|
+
const [servers, setServers] = useState{{#if typescript}}<McpServer[]>{{/if}}([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [domain, setDomain] = useState('');
|
|
14
|
+
const [registering, setRegistering] = useState(false);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
|
|
17
|
+
const loadServers = () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
githatApi.get{{#if typescript}}<{ servers: McpServer[] }>{{/if}}('/mcp/servers')
|
|
20
|
+
.then((data) => setServers(data.servers || []))
|
|
21
|
+
.catch(() => {})
|
|
22
|
+
.finally(() => setLoading(false));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
useEffect(() => { loadServers(); }, []);
|
|
26
|
+
|
|
27
|
+
const handleRegister = async (e{{#if typescript}}: React.FormEvent{{/if}}) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
if (!domain.trim()) return;
|
|
30
|
+
setRegistering(true);
|
|
31
|
+
setError('');
|
|
32
|
+
try {
|
|
33
|
+
await githatApi.post('/mcp/servers', { domain: domain.trim() });
|
|
34
|
+
setDomain('');
|
|
35
|
+
loadServers();
|
|
36
|
+
} catch {
|
|
37
|
+
setError('Failed to register server. Check your domain and try again.');
|
|
38
|
+
} finally {
|
|
39
|
+
setRegistering(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleDelete = async (id{{#if typescript}}: string{{/if}}) => {
|
|
44
|
+
if (!confirm('Remove this MCP server?')) return;
|
|
45
|
+
try {
|
|
46
|
+
await githatApi.del(`/mcp/servers/${id}`);
|
|
47
|
+
loadServers();
|
|
48
|
+
} catch {
|
|
49
|
+
setError('Failed to remove server.');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (loading) {
|
|
54
|
+
return <p style=\{{ color: '#71717a' }}>Loading MCP servers...</p>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '1.5rem' }}>MCP Servers</h1>
|
|
60
|
+
|
|
61
|
+
<form onSubmit={handleRegister} style=\{{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
value={domain}
|
|
65
|
+
onChange={(e) => setDomain(e.target.value)}
|
|
66
|
+
placeholder="mcp.yourcompany.com"
|
|
67
|
+
required
|
|
68
|
+
style=\{{ flex: 1, padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', outline: 'none' }}
|
|
69
|
+
/>
|
|
70
|
+
<button
|
|
71
|
+
type="submit"
|
|
72
|
+
disabled={registering}
|
|
73
|
+
style=\{{ padding: '0.625rem 1.25rem', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: '0.375rem', fontWeight: 600, cursor: 'pointer', opacity: registering ? 0.6 : 1 }}
|
|
74
|
+
>
|
|
75
|
+
{registering ? 'Registering...' : 'Register'}
|
|
76
|
+
</button>
|
|
77
|
+
</form>
|
|
78
|
+
|
|
79
|
+
{error && <p style=\{{ color: '#ef4444', fontSize: '0.875rem', marginBottom: '1rem' }}>{error}</p>}
|
|
80
|
+
|
|
81
|
+
{servers.length === 0 ? (
|
|
82
|
+
<div style=\{{ padding: '3rem', textAlign: 'center', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.5rem' }}>
|
|
83
|
+
<p style=\{{ color: '#71717a' }}>No MCP servers registered yet.</p>
|
|
84
|
+
</div>
|
|
85
|
+
) : (
|
|
86
|
+
<div style=\{{ display: 'grid', gap: '0.5rem' }}>
|
|
87
|
+
{servers.map((server) => (
|
|
88
|
+
<div key={server.id} style=\{{ padding: '1rem 1.5rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
89
|
+
<div>
|
|
90
|
+
<p style=\{{ fontWeight: 600, color: '#fafafa' }}>{server.domain}</p>
|
|
91
|
+
<p style=\{{ fontSize: '0.875rem', color: '#71717a' }}>Client ID: {server.clientId}</p>
|
|
92
|
+
</div>
|
|
93
|
+
<div style=\{{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
94
|
+
<span style=\{{ fontSize: '0.75rem', padding: '0.25rem 0.75rem', borderRadius: '9999px', background: server.verified ? '#064e3b' : '#451a03', color: server.verified ? '#34d399' : '#f59e0b' }}>
|
|
95
|
+
{server.verified ? 'Verified' : 'Pending'}
|
|
96
|
+
</span>
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => handleDelete(server.id)}
|
|
99
|
+
style=\{{ fontSize: '0.75rem', padding: '0.25rem 0.5rem', background: 'transparent', border: '1px solid #3f3f46', borderRadius: '0.25rem', color: '#ef4444', cursor: 'pointer' }}
|
|
100
|
+
>
|
|
101
|
+
Remove
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
{{/if}}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{{#if includeOrgManagement}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { useAuth } from '@githat/nextjs';
|
|
6
|
+
import { orgsApi } from '../api/orgs{{#unless typescript}}.js{{/unless}}';
|
|
7
|
+
{{#if typescript}}
|
|
8
|
+
import type { ApiOrgMember } from '../api/types';
|
|
9
|
+
{{/if}}
|
|
10
|
+
|
|
11
|
+
export function DashboardMembers() {
|
|
12
|
+
const { org } = useAuth();
|
|
13
|
+
const [members, setMembers] = useState{{#if typescript}}<ApiOrgMember[]>{{/if}}([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [inviteEmail, setInviteEmail] = useState('');
|
|
16
|
+
const [inviteRole, setInviteRole] = useState{{#if typescript}}<'admin' | 'member'>{{/if}}('member');
|
|
17
|
+
const [inviting, setInviting] = useState(false);
|
|
18
|
+
const [error, setError] = useState('');
|
|
19
|
+
|
|
20
|
+
const loadMembers = () => {
|
|
21
|
+
if (!org) return;
|
|
22
|
+
setLoading(true);
|
|
23
|
+
orgsApi.listMembers(org.id)
|
|
24
|
+
.then((data) => setMembers(data.members || []))
|
|
25
|
+
.catch(() => {})
|
|
26
|
+
.finally(() => setLoading(false));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => { loadMembers(); }, [org]);
|
|
30
|
+
|
|
31
|
+
const handleInvite = async (e{{#if typescript}}: React.FormEvent{{/if}}) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
if (!org || !inviteEmail.trim()) return;
|
|
34
|
+
setInviting(true);
|
|
35
|
+
setError('');
|
|
36
|
+
try {
|
|
37
|
+
await orgsApi.inviteMember(org.id, inviteEmail.trim(), inviteRole);
|
|
38
|
+
setInviteEmail('');
|
|
39
|
+
loadMembers();
|
|
40
|
+
} catch {
|
|
41
|
+
setError('Failed to send invite. Please try again.');
|
|
42
|
+
} finally {
|
|
43
|
+
setInviting(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleRemove = async (userId{{#if typescript}}: string{{/if}}) => {
|
|
48
|
+
if (!org || !confirm('Remove this member?')) return;
|
|
49
|
+
try {
|
|
50
|
+
await orgsApi.removeMember(org.id, userId);
|
|
51
|
+
loadMembers();
|
|
52
|
+
} catch {
|
|
53
|
+
setError('Failed to remove member.');
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (!org) {
|
|
58
|
+
return <p style=\{{ color: '#71717a' }}>Select an organization to manage members.</p>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (loading) {
|
|
62
|
+
return <p style=\{{ color: '#71717a' }}>Loading members...</p>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '1.5rem' }}>Members</h1>
|
|
68
|
+
|
|
69
|
+
<form onSubmit={handleInvite} style=\{{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
|
70
|
+
<input
|
|
71
|
+
type="email"
|
|
72
|
+
value={inviteEmail}
|
|
73
|
+
onChange={(e) => setInviteEmail(e.target.value)}
|
|
74
|
+
placeholder="colleague@company.com"
|
|
75
|
+
required
|
|
76
|
+
style=\{{ flex: 1, minWidth: '12rem', padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', outline: 'none' }}
|
|
77
|
+
/>
|
|
78
|
+
<select
|
|
79
|
+
value={inviteRole}
|
|
80
|
+
onChange={(e) => setInviteRole(e.target.value{{#if typescript}} as 'admin' | 'member'{{/if}})}
|
|
81
|
+
style=\{{ padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa' }}
|
|
82
|
+
>
|
|
83
|
+
<option value="member">Member</option>
|
|
84
|
+
<option value="admin">Admin</option>
|
|
85
|
+
</select>
|
|
86
|
+
<button
|
|
87
|
+
type="submit"
|
|
88
|
+
disabled={inviting}
|
|
89
|
+
style=\{{ padding: '0.625rem 1.25rem', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: '0.375rem', fontWeight: 600, cursor: 'pointer', opacity: inviting ? 0.6 : 1 }}
|
|
90
|
+
>
|
|
91
|
+
{inviting ? 'Inviting...' : 'Invite'}
|
|
92
|
+
</button>
|
|
93
|
+
</form>
|
|
94
|
+
|
|
95
|
+
{error && <p style=\{{ color: '#ef4444', fontSize: '0.875rem', marginBottom: '1rem' }}>{error}</p>}
|
|
96
|
+
|
|
97
|
+
<div style=\{{ display: 'grid', gap: '0.5rem' }}>
|
|
98
|
+
{members.map((member) => (
|
|
99
|
+
<div key={member.userId} style=\{{ padding: '1rem 1.5rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
100
|
+
<div>
|
|
101
|
+
<p style=\{{ fontWeight: 600, color: '#fafafa' }}>{member.name}</p>
|
|
102
|
+
<p style=\{{ fontSize: '0.875rem', color: '#71717a' }}>{member.email}</p>
|
|
103
|
+
</div>
|
|
104
|
+
<div style=\{{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
105
|
+
<span style=\{{ fontSize: '0.75rem', padding: '0.25rem 0.75rem', borderRadius: '9999px', background: '#1e1e2e', color: member.role === 'owner' ? '#7c3aed' : '#a1a1aa' }}>
|
|
106
|
+
{member.role}
|
|
107
|
+
</span>
|
|
108
|
+
{member.role !== 'owner' && (
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => handleRemove(member.userId)}
|
|
111
|
+
style=\{{ fontSize: '0.75rem', padding: '0.25rem 0.5rem', background: 'transparent', border: '1px solid #3f3f46', borderRadius: '0.25rem', color: '#ef4444', cursor: 'pointer' }}
|
|
112
|
+
>
|
|
113
|
+
Remove
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
{{/if}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{{#unless includeDashboard}}{{else}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useAuth } from '@githat/nextjs';
|
|
5
|
+
|
|
6
|
+
export function DashboardOverview() {
|
|
7
|
+
const { user, org } = useAuth();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<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
|
+
)}
|
|
19
|
+
|
|
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" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function StatCard({ title, value }{{#if typescript}}: { title: string; value: string }{{/if}}) {
|
|
30
|
+
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>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
{{/unless}}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{{#if includeOrgManagement}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useAuth } from '@githat/nextjs';
|
|
6
|
+
import { orgsApi } from '../api/orgs{{#unless typescript}}.js{{/unless}}';
|
|
7
|
+
|
|
8
|
+
export function DashboardSettings() {
|
|
9
|
+
const { org } = useAuth();
|
|
10
|
+
const [name, setName] = useState(org?.name || '');
|
|
11
|
+
const [brandColor, setBrandColor] = useState(org?.brandColor || '#7c3aed');
|
|
12
|
+
const [saving, setSaving] = useState(false);
|
|
13
|
+
const [saved, setSaved] = useState(false);
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
|
|
16
|
+
if (!org) {
|
|
17
|
+
return <p style=\{{ color: '#71717a' }}>Select an organization to view settings.</p>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const handleSave = async (e{{#if typescript}}: React.FormEvent{{/if}}) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setSaving(true);
|
|
23
|
+
setError('');
|
|
24
|
+
setSaved(false);
|
|
25
|
+
try {
|
|
26
|
+
await orgsApi.update(org.id, { name, brandColor });
|
|
27
|
+
setSaved(true);
|
|
28
|
+
} catch {
|
|
29
|
+
setError('Failed to save settings.');
|
|
30
|
+
} finally {
|
|
31
|
+
setSaving(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '1.5rem' }}>Settings</h1>
|
|
38
|
+
<form onSubmit={handleSave} style=\{{ maxWidth: '32rem', display: 'grid', gap: '1.5rem' }}>
|
|
39
|
+
<div>
|
|
40
|
+
<label style=\{{ fontSize: '0.875rem', color: '#71717a', display: 'block', marginBottom: '0.375rem' }}>Organization Name</label>
|
|
41
|
+
<input
|
|
42
|
+
value={name}
|
|
43
|
+
onChange={(e) => setName(e.target.value)}
|
|
44
|
+
style=\{{ width: '100%', padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', outline: 'none' }}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<SettingField label="Slug" value={org.slug} />
|
|
48
|
+
<SettingField label="Tier" value={org.tier} />
|
|
49
|
+
<div>
|
|
50
|
+
<label style=\{{ fontSize: '0.875rem', color: '#71717a', display: 'block', marginBottom: '0.375rem' }}>Brand Color</label>
|
|
51
|
+
<div style=\{{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
52
|
+
<input
|
|
53
|
+
type="color"
|
|
54
|
+
value={brandColor}
|
|
55
|
+
onChange={(e) => setBrandColor(e.target.value)}
|
|
56
|
+
style=\{{ width: '2.5rem', height: '2.5rem', border: '1px solid #1e1e2e', borderRadius: '0.375rem', background: 'transparent', cursor: 'pointer' }}
|
|
57
|
+
/>
|
|
58
|
+
<span style=\{{ color: '#a1a1aa', fontSize: '0.875rem', fontFamily: 'monospace' }}>{brandColor}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{error && <p style=\{{ color: '#ef4444', fontSize: '0.875rem' }}>{error}</p>}
|
|
63
|
+
{saved && <p style=\{{ color: '#34d399', fontSize: '0.875rem' }}>Settings saved.</p>}
|
|
64
|
+
|
|
65
|
+
<button
|
|
66
|
+
type="submit"
|
|
67
|
+
disabled={saving}
|
|
68
|
+
style=\{{ padding: '0.625rem 1.25rem', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: '0.375rem', fontWeight: 600, cursor: 'pointer', opacity: saving ? 0.6 : 1, justifySelf: 'start' }}
|
|
69
|
+
>
|
|
70
|
+
{saving ? 'Saving...' : 'Save changes'}
|
|
71
|
+
</button>
|
|
72
|
+
</form>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function SettingField({ label, value }{{#if typescript}}: { label: string; value: string }{{/if}}) {
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<label style=\{{ fontSize: '0.875rem', color: '#71717a', display: 'block', marginBottom: '0.375rem' }}>{label}</label>
|
|
81
|
+
<div style=\{{ padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa' }}>
|
|
82
|
+
{value}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
{{/if}}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{{#if includeForgotPassword}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
{{#if includeGithatFolder}}
|
|
6
|
+
import { authApi } from '../../../githat/api/auth{{#unless typescript}}.js{{/unless}}';
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
export default function ForgotPasswordPage() {
|
|
10
|
+
const [email, setEmail] = useState('');
|
|
11
|
+
const [sent, setSent] = useState(false);
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
|
|
14
|
+
const handleSubmit = async (e{{#if typescript}}: React.FormEvent{{/if}}) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
setError('');
|
|
17
|
+
try {
|
|
18
|
+
{{#if includeGithatFolder}}
|
|
19
|
+
await authApi.forgotPassword(email);
|
|
20
|
+
{{else}}
|
|
21
|
+
await fetch('{{apiUrl}}/auth/forgot-password', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ email }),
|
|
25
|
+
});
|
|
26
|
+
{{/if}}
|
|
27
|
+
setSent(true);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
setError('Something went wrong. Please try again.');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<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}}>
|
|
35
|
+
<div style=\{{ width: '100%', maxWidth: '24rem', padding: '2rem' }}>
|
|
36
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Reset password</h1>
|
|
37
|
+
{sent ? (
|
|
38
|
+
<p style=\{{ color: '#a1a1aa' }}>Check your email for a reset link.</p>
|
|
39
|
+
) : (
|
|
40
|
+
<form onSubmit={handleSubmit}>
|
|
41
|
+
<p style=\{{ color: '#a1a1aa', marginBottom: '1.5rem' }}>Enter your email to receive a reset link.</p>
|
|
42
|
+
{error && <p style=\{{ color: '#ef4444', marginBottom: '1rem', fontSize: '0.875rem' }}>{error}</p>}
|
|
43
|
+
<input
|
|
44
|
+
type="email"
|
|
45
|
+
value={email}
|
|
46
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
47
|
+
placeholder="you@example.com"
|
|
48
|
+
required
|
|
49
|
+
style=\{{ width: '100%', padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', marginBottom: '1rem', outline: 'none' }}
|
|
50
|
+
/>
|
|
51
|
+
<button
|
|
52
|
+
type="submit"
|
|
53
|
+
style=\{{ width: '100%', padding: '0.625rem', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: '0.375rem', fontWeight: 600, cursor: 'pointer' }}
|
|
54
|
+
>
|
|
55
|
+
Send reset link
|
|
56
|
+
</button>
|
|
57
|
+
</form>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</main>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
{{/if}}
|
|
@@ -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,58 @@
|
|
|
1
|
+
{{#if includeEmailVerification}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
{{#if includeGithatFolder}}
|
|
6
|
+
import { authApi } from '../../../githat/api/auth{{#unless typescript}}.js{{/unless}}';
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
export default function VerifyEmailPage() {
|
|
10
|
+
const [status, setStatus] = useState{{#if typescript}}<'loading' | 'success' | 'error'>{{/if}}('loading');
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const params = new URLSearchParams(window.location.search);
|
|
14
|
+
const token = params.get('token');
|
|
15
|
+
if (!token) {
|
|
16
|
+
setStatus('error');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
(async () => {
|
|
21
|
+
try {
|
|
22
|
+
{{#if includeGithatFolder}}
|
|
23
|
+
await authApi.verifyEmail(token);
|
|
24
|
+
{{else}}
|
|
25
|
+
await fetch('{{apiUrl}}/auth/verify-email', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ token }),
|
|
29
|
+
});
|
|
30
|
+
{{/if}}
|
|
31
|
+
setStatus('success');
|
|
32
|
+
} catch {
|
|
33
|
+
setStatus('error');
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<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}}>
|
|
40
|
+
<div style=\{{ textAlign: 'center' }}>
|
|
41
|
+
{status === 'loading' && <p style=\{{ color: '#a1a1aa' }}>Verifying your email...</p>}
|
|
42
|
+
{status === 'success' && (
|
|
43
|
+
<>
|
|
44
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Email verified!</h1>
|
|
45
|
+
<p style=\{{ color: '#a1a1aa' }}>You can now <a href="/sign-in" style=\{{ color: '#7c3aed' }}>sign in</a>.</p>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
{status === 'error' && (
|
|
49
|
+
<>
|
|
50
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Verification failed</h1>
|
|
51
|
+
<p style=\{{ color: '#a1a1aa' }}>The link may have expired. <a href="/sign-up" style=\{{ color: '#7c3aed' }}>Try again</a>.</p>
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
{{/if}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{{#if includeDashboard}}
|
|
2
|
+
{{#if includeGithatFolder}}
|
|
3
|
+
import { DashboardLayout } from '../../githat/dashboard/layout{{#unless typescript}}.jsx{{/unless}}';
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
|
|
6
|
+
return <DashboardLayout>{children}</DashboardLayout>;
|
|
7
|
+
}
|
|
8
|
+
{{else}}
|
|
9
|
+
import { ProtectedRoute, UserButton, OrgSwitcher } from '@githat/nextjs';
|
|
10
|
+
|
|
11
|
+
export default function DashboardLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
|
|
12
|
+
return (
|
|
13
|
+
<ProtectedRoute>
|
|
14
|
+
<div style=\{{ minHeight: '100vh', background: '#09090b' }}>
|
|
15
|
+
<header style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1rem 2rem', borderBottom: '1px solid #1e1e2e' }}>
|
|
16
|
+
<h2 style=\{{ fontSize: '1.125rem', fontWeight: 600, color: '#fafafa' }}>{{businessName}}</h2>
|
|
17
|
+
<div style=\{{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
18
|
+
<OrgSwitcher />
|
|
19
|
+
<UserButton />
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
<main style=\{{ padding: '2rem' }}>{children}</main>
|
|
23
|
+
</div>
|
|
24
|
+
</ProtectedRoute>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
{{/if}}
|
|
28
|
+
{{/if}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{{#if includeMcpModule}}
|
|
2
|
+
{{#if includeGithatFolder}}
|
|
3
|
+
import { DashboardMcpServers } from '../../../githat/dashboard/mcp-servers{{#unless typescript}}.jsx{{/unless}}';
|
|
4
|
+
|
|
5
|
+
export default function McpPage() {
|
|
6
|
+
return <DashboardMcpServers />;
|
|
7
|
+
}
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{/if}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{{#if includeOrgManagement}}
|
|
2
|
+
{{#if includeGithatFolder}}
|
|
3
|
+
import { DashboardMembers } from '../../../githat/dashboard/members{{#unless typescript}}.jsx{{/unless}}';
|
|
4
|
+
|
|
5
|
+
export default function MembersPage() {
|
|
6
|
+
return <DashboardMembers />;
|
|
7
|
+
}
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{/if}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{#if includeDashboard}}
|
|
2
|
+
{{#if includeGithatFolder}}
|
|
3
|
+
import { DashboardOverview } from '../../githat/dashboard/overview{{#unless typescript}}.jsx{{/unless}}';
|
|
4
|
+
|
|
5
|
+
export default function DashboardPage() {
|
|
6
|
+
return <DashboardOverview />;
|
|
7
|
+
}
|
|
8
|
+
{{else}}
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import { useAuth } from '@githat/nextjs';
|
|
12
|
+
|
|
13
|
+
export default function DashboardPage() {
|
|
14
|
+
const { user, org } = useAuth();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div>
|
|
18
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '1rem' }}>
|
|
19
|
+
Welcome{user?.name ? `, ${user.name}` : ''}
|
|
20
|
+
</h1>
|
|
21
|
+
{org && (
|
|
22
|
+
<p style=\{{ color: '#a1a1aa' }}>
|
|
23
|
+
Organization: <strong style=\{{ color: '#7c3aed' }}>{org.name}</strong> ({org.role})
|
|
24
|
+
</p>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{/if}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{{#if includeOrgManagement}}
|
|
2
|
+
{{#if includeGithatFolder}}
|
|
3
|
+
import { DashboardSettings } from '../../../githat/dashboard/settings{{#unless typescript}}.jsx{{/unless}}';
|
|
4
|
+
|
|
5
|
+
export default function SettingsPage() {
|
|
6
|
+
return <DashboardSettings />;
|
|
7
|
+
}
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{/if}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{{#if useTailwind}}
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
{{/if}}
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
13
|
+
background: #09090b;
|
|
14
|
+
color: #fafafa;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
a {
|
|
18
|
+
color: inherit;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
}
|