@unbrained/pm-web 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/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SETTINGS VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
import { confirmDialog } from '../components/modals.js';
|
|
9
|
+
function avatarInitial(name) {
|
|
10
|
+
return (name.trim()[0] || '?').toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
function avatarBg(seed) {
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (let i = 0; i < seed.length; i++) {
|
|
15
|
+
hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
const hue = hash % 360;
|
|
18
|
+
return `hsl(${hue},55%,45%)`;
|
|
19
|
+
}
|
|
20
|
+
export function renderSettingsView() {
|
|
21
|
+
const el = document.getElementById('content-settings');
|
|
22
|
+
if (!el)
|
|
23
|
+
return;
|
|
24
|
+
const u = state.user || {};
|
|
25
|
+
const createdInfo = u.created_at
|
|
26
|
+
? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">Account created ${new Date(u.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>`
|
|
27
|
+
: '';
|
|
28
|
+
el.innerHTML = `
|
|
29
|
+
<div class="page-header">
|
|
30
|
+
<div><div class="page-title">Settings</div><div class="page-subtitle">Manage your profile and account</div></div>
|
|
31
|
+
</div>
|
|
32
|
+
<div style="max-width:560px;display:flex;flex-direction:column;gap:20px">
|
|
33
|
+
<div class="card">
|
|
34
|
+
<div class="card-header"><div class="card-title">Profile</div></div>
|
|
35
|
+
<div class="card-body">
|
|
36
|
+
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
|
37
|
+
<div style="width:64px;height:64px;border-radius:50%;background:${avatarBg(u.email || u.display_name || '?')};display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;flex-shrink:0;user-select:none" aria-hidden="true">${escHtml(avatarInitial(u.display_name || u.email || '?'))}</div>
|
|
38
|
+
<div>
|
|
39
|
+
<div style="font-weight:600;font-size:16px">${escHtml(u.display_name || u.email || '')}</div>
|
|
40
|
+
<div style="font-size:13px;color:var(--text-muted)">${escHtml(u.email || '')}</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<label class="form-label" for="settings-display-name">Display Name</label>
|
|
45
|
+
<input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name || u.email || '')}" placeholder="Your display name" aria-label="Display name">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label class="form-label">Email</label>
|
|
49
|
+
<input class="form-input" type="text" value="${escHtml(u.email || '')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="Email address (read only)">
|
|
50
|
+
${createdInfo}
|
|
51
|
+
</div>
|
|
52
|
+
<div class="form-error" id="settings-profile-error" style="display:none" role="alert"></div>
|
|
53
|
+
<button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="Save profile changes"><span>Save Profile</span></button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="card">
|
|
57
|
+
<div class="card-header"><div class="card-title">Change Password</div></div>
|
|
58
|
+
<div class="card-body">
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label class="form-label" for="settings-current-pw">Current Password</label>
|
|
61
|
+
<input class="form-input" id="settings-current-pw" type="password" placeholder="Current password" autocomplete="current-password" aria-label="Current password">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label class="form-label" for="settings-new-pw">New Password</label>
|
|
65
|
+
<input class="form-input" id="settings-new-pw" type="password" placeholder="New password" autocomplete="new-password" aria-label="New password">
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-group">
|
|
68
|
+
<label class="form-label" for="settings-confirm-pw">Confirm New Password</label>
|
|
69
|
+
<input class="form-input" id="settings-confirm-pw" type="password" placeholder="Confirm new password" autocomplete="new-password" aria-label="Confirm new password">
|
|
70
|
+
</div>
|
|
71
|
+
<div class="form-error" id="settings-pw-error" style="display:none" role="alert"></div>
|
|
72
|
+
<button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="Change password"><span>Change Password</span></button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<div class="card-header">
|
|
77
|
+
<div class="card-title">GitHub Token</div>
|
|
78
|
+
${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">✓ Token configured</span>` : `<span style="font-size:12px;color:var(--text-muted)">No token set</span>`}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="card-body">
|
|
81
|
+
<div style="margin-bottom:12px;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);font-size:12px;color:var(--text-secondary)">
|
|
82
|
+
<strong style="color:var(--text-primary)">How to get a token:</strong>
|
|
83
|
+
<ol style="margin-top:6px;padding-left:18px;line-height:1.8">
|
|
84
|
+
<li>Go to GitHub → Settings → Developer settings → Personal access tokens</li>
|
|
85
|
+
<li>Generate a new token with <code style="font-family:monospace;background:var(--bg-base);padding:0 3px;border-radius:3px">repo</code> scope</li>
|
|
86
|
+
<li>Paste the token below and click Save</li>
|
|
87
|
+
</ol>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label class="form-label" for="settings-github-token">Personal Access Token (PAT)</label>
|
|
91
|
+
<input class="form-input" id="settings-github-token" type="password" placeholder="${u.has_github_token ? 'Leave blank to keep current token' : 'ghp_…'}" autocomplete="off" aria-label="GitHub personal access token">
|
|
92
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Used for GitHub integration. Needs <code style="font-family:monospace;background:var(--bg-input);padding:0 3px;border-radius:3px">repo</code> scope.</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-error" id="settings-github-error" style="display:none" role="alert"></div>
|
|
95
|
+
<div style="display:flex;gap:8px">
|
|
96
|
+
<button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="Save GitHub token"><span>Save Token</span></button>
|
|
97
|
+
${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="Clear GitHub token">Clear Token</button>` : ''}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>`;
|
|
102
|
+
}
|
|
103
|
+
export async function saveProfile() {
|
|
104
|
+
const displayName = document.getElementById('settings-display-name')?.value?.trim() || '';
|
|
105
|
+
const errEl = document.getElementById('settings-profile-error');
|
|
106
|
+
const btn = document.getElementById('settings-profile-btn');
|
|
107
|
+
if (!displayName) {
|
|
108
|
+
if (errEl) {
|
|
109
|
+
errEl.textContent = 'Display name cannot be empty';
|
|
110
|
+
errEl.style.display = 'block';
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (errEl)
|
|
115
|
+
errEl.style.display = 'none';
|
|
116
|
+
if (btn) {
|
|
117
|
+
btn.disabled = true;
|
|
118
|
+
const sp = btn.querySelector('span');
|
|
119
|
+
if (sp)
|
|
120
|
+
sp.textContent = 'Saving…';
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const data = await api('PATCH', '/auth/profile', { displayName });
|
|
124
|
+
if (data.user) {
|
|
125
|
+
state.user = { ...state.user, ...data.user };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
state.user.display_name = displayName;
|
|
129
|
+
}
|
|
130
|
+
const u = state.user;
|
|
131
|
+
const initials = (u.display_name || u.email || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
132
|
+
const avatarEl = document.getElementById('user-avatar');
|
|
133
|
+
if (avatarEl)
|
|
134
|
+
avatarEl.textContent = initials;
|
|
135
|
+
const nameEl = document.getElementById('user-name-display');
|
|
136
|
+
if (nameEl)
|
|
137
|
+
nameEl.textContent = u.display_name || u.email;
|
|
138
|
+
toast('Profile saved', 'success');
|
|
139
|
+
renderSettingsView();
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (errEl) {
|
|
143
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
144
|
+
errEl.style.display = 'block';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (btn) {
|
|
149
|
+
btn.disabled = false;
|
|
150
|
+
const sp = btn.querySelector('span');
|
|
151
|
+
if (sp)
|
|
152
|
+
sp.textContent = 'Save Profile';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function changePassword() {
|
|
157
|
+
const currentPassword = document.getElementById('settings-current-pw')?.value || '';
|
|
158
|
+
const newPassword = document.getElementById('settings-new-pw')?.value || '';
|
|
159
|
+
const confirmPassword = document.getElementById('settings-confirm-pw')?.value || '';
|
|
160
|
+
const errEl = document.getElementById('settings-pw-error');
|
|
161
|
+
const btn = document.getElementById('settings-pw-btn');
|
|
162
|
+
if (errEl)
|
|
163
|
+
errEl.style.display = 'none';
|
|
164
|
+
if (!currentPassword || !newPassword || !confirmPassword) {
|
|
165
|
+
if (errEl) {
|
|
166
|
+
errEl.textContent = 'All fields are required';
|
|
167
|
+
errEl.style.display = 'block';
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (newPassword !== confirmPassword) {
|
|
172
|
+
if (errEl) {
|
|
173
|
+
errEl.textContent = 'New passwords do not match';
|
|
174
|
+
errEl.style.display = 'block';
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (newPassword.length < 6) {
|
|
179
|
+
if (errEl) {
|
|
180
|
+
errEl.textContent = 'New password must be at least 6 characters';
|
|
181
|
+
errEl.style.display = 'block';
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (btn) {
|
|
186
|
+
btn.disabled = true;
|
|
187
|
+
const sp = btn.querySelector('span');
|
|
188
|
+
if (sp)
|
|
189
|
+
sp.textContent = 'Changing…';
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await api('POST', '/auth/change-password', { currentPassword, newPassword });
|
|
193
|
+
toast('Password changed successfully', 'success');
|
|
194
|
+
const curEl = document.getElementById('settings-current-pw');
|
|
195
|
+
const newEl = document.getElementById('settings-new-pw');
|
|
196
|
+
const confEl = document.getElementById('settings-confirm-pw');
|
|
197
|
+
if (curEl)
|
|
198
|
+
curEl.value = '';
|
|
199
|
+
if (newEl)
|
|
200
|
+
newEl.value = '';
|
|
201
|
+
if (confEl)
|
|
202
|
+
confEl.value = '';
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (errEl) {
|
|
206
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
207
|
+
errEl.style.display = 'block';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (btn) {
|
|
212
|
+
btn.disabled = false;
|
|
213
|
+
const sp = btn.querySelector('span');
|
|
214
|
+
if (sp)
|
|
215
|
+
sp.textContent = 'Change Password';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export async function saveGitHubToken() {
|
|
220
|
+
const token = document.getElementById('settings-github-token')?.value?.trim() || '';
|
|
221
|
+
const errEl = document.getElementById('settings-github-error');
|
|
222
|
+
const btn = document.getElementById('settings-github-btn');
|
|
223
|
+
if (!token) {
|
|
224
|
+
if (errEl) {
|
|
225
|
+
errEl.textContent = 'Please enter a token';
|
|
226
|
+
errEl.style.display = 'block';
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (errEl)
|
|
231
|
+
errEl.style.display = 'none';
|
|
232
|
+
if (btn) {
|
|
233
|
+
btn.disabled = true;
|
|
234
|
+
const sp = btn.querySelector('span');
|
|
235
|
+
if (sp)
|
|
236
|
+
sp.textContent = 'Saving…';
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const data = await api('PATCH', '/auth/github-token', { token });
|
|
240
|
+
state.user.has_github_token = data.hasToken !== undefined ? data.hasToken : true;
|
|
241
|
+
toast('GitHub token saved', 'success');
|
|
242
|
+
renderSettingsView();
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (errEl) {
|
|
246
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
247
|
+
errEl.style.display = 'block';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (btn) {
|
|
252
|
+
btn.disabled = false;
|
|
253
|
+
const sp = btn.querySelector('span');
|
|
254
|
+
if (sp)
|
|
255
|
+
sp.textContent = 'Save Token';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export function clearGitHubToken() {
|
|
260
|
+
confirmDialog('Clear GitHub Token?', 'Your GitHub integration will stop working.', async () => {
|
|
261
|
+
try {
|
|
262
|
+
const data = await api('PATCH', '/auth/github-token', { token: '' });
|
|
263
|
+
state.user.has_github_token = false;
|
|
264
|
+
toast('GitHub token cleared', 'success');
|
|
265
|
+
renderSettingsView();
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
toast(err instanceof Error ? err.message : String(err), 'error');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SETTINGS VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
import { confirmDialog } from '../components/modals.js';
|
|
9
|
+
|
|
10
|
+
function avatarInitial(name: string): string {
|
|
11
|
+
return (name.trim()[0] || '?').toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function avatarBg(seed: string): string {
|
|
15
|
+
let hash = 0;
|
|
16
|
+
for (let i = 0; i < seed.length; i++) { hash = (hash * 31 + seed.charCodeAt(i)) >>> 0; }
|
|
17
|
+
const hue = hash % 360;
|
|
18
|
+
return `hsl(${hue},55%,45%)`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function renderSettingsView(): void {
|
|
22
|
+
const el = document.getElementById('content-settings');
|
|
23
|
+
if (!el) return;
|
|
24
|
+
const u = state.user || ({} as any);
|
|
25
|
+
const createdInfo = u.created_at
|
|
26
|
+
? `<span style="display:block;margin-top:4px;font-size:12px;color:var(--text-muted)">Account created ${new Date(u.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>`
|
|
27
|
+
: '';
|
|
28
|
+
el.innerHTML = `
|
|
29
|
+
<div class="page-header">
|
|
30
|
+
<div><div class="page-title">Settings</div><div class="page-subtitle">Manage your profile and account</div></div>
|
|
31
|
+
</div>
|
|
32
|
+
<div style="max-width:560px;display:flex;flex-direction:column;gap:20px">
|
|
33
|
+
<div class="card">
|
|
34
|
+
<div class="card-header"><div class="card-title">Profile</div></div>
|
|
35
|
+
<div class="card-body">
|
|
36
|
+
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
|
37
|
+
<div style="width:64px;height:64px;border-radius:50%;background:${avatarBg(u.email||u.display_name||'?')};display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;flex-shrink:0;user-select:none" aria-hidden="true">${escHtml(avatarInitial(u.display_name||u.email||'?'))}</div>
|
|
38
|
+
<div>
|
|
39
|
+
<div style="font-weight:600;font-size:16px">${escHtml(u.display_name||u.email||'')}</div>
|
|
40
|
+
<div style="font-size:13px;color:var(--text-muted)">${escHtml(u.email||'')}</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<label class="form-label" for="settings-display-name">Display Name</label>
|
|
45
|
+
<input class="form-input" id="settings-display-name" type="text" value="${escHtml(u.display_name||u.email||'')}" placeholder="Your display name" aria-label="Display name">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label class="form-label">Email</label>
|
|
49
|
+
<input class="form-input" type="text" value="${escHtml(u.email||'')}" disabled style="opacity:0.6;cursor:not-allowed" aria-label="Email address (read only)">
|
|
50
|
+
${createdInfo}
|
|
51
|
+
</div>
|
|
52
|
+
<div class="form-error" id="settings-profile-error" style="display:none" role="alert"></div>
|
|
53
|
+
<button class="btn btn-primary btn-sm" id="settings-profile-btn" onclick="window.__app.saveProfile()" aria-label="Save profile changes"><span>Save Profile</span></button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="card">
|
|
57
|
+
<div class="card-header"><div class="card-title">Change Password</div></div>
|
|
58
|
+
<div class="card-body">
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label class="form-label" for="settings-current-pw">Current Password</label>
|
|
61
|
+
<input class="form-input" id="settings-current-pw" type="password" placeholder="Current password" autocomplete="current-password" aria-label="Current password">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label class="form-label" for="settings-new-pw">New Password</label>
|
|
65
|
+
<input class="form-input" id="settings-new-pw" type="password" placeholder="New password" autocomplete="new-password" aria-label="New password">
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-group">
|
|
68
|
+
<label class="form-label" for="settings-confirm-pw">Confirm New Password</label>
|
|
69
|
+
<input class="form-input" id="settings-confirm-pw" type="password" placeholder="Confirm new password" autocomplete="new-password" aria-label="Confirm new password">
|
|
70
|
+
</div>
|
|
71
|
+
<div class="form-error" id="settings-pw-error" style="display:none" role="alert"></div>
|
|
72
|
+
<button class="btn btn-primary btn-sm" id="settings-pw-btn" onclick="window.__app.changePassword()" aria-label="Change password"><span>Change Password</span></button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<div class="card-header">
|
|
77
|
+
<div class="card-title">GitHub Token</div>
|
|
78
|
+
${u.has_github_token ? `<span style="font-size:12px;color:var(--status-closed)">✓ Token configured</span>` : `<span style="font-size:12px;color:var(--text-muted)">No token set</span>`}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="card-body">
|
|
81
|
+
<div style="margin-bottom:12px;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);font-size:12px;color:var(--text-secondary)">
|
|
82
|
+
<strong style="color:var(--text-primary)">How to get a token:</strong>
|
|
83
|
+
<ol style="margin-top:6px;padding-left:18px;line-height:1.8">
|
|
84
|
+
<li>Go to GitHub → Settings → Developer settings → Personal access tokens</li>
|
|
85
|
+
<li>Generate a new token with <code style="font-family:monospace;background:var(--bg-base);padding:0 3px;border-radius:3px">repo</code> scope</li>
|
|
86
|
+
<li>Paste the token below and click Save</li>
|
|
87
|
+
</ol>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label class="form-label" for="settings-github-token">Personal Access Token (PAT)</label>
|
|
91
|
+
<input class="form-input" id="settings-github-token" type="password" placeholder="${u.has_github_token ? 'Leave blank to keep current token' : 'ghp_…'}" autocomplete="off" aria-label="GitHub personal access token">
|
|
92
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">Used for GitHub integration. Needs <code style="font-family:monospace;background:var(--bg-input);padding:0 3px;border-radius:3px">repo</code> scope.</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-error" id="settings-github-error" style="display:none" role="alert"></div>
|
|
95
|
+
<div style="display:flex;gap:8px">
|
|
96
|
+
<button class="btn btn-primary btn-sm" id="settings-github-btn" onclick="window.__app.saveGitHubToken()" aria-label="Save GitHub token"><span>Save Token</span></button>
|
|
97
|
+
${u.has_github_token ? `<button class="btn btn-danger btn-sm" onclick="window.__app.clearGitHubToken()" aria-label="Clear GitHub token">Clear Token</button>` : ''}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function saveProfile(): Promise<void> {
|
|
105
|
+
const displayName = (document.getElementById('settings-display-name') as HTMLInputElement | null)?.value?.trim() || '';
|
|
106
|
+
const errEl = document.getElementById('settings-profile-error') as HTMLElement | null;
|
|
107
|
+
const btn = document.getElementById('settings-profile-btn') as HTMLButtonElement | null;
|
|
108
|
+
if (!displayName) { if (errEl) { errEl.textContent = 'Display name cannot be empty'; errEl.style.display = 'block'; } return; }
|
|
109
|
+
if (errEl) errEl.style.display = 'none';
|
|
110
|
+
if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Saving…'; }
|
|
111
|
+
try {
|
|
112
|
+
const data = await api('PATCH','/auth/profile',{displayName});
|
|
113
|
+
if ((data as any).user) {
|
|
114
|
+
state.user = { ...state.user!, ...(data as any).user };
|
|
115
|
+
} else {
|
|
116
|
+
state.user!.display_name = displayName;
|
|
117
|
+
}
|
|
118
|
+
const u = state.user!;
|
|
119
|
+
const initials = (u.display_name||u.email||'?').split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase();
|
|
120
|
+
const avatarEl = document.getElementById('user-avatar');
|
|
121
|
+
if (avatarEl) avatarEl.textContent = initials;
|
|
122
|
+
const nameEl = document.getElementById('user-name-display');
|
|
123
|
+
if (nameEl) nameEl.textContent = u.display_name||u.email;
|
|
124
|
+
toast('Profile saved','success');
|
|
125
|
+
renderSettingsView();
|
|
126
|
+
} catch(err: unknown) {
|
|
127
|
+
if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
|
|
128
|
+
} finally {
|
|
129
|
+
if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Save Profile'; }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function changePassword(): Promise<void> {
|
|
134
|
+
const currentPassword = (document.getElementById('settings-current-pw') as HTMLInputElement | null)?.value || '';
|
|
135
|
+
const newPassword = (document.getElementById('settings-new-pw') as HTMLInputElement | null)?.value || '';
|
|
136
|
+
const confirmPassword = (document.getElementById('settings-confirm-pw') as HTMLInputElement | null)?.value || '';
|
|
137
|
+
const errEl = document.getElementById('settings-pw-error') as HTMLElement | null;
|
|
138
|
+
const btn = document.getElementById('settings-pw-btn') as HTMLButtonElement | null;
|
|
139
|
+
if (errEl) errEl.style.display = 'none';
|
|
140
|
+
if (!currentPassword || !newPassword || !confirmPassword) { if (errEl) { errEl.textContent = 'All fields are required'; errEl.style.display = 'block'; } return; }
|
|
141
|
+
if (newPassword !== confirmPassword) { if (errEl) { errEl.textContent = 'New passwords do not match'; errEl.style.display = 'block'; } return; }
|
|
142
|
+
if (newPassword.length < 6) { if (errEl) { errEl.textContent = 'New password must be at least 6 characters'; errEl.style.display = 'block'; } return; }
|
|
143
|
+
if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Changing…'; }
|
|
144
|
+
try {
|
|
145
|
+
await api('POST','/auth/change-password',{currentPassword,newPassword});
|
|
146
|
+
toast('Password changed successfully','success');
|
|
147
|
+
const curEl = document.getElementById('settings-current-pw') as HTMLInputElement | null;
|
|
148
|
+
const newEl = document.getElementById('settings-new-pw') as HTMLInputElement | null;
|
|
149
|
+
const confEl = document.getElementById('settings-confirm-pw') as HTMLInputElement | null;
|
|
150
|
+
if (curEl) curEl.value = '';
|
|
151
|
+
if (newEl) newEl.value = '';
|
|
152
|
+
if (confEl) confEl.value = '';
|
|
153
|
+
} catch(err: unknown) {
|
|
154
|
+
if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
|
|
155
|
+
} finally {
|
|
156
|
+
if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Change Password'; }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function saveGitHubToken(): Promise<void> {
|
|
161
|
+
const token = (document.getElementById('settings-github-token') as HTMLInputElement | null)?.value?.trim() || '';
|
|
162
|
+
const errEl = document.getElementById('settings-github-error') as HTMLElement | null;
|
|
163
|
+
const btn = document.getElementById('settings-github-btn') as HTMLButtonElement | null;
|
|
164
|
+
if (!token) { if (errEl) { errEl.textContent = 'Please enter a token'; errEl.style.display = 'block'; } return; }
|
|
165
|
+
if (errEl) errEl.style.display = 'none';
|
|
166
|
+
if (btn) { btn.disabled = true; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Saving…'; }
|
|
167
|
+
try {
|
|
168
|
+
const data = await api('PATCH','/auth/github-token',{token});
|
|
169
|
+
state.user!.has_github_token = (data as any).hasToken !== undefined ? (data as any).hasToken : true;
|
|
170
|
+
toast('GitHub token saved','success');
|
|
171
|
+
renderSettingsView();
|
|
172
|
+
} catch(err: unknown) {
|
|
173
|
+
if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
|
|
174
|
+
} finally {
|
|
175
|
+
if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Save Token'; }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function clearGitHubToken(): void {
|
|
180
|
+
confirmDialog('Clear GitHub Token?', 'Your GitHub integration will stop working.', async () => {
|
|
181
|
+
try {
|
|
182
|
+
const data = await api('PATCH','/auth/github-token',{token:''});
|
|
183
|
+
state.user!.has_github_token = false;
|
|
184
|
+
toast('GitHub token cleared','success');
|
|
185
|
+
renderSettingsView();
|
|
186
|
+
} catch(err: unknown) {
|
|
187
|
+
toast(err instanceof Error ? err.message : String(err),'error');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { api } from '../api.js';
|
|
2
|
+
import { escHtml } from '../utils.js';
|
|
3
|
+
import { skeletonCards } from '../utils.js';
|
|
4
|
+
export async function renderSharedView() {
|
|
5
|
+
const el = document.getElementById('content-shared');
|
|
6
|
+
if (!el)
|
|
7
|
+
return;
|
|
8
|
+
el.innerHTML = `
|
|
9
|
+
<div class="page-header">
|
|
10
|
+
<div><div class="page-title">Shared with Me</div><div class="page-subtitle">Projects others have shared with you</div></div>
|
|
11
|
+
<div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderSharedView()">↺ Refresh</button></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div id="shared-content">${skeletonCards(3)}</div>`;
|
|
14
|
+
try {
|
|
15
|
+
const data = await api('GET', '/shared');
|
|
16
|
+
const projects = data.projects || [];
|
|
17
|
+
const el2 = document.getElementById('shared-content');
|
|
18
|
+
if (!el2)
|
|
19
|
+
return;
|
|
20
|
+
if (projects.length === 0) {
|
|
21
|
+
el2.innerHTML = `
|
|
22
|
+
<div class="empty-state">
|
|
23
|
+
<div class="empty-state-icon">⇄</div>
|
|
24
|
+
<div class="empty-state-text">No shared projects yet</div>
|
|
25
|
+
<div class="empty-state-sub">When someone shares a project with you, it will appear here</div>
|
|
26
|
+
</div>`;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
el2.innerHTML = `
|
|
30
|
+
<div class="projects-grid">
|
|
31
|
+
${projects.map((p) => `
|
|
32
|
+
<div class="project-card" onclick="window.__app.selectProject('${p.id}')">
|
|
33
|
+
<div class="project-card-name">${escHtml(p.name)}</div>
|
|
34
|
+
<div class="project-card-slug mono">${escHtml(p.slug)}</div>
|
|
35
|
+
<div class="project-card-desc">${escHtml(p.description || 'No description')}</div>
|
|
36
|
+
<div class="project-card-meta">
|
|
37
|
+
<span class="share-perm">${escHtml(p.permission || 'view')}</span>
|
|
38
|
+
<span class="project-card-date">by ${escHtml(p.owner_display_name || p.owner_email || 'Unknown')}</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>`).join('')}
|
|
41
|
+
</div>`;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const el2 = document.getElementById('shared-content');
|
|
45
|
+
if (el2)
|
|
46
|
+
el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["shared.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE;QAAE,OAAO;IAChB,EAAE,CAAC,SAAS,GAAG;;;;;+BAKc,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAI,IAAY,CAAC,QAAQ,IAAI,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;QACtD,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,SAAS,GAAG;;;;;eAKP,CAAC;YACV,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,GAAG;;UAEV,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC;2EACwC,CAAC,CAAC,EAAE;6CAClC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;kDACV,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;6CACpB,OAAO,CAAC,CAAC,CAAC,WAAW,IAAI,gBAAgB,CAAC;;yCAE9C,OAAO,CAAC,CAAC,CAAC,UAAU,IAAI,MAAM,CAAC;mDACrB,OAAO,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,WAAW,IAAI,SAAS,CAAC;;iBAE7F,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;aACd,CAAC;IACZ,CAAC;IAAC,OAAM,GAAY,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;QACtD,IAAI,GAAG;YAAE,GAAG,CAAC,SAAS,GAAG,iEAAiE,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC;IACpK,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SHARED WITH ME VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { skeletonCards } from '../utils.js';
|
|
8
|
+
|
|
9
|
+
export async function renderSharedView(): Promise<void> {
|
|
10
|
+
const el = document.getElementById('content-shared');
|
|
11
|
+
if (!el) return;
|
|
12
|
+
el.innerHTML = `
|
|
13
|
+
<div class="page-header">
|
|
14
|
+
<div><div class="page-title">Shared with Me</div><div class="page-subtitle">Projects others have shared with you</div></div>
|
|
15
|
+
<div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderSharedView()">↺ Refresh</button></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="shared-content">${skeletonCards(3)}</div>`;
|
|
18
|
+
try {
|
|
19
|
+
const data = await api('GET', '/shared');
|
|
20
|
+
const projects = (data as any).projects || [];
|
|
21
|
+
const el2 = document.getElementById('shared-content');
|
|
22
|
+
if (!el2) return;
|
|
23
|
+
if (projects.length === 0) {
|
|
24
|
+
el2.innerHTML = `
|
|
25
|
+
<div class="empty-state">
|
|
26
|
+
<div class="empty-state-icon">⇄</div>
|
|
27
|
+
<div class="empty-state-text">No shared projects yet</div>
|
|
28
|
+
<div class="empty-state-sub">When someone shares a project with you, it will appear here</div>
|
|
29
|
+
</div>`;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
el2.innerHTML = `
|
|
33
|
+
<div class="projects-grid">
|
|
34
|
+
${projects.map((p: any) => `
|
|
35
|
+
<div class="project-card" onclick="window.__app.selectProject('${p.id}')">
|
|
36
|
+
<div class="project-card-name">${escHtml(p.name)}</div>
|
|
37
|
+
<div class="project-card-slug mono">${escHtml(p.slug)}</div>
|
|
38
|
+
<div class="project-card-desc">${escHtml(p.description || 'No description')}</div>
|
|
39
|
+
<div class="project-card-meta">
|
|
40
|
+
<span class="share-perm">${escHtml(p.permission || 'view')}</span>
|
|
41
|
+
<span class="project-card-date">by ${escHtml(p.owner_display_name || p.owner_email || 'Unknown')}</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>`).join('')}
|
|
44
|
+
</div>`;
|
|
45
|
+
} catch(err: unknown) {
|
|
46
|
+
const el2 = document.getElementById('shared-content');
|
|
47
|
+
if (el2) el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
48
|
+
}
|
|
49
|
+
}
|