@leadertechie/personal-site-kit 0.1.0-alpha.7 → 0.1.0-alpha.8
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/api/handlers/about-me.d.ts.map +1 -1
- package/dist/api/website-api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/chunks/index-CGvOrVf8.js +213 -0
- package/dist/chunks/{index-Bq8WDk9L.js → index-CYd_Pe2U.js} +1321 -473
- package/dist/chunks/{template-Boh_MKY5.js → template-D1uGvdWZ.js} +8 -7
- package/dist/chunks/{website-api-XoeLwo_N.js → website-api-FLejlWxJ.js} +47 -25
- package/dist/index.js +19 -9
- package/dist/prerender/data-fetcher.d.ts +19 -0
- package/dist/prerender/data-fetcher.d.ts.map +1 -0
- package/dist/prerender/page-content.d.ts.map +1 -1
- package/dist/prerender/page-generators/about.d.ts +16 -0
- package/dist/prerender/page-generators/about.d.ts.map +1 -0
- package/dist/prerender/page-generators/base.d.ts +26 -0
- package/dist/prerender/page-generators/base.d.ts.map +1 -0
- package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
- package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
- package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
- package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/home.d.ts +19 -0
- package/dist/prerender/page-generators/home.d.ts.map +1 -0
- package/dist/prerender/page-generators/index.d.ts +9 -0
- package/dist/prerender/page-generators/index.d.ts.map +1 -0
- package/dist/prerender/page-generators/not-found.d.ts +14 -0
- package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
- package/dist/prerender/page-generators/stories-list.d.ts +17 -0
- package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
- package/dist/prerender/page-generators/story-detail.d.ts +15 -0
- package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
- package/dist/prerender.js +109 -102
- package/dist/shared.js +1 -1
- package/dist/ui/about-me/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -1
- package/dist/ui/admin/api.d.ts +16 -0
- package/dist/ui/admin/api.d.ts.map +1 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
- package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/AdminSection.d.ts +13 -0
- package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
- package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
- package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
- package/dist/ui/admin/components/HomeSection.d.ts +7 -0
- package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
- package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/LoginForm.d.ts +9 -0
- package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
- package/dist/ui/admin/components/LogoSection.d.ts +7 -0
- package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
- package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
- package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StaticSection.d.ts +13 -0
- package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
- package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
- package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
- package/dist/ui/admin/components/index.d.ts +11 -0
- package/dist/ui/admin/components/index.d.ts.map +1 -0
- package/dist/ui/admin/index.d.ts +10 -26
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui/admin/types.d.ts +24 -0
- package/dist/ui/admin/types.d.ts.map +1 -0
- package/dist/ui/blog-viewer/index.d.ts.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/story-viewer/index.d.ts.map +1 -1
- package/dist/ui.js +14 -4
- package/package.json +1 -1
- package/src/api/handlers/about-me.ts +19 -9
- package/src/api/handlers/auth-handler.ts +18 -8
- package/src/api/handlers/content.ts +1 -1
- package/src/api/handlers/home.ts +2 -2
- package/src/api/website-api.ts +11 -7
- package/src/prerender/__tests__/page-content.test.ts +1 -11
- package/src/prerender/data-fetcher.ts +93 -0
- package/src/prerender/page-content.ts +109 -106
- package/src/prerender/page-generators/about.ts +38 -0
- package/src/prerender/page-generators/base.ts +77 -0
- package/src/prerender/page-generators/blog-detail.ts +35 -0
- package/src/prerender/page-generators/blogs-list.ts +43 -0
- package/src/prerender/page-generators/home.ts +54 -0
- package/src/prerender/page-generators/index.ts +8 -0
- package/src/prerender/page-generators/not-found.ts +36 -0
- package/src/prerender/page-generators/stories-list.ts +43 -0
- package/src/prerender/page-generators/story-detail.ts +35 -0
- package/src/shared/config/index.ts +1 -1
- package/src/shared/page-content.ts +1 -1
- package/src/shared/router.ts +5 -5
- package/src/ui/about-me/index.ts +14 -57
- package/src/ui/admin/api.ts +93 -0
- package/src/ui/admin/components/AboutMeSection.ts +47 -0
- package/src/ui/admin/components/AdminSection.ts +134 -0
- package/src/ui/admin/components/BlogsSection.ts +62 -0
- package/src/ui/admin/components/HomeSection.ts +47 -0
- package/src/ui/admin/components/ImagesSection.ts +54 -0
- package/src/ui/admin/components/LoginForm.ts +116 -0
- package/src/ui/admin/components/LogoSection.ts +51 -0
- package/src/ui/admin/components/ProfileSection.ts +47 -0
- package/src/ui/admin/components/StaticSection.ts +67 -0
- package/src/ui/admin/components/StoriesSection.ts +62 -0
- package/src/ui/admin/components/index.ts +10 -0
- package/src/ui/admin/index.ts +192 -434
- package/src/ui/admin/types.ts +26 -0
- package/src/ui/blog-viewer/index.ts +4 -1
- package/src/ui/index.ts +7 -0
- package/src/ui/story-viewer/index.ts +4 -1
- package/dist/ui/about-me/renderer.d.ts +0 -6
- package/dist/ui/about-me/renderer.d.ts.map +0 -1
- package/src/ui/about-me/renderer.ts +0 -23
package/src/ui/admin/index.ts
CHANGED
|
@@ -2,25 +2,32 @@ import { LitElement, html, css } from 'lit';
|
|
|
2
2
|
import { customElement, state } from 'lit/decorators.js';
|
|
3
3
|
|
|
4
4
|
import { adminStyles } from './styles';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
import { AdminApiService } from './api';
|
|
6
|
+
import type { ContentItem, StaticDetails } from './types';
|
|
7
|
+
import { SiteStore } from '../../shared/core/site-store';
|
|
8
|
+
import {
|
|
9
|
+
AdminLoginForm,
|
|
10
|
+
AdminHomeSection,
|
|
11
|
+
AdminProfileSection,
|
|
12
|
+
AdminAboutMeSection,
|
|
13
|
+
AdminBlogsSection,
|
|
14
|
+
AdminStoriesSection,
|
|
15
|
+
AdminImagesSection,
|
|
16
|
+
AdminLogoSection,
|
|
17
|
+
AdminStaticSection
|
|
18
|
+
} from './components';
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
AdminLoginForm,
|
|
22
|
+
AdminHomeSection,
|
|
23
|
+
AdminProfileSection,
|
|
24
|
+
AdminAboutMeSection,
|
|
25
|
+
AdminBlogsSection,
|
|
26
|
+
AdminStoriesSection,
|
|
27
|
+
AdminImagesSection,
|
|
28
|
+
AdminLogoSection,
|
|
29
|
+
AdminStaticSection
|
|
30
|
+
};
|
|
24
31
|
|
|
25
32
|
@customElement('admin-portal')
|
|
26
33
|
export class AdminPortal extends LitElement {
|
|
@@ -50,145 +57,102 @@ export class AdminPortal extends LitElement {
|
|
|
50
57
|
@state()
|
|
51
58
|
accessor loginError = '';
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
private apiService: AdminApiService;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
const apiUrl = window.location.origin;
|
|
65
|
+
this.apiService = new AdminApiService(apiUrl);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private hasSessionCookie(): boolean {
|
|
69
|
+
return document.cookie.includes('session=');
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
async connectedCallback() {
|
|
58
73
|
super.connectedCallback();
|
|
59
74
|
await this.checkAuthStatus();
|
|
75
|
+
this.requestUpdate();
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
async checkAuthStatus() {
|
|
63
79
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const status: AuthStatus = await res.json();
|
|
69
|
-
this.isSetup = status.configured;
|
|
70
|
-
|
|
71
|
-
if (status.configured) {
|
|
72
|
-
await this.tryAutoLogin();
|
|
73
|
-
} else {
|
|
74
|
-
this.isLoading = false;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
this.isSetup = false;
|
|
78
|
-
this.isLoading = false;
|
|
80
|
+
const status = await this.apiService.checkAuthStatus();
|
|
81
|
+
this.isSetup = status.configured;
|
|
82
|
+
if (status.configured && this.hasSessionCookie()) {
|
|
83
|
+
await this.tryAutoLogin();
|
|
79
84
|
}
|
|
80
85
|
} catch (e) {
|
|
81
|
-
|
|
86
|
+
console.error('Auth status check failed:', e);
|
|
87
|
+
} finally {
|
|
82
88
|
this.isLoading = false;
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
async tryAutoLogin() {
|
|
87
93
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.isAuthenticated = true;
|
|
94
|
-
}
|
|
95
|
-
} catch (e) {}
|
|
96
|
-
this.isLoading = false;
|
|
94
|
+
this.contentList = await this.apiService.tryAutoLogin();
|
|
95
|
+
this.isAuthenticated = true;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error('Auto login failed:', e);
|
|
98
|
+
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
async handleLogin(e:
|
|
100
|
-
e.
|
|
101
|
+
async handleLogin(e: CustomEvent) {
|
|
102
|
+
const { username, password } = e.detail;
|
|
101
103
|
this.loginError = '';
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
const passwordInput = this.shadowRoot?.querySelector('#password') as HTMLInputElement;
|
|
105
|
-
|
|
106
|
-
if (!usernameInput?.value || !passwordInput?.value) {
|
|
105
|
+
if (!username || !password) {
|
|
107
106
|
this.loginError = 'Username and password required';
|
|
108
107
|
return;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
headers: { 'Content-Type': 'application/json' },
|
|
116
|
-
body: JSON.stringify({
|
|
117
|
-
username: usernameInput.value,
|
|
118
|
-
password: passwordInput.value
|
|
119
|
-
})
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (res.ok) {
|
|
123
|
-
this.isAuthenticated = true;
|
|
124
|
-
await this.fetchContent();
|
|
125
|
-
} else {
|
|
126
|
-
const data = await res.json();
|
|
127
|
-
this.loginError = data.error || 'Login failed';
|
|
128
|
-
}
|
|
111
|
+
await this.apiService.login(username, password);
|
|
112
|
+
this.isAuthenticated = true;
|
|
113
|
+
await this.fetchContent();
|
|
129
114
|
} catch (e) {
|
|
130
|
-
this.loginError =
|
|
115
|
+
this.loginError = (e as Error).message;
|
|
131
116
|
}
|
|
132
117
|
}
|
|
133
118
|
|
|
134
|
-
async handleSetup(e:
|
|
135
|
-
e.
|
|
119
|
+
async handleSetup(e: CustomEvent) {
|
|
120
|
+
const { username, password, confirmPassword } = e.detail;
|
|
136
121
|
this.loginError = '';
|
|
137
122
|
|
|
138
|
-
|
|
139
|
-
const passwordInput = this.shadowRoot?.querySelector('#password') as HTMLInputElement;
|
|
140
|
-
const confirmInput = this.shadowRoot?.querySelector('#confirmPassword') as HTMLInputElement;
|
|
141
|
-
|
|
142
|
-
if (!usernameInput?.value || !passwordInput?.value) {
|
|
123
|
+
if (!username || !password) {
|
|
143
124
|
this.loginError = 'Username and password required';
|
|
144
125
|
return;
|
|
145
126
|
}
|
|
146
127
|
|
|
147
|
-
if (
|
|
128
|
+
if (username.length < 3) {
|
|
148
129
|
this.loginError = 'Username must be at least 3 characters';
|
|
149
130
|
return;
|
|
150
131
|
}
|
|
151
132
|
|
|
152
|
-
if (
|
|
133
|
+
if (password.length < 8) {
|
|
153
134
|
this.loginError = 'Password must be at least 8 characters';
|
|
154
135
|
return;
|
|
155
136
|
}
|
|
156
137
|
|
|
157
|
-
if (
|
|
138
|
+
if (password !== confirmPassword) {
|
|
158
139
|
this.loginError = 'Passwords do not match';
|
|
159
140
|
return;
|
|
160
141
|
}
|
|
161
142
|
|
|
162
143
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
body: JSON.stringify({
|
|
168
|
-
username: usernameInput.value,
|
|
169
|
-
password: passwordInput.value
|
|
170
|
-
})
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (res.ok) {
|
|
174
|
-
this.isAuthenticated = true;
|
|
175
|
-
this.isSetup = true;
|
|
176
|
-
await this.fetchContent();
|
|
177
|
-
} else {
|
|
178
|
-
const data = await res.json();
|
|
179
|
-
this.loginError = data.error || 'Setup failed';
|
|
180
|
-
}
|
|
144
|
+
await this.apiService.setup(username, password);
|
|
145
|
+
this.isAuthenticated = true;
|
|
146
|
+
this.isSetup = true;
|
|
147
|
+
await this.fetchContent();
|
|
181
148
|
} catch (e) {
|
|
182
|
-
this.loginError =
|
|
149
|
+
this.loginError = (e as Error).message;
|
|
183
150
|
}
|
|
184
151
|
}
|
|
185
152
|
|
|
186
153
|
async handleLogout() {
|
|
187
154
|
try {
|
|
188
|
-
await
|
|
189
|
-
method: 'POST',
|
|
190
|
-
credentials: 'include'
|
|
191
|
-
});
|
|
155
|
+
await this.apiService.logout();
|
|
192
156
|
} catch (e) {}
|
|
193
157
|
this.isAuthenticated = false;
|
|
194
158
|
this.contentList = [];
|
|
@@ -196,14 +160,7 @@ export class AdminPortal extends LitElement {
|
|
|
196
160
|
|
|
197
161
|
async fetchContent() {
|
|
198
162
|
try {
|
|
199
|
-
|
|
200
|
-
credentials: 'include'
|
|
201
|
-
});
|
|
202
|
-
if (res.ok) {
|
|
203
|
-
this.contentList = await res.json();
|
|
204
|
-
} else {
|
|
205
|
-
this.statusMessage = 'Failed to fetch content.';
|
|
206
|
-
}
|
|
163
|
+
this.contentList = await this.apiService.fetchContent();
|
|
207
164
|
} catch (e) {
|
|
208
165
|
this.statusMessage = 'Error fetching content.';
|
|
209
166
|
}
|
|
@@ -211,30 +168,16 @@ export class AdminPortal extends LitElement {
|
|
|
211
168
|
|
|
212
169
|
async fetchStaticDetails() {
|
|
213
170
|
try {
|
|
214
|
-
|
|
215
|
-
credentials: 'include'
|
|
216
|
-
});
|
|
217
|
-
if (res.ok) {
|
|
218
|
-
this.staticDetails = await res.json();
|
|
219
|
-
}
|
|
171
|
+
this.staticDetails = await this.apiService.fetchStaticDetails();
|
|
220
172
|
} catch (e) {}
|
|
221
173
|
}
|
|
222
174
|
|
|
223
175
|
async handleUpload(key: string, file: File) {
|
|
224
176
|
try {
|
|
225
177
|
this.statusMessage = 'Uploading...';
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
body: file
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
if (res.ok) {
|
|
233
|
-
this.statusMessage = 'Upload successful!';
|
|
234
|
-
this.fetchContent();
|
|
235
|
-
} else {
|
|
236
|
-
this.statusMessage = 'Upload failed.';
|
|
237
|
-
}
|
|
178
|
+
await this.apiService.uploadContent(key, file);
|
|
179
|
+
this.statusMessage = 'Upload successful!';
|
|
180
|
+
await this.fetchContent();
|
|
238
181
|
} catch (e) {
|
|
239
182
|
this.statusMessage = 'Error uploading.';
|
|
240
183
|
}
|
|
@@ -243,351 +186,164 @@ export class AdminPortal extends LitElement {
|
|
|
243
186
|
async handleClearCache() {
|
|
244
187
|
try {
|
|
245
188
|
this.statusMessage = 'Clearing cache...';
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
credentials: 'include'
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
if (res.ok) {
|
|
252
|
-
this.statusMessage = 'Cache cleared!';
|
|
253
|
-
} else {
|
|
254
|
-
this.statusMessage = 'Failed to clear cache.';
|
|
255
|
-
}
|
|
189
|
+
await this.apiService.clearCache();
|
|
190
|
+
this.statusMessage = 'Cache cleared!';
|
|
256
191
|
} catch (e) {
|
|
257
192
|
this.statusMessage = 'Error clearing cache.';
|
|
258
193
|
}
|
|
259
194
|
}
|
|
260
195
|
|
|
261
196
|
async handleDelete(key: string) {
|
|
262
|
-
if (!confirm(`Delete ${key}?`)) return;
|
|
263
|
-
|
|
264
197
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
credentials: 'include'
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
if (res.ok) {
|
|
271
|
-
this.fetchContent();
|
|
272
|
-
} else {
|
|
273
|
-
this.statusMessage = 'Delete failed.';
|
|
274
|
-
}
|
|
198
|
+
await this.apiService.deleteContent(key);
|
|
199
|
+
await this.fetchContent();
|
|
275
200
|
} catch (e) {
|
|
276
201
|
this.statusMessage = 'Error deleting.';
|
|
277
202
|
}
|
|
278
203
|
}
|
|
279
204
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
getSectionFiles(prefix: string): ContentItem[] {
|
|
285
|
-
return this.contentList.filter(c => c.key.startsWith(prefix));
|
|
205
|
+
private handleStatusMessage(message: string) {
|
|
206
|
+
this.statusMessage = message;
|
|
286
207
|
}
|
|
287
208
|
|
|
288
209
|
renderLoginForm() {
|
|
289
210
|
return html`
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<input type="text" id="username" placeholder="Username (3+ chars)" />
|
|
296
|
-
<input type="password" id="password" placeholder="Password (8+ chars)" />
|
|
297
|
-
<input type="password" id="confirmPassword" placeholder="Confirm Password" />
|
|
298
|
-
${this.loginError ? html`<div class="error-message">${this.loginError}</div>` : ''}
|
|
299
|
-
<button type="submit" class="btn-primary">Create Account</button>
|
|
300
|
-
</form>
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
211
|
+
<admin-login-form
|
|
212
|
+
.errorMessage=${this.loginError}
|
|
213
|
+
.isSetup=${false}
|
|
214
|
+
@login-submit=${this.handleSetup}
|
|
215
|
+
></admin-login-form>
|
|
303
216
|
`;
|
|
304
217
|
}
|
|
305
218
|
|
|
306
219
|
renderLogin() {
|
|
307
220
|
return html`
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<input type="text" id="username" placeholder="Username" autocomplete="username" />
|
|
314
|
-
<input type="password" id="password" placeholder="Password" autocomplete="current-password" />
|
|
315
|
-
${this.loginError ? html`<div class="error-message">${this.loginError}</div>` : ''}
|
|
316
|
-
<button type="submit" class="btn-primary">Login</button>
|
|
317
|
-
</form>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
221
|
+
<admin-login-form
|
|
222
|
+
.errorMessage=${this.loginError}
|
|
223
|
+
.isSetup=${true}
|
|
224
|
+
@login-submit=${this.handleLogin}
|
|
225
|
+
></admin-login-form>
|
|
320
226
|
`;
|
|
321
227
|
}
|
|
322
228
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<h3>Home Page</h3>
|
|
328
|
-
<p class="help-text">Content for your home page. Upload home.md with your main content.</p>
|
|
329
|
-
|
|
330
|
-
${home ? html`
|
|
331
|
-
<div class="current-file">
|
|
332
|
-
<strong>Current:</strong> home.md (${home.size} bytes)
|
|
333
|
-
<button class="btn-danger" @click=${() => this.handleDelete('home.md')}>Delete</button>
|
|
334
|
-
</div>
|
|
335
|
-
` : ''}
|
|
229
|
+
render() {
|
|
230
|
+
if (this.isLoading) {
|
|
231
|
+
return html`<div class="container"><div class="loading">Loading...</div></div>`;
|
|
232
|
+
}
|
|
336
233
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
234
|
+
if (!this.isSetup) {
|
|
235
|
+
return this.renderLoginForm();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!this.isAuthenticated) {
|
|
239
|
+
return this.renderLogin();
|
|
240
|
+
}
|
|
345
241
|
|
|
346
|
-
renderProfileSection() {
|
|
347
|
-
const profile = this.getContent('profile.json');
|
|
348
242
|
return html`
|
|
349
|
-
<div class="
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<strong>Current:</strong> profile.json (${profile.size} bytes)
|
|
356
|
-
<button class="btn-danger" @click=${() => this.handleDelete('profile.json')}>Delete</button>
|
|
357
|
-
</div>
|
|
358
|
-
` : ''}
|
|
243
|
+
<div class="container">
|
|
244
|
+
<div class="header">
|
|
245
|
+
<h1>Content Manager</h1>
|
|
246
|
+
<button class="btn-secondary" @click=${() => this.handleLogout()}>Logout</button>
|
|
247
|
+
<button class="btn-secondary" @click=${() => this.handleClearCache()}>Clear Cache</button>
|
|
248
|
+
</div>
|
|
359
249
|
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
250
|
+
<div class="nav-tabs">
|
|
251
|
+
<button class="nav-tab ${this.activeSection === 'home' ? 'active' : ''}"
|
|
252
|
+
@click=${() => this.activeSection = 'home'}>Home</button>
|
|
253
|
+
<button class="nav-tab ${this.activeSection === 'profile' ? 'active' : ''}"
|
|
254
|
+
@click=${() => this.activeSection = 'profile'}>Profile</button>
|
|
255
|
+
<button class="nav-tab ${this.activeSection === 'aboutme' ? 'active' : ''}"
|
|
256
|
+
@click=${() => this.activeSection = 'aboutme'}>About Me</button>
|
|
257
|
+
<button class="nav-tab ${this.activeSection === 'blogs' ? 'active' : ''}"
|
|
258
|
+
@click=${() => this.activeSection = 'blogs'}>Blogs</button>
|
|
259
|
+
<button class="nav-tab ${this.activeSection === 'stories' ? 'active' : ''}"
|
|
260
|
+
@click=${() => this.activeSection = 'stories'}>Stories</button>
|
|
261
|
+
<button class="nav-tab ${this.activeSection === 'images' ? 'active' : ''}"
|
|
262
|
+
@click=${() => this.activeSection = 'images'}>Images</button>
|
|
263
|
+
<button class="nav-tab ${this.activeSection === 'logo' ? 'active' : ''}"
|
|
264
|
+
@click=${() => this.activeSection = 'logo'}>Logo</button>
|
|
265
|
+
<button class="nav-tab ${this.activeSection === 'static' ? 'active' : ''}"
|
|
266
|
+
@click=${() => { this.activeSection = 'static'; this.fetchStaticDetails(); }}>Site Settings</button>
|
|
267
|
+
</div>
|
|
368
268
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
<div class="section">
|
|
373
|
-
<h3>About Me Page <span class="required-badge">Required</span></h3>
|
|
374
|
-
<p class="help-text">Content for your About Me page. Supports Markdown with frontmatter.</p>
|
|
375
|
-
|
|
376
|
-
${aboutMe ? html`
|
|
377
|
-
<div class="current-file">
|
|
378
|
-
<strong>Current:</strong> about-me.md (${aboutMe.size} bytes)
|
|
379
|
-
<button class="btn-danger" @click=${() => this.handleDelete('about-me.md')}>Delete</button>
|
|
269
|
+
${this.statusMessage ? html`
|
|
270
|
+
<div class="status-message ${this.statusMessage.includes('successful') || this.statusMessage.includes('cleared') ? 'success' : this.statusMessage.includes('failed') || this.statusMessage.includes('Error') ? 'error' : ''}">
|
|
271
|
+
${this.statusMessage}
|
|
380
272
|
</div>
|
|
381
273
|
` : ''}
|
|
382
274
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
275
|
+
${this.activeSection === 'home' ? html`
|
|
276
|
+
<admin-home-section
|
|
277
|
+
.contentList=${this.contentList}
|
|
278
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
279
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
280
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
281
|
+
></admin-home-section>
|
|
282
|
+
` : ''}
|
|
391
283
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<input type="file" id="blogMetaFile" accept=".json" />
|
|
401
|
-
<input type="file" id="blogContentFile" accept=".md" />
|
|
402
|
-
<input type="text" id="blogSlug" placeholder="Slug (e.g., my-new-post)" class="mt-1" />
|
|
403
|
-
<button class="btn-primary" @click=${() => {
|
|
404
|
-
const metaInput = this.shadowRoot?.querySelector('#blogMetaFile') as HTMLInputElement;
|
|
405
|
-
const contentInput = this.shadowRoot?.querySelector('#blogContentFile') as HTMLInputElement;
|
|
406
|
-
const slugInput = this.shadowRoot?.querySelector('#blogSlug') as HTMLInputElement;
|
|
407
|
-
if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
|
|
408
|
-
this.handleUpload(`blogs/${slugInput.value}.json`, metaInput.files[0]);
|
|
409
|
-
this.handleUpload(`blogs/${slugInput.value}.md`, contentInput.files[0]);
|
|
410
|
-
}
|
|
411
|
-
}}>Upload Blog (JSON + MD)</button>
|
|
412
|
-
|
|
413
|
-
<div class="file-list">
|
|
414
|
-
<h4>Current Blogs (${blogs.length})</h4>
|
|
415
|
-
${blogs.map(b => html`
|
|
416
|
-
<div class="file-item">
|
|
417
|
-
<span>${b.key.replace('.json', '')}</span>
|
|
418
|
-
<button class="btn-danger" @click=${() => {
|
|
419
|
-
const slug = b.key.replace('blogs/', '').replace('.json', '');
|
|
420
|
-
this.handleDelete(`blogs/${slug}.json`);
|
|
421
|
-
this.handleDelete(`blogs/${slug}.md`);
|
|
422
|
-
}}>Delete</button>
|
|
423
|
-
</div>
|
|
424
|
-
`)}
|
|
425
|
-
${blogs.length === 0 ? html`<p>No blogs yet.</p>` : ''}
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
`;
|
|
429
|
-
}
|
|
284
|
+
${this.activeSection === 'profile' ? html`
|
|
285
|
+
<admin-profile-section
|
|
286
|
+
.contentList=${this.contentList}
|
|
287
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
288
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
289
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
290
|
+
></admin-profile-section>
|
|
291
|
+
` : ''}
|
|
430
292
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
<input type="file" id="storyMetaFile" accept=".json" />
|
|
440
|
-
<input type="file" id="storyContentFile" accept=".md" />
|
|
441
|
-
<input type="text" id="storySlug" placeholder="Slug (e.g., my-story)" class="mt-1" />
|
|
442
|
-
<button class="btn-primary" @click=${() => {
|
|
443
|
-
const metaInput = this.shadowRoot?.querySelector('#storyMetaFile') as HTMLInputElement;
|
|
444
|
-
const contentInput = this.shadowRoot?.querySelector('#storyContentFile') as HTMLInputElement;
|
|
445
|
-
const slugInput = this.shadowRoot?.querySelector('#storySlug') as HTMLInputElement;
|
|
446
|
-
if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
|
|
447
|
-
this.handleUpload(`stories/${slugInput.value}.json`, metaInput.files[0]);
|
|
448
|
-
this.handleUpload(`stories/${slugInput.value}.md`, contentInput.files[0]);
|
|
449
|
-
}
|
|
450
|
-
}}>Upload Story (JSON + MD)</button>
|
|
451
|
-
|
|
452
|
-
<div class="file-list">
|
|
453
|
-
<h4>Current Stories (${stories.length})</h4>
|
|
454
|
-
${stories.map(s => html`
|
|
455
|
-
<div class="file-item">
|
|
456
|
-
<span>${s.key.replace('.json', '')}</span>
|
|
457
|
-
<button class="btn-danger" @click=${() => {
|
|
458
|
-
const slug = s.key.replace('stories/', '').replace('.json', '');
|
|
459
|
-
this.handleDelete(`stories/${slug}.json`);
|
|
460
|
-
this.handleDelete(`stories/${slug}.md`);
|
|
461
|
-
}}>Delete</button>
|
|
462
|
-
</div>
|
|
463
|
-
`)}
|
|
464
|
-
${stories.length === 0 ? html`<p>No stories yet.</p>` : ''}
|
|
465
|
-
</div>
|
|
466
|
-
</div>
|
|
467
|
-
`;
|
|
468
|
-
}
|
|
293
|
+
${this.activeSection === 'aboutme' ? html`
|
|
294
|
+
<admin-about-me-section
|
|
295
|
+
.contentList=${this.contentList}
|
|
296
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
297
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
298
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
299
|
+
></admin-about-me-section>
|
|
300
|
+
` : ''}
|
|
469
301
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
<input type="text" id="imagePath" placeholder="Image name (e.g., profile-photo.jpg)" class="mt-1" />
|
|
479
|
-
<button class="btn-primary" @click=${() => {
|
|
480
|
-
const fileInput = this.shadowRoot?.querySelector('#imageFile') as HTMLInputElement;
|
|
481
|
-
const pathInput = this.shadowRoot?.querySelector('#imagePath') as HTMLInputElement;
|
|
482
|
-
if (fileInput.files?.[0] && pathInput.value) {
|
|
483
|
-
this.handleUpload(`images/${pathInput.value}`, fileInput.files[0]);
|
|
484
|
-
}
|
|
485
|
-
}}>Upload to images/</button>
|
|
486
|
-
|
|
487
|
-
<div class="file-list">
|
|
488
|
-
<h4>Current Images (${images.length})</h4>
|
|
489
|
-
${images.map(img => html`
|
|
490
|
-
<div class="file-item">
|
|
491
|
-
<span>${img.key} (${img.size} bytes)</span>
|
|
492
|
-
<button class="btn-danger" @click=${() => this.handleDelete(img.key)}>Delete</button>
|
|
493
|
-
</div>
|
|
494
|
-
`)}
|
|
495
|
-
${images.length === 0 ? html`<p>No images yet.</p>` : ''}
|
|
496
|
-
</div>
|
|
497
|
-
</div>
|
|
498
|
-
`;
|
|
499
|
-
}
|
|
302
|
+
${this.activeSection === 'blogs' ? html`
|
|
303
|
+
<admin-blogs-section
|
|
304
|
+
.contentList=${this.contentList}
|
|
305
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
306
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
307
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
308
|
+
></admin-blogs-section>
|
|
309
|
+
` : ''}
|
|
500
310
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
${logo ? html`
|
|
509
|
-
<div class="current-file">
|
|
510
|
-
<strong>Current:</strong> logo.svg (${logo.size} bytes)
|
|
511
|
-
<button class="btn-danger" @click=${() => this.handleDelete('logo.svg')}>Delete</button>
|
|
512
|
-
</div>
|
|
311
|
+
${this.activeSection === 'stories' ? html`
|
|
312
|
+
<admin-stories-section
|
|
313
|
+
.contentList=${this.contentList}
|
|
314
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
315
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
316
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
317
|
+
></admin-stories-section>
|
|
513
318
|
` : ''}
|
|
514
319
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
320
|
+
${this.activeSection === 'images' ? html`
|
|
321
|
+
<admin-images-section
|
|
322
|
+
.contentList=${this.contentList}
|
|
323
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
324
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
325
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
326
|
+
></admin-images-section>
|
|
327
|
+
` : ''}
|
|
520
328
|
|
|
521
|
-
|
|
522
|
-
<
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
329
|
+
${this.activeSection === 'logo' ? html`
|
|
330
|
+
<admin-logo-section
|
|
331
|
+
.contentList=${this.contentList}
|
|
332
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
333
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
334
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
335
|
+
></admin-logo-section>
|
|
336
|
+
` : ''}
|
|
527
337
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
</div>
|
|
538
|
-
|
|
539
|
-
<div class="mb-1">
|
|
540
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">Copyright Text</label>
|
|
541
|
-
<input type="text" id="copyright" .value=${this.staticDetails?.copyright || ''} />
|
|
542
|
-
</div>
|
|
543
|
-
|
|
544
|
-
<div class="mb-1">
|
|
545
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">LinkedIn URL</label>
|
|
546
|
-
<input type="text" id="linkedin" .value=${this.staticDetails?.linkedin || ''} />
|
|
547
|
-
</div>
|
|
548
|
-
|
|
549
|
-
<div class="mb-1">
|
|
550
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">GitHub URL</label>
|
|
551
|
-
<input type="text" id="github" .value=${this.staticDetails?.github || ''} />
|
|
552
|
-
</div>
|
|
553
|
-
|
|
554
|
-
<div class="mb-1">
|
|
555
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">Email</label>
|
|
556
|
-
<input type="text" id="email" .value=${this.staticDetails?.email || ''} />
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
|
-
<button class="btn-primary" @click=${async () => {
|
|
560
|
-
const siteTitle = (this.shadowRoot?.querySelector('#siteTitle') as HTMLInputElement)?.value;
|
|
561
|
-
const copyright = (this.shadowRoot?.querySelector('#copyright') as HTMLInputElement)?.value;
|
|
562
|
-
const linkedin = (this.shadowRoot?.querySelector('#linkedin') as HTMLInputElement)?.value;
|
|
563
|
-
const github = (this.shadowRoot?.querySelector('#github') as HTMLInputElement)?.value;
|
|
564
|
-
const email = (this.shadowRoot?.querySelector('#email') as HTMLInputElement)?.value;
|
|
565
|
-
|
|
566
|
-
const data: Record<string, string> = {};
|
|
567
|
-
if (siteTitle) data.siteTitle = siteTitle;
|
|
568
|
-
if (copyright) data.copyright = copyright;
|
|
569
|
-
if (linkedin) data.linkedin = linkedin;
|
|
570
|
-
if (github) data.github = github;
|
|
571
|
-
if (email) data.email = email;
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
const url = `${this.apiUrl}/content/staticdetails.json`;
|
|
575
|
-
const res = await fetch(url, {
|
|
576
|
-
method: 'PUT',
|
|
577
|
-
credentials: 'include',
|
|
578
|
-
headers: { 'Content-Type': 'application/json' },
|
|
579
|
-
body: JSON.stringify(data)
|
|
580
|
-
});
|
|
581
|
-
if (res.ok) {
|
|
582
|
-
this.statusMessage = 'Settings saved!';
|
|
583
|
-
this.fetchContent();
|
|
584
|
-
} else {
|
|
585
|
-
this.statusMessage = 'Failed to save settings.';
|
|
586
|
-
}
|
|
587
|
-
} catch (e) {
|
|
588
|
-
this.statusMessage = 'Error saving settings.';
|
|
589
|
-
}
|
|
590
|
-
}}>Save Settings</button>
|
|
338
|
+
${this.activeSection === 'static' ? html`
|
|
339
|
+
<admin-static-section
|
|
340
|
+
.contentList=${this.contentList}
|
|
341
|
+
.staticDetails=${this.staticDetails}
|
|
342
|
+
.onUpload=${this.handleUpload.bind(this)}
|
|
343
|
+
.onDelete=${this.handleDelete.bind(this)}
|
|
344
|
+
.onStatusMessage=${this.handleStatusMessage.bind(this)}
|
|
345
|
+
></admin-static-section>
|
|
346
|
+
` : ''}
|
|
591
347
|
</div>
|
|
592
348
|
`;
|
|
593
349
|
}
|
|
@@ -653,3 +409,5 @@ export class AdminPortal extends LitElement {
|
|
|
653
409
|
`;
|
|
654
410
|
}
|
|
655
411
|
}
|
|
412
|
+
|
|
413
|
+
export const adminLoaded = true;
|