@leadertechie/personal-site-kit 0.1.0-alpha.6 → 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 (109) hide show
  1. package/dist/api/handlers/about-me.d.ts.map +1 -1
  2. package/dist/api/handlers/auth-handler.d.ts.map +1 -1
  3. package/dist/api/website-api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/chunks/index-CGvOrVf8.js +213 -0
  6. package/dist/chunks/{index-C3wLSCKU.js → index-CYd_Pe2U.js} +1353 -482
  7. package/dist/chunks/{template-MawmknFQ.js → template-D1uGvdWZ.js} +10 -8
  8. package/dist/chunks/{website-api-DI3muo2s.js → website-api-FLejlWxJ.js} +78 -41
  9. package/dist/index.js +19 -9
  10. package/dist/prerender/data-fetcher.d.ts +19 -0
  11. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  12. package/dist/prerender/page-content.d.ts.map +1 -1
  13. package/dist/prerender/page-generators/about.d.ts +16 -0
  14. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  15. package/dist/prerender/page-generators/base.d.ts +26 -0
  16. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  17. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  18. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  19. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  20. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  21. package/dist/prerender/page-generators/home.d.ts +19 -0
  22. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  23. package/dist/prerender/page-generators/index.d.ts +9 -0
  24. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  25. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  26. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  27. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  28. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  29. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  30. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  31. package/dist/prerender.js +109 -102
  32. package/dist/shared/config/index.d.ts.map +1 -1
  33. package/dist/shared.js +1 -1
  34. package/dist/ui/about-me/index.d.ts +2 -10
  35. package/dist/ui/about-me/index.d.ts.map +1 -1
  36. package/dist/ui/admin/api.d.ts +16 -0
  37. package/dist/ui/admin/api.d.ts.map +1 -0
  38. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  39. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  40. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  41. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  42. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  43. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  44. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  45. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  46. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  47. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  48. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  49. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  50. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  51. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  52. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  53. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  54. package/dist/ui/admin/components/StaticSection.d.ts +13 -0
  55. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  56. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  57. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  58. package/dist/ui/admin/components/index.d.ts +11 -0
  59. package/dist/ui/admin/components/index.d.ts.map +1 -0
  60. package/dist/ui/admin/index.d.ts +10 -26
  61. package/dist/ui/admin/index.d.ts.map +1 -1
  62. package/dist/ui/admin/types.d.ts +24 -0
  63. package/dist/ui/admin/types.d.ts.map +1 -0
  64. package/dist/ui/blog-viewer/index.d.ts.map +1 -1
  65. package/dist/ui/index.d.ts.map +1 -1
  66. package/dist/ui/story-viewer/index.d.ts.map +1 -1
  67. package/dist/ui.js +14 -4
  68. package/package.json +1 -1
  69. package/src/api/handlers/about-me.ts +19 -9
  70. package/src/api/handlers/auth-handler.ts +41 -18
  71. package/src/api/handlers/content.ts +1 -1
  72. package/src/api/handlers/home.ts +2 -2
  73. package/src/api/website-api.ts +25 -13
  74. package/src/prerender/__tests__/page-content.test.ts +1 -11
  75. package/src/prerender/data-fetcher.ts +93 -0
  76. package/src/prerender/page-content.ts +109 -106
  77. package/src/prerender/page-generators/about.ts +38 -0
  78. package/src/prerender/page-generators/base.ts +77 -0
  79. package/src/prerender/page-generators/blog-detail.ts +35 -0
  80. package/src/prerender/page-generators/blogs-list.ts +43 -0
  81. package/src/prerender/page-generators/home.ts +54 -0
  82. package/src/prerender/page-generators/index.ts +8 -0
  83. package/src/prerender/page-generators/not-found.ts +36 -0
  84. package/src/prerender/page-generators/stories-list.ts +43 -0
  85. package/src/prerender/page-generators/story-detail.ts +35 -0
  86. package/src/shared/config/index.ts +4 -2
  87. package/src/shared/page-content.ts +1 -1
  88. package/src/shared/router.ts +5 -5
  89. package/src/ui/about-me/index.ts +23 -57
  90. package/src/ui/admin/api.ts +93 -0
  91. package/src/ui/admin/components/AboutMeSection.ts +47 -0
  92. package/src/ui/admin/components/AdminSection.ts +134 -0
  93. package/src/ui/admin/components/BlogsSection.ts +62 -0
  94. package/src/ui/admin/components/HomeSection.ts +47 -0
  95. package/src/ui/admin/components/ImagesSection.ts +54 -0
  96. package/src/ui/admin/components/LoginForm.ts +116 -0
  97. package/src/ui/admin/components/LogoSection.ts +51 -0
  98. package/src/ui/admin/components/ProfileSection.ts +47 -0
  99. package/src/ui/admin/components/StaticSection.ts +67 -0
  100. package/src/ui/admin/components/StoriesSection.ts +62 -0
  101. package/src/ui/admin/components/index.ts +10 -0
  102. package/src/ui/admin/index.ts +192 -434
  103. package/src/ui/admin/types.ts +26 -0
  104. package/src/ui/blog-viewer/index.ts +4 -1
  105. package/src/ui/index.ts +7 -0
  106. package/src/ui/story-viewer/index.ts +4 -1
  107. package/dist/ui/about-me/renderer.d.ts +0 -5
  108. package/dist/ui/about-me/renderer.d.ts.map +0 -1
  109. package/src/ui/about-me/renderer.ts +0 -7
@@ -35,10 +35,10 @@ export class Router {
35
35
  };
36
36
 
37
37
  this.footerLinks = [
38
- { text: 'LinkedIn', link: normalizeUrl(config.linkedin) },
39
- { text: 'GitHub', link: normalizeUrl(config.github) },
40
- { text: 'Email', link: config.email ? `mailto:${config.email}` : '' },
41
- ].filter(link => link.link !== '');
38
+ { text: 'LinkedIn', link: normalizeUrl(config.linkedin) || 'https://linkedin.com' },
39
+ { text: 'GitHub', link: normalizeUrl(config.github) || 'https://github.com' },
40
+ { text: 'Email', link: config.email ? `mailto:${config.email}` : 'mailto:hello@example.com' },
41
+ ];
42
42
  }
43
43
 
44
44
  public init(appElementId: string = 'app') {
@@ -243,7 +243,7 @@ export class Router {
243
243
  <main class="container container-medium">
244
244
  <admin-portal></admin-portal>
245
245
  </main>
246
- <my-footer copyright="${this.copyright}" footerlinks='[]'></my-footer>
246
+ <my-footer copyright="${this.copyright}" footerLinks='[]'></my-footer>
247
247
  `;
248
248
  }
249
249
  }
@@ -1,19 +1,10 @@
1
1
  import { LitElement, html, css } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
3
 
4
- import { ContentNode } from '@leadertechie/md2html';
5
4
  import { Profile } from './api';
6
-
7
5
  import { aboutmeStyles } from './styles';
8
-
9
- import { AboutMeRenderer } from './renderer';
10
6
  import { fetchAboutMe } from './api';
11
7
 
12
- interface AboutMeData {
13
- profile: Profile;
14
- contentNodes: ContentNode[];
15
- }
16
-
17
8
  @customElement('my-aboutme')
18
9
  export class MyAboutme extends LitElement {
19
10
  static styles = aboutmeStyles;
@@ -25,22 +16,15 @@ export class MyAboutme extends LitElement {
25
16
  accessor profile: Profile | null = null;
26
17
 
27
18
  @state()
28
- accessor contentNodes: ContentNode[] = [];
19
+ accessor htmlContent: string = '';
29
20
 
30
21
  @state()
31
22
  accessor loading = true;
32
23
 
33
- private renderer: AboutMeRenderer;
34
- /**
35
- * Injectable fetcher function used to retrieve About Me data.
36
- * Tests can override this (e.g. componentInstance.fetcher = myMock)
37
- * to avoid network I/O. By default it uses the stable `fetchAboutMe` helper.
38
- */
39
24
  public fetcher = fetchAboutMe;
40
25
 
41
- constructor(renderer?: AboutMeRenderer) {
26
+ constructor() {
42
27
  super();
43
- this.renderer = renderer || new AboutMeRenderer();
44
28
  }
45
29
 
46
30
  private get apiBaseUrl(): string {
@@ -50,70 +34,59 @@ export class MyAboutme extends LitElement {
50
34
  async connectedCallback() {
51
35
  super.connectedCallback();
52
36
 
53
- // If we have initial data from SSR, use it immediately.
54
37
  if (typeof window !== 'undefined' && (window as any).__HYDRATION_DATA__) {
55
38
  const hydrationData = (window as any).__HYDRATION_DATA__;
56
39
  this.profile = hydrationData.profile;
57
- this.contentNodes = hydrationData.contentNodes;
40
+ this.htmlContent = hydrationData.processedMarkdown;
58
41
  this.loading = false;
59
42
  return;
60
43
  }
61
44
 
62
- // Load content if a baseUrl is provided. In dev/prod, this will be set.
63
- const url = this.apiBaseUrl;
64
- if (url) {
65
- this.loadContent();
66
- }
45
+ this.loadContent();
67
46
  }
68
47
 
69
48
  updated(changedProperties: Map<string, any>) {
70
49
  super.updated(changedProperties);
71
50
 
72
- // Only load content from API if we haven't already loaded via hydration data
73
- if (this.loading === false && this.profile && this.contentNodes.length > 0) {
51
+ if (this.loading === false && this.profile && this.htmlContent) {
74
52
  return;
75
53
  }
76
54
 
77
- // If baseUrl changed and we have a valid baseUrl, load content
78
55
  if (changedProperties.has('baseUrl') || changedProperties.has('base-url')) {
79
- const url = this.apiBaseUrl;
80
- if (url) {
81
- this.loadContent();
82
- }
56
+ this.loadContent();
83
57
  }
84
58
  }
85
59
 
86
60
  private async loadContent() {
61
+ const url = this.apiBaseUrl;
62
+ if (!url) {
63
+ this.loading = false;
64
+ this.setFallbackContent();
65
+ return;
66
+ }
67
+
87
68
  try {
88
69
  this.loading = true;
89
-
90
- // Fetch content from API using the injectable fetcher.
91
- // Use the apiBaseUrl getter to get either property or attribute
92
- const url = this.apiBaseUrl;
93
70
  const data = await this.fetcher(url);
94
71
 
95
72
  this.profile = data.profile;
96
- this.contentNodes = data.contentNodes;
73
+ this.htmlContent = (data as any).processedMarkdown;
97
74
  this.loading = false;
98
- // Wait for the render to complete
99
75
  await this.updateComplete;
100
76
  } catch (error) {
101
77
  this.loading = false;
102
- // Set fallback content
103
78
  this.setFallbackContent();
104
- // Wait for the render to complete
105
79
  await this.updateComplete;
106
80
  }
107
81
  }
108
82
 
109
83
  private setFallbackContent() {
110
- this.profile = null; // No profile data
111
- this.contentNodes = [
112
- {
113
- type: 'paragraph',
114
- content: 'Unable to Load Content'
115
- }
116
- ];
84
+ this.profile = null;
85
+ this.htmlContent = `
86
+ <h2>Profile Not Found</h2>
87
+ <p>Your about-me profile has not been initialized yet. Please run the seed command to get started:</p>
88
+ <pre>npm run seed -- &lt;username&gt; '&lt;password&gt;'</pre>
89
+ `;
117
90
  }
118
91
 
119
92
  render() {
@@ -121,15 +94,10 @@ export class MyAboutme extends LitElement {
121
94
  return html`<div class="aboutme"><div class="loading">Loading...</div></div>`;
122
95
  }
123
96
 
124
- // If profile is not available, check if fallback content is present.
125
97
  if (!this.profile) {
126
- if (this.contentNodes && this.contentNodes.length > 0) {
127
- return html`<div class="aboutme">${this.renderer.renderContent(this.contentNodes)}</div>`;
128
- }
129
- return html`<div class="aboutme"><div class="loading">Failed to load content</div></div>`;
98
+ return html`<div class="aboutme"><div class="content-section" .innerHTML="${this.htmlContent}"></div></div>`;
130
99
  }
131
100
 
132
- // Render profile header + content
133
101
  const profileImageUrl = this.profile.profileImageUrl
134
102
  ? (this.profile.profileImageUrl.startsWith('http') || this.profile.profileImageUrl.startsWith('/')
135
103
  ? this.profile.profileImageUrl
@@ -146,10 +114,8 @@ export class MyAboutme extends LitElement {
146
114
  <h1>${this.profile.name}</h1>
147
115
  <p class="profile-title">${this.profile.title} • ${this.profile.experience}</p>
148
116
  </div>
149
- <div class="content-section">
150
- ${this.renderer.renderContent(this.contentNodes)}
151
- </div>
117
+ <div class="content-section" .innerHTML="${this.htmlContent}"></div>
152
118
  </div>
153
119
  `;
154
120
  }
155
- }
121
+ }
@@ -0,0 +1,93 @@
1
+ import type { ContentItem, AuthStatus, StaticDetails } from './types';
2
+
3
+ export class AdminApiService {
4
+ constructor(private apiUrl: string) {}
5
+
6
+ async checkAuthStatus(): Promise<AuthStatus> {
7
+ const res = await fetch(`${this.apiUrl}/api/auth/status`);
8
+ if (!res.ok) throw new Error('Auth status check failed');
9
+ return res.json();
10
+ }
11
+
12
+ async tryAutoLogin(): Promise<ContentItem[]> {
13
+ const res = await fetch(`${this.apiUrl}/api/content`, {
14
+ credentials: 'include'
15
+ });
16
+ if (!res.ok) throw new Error('Auto login failed');
17
+ return res.json();
18
+ }
19
+
20
+ async login(username: string, password: string): Promise<void> {
21
+ const res = await fetch(`${this.apiUrl}/api/auth/login`, {
22
+ method: 'POST',
23
+ credentials: 'include',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ username, password })
26
+ });
27
+ if (!res.ok) {
28
+ const data = await res.json();
29
+ throw new Error(data.error || 'Login failed');
30
+ }
31
+ }
32
+
33
+ async setup(username: string, password: string): Promise<void> {
34
+ const res = await fetch(`${this.apiUrl}/api/auth/setup`, {
35
+ method: 'POST',
36
+ credentials: 'include',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ username, password })
39
+ });
40
+ if (!res.ok) {
41
+ const data = await res.json();
42
+ throw new Error(data.error || 'Setup failed');
43
+ }
44
+ }
45
+
46
+ async logout(): Promise<void> {
47
+ await fetch(`${this.apiUrl}/api/auth/logout`, {
48
+ method: 'POST',
49
+ credentials: 'include'
50
+ });
51
+ }
52
+
53
+ async fetchContent(): Promise<ContentItem[]> {
54
+ const res = await fetch(`${this.apiUrl}/api/content`, {
55
+ credentials: 'include'
56
+ });
57
+ if (!res.ok) throw new Error('Failed to fetch content');
58
+ return res.json();
59
+ }
60
+
61
+ async fetchStaticDetails(): Promise<StaticDetails> {
62
+ const res = await fetch(`${this.apiUrl}/api/static`, {
63
+ credentials: 'include'
64
+ });
65
+ if (!res.ok) throw new Error('Failed to fetch static details');
66
+ return res.json();
67
+ }
68
+
69
+ async uploadContent(key: string, file: File): Promise<void> {
70
+ const res = await fetch(`${this.apiUrl}/api/content/${key}`, {
71
+ method: 'PUT',
72
+ credentials: 'include',
73
+ body: file
74
+ });
75
+ if (!res.ok) throw new Error('Upload failed');
76
+ }
77
+
78
+ async deleteContent(key: string): Promise<void> {
79
+ const res = await fetch(`${this.apiUrl}/api/content/${key}`, {
80
+ method: 'DELETE',
81
+ credentials: 'include'
82
+ });
83
+ if (!res.ok) throw new Error('Delete failed');
84
+ }
85
+
86
+ async clearCache(): Promise<void> {
87
+ const res = await fetch(`${this.apiUrl}/api/cache-clear`, {
88
+ method: 'POST',
89
+ credentials: 'include'
90
+ });
91
+ if (!res.ok) throw new Error('Failed to clear cache');
92
+ }
93
+ }
@@ -0,0 +1,47 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-about-me-section')
6
+ export class AdminAboutMeSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const input = this.shadowRoot?.querySelector('#aboutFile') as HTMLInputElement;
9
+ if (input.files?.[0]) {
10
+ try {
11
+ await this.onUpload('about-me.md', input.files[0]);
12
+ this.onStatusMessage('Upload successful!');
13
+ } catch (e) {
14
+ this.onStatusMessage('Upload failed.');
15
+ }
16
+ }
17
+ }
18
+
19
+ private async handleDelete() {
20
+ if (!confirm('Delete about-me.md?')) return;
21
+ try {
22
+ await this.onDelete('about-me.md');
23
+ } catch (e) {
24
+ this.onStatusMessage('Delete failed.');
25
+ }
26
+ }
27
+
28
+ render() {
29
+ const aboutMe = this.getContent('about-me.md');
30
+ return html`
31
+ <div class="section">
32
+ <h3>About Me Page <span class="required-badge">Required</span></h3>
33
+ <p class="help-text">Content for your About Me page. Supports Markdown with frontmatter.</p>
34
+
35
+ ${aboutMe ? html`
36
+ <div class="current-file">
37
+ <strong>Current:</strong> about-me.md (${aboutMe.size} bytes)
38
+ <button class="btn-danger" @click=${this.handleDelete}>Delete</button>
39
+ </div>
40
+ ` : ''}
41
+
42
+ <input type="file" id="aboutFile" accept=".md" />
43
+ <button class="btn-primary" @click=${this.handleUpload}>Upload about-me.md</button>
44
+ </div>
45
+ `;
46
+ }
47
+ }
@@ -0,0 +1,134 @@
1
+ import { LitElement, css } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import type { ContentItem, StaticDetails, AdminSectionProps } from '../types';
4
+
5
+ export abstract class AdminSection extends LitElement implements AdminSectionProps {
6
+
7
+ static styles = css`
8
+ :host {
9
+ display: block;
10
+ width: 100%;
11
+ }
12
+
13
+ .section {
14
+ width: 100%;
15
+ border: none;
16
+ padding: 24px;
17
+ border-radius: 16px;
18
+ background: var(--card-bg, #fff);
19
+ box-shadow: 0 4px 6px rgba(0,0,0,0.07), 0 10px 20px rgba(0,0,0,0.05);
20
+ margin-bottom: 20px;
21
+ min-height: 450px;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ .section h3 {
26
+ margin: 0 0 0.5rem 0;
27
+ font-size: 1.25rem;
28
+ }
29
+
30
+ .help-text {
31
+ color: var(--secondary-text, #666);
32
+ margin-bottom: 1rem;
33
+ }
34
+
35
+ .current-file {
36
+ margin-bottom: 1rem;
37
+ padding: 1rem;
38
+ background: var(--nav-link-hover-bg, #f5f5f5);
39
+ border-radius: 8px;
40
+ }
41
+
42
+ input[type="text"],
43
+ input[type="file"] {
44
+ width: 100%;
45
+ padding: 10px;
46
+ margin-bottom: 1rem;
47
+ border: 1px solid var(--border-color);
48
+ border-radius: 4px;
49
+ background: var(--background-color);
50
+ color: var(--text-color);
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ input[type="file"] {
55
+ margin-bottom: 1rem;
56
+ }
57
+
58
+ button {
59
+ padding: 10px 20px;
60
+ border: none;
61
+ border-radius: 4px;
62
+ cursor: pointer;
63
+ font-size: 1rem;
64
+ transition: background-color 0.3s ease;
65
+ margin-right: 10px;
66
+ }
67
+
68
+ .btn-primary {
69
+ background: var(--link-color, #646cff);
70
+ color: white;
71
+ }
72
+
73
+ .btn-primary:hover {
74
+ background: var(--link-hover-color, #535bf2);
75
+ }
76
+
77
+ .btn-secondary {
78
+ background: var(--nav-link-hover-bg, #f0f0f0);
79
+ color: var(--text-color, #213547);
80
+ }
81
+
82
+ .btn-secondary:hover {
83
+ background: var(--border-color, #e0e0e0);
84
+ }
85
+
86
+ .btn-danger {
87
+ background: #dc3545;
88
+ color: white;
89
+ }
90
+
91
+ .btn-danger:hover {
92
+ background: #c82333;
93
+ }
94
+
95
+ .file-list {
96
+ margin-top: 1rem;
97
+ }
98
+
99
+ .file-item {
100
+ display: flex;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+ padding: 0.5rem;
104
+ border-bottom: 1px solid var(--border-color, #eee);
105
+ }
106
+
107
+ .file-item:last-child {
108
+ border-bottom: none;
109
+ }
110
+ `;
111
+
112
+ @property({ type: Array })
113
+ accessor contentList: ContentItem[] = [];
114
+
115
+ @property({ type: Object })
116
+ accessor staticDetails: StaticDetails = {};
117
+
118
+ @property({ type: Function })
119
+ accessor onUpload: (key: string, file: File) => Promise<void> = async () => {};
120
+
121
+ @property({ type: Function })
122
+ accessor onDelete: (key: string) => Promise<void> = async () => {};
123
+
124
+ @property({ type: Function })
125
+ accessor onStatusMessage: (message: string) => void = () => {};
126
+
127
+ protected getContent(key: string): ContentItem | undefined {
128
+ return this.contentList.find(c => c.key === key);
129
+ }
130
+
131
+ protected getSectionFiles(prefix: string): ContentItem[] {
132
+ return this.contentList.filter(c => c.key.startsWith(prefix));
133
+ }
134
+ }
@@ -0,0 +1,62 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-blogs-section')
6
+ export class AdminBlogsSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const metaInput = this.shadowRoot?.querySelector('#blogMetaFile') as HTMLInputElement;
9
+ const contentInput = this.shadowRoot?.querySelector('#blogContentFile') as HTMLInputElement;
10
+ const slugInput = this.shadowRoot?.querySelector('#blogSlug') as HTMLInputElement;
11
+
12
+ if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
13
+ try {
14
+ await this.onUpload(`blogs/${slugInput.value}.json`, metaInput.files[0]);
15
+ await this.onUpload(`blogs/${slugInput.value}.md`, contentInput.files[0]);
16
+ this.onStatusMessage('Upload successful!');
17
+ } catch (e) {
18
+ this.onStatusMessage('Upload failed.');
19
+ }
20
+ }
21
+ }
22
+
23
+ private async handleDelete(slug: string) {
24
+ if (!confirm(`Delete blog ${slug}?`)) return;
25
+ try {
26
+ await this.onDelete(`blogs/${slug}.json`);
27
+ await this.onDelete(`blogs/${slug}.md`);
28
+ } catch (e) {
29
+ this.onStatusMessage('Delete failed.');
30
+ }
31
+ }
32
+
33
+ render() {
34
+ const blogs = this.getSectionFiles('blogs/').filter(b => b.key.endsWith('.json'));
35
+ return html`
36
+ <div class="section">
37
+ <h3>Blog Posts</h3>
38
+ <p class="help-text">Each blog needs 2 files: a JSON (metadata) and MD (content) file.</p>
39
+
40
+ <h4>Upload New Blog</h4>
41
+ <input type="file" id="blogMetaFile" accept=".json" />
42
+ <input type="file" id="blogContentFile" accept=".md" />
43
+ <input type="text" id="blogSlug" placeholder="Slug (e.g., my-new-post)" class="mt-1" />
44
+ <button class="btn-primary" @click=${this.handleUpload}>Upload Blog (JSON + MD)</button>
45
+
46
+ <div class="file-list">
47
+ <h4>Current Blogs (${blogs.length})</h4>
48
+ ${blogs.map(b => {
49
+ const slug = b.key.replace('blogs/', '').replace('.json', '');
50
+ return html`
51
+ <div class="file-item">
52
+ <span>${b.key.replace('.json', '')}</span>
53
+ <button class="btn-danger" @click=${() => this.handleDelete(slug)}>Delete</button>
54
+ </div>
55
+ `;
56
+ })}
57
+ ${blogs.length === 0 ? html`<p>No blogs yet.</p>` : ''}
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
62
+ }
@@ -0,0 +1,47 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-home-section')
6
+ export class AdminHomeSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const input = this.shadowRoot?.querySelector('#homeFile') as HTMLInputElement;
9
+ if (input.files?.[0]) {
10
+ try {
11
+ await this.onUpload('home.md', input.files[0]);
12
+ this.onStatusMessage('Upload successful!');
13
+ } catch (e) {
14
+ this.onStatusMessage('Upload failed.');
15
+ }
16
+ }
17
+ }
18
+
19
+ private async handleDelete() {
20
+ if (!confirm('Delete home.md?')) return;
21
+ try {
22
+ await this.onDelete('home.md');
23
+ } catch (e) {
24
+ this.onStatusMessage('Delete failed.');
25
+ }
26
+ }
27
+
28
+ render() {
29
+ const home = this.getContent('home.md');
30
+ return html`
31
+ <div class="section">
32
+ <h3>Home Page</h3>
33
+ <p class="help-text">Content for your home page. Upload home.md with your main content.</p>
34
+
35
+ ${home ? html`
36
+ <div class="current-file">
37
+ <strong>Current:</strong> home.md (${home.size} bytes)
38
+ <button class="btn-danger" @click=${this.handleDelete}>Delete</button>
39
+ </div>
40
+ ` : ''}
41
+
42
+ <input type="file" id="homeFile" accept=".md" />
43
+ <button class="btn-primary" @click=${this.handleUpload}>Upload home.md</button>
44
+ </div>
45
+ `;
46
+ }
47
+ }
@@ -0,0 +1,54 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-images-section')
6
+ export class AdminImagesSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const fileInput = this.shadowRoot?.querySelector('#imageFile') as HTMLInputElement;
9
+ const pathInput = this.shadowRoot?.querySelector('#imagePath') as HTMLInputElement;
10
+
11
+ if (fileInput.files?.[0] && pathInput.value) {
12
+ try {
13
+ await this.onUpload(`images/${pathInput.value}`, fileInput.files[0]);
14
+ this.onStatusMessage('Upload successful!');
15
+ } catch (e) {
16
+ this.onStatusMessage('Upload failed.');
17
+ }
18
+ }
19
+ }
20
+
21
+ private async handleDelete(key: string) {
22
+ if (!confirm(`Delete ${key}?`)) return;
23
+ try {
24
+ await this.onDelete(key);
25
+ } catch (e) {
26
+ this.onStatusMessage('Delete failed.');
27
+ }
28
+ }
29
+
30
+ render() {
31
+ const images = this.getSectionFiles('images/');
32
+ return html`
33
+ <div class="section">
34
+ <h3>Images</h3>
35
+ <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>
36
+
37
+ <input type="file" id="imageFile" accept="image/*" />
38
+ <input type="text" id="imagePath" placeholder="Image name (e.g., profile-photo.jpg)" class="mt-1" />
39
+ <button class="btn-primary" @click=${this.handleUpload}>Upload to images/</button>
40
+
41
+ <div class="file-list">
42
+ <h4>Current Images (${images.length})</h4>
43
+ ${images.map(img => html`
44
+ <div class="file-item">
45
+ <span>${img.key} (${img.size} bytes)</span>
46
+ <button class="btn-danger" @click=${() => this.handleDelete(img.key)}>Delete</button>
47
+ </div>
48
+ `)}
49
+ ${images.length === 0 ? html`<p>No images yet.</p>` : ''}
50
+ </div>
51
+ </div>
52
+ `;
53
+ }
54
+ }