@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.
Files changed (107) hide show
  1. package/dist/api/handlers/about-me.d.ts.map +1 -1
  2. package/dist/api/website-api.d.ts.map +1 -1
  3. package/dist/api.js +2 -2
  4. package/dist/chunks/index-CGvOrVf8.js +213 -0
  5. package/dist/chunks/{index-Bq8WDk9L.js → index-CYd_Pe2U.js} +1321 -473
  6. package/dist/chunks/{template-Boh_MKY5.js → template-D1uGvdWZ.js} +8 -7
  7. package/dist/chunks/{website-api-XoeLwo_N.js → website-api-FLejlWxJ.js} +47 -25
  8. package/dist/index.js +19 -9
  9. package/dist/prerender/data-fetcher.d.ts +19 -0
  10. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  11. package/dist/prerender/page-content.d.ts.map +1 -1
  12. package/dist/prerender/page-generators/about.d.ts +16 -0
  13. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  14. package/dist/prerender/page-generators/base.d.ts +26 -0
  15. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  16. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  17. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  18. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  19. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  20. package/dist/prerender/page-generators/home.d.ts +19 -0
  21. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  22. package/dist/prerender/page-generators/index.d.ts +9 -0
  23. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  24. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  25. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  26. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  27. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  28. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  29. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  30. package/dist/prerender.js +109 -102
  31. package/dist/shared.js +1 -1
  32. package/dist/ui/about-me/index.d.ts +2 -10
  33. package/dist/ui/about-me/index.d.ts.map +1 -1
  34. package/dist/ui/admin/api.d.ts +16 -0
  35. package/dist/ui/admin/api.d.ts.map +1 -0
  36. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  37. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  38. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  39. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  40. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  41. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  42. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  43. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  44. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  45. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  46. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  47. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  48. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  49. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  50. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  51. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  52. package/dist/ui/admin/components/StaticSection.d.ts +13 -0
  53. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  54. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  55. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  56. package/dist/ui/admin/components/index.d.ts +11 -0
  57. package/dist/ui/admin/components/index.d.ts.map +1 -0
  58. package/dist/ui/admin/index.d.ts +10 -26
  59. package/dist/ui/admin/index.d.ts.map +1 -1
  60. package/dist/ui/admin/types.d.ts +24 -0
  61. package/dist/ui/admin/types.d.ts.map +1 -0
  62. package/dist/ui/blog-viewer/index.d.ts.map +1 -1
  63. package/dist/ui/index.d.ts.map +1 -1
  64. package/dist/ui/story-viewer/index.d.ts.map +1 -1
  65. package/dist/ui.js +14 -4
  66. package/package.json +1 -1
  67. package/src/api/handlers/about-me.ts +19 -9
  68. package/src/api/handlers/auth-handler.ts +18 -8
  69. package/src/api/handlers/content.ts +1 -1
  70. package/src/api/handlers/home.ts +2 -2
  71. package/src/api/website-api.ts +11 -7
  72. package/src/prerender/__tests__/page-content.test.ts +1 -11
  73. package/src/prerender/data-fetcher.ts +93 -0
  74. package/src/prerender/page-content.ts +109 -106
  75. package/src/prerender/page-generators/about.ts +38 -0
  76. package/src/prerender/page-generators/base.ts +77 -0
  77. package/src/prerender/page-generators/blog-detail.ts +35 -0
  78. package/src/prerender/page-generators/blogs-list.ts +43 -0
  79. package/src/prerender/page-generators/home.ts +54 -0
  80. package/src/prerender/page-generators/index.ts +8 -0
  81. package/src/prerender/page-generators/not-found.ts +36 -0
  82. package/src/prerender/page-generators/stories-list.ts +43 -0
  83. package/src/prerender/page-generators/story-detail.ts +35 -0
  84. package/src/shared/config/index.ts +1 -1
  85. package/src/shared/page-content.ts +1 -1
  86. package/src/shared/router.ts +5 -5
  87. package/src/ui/about-me/index.ts +14 -57
  88. package/src/ui/admin/api.ts +93 -0
  89. package/src/ui/admin/components/AboutMeSection.ts +47 -0
  90. package/src/ui/admin/components/AdminSection.ts +134 -0
  91. package/src/ui/admin/components/BlogsSection.ts +62 -0
  92. package/src/ui/admin/components/HomeSection.ts +47 -0
  93. package/src/ui/admin/components/ImagesSection.ts +54 -0
  94. package/src/ui/admin/components/LoginForm.ts +116 -0
  95. package/src/ui/admin/components/LogoSection.ts +51 -0
  96. package/src/ui/admin/components/ProfileSection.ts +47 -0
  97. package/src/ui/admin/components/StaticSection.ts +67 -0
  98. package/src/ui/admin/components/StoriesSection.ts +62 -0
  99. package/src/ui/admin/components/index.ts +10 -0
  100. package/src/ui/admin/index.ts +192 -434
  101. package/src/ui/admin/types.ts +26 -0
  102. package/src/ui/blog-viewer/index.ts +4 -1
  103. package/src/ui/index.ts +7 -0
  104. package/src/ui/story-viewer/index.ts +4 -1
  105. package/dist/ui/about-me/renderer.d.ts +0 -6
  106. package/dist/ui/about-me/renderer.d.ts.map +0 -1
  107. package/src/ui/about-me/renderer.ts +0 -23
@@ -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
- interface ContentItem {
7
- key: string;
8
- size: number;
9
- uploaded?: string;
10
- }
11
-
12
- interface StaticDetails {
13
- siteTitle?: string;
14
- copyright?: string;
15
- linkedin?: string;
16
- github?: string;
17
- email?: string;
18
- }
19
-
20
- interface AuthStatus {
21
- configured: boolean;
22
- username: string | null;
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
- get apiUrl() {
54
- return (window as any).__VITE_API_URL__ || import.meta.env.VITE_API_URL || 'http://localhost:8787';
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 res = await fetch(`${this.apiUrl}/auth/status`, {
65
- credentials: 'include'
66
- });
67
- if (res.ok) {
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
- this.isSetup = false;
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
- const res = await fetch(`${this.apiUrl}/content`, {
89
- credentials: 'include'
90
- });
91
- if (res.ok) {
92
- this.contentList = await res.json();
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: Event) {
100
- e.preventDefault();
101
+ async handleLogin(e: CustomEvent) {
102
+ const { username, password } = e.detail;
101
103
  this.loginError = '';
102
104
 
103
- const usernameInput = this.shadowRoot?.querySelector('#username') as HTMLInputElement;
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
- const res = await fetch(`${this.apiUrl}/auth/login`, {
113
- method: 'POST',
114
- credentials: 'include',
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 = 'Connection error';
115
+ this.loginError = (e as Error).message;
131
116
  }
132
117
  }
133
118
 
134
- async handleSetup(e: Event) {
135
- e.preventDefault();
119
+ async handleSetup(e: CustomEvent) {
120
+ const { username, password, confirmPassword } = e.detail;
136
121
  this.loginError = '';
137
122
 
138
- const usernameInput = this.shadowRoot?.querySelector('#username') as HTMLInputElement;
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 (usernameInput.value.length < 3) {
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 (passwordInput.value.length < 8) {
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 (passwordInput.value !== confirmInput?.value) {
138
+ if (password !== confirmPassword) {
158
139
  this.loginError = 'Passwords do not match';
159
140
  return;
160
141
  }
161
142
 
162
143
  try {
163
- const res = await fetch(`${this.apiUrl}/auth/setup`, {
164
- method: 'POST',
165
- credentials: 'include',
166
- headers: { 'Content-Type': 'application/json' },
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 = 'Connection error';
149
+ this.loginError = (e as Error).message;
183
150
  }
184
151
  }
185
152
 
186
153
  async handleLogout() {
187
154
  try {
188
- await fetch(`${this.apiUrl}/auth/logout`, {
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
- const res = await fetch(`${this.apiUrl}/content`, {
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
- const res = await fetch(`${this.apiUrl}/static`, {
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
- const res = await fetch(`${this.apiUrl}/content/${key}`, {
227
- method: 'PUT',
228
- credentials: 'include',
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
- const res = await fetch(`${this.apiUrl}/cache-clear`, {
247
- method: 'POST',
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
- const res = await fetch(`${this.apiUrl}/content/${key}`, {
266
- method: 'DELETE',
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
- getContent(key: string): ContentItem | undefined {
281
- return this.contentList.find(c => c.key === key);
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
- <div class="container">
291
- <div class="login-box">
292
- <h2>Admin Setup</h2>
293
- <p>Create your admin credentials</p>
294
- <form @submit=${this.handleSetup}>
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
- <div class="container">
309
- <div class="login-box">
310
- <h2>Admin Login</h2>
311
- <p>Enter your credentials</p>
312
- <form @submit=${this.handleLogin}>
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
- renderHomeSection() {
324
- const home = this.getContent('home.md');
325
- return html`
326
- <div class="section">
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
- <input type="file" id="homeFile" accept=".md" />
338
- <button class="btn-primary" @click=${() => {
339
- const input = this.shadowRoot?.querySelector('#homeFile') as HTMLInputElement;
340
- if (input.files?.[0]) this.handleUpload('home.md', input.files[0]);
341
- }}>Upload home.md</button>
342
- </div>
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="section">
350
- <h3>Profile <span class="required-badge">Required</span></h3>
351
- <p class="help-text">This file contains your profile information (name, title, experience).</p>
352
-
353
- ${profile ? html`
354
- <div class="current-file">
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
- <input type="file" id="profileFile" accept=".json" />
361
- <button class="btn-primary" @click=${() => {
362
- const input = this.shadowRoot?.querySelector('#profileFile') as HTMLInputElement;
363
- if (input.files?.[0]) this.handleUpload('profile.json', input.files[0]);
364
- }}>Upload profile.json</button>
365
- </div>
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
- renderAboutMeSection() {
370
- const aboutMe = this.getContent('about-me.md');
371
- return html`
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
- <input type="file" id="aboutFile" accept=".md" />
384
- <button class="btn-primary" @click=${() => {
385
- const input = this.shadowRoot?.querySelector('#aboutFile') as HTMLInputElement;
386
- if (input.files?.[0]) this.handleUpload('about-me.md', input.files[0]);
387
- }}>Upload about-me.md</button>
388
- </div>
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
- renderBlogsSection() {
393
- const blogs = this.getSectionFiles('blogs/').filter(b => b.key.endsWith('.json'));
394
- return html`
395
- <div class="section">
396
- <h3>Blog Posts</h3>
397
- <p class="help-text">Each blog needs 2 files: a JSON (metadata) and MD (content) file.</p>
398
-
399
- <h4>Upload New Blog</h4>
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
- renderStoriesSection() {
432
- const stories = this.getSectionFiles('stories/').filter(s => s.key.endsWith('.json'));
433
- return html`
434
- <div class="section">
435
- <h3>Stories</h3>
436
- <p class="help-text">Each story needs 2 files: a JSON (metadata) and MD (content) file.</p>
437
-
438
- <h4>Upload New Story</h4>
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
- renderImagesSection() {
471
- const images = this.getSectionFiles('images/');
472
- return html`
473
- <div class="section">
474
- <h3>Images</h3>
475
- <p class="help-text">Upload images for use in your content. In markdown, reference images by filename only (e.g., use <code>my-photo.jpg</code> not <code>/images/my-photo.jpg</code>). The renderer automatically prepends <code>images/</code>.</p>
476
-
477
- <input type="file" id="imageFile" accept="image/*" />
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
- renderLogoSection() {
502
- const logo = this.getContent('logo.svg');
503
- return html`
504
- <div class="section">
505
- <h3>Site Logo</h3>
506
- <p class="help-text">Upload your site logo (SVG format recommended). This appears in the header of your site.</p>
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
- <input type="file" id="logoFile" accept=".svg,image/svg+xml" />
516
- <button class="btn-primary mt-1 mb-1" @click=${() => {
517
- const input = this.shadowRoot?.querySelector('#logoFile') as HTMLInputElement;
518
- if (input.files?.[0]) this.handleUpload('logo.svg', input.files[0]);
519
- }}>Upload logo.svg</button>
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
- <div class="info-box">
522
- <strong>Tip:</strong> Use an SVG with transparent background. The logo will automatically adapt to light/dark themes.
523
- </div>
524
- </div>
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
- renderStaticSection() {
529
- return html`
530
- <div class="section">
531
- <h3>Site Settings</h3>
532
- <p class="help-text">Manage your site's static details like title, footer links, etc.</p>
533
-
534
- <div class="mb-1">
535
- <label style="display:block;margin-bottom:4px;font-weight:500">Site Title</label>
536
- <input type="text" id="siteTitle" .value=${this.staticDetails?.siteTitle || ''} />
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;