@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
@@ -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
+ }
@@ -0,0 +1,116 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ @customElement('admin-login-form')
5
+ export class AdminLoginForm extends LitElement {
6
+
7
+ static styles = css`
8
+ :host {
9
+ display: block;
10
+ width: 100%;
11
+ }
12
+
13
+ .container {
14
+ width: 100%;
15
+ max-width: 900px;
16
+ margin: 0 auto;
17
+ padding: 1rem;
18
+ }
19
+
20
+ .login-box {
21
+ border: 1px solid var(--border-color, #e0e0e0);
22
+ padding: 3rem 2.5rem;
23
+ border-radius: 16px;
24
+ width: 100%;
25
+ max-width: 420px;
26
+ margin: 80px auto;
27
+ text-align: center;
28
+ background: var(--card-bg, #fff);
29
+ box-shadow: 0 4px 6px rgba(0,0,0,0.07), 0 10px 20px rgba(0,0,0,0.05);
30
+ }
31
+
32
+ .login-box h2 {
33
+ margin: 0 0 0.5rem 0;
34
+ font-size: 1.75rem;
35
+ font-weight: 600;
36
+ color: var(--text-color, #213547);
37
+ }
38
+
39
+ .login-box p {
40
+ color: var(--secondary-text, #666);
41
+ margin-bottom: 1.5rem;
42
+ }
43
+
44
+ input {
45
+ width: 100%;
46
+ padding: 10px;
47
+ margin-bottom: 20px;
48
+ border: 1px solid var(--border-color);
49
+ border-radius: 4px;
50
+ background: var(--background-color);
51
+ color: var(--text-color);
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ button {
56
+ padding: 10px 20px;
57
+ border: none;
58
+ border-radius: 4px;
59
+ cursor: pointer;
60
+ font-size: 1rem;
61
+ transition: background-color 0.3s ease;
62
+ }
63
+
64
+ .btn-primary {
65
+ background: var(--link-color, #646cff);
66
+ color: white;
67
+ }
68
+
69
+ .btn-primary:hover {
70
+ background: var(--link-hover-color, #535bf2);
71
+ }
72
+
73
+ .error-message {
74
+ color: red;
75
+ margin-bottom: 10px;
76
+ }
77
+ `;
78
+
79
+ @property({ type: String })
80
+ accessor errorMessage = '';
81
+
82
+ @property({ type: Boolean })
83
+ accessor isSetup = false;
84
+
85
+ private handleSubmit(e: Event) {
86
+ e.preventDefault();
87
+ const formData = new FormData(e.target as HTMLFormElement);
88
+ const username = formData.get('username') as string;
89
+ const password = formData.get('password') as string;
90
+ const confirmPassword = formData.get('confirmPassword') as string;
91
+
92
+ this.dispatchEvent(new CustomEvent('login-submit', {
93
+ detail: { username, password, confirmPassword },
94
+ bubbles: true,
95
+ composed: true
96
+ }));
97
+ }
98
+
99
+ render() {
100
+ return html`
101
+ <div class="container">
102
+ <div class="login-box">
103
+ <h2>${this.isSetup ? 'Admin Login' : 'Admin Setup'}</h2>
104
+ <p>${this.isSetup ? 'Enter your credentials' : 'Create your admin credentials'}</p>
105
+ <form @submit=${this.handleSubmit}>
106
+ <input type="text" name="username" placeholder="Username${this.isSetup ? '' : ' (3+ chars)'}" autocomplete="username" required />
107
+ <input type="password" name="password" placeholder="Password${this.isSetup ? '' : ' (8+ chars)'}" autocomplete="current-password" required />
108
+ ${!this.isSetup ? html`<input type="password" name="confirmPassword" placeholder="Confirm Password" required />` : ''}
109
+ ${this.errorMessage ? html`<div class="error-message">${this.errorMessage}</div>` : ''}
110
+ <button type="submit" class="btn-primary">${this.isSetup ? 'Login' : 'Create Account'}</button>
111
+ </form>
112
+ </div>
113
+ </div>
114
+ `;
115
+ }
116
+ }
@@ -0,0 +1,51 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-logo-section')
6
+ export class AdminLogoSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const input = this.shadowRoot?.querySelector('#logoFile') as HTMLInputElement;
9
+ if (input.files?.[0]) {
10
+ try {
11
+ await this.onUpload('logo.svg', 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 logo.svg?')) return;
21
+ try {
22
+ await this.onDelete('logo.svg');
23
+ } catch (e) {
24
+ this.onStatusMessage('Delete failed.');
25
+ }
26
+ }
27
+
28
+ render() {
29
+ const logo = this.getContent('logo.svg');
30
+ return html`
31
+ <div class="section">
32
+ <h3>Site Logo</h3>
33
+ <p class="help-text">Upload your site logo (SVG format recommended). This appears in the header of your site.</p>
34
+
35
+ ${logo ? html`
36
+ <div class="current-file">
37
+ <strong>Current:</strong> logo.svg (${logo.size} bytes)
38
+ <button class="btn-danger" @click=${this.handleDelete}>Delete</button>
39
+ </div>
40
+ ` : ''}
41
+
42
+ <input type="file" id="logoFile" accept=".svg,image/svg+xml" />
43
+ <button class="btn-primary mt-1 mb-1" @click=${this.handleUpload}>Upload logo.svg</button>
44
+
45
+ <div class="info-box">
46
+ <strong>Tip:</strong> Use an SVG with transparent background. The logo will automatically adapt to light/dark themes.
47
+ </div>
48
+ </div>
49
+ `;
50
+ }
51
+ }
@@ -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-profile-section')
6
+ export class AdminProfileSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const input = this.shadowRoot?.querySelector('#profileFile') as HTMLInputElement;
9
+ if (input.files?.[0]) {
10
+ try {
11
+ await this.onUpload('profile.json', 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 profile.json?')) return;
21
+ try {
22
+ await this.onDelete('profile.json');
23
+ } catch (e) {
24
+ this.onStatusMessage('Delete failed.');
25
+ }
26
+ }
27
+
28
+ render() {
29
+ const profile = this.getContent('profile.json');
30
+ return html`
31
+ <div class="section">
32
+ <h3>Profile <span class="required-badge">Required</span></h3>
33
+ <p class="help-text">This file contains your profile information (name, title, experience).</p>
34
+
35
+ ${profile ? html`
36
+ <div class="current-file">
37
+ <strong>Current:</strong> profile.json (${profile.size} bytes)
38
+ <button class="btn-danger" @click=${this.handleDelete}>Delete</button>
39
+ </div>
40
+ ` : ''}
41
+
42
+ <input type="file" id="profileFile" accept=".json" />
43
+ <button class="btn-primary" @click=${this.handleUpload}>Upload profile.json</button>
44
+ </div>
45
+ `;
46
+ }
47
+ }
@@ -0,0 +1,67 @@
1
+ import { html } from 'lit';
2
+ import { customElement, state } from 'lit/decorators.js';
3
+ import { AdminSection } from './AdminSection';
4
+
5
+ @customElement('admin-static-section')
6
+ export class AdminStaticSection extends AdminSection {
7
+ @state()
8
+ accessor localDetails = { ...this.staticDetails };
9
+
10
+ private async handleSave() {
11
+ const siteTitle = (this.shadowRoot?.querySelector('#siteTitle') as HTMLInputElement)?.value;
12
+ const copyright = (this.shadowRoot?.querySelector('#copyright') as HTMLInputElement)?.value;
13
+ const linkedin = (this.shadowRoot?.querySelector('#linkedin') as HTMLInputElement)?.value;
14
+ const github = (this.shadowRoot?.querySelector('#github') as HTMLInputElement)?.value;
15
+ const email = (this.shadowRoot?.querySelector('#email') as HTMLInputElement)?.value;
16
+
17
+ const data: Record<string, string> = {};
18
+ if (siteTitle) data.siteTitle = siteTitle;
19
+ if (copyright) data.copyright = copyright;
20
+ if (linkedin) data.linkedin = linkedin;
21
+ if (github) data.github = github;
22
+ if (email) data.email = email;
23
+
24
+ try {
25
+ await this.onUpload('staticdetails.json', new File([JSON.stringify(data)], 'staticdetails.json', { type: 'application/json' }));
26
+ this.onStatusMessage('Settings saved!');
27
+ } catch (e) {
28
+ this.onStatusMessage('Failed to save settings.');
29
+ }
30
+ }
31
+
32
+ render() {
33
+ return html`
34
+ <div class="section">
35
+ <h3>Site Settings</h3>
36
+ <p class="help-text">Manage your site's static details like title, footer links, etc.</p>
37
+
38
+ <div class="mb-1">
39
+ <label style="display:block;margin-bottom:4px;font-weight:500">Site Title</label>
40
+ <input type="text" id="siteTitle" .value=${this.staticDetails?.siteTitle || ''} />
41
+ </div>
42
+
43
+ <div class="mb-1">
44
+ <label style="display:block;margin-bottom:4px;font-weight:500">Copyright Text</label>
45
+ <input type="text" id="copyright" .value=${this.staticDetails?.copyright || ''} />
46
+ </div>
47
+
48
+ <div class="mb-1">
49
+ <label style="display:block;margin-bottom:4px;font-weight:500">LinkedIn URL</label>
50
+ <input type="text" id="linkedin" .value=${this.staticDetails?.linkedin || ''} />
51
+ </div>
52
+
53
+ <div class="mb-1">
54
+ <label style="display:block;margin-bottom:4px;font-weight:500">GitHub URL</label>
55
+ <input type="text" id="github" .value=${this.staticDetails?.github || ''} />
56
+ </div>
57
+
58
+ <div class="mb-1">
59
+ <label style="display:block;margin-bottom:4px;font-weight:500">Email</label>
60
+ <input type="text" id="email" .value=${this.staticDetails?.email || ''} />
61
+ </div>
62
+
63
+ <button class="btn-primary" @click=${this.handleSave}>Save Settings</button>
64
+ </div>
65
+ `;
66
+ }
67
+ }
@@ -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-stories-section')
6
+ export class AdminStoriesSection extends AdminSection {
7
+ private async handleUpload() {
8
+ const metaInput = this.shadowRoot?.querySelector('#storyMetaFile') as HTMLInputElement;
9
+ const contentInput = this.shadowRoot?.querySelector('#storyContentFile') as HTMLInputElement;
10
+ const slugInput = this.shadowRoot?.querySelector('#storySlug') as HTMLInputElement;
11
+
12
+ if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
13
+ try {
14
+ await this.onUpload(`stories/${slugInput.value}.json`, metaInput.files[0]);
15
+ await this.onUpload(`stories/${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 story ${slug}?`)) return;
25
+ try {
26
+ await this.onDelete(`stories/${slug}.json`);
27
+ await this.onDelete(`stories/${slug}.md`);
28
+ } catch (e) {
29
+ this.onStatusMessage('Delete failed.');
30
+ }
31
+ }
32
+
33
+ render() {
34
+ const stories = this.getSectionFiles('stories/').filter(s => s.key.endsWith('.json'));
35
+ return html`
36
+ <div class="section">
37
+ <h3>Stories</h3>
38
+ <p class="help-text">Each story needs 2 files: a JSON (metadata) and MD (content) file.</p>
39
+
40
+ <h4>Upload New Story</h4>
41
+ <input type="file" id="storyMetaFile" accept=".json" />
42
+ <input type="file" id="storyContentFile" accept=".md" />
43
+ <input type="text" id="storySlug" placeholder="Slug (e.g., my-story)" class="mt-1" />
44
+ <button class="btn-primary" @click=${this.handleUpload}>Upload Story (JSON + MD)</button>
45
+
46
+ <div class="file-list">
47
+ <h4>Current Stories (${stories.length})</h4>
48
+ ${stories.map(s => {
49
+ const slug = s.key.replace('stories/', '').replace('.json', '');
50
+ return html`
51
+ <div class="file-item">
52
+ <span>${s.key.replace('.json', '')}</span>
53
+ <button class="btn-danger" @click=${() => this.handleDelete(slug)}>Delete</button>
54
+ </div>
55
+ `;
56
+ })}
57
+ ${stories.length === 0 ? html`<p>No stories yet.</p>` : ''}
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ export * from './AdminSection';
2
+ export * from './LoginForm';
3
+ export * from './HomeSection';
4
+ export * from './ProfileSection';
5
+ export * from './AboutMeSection';
6
+ export * from './BlogsSection';
7
+ export * from './StoriesSection';
8
+ export * from './ImagesSection';
9
+ export * from './LogoSection';
10
+ export * from './StaticSection';