@leadertechie/personal-site-kit 0.0.0 → 0.1.0-alpha.10
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/__tests__/info.test.d.ts +2 -0
- package/dist/api/__tests__/info.test.d.ts.map +1 -0
- package/dist/api/__tests__/utils.test.d.ts +2 -0
- package/dist/api/__tests__/utils.test.d.ts.map +1 -0
- package/dist/api/content-utils.d.ts +27 -0
- package/dist/api/content-utils.d.ts.map +1 -0
- package/dist/api/handlers/{aboutme.d.ts → about-me.d.ts} +1 -1
- package/dist/api/handlers/about-me.d.ts.map +1 -0
- package/dist/api/handlers/auth-handler.d.ts +2 -0
- package/dist/api/handlers/auth-handler.d.ts.map +1 -0
- package/dist/api/handlers/auth.d.ts +23 -0
- package/dist/api/handlers/auth.d.ts.map +1 -0
- package/dist/api/handlers/content-api.d.ts +0 -1
- package/dist/api/handlers/content-api.d.ts.map +1 -1
- package/dist/api/handlers/content.d.ts.map +1 -1
- package/dist/api/handlers/home.d.ts.map +1 -1
- package/dist/api/handlers/{staticdetails.d.ts → static-details.d.ts} +1 -1
- package/dist/api/handlers/static-details.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -8
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/website-api.d.ts +10 -0
- package/dist/api/website-api.d.ts.map +1 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +19 -589
- package/dist/assets/logo-placeholder.svg +21 -0
- package/dist/chunks/index-CGvOrVf8.js +213 -0
- package/dist/chunks/index-_AMi6ort.js +2690 -0
- package/dist/chunks/site-store-Vqmjjz9c.js +86 -0
- package/dist/chunks/template-C1tMqlPY.js +597 -0
- package/dist/chunks/website-api-CuyeBej-.js +920 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -352
- package/dist/prerender/__tests__/page-content.test.d.ts +2 -0
- package/dist/prerender/__tests__/page-content.test.d.ts.map +1 -0
- package/dist/prerender/__tests__/template.test.d.ts +2 -0
- package/dist/prerender/__tests__/template.test.d.ts.map +1 -0
- package/dist/prerender/data-fetcher.d.ts +19 -0
- package/dist/prerender/data-fetcher.d.ts.map +1 -0
- package/dist/prerender/index.d.ts +5 -4
- package/dist/prerender/index.d.ts.map +1 -1
- package/dist/prerender/{pageContent.d.ts → page-content.d.ts} +1 -1
- package/dist/prerender/page-content.d.ts.map +1 -0
- 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/website-prerender.d.ts +22 -0
- package/dist/prerender/website-prerender.d.ts.map +1 -0
- package/dist/prerender.d.ts +2 -0
- package/dist/prerender.js +163 -151
- package/dist/shared/config/index.d.ts +1 -0
- package/dist/shared/config/index.d.ts.map +1 -1
- package/dist/shared/core/__tests__/theme-toggle.test.d.ts +2 -0
- package/dist/shared/core/__tests__/theme-toggle.test.d.ts.map +1 -0
- package/dist/shared/core/site-store.d.ts +1 -0
- package/dist/shared/core/site-store.d.ts.map +1 -1
- package/dist/shared/index.d.ts +5 -3
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/interfaces/{iFooterLink.d.ts → ifooter-link.d.ts} +1 -1
- package/dist/shared/interfaces/ifooter-link.d.ts.map +1 -0
- package/dist/shared/interfaces/{iRoute.d.ts → iroute.d.ts} +1 -1
- package/dist/shared/interfaces/iroute.d.ts.map +1 -0
- package/dist/shared/{pageContent.d.ts → page-content.d.ts} +4 -3
- package/dist/shared/page-content.d.ts.map +1 -0
- package/dist/shared/router.d.ts +23 -0
- package/dist/shared/router.d.ts.map +1 -0
- package/dist/shared/runtime.d.ts +6 -6
- package/dist/shared/runtime.d.ts.map +1 -1
- package/dist/shared/website-ui.d.ts +32 -0
- package/dist/shared/website-ui.d.ts.map +1 -0
- package/dist/shared.js +13 -8
- package/dist/ui/about-me/api.d.ts.map +1 -0
- package/dist/ui/{aboutme → about-me}/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -0
- package/dist/ui/about-me/styles.d.ts.map +1 -0
- 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 +9 -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 +27 -26
- package/dist/ui/admin/index.d.ts.map +1 -1
- package/dist/ui/admin/styles.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/banner/styles.d.ts.map +1 -1
- package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts +2 -0
- package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts.map +1 -0
- package/dist/ui/blog-viewer/index.d.ts +25 -0
- package/dist/ui/blog-viewer/index.d.ts.map +1 -0
- package/dist/ui/blog-viewer/styles.d.ts +2 -0
- package/dist/ui/blog-viewer/styles.d.ts.map +1 -0
- package/dist/ui/footer/index.d.ts +1 -1
- package/dist/ui/footer/index.d.ts.map +1 -1
- package/dist/ui/footer/styles.d.ts.map +1 -1
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts +2 -0
- package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts.map +1 -0
- package/dist/ui/story-viewer/index.d.ts +25 -0
- package/dist/ui/story-viewer/index.d.ts.map +1 -0
- package/dist/ui/story-viewer/styles.d.ts +2 -0
- package/dist/ui/story-viewer/styles.d.ts.map +1 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +17 -818
- package/package.json +35 -12
- package/public/assets/logo-placeholder.svg +21 -0
- package/dist/api/handlers/aboutme.d.ts.map +0 -1
- package/dist/api/handlers/staticdetails.d.ts.map +0 -1
- package/dist/prerender/pageContent.d.ts.map +0 -1
- package/dist/shared/interfaces/iFooterLink.d.ts.map +0 -1
- package/dist/shared/interfaces/iRoute.d.ts.map +0 -1
- package/dist/shared/pageContent.d.ts.map +0 -1
- package/dist/ui/aboutme/api.d.ts.map +0 -1
- package/dist/ui/aboutme/index.d.ts.map +0 -1
- package/dist/ui/aboutme/renderer.d.ts +0 -5
- package/dist/ui/aboutme/renderer.d.ts.map +0 -1
- package/dist/ui/aboutme/styles.d.ts.map +0 -1
- package/src/api/__tests__/info.test.ts +0 -44
- package/src/api/__tests__/utils.test.ts +0 -78
- package/src/api/handlers/aboutme.ts +0 -99
- package/src/api/handlers/content-api.ts +0 -268
- package/src/api/handlers/content.ts +0 -72
- package/src/api/handlers/home.ts +0 -79
- package/src/api/handlers/info.ts +0 -12
- package/src/api/handlers/logo.ts +0 -55
- package/src/api/handlers/staticdetails.ts +0 -48
- package/src/api/index.ts +0 -125
- package/src/api/utils.ts +0 -16
- package/src/prerender/__tests__/pageContent.test.ts +0 -54
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/index.ts +0 -138
- package/src/prerender/pageContent.ts +0 -263
- package/src/prerender/prerender.ts +0 -25
- package/src/prerender/template.ts +0 -65
- package/src/shared/config/api.ts +0 -16
- package/src/shared/config/index.ts +0 -41
- package/src/shared/config/types.ts +0 -16
- package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
- package/src/shared/core/site-store.ts +0 -38
- package/src/shared/core/theme-toggle.ts +0 -118
- package/src/shared/index.ts +0 -15
- package/src/shared/interfaces/iFooterLink.ts +0 -4
- package/src/shared/interfaces/iRoute.ts +0 -4
- package/src/shared/models/theme-variables.css +0 -25
- package/src/shared/pageContent.ts +0 -209
- package/src/shared/runtime.ts +0 -11
- package/src/shared/template.ts +0 -35
- package/src/styles/markdown.css +0 -129
- package/src/ui/aboutme/api.ts +0 -12
- package/src/ui/aboutme/index.ts +0 -155
- package/src/ui/aboutme/renderer.ts +0 -7
- package/src/ui/aboutme/styles.ts +0 -10
- package/src/ui/admin/index.ts +0 -492
- package/src/ui/admin/styles.ts +0 -317
- package/src/ui/banner/index.ts +0 -38
- package/src/ui/banner/styles.ts +0 -10
- package/src/ui/footer/index.ts +0 -37
- package/src/ui/footer/styles.ts +0 -9
- /package/{src/shared → dist}/styles/markdown.css +0 -0
- /package/{src → dist}/styles/theme.css +0 -0
- /package/dist/ui/{aboutme → about-me}/api.d.ts +0 -0
- /package/dist/ui/{aboutme → about-me}/styles.d.ts +0 -0
package/src/ui/aboutme/index.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
import { customElement, property, state } from 'lit/decorators.js';
|
|
3
|
-
|
|
4
|
-
import { ContentNode } from '@leadertechie/md2html';
|
|
5
|
-
import { Profile } from './api';
|
|
6
|
-
|
|
7
|
-
import { aboutmeStyles } from './styles';
|
|
8
|
-
|
|
9
|
-
import { AboutMeRenderer } from './renderer';
|
|
10
|
-
import { fetchAboutMe } from './api';
|
|
11
|
-
|
|
12
|
-
interface AboutMeData {
|
|
13
|
-
profile: Profile;
|
|
14
|
-
contentNodes: ContentNode[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@customElement('my-aboutme')
|
|
18
|
-
export class MyAboutme extends LitElement {
|
|
19
|
-
static styles = aboutmeStyles;
|
|
20
|
-
|
|
21
|
-
@property({ type: String })
|
|
22
|
-
accessor baseUrl = '';
|
|
23
|
-
|
|
24
|
-
@state()
|
|
25
|
-
accessor profile: Profile | null = null;
|
|
26
|
-
|
|
27
|
-
@state()
|
|
28
|
-
accessor contentNodes: ContentNode[] = [];
|
|
29
|
-
|
|
30
|
-
@state()
|
|
31
|
-
accessor loading = true;
|
|
32
|
-
|
|
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
|
-
public fetcher = fetchAboutMe;
|
|
40
|
-
|
|
41
|
-
constructor(renderer?: AboutMeRenderer) {
|
|
42
|
-
super();
|
|
43
|
-
this.renderer = renderer || new AboutMeRenderer();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private get apiBaseUrl(): string {
|
|
47
|
-
return this.baseUrl || this.getAttribute('base-url') || '';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async connectedCallback() {
|
|
51
|
-
super.connectedCallback();
|
|
52
|
-
|
|
53
|
-
// If we have initial data from SSR, use it immediately.
|
|
54
|
-
if (typeof window !== 'undefined' && (window as any).__HYDRATION_DATA__) {
|
|
55
|
-
const hydrationData = (window as any).__HYDRATION_DATA__;
|
|
56
|
-
this.profile = hydrationData.profile;
|
|
57
|
-
this.contentNodes = hydrationData.contentNodes;
|
|
58
|
-
this.loading = false;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
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
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
updated(changedProperties: Map<string, any>) {
|
|
70
|
-
super.updated(changedProperties);
|
|
71
|
-
|
|
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) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// If baseUrl changed and we have a valid baseUrl, load content
|
|
78
|
-
if (changedProperties.has('baseUrl') || changedProperties.has('base-url')) {
|
|
79
|
-
const url = this.apiBaseUrl;
|
|
80
|
-
if (url) {
|
|
81
|
-
this.loadContent();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private async loadContent() {
|
|
87
|
-
try {
|
|
88
|
-
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
|
-
const data = await this.fetcher(url);
|
|
94
|
-
|
|
95
|
-
this.profile = data.profile;
|
|
96
|
-
this.contentNodes = data.contentNodes;
|
|
97
|
-
this.loading = false;
|
|
98
|
-
// Wait for the render to complete
|
|
99
|
-
await this.updateComplete;
|
|
100
|
-
} catch (error) {
|
|
101
|
-
this.loading = false;
|
|
102
|
-
// Set fallback content
|
|
103
|
-
this.setFallbackContent();
|
|
104
|
-
// Wait for the render to complete
|
|
105
|
-
await this.updateComplete;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private setFallbackContent() {
|
|
110
|
-
this.profile = null; // No profile data
|
|
111
|
-
this.contentNodes = [
|
|
112
|
-
{
|
|
113
|
-
type: 'paragraph',
|
|
114
|
-
content: 'Unable to Load Content'
|
|
115
|
-
}
|
|
116
|
-
];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
render() {
|
|
120
|
-
if (this.loading) {
|
|
121
|
-
return html`<div class="aboutme"><div class="loading">Loading...</div></div>`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// If profile is not available, check if fallback content is present.
|
|
125
|
-
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>`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Render profile header + content
|
|
133
|
-
const profileImageUrl = this.profile.profileImageUrl
|
|
134
|
-
? (this.profile.profileImageUrl.startsWith('http') || this.profile.profileImageUrl.startsWith('/')
|
|
135
|
-
? this.profile.profileImageUrl
|
|
136
|
-
: `images/${this.profile.profileImageUrl}`)
|
|
137
|
-
: '';
|
|
138
|
-
|
|
139
|
-
return html`
|
|
140
|
-
<div class="aboutme">
|
|
141
|
-
<div class="profile-section">
|
|
142
|
-
${profileImageUrl ? html`
|
|
143
|
-
<img src="${profileImageUrl}" alt="${this.profile.name}"
|
|
144
|
-
class="profile-picture">
|
|
145
|
-
` : ''}
|
|
146
|
-
<h1>${this.profile.name}</h1>
|
|
147
|
-
<p class="profile-title">${this.profile.title} • ${this.profile.experience}</p>
|
|
148
|
-
</div>
|
|
149
|
-
<div class="content-section">
|
|
150
|
-
${this.renderer.renderContent(this.contentNodes)}
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
`;
|
|
154
|
-
}
|
|
155
|
-
}
|
package/src/ui/aboutme/styles.ts
DELETED
package/src/ui/admin/index.ts
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
2
|
-
import { customElement, state } from 'lit/decorators.js';
|
|
3
|
-
|
|
4
|
-
import { adminStyles } from './styles';
|
|
5
|
-
|
|
6
|
-
console.log('[AdminPortal] Module loading');
|
|
7
|
-
|
|
8
|
-
interface ContentItem {
|
|
9
|
-
key: string;
|
|
10
|
-
size: number;
|
|
11
|
-
uploaded?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface StaticDetails {
|
|
15
|
-
siteTitle?: string;
|
|
16
|
-
copyright?: string;
|
|
17
|
-
linkedin?: string;
|
|
18
|
-
github?: string;
|
|
19
|
-
email?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
console.log('[AdminPortal] About to define custom element');
|
|
23
|
-
|
|
24
|
-
@customElement('admin-portal')
|
|
25
|
-
export class AdminPortal extends LitElement {
|
|
26
|
-
static styles = adminStyles;
|
|
27
|
-
|
|
28
|
-
@state()
|
|
29
|
-
accessor apiKey = '';
|
|
30
|
-
|
|
31
|
-
@state()
|
|
32
|
-
accessor isAuthenticated = false;
|
|
33
|
-
|
|
34
|
-
@state()
|
|
35
|
-
accessor contentList: ContentItem[] = [];
|
|
36
|
-
|
|
37
|
-
@state()
|
|
38
|
-
accessor statusMessage = '';
|
|
39
|
-
|
|
40
|
-
@state()
|
|
41
|
-
accessor activeSection = 'profile';
|
|
42
|
-
|
|
43
|
-
@state()
|
|
44
|
-
accessor staticDetails: StaticDetails = {};
|
|
45
|
-
|
|
46
|
-
get apiUrl() {
|
|
47
|
-
return (window as any).__VITE_API_URL__ || import.meta.env.VITE_API_URL || 'http://localhost:8787';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
handleLogin(e: Event) {
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
const input = this.shadowRoot?.querySelector('#apiKey') as HTMLInputElement;
|
|
53
|
-
if (input.value) {
|
|
54
|
-
this.apiKey = input.value;
|
|
55
|
-
this.isAuthenticated = true;
|
|
56
|
-
this.fetchContent();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async fetchContent() {
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch(`${this.apiUrl}/content`, {
|
|
63
|
-
headers: {
|
|
64
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
if (res.ok) {
|
|
68
|
-
this.contentList = await res.json();
|
|
69
|
-
} else {
|
|
70
|
-
this.statusMessage = 'Failed to fetch content.';
|
|
71
|
-
}
|
|
72
|
-
} catch (e) {
|
|
73
|
-
this.statusMessage = 'Error fetching content.';
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async fetchStaticDetails() {
|
|
78
|
-
try {
|
|
79
|
-
const res = await fetch(`${this.apiUrl}/api/static`);
|
|
80
|
-
if (res.ok) {
|
|
81
|
-
this.staticDetails = await res.json();
|
|
82
|
-
}
|
|
83
|
-
} catch (e) {
|
|
84
|
-
// Ignore errors
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async handleUpload(key: string, file: File) {
|
|
89
|
-
try {
|
|
90
|
-
this.statusMessage = 'Uploading...';
|
|
91
|
-
const res = await fetch(`${this.apiUrl}/content/${key}`, {
|
|
92
|
-
method: 'PUT',
|
|
93
|
-
headers: {
|
|
94
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
95
|
-
},
|
|
96
|
-
body: file
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (res.ok) {
|
|
100
|
-
this.statusMessage = 'Upload successful!';
|
|
101
|
-
this.fetchContent();
|
|
102
|
-
} else {
|
|
103
|
-
this.statusMessage = 'Upload failed.';
|
|
104
|
-
}
|
|
105
|
-
} catch (e) {
|
|
106
|
-
this.statusMessage = 'Error uploading.';
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async handleClearCache() {
|
|
111
|
-
try {
|
|
112
|
-
this.statusMessage = 'Clearing cache...';
|
|
113
|
-
const res = await fetch(`${this.apiUrl}/cache-clear`, {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: {
|
|
116
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (res.ok) {
|
|
121
|
-
this.statusMessage = 'Cache cleared!';
|
|
122
|
-
} else {
|
|
123
|
-
this.statusMessage = 'Failed to clear cache.';
|
|
124
|
-
}
|
|
125
|
-
} catch (e) {
|
|
126
|
-
this.statusMessage = 'Error clearing cache.';
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async handleDelete(key: string) {
|
|
131
|
-
if (!confirm(`Delete ${key}?`)) return;
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const res = await fetch(`${this.apiUrl}/content/${key}`, {
|
|
135
|
-
method: 'DELETE',
|
|
136
|
-
headers: {
|
|
137
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (res.ok) {
|
|
142
|
-
this.fetchContent();
|
|
143
|
-
} else {
|
|
144
|
-
this.statusMessage = 'Delete failed.';
|
|
145
|
-
}
|
|
146
|
-
} catch (e) {
|
|
147
|
-
this.statusMessage = 'Error deleting.';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
getContent(key: string): ContentItem | undefined {
|
|
152
|
-
return this.contentList.find(c => c.key === key);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getSectionFiles(prefix: string): ContentItem[] {
|
|
156
|
-
return this.contentList.filter(c => c.key.startsWith(prefix));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
renderHomeSection() {
|
|
160
|
-
const home = this.getContent('home.md');
|
|
161
|
-
return html`
|
|
162
|
-
<div class="section">
|
|
163
|
-
<h3>Home Page</h3>
|
|
164
|
-
<p class="help-text">Content for your home page. Upload home.md with your main content.</p>
|
|
165
|
-
|
|
166
|
-
${home ? html`
|
|
167
|
-
<div class="current-file">
|
|
168
|
-
<strong>Current:</strong> home.md (${home.size} bytes)
|
|
169
|
-
<button class="btn-danger" @click=${() => this.handleDelete('home.md')}>Delete</button>
|
|
170
|
-
</div>
|
|
171
|
-
` : ''}
|
|
172
|
-
|
|
173
|
-
<input type="file" id="homeFile" accept=".md" />
|
|
174
|
-
<button class="btn-primary" @click=${() => {
|
|
175
|
-
const input = this.shadowRoot?.querySelector('#homeFile') as HTMLInputElement;
|
|
176
|
-
if (input.files?.[0]) this.handleUpload('home.md', input.files[0]);
|
|
177
|
-
}}>Upload home.md</button>
|
|
178
|
-
</div>
|
|
179
|
-
`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
renderProfileSection() {
|
|
183
|
-
const profile = this.getContent('profile.json');
|
|
184
|
-
return html`
|
|
185
|
-
<div class="section">
|
|
186
|
-
<h3>Profile <span class="required-badge">Required</span></h3>
|
|
187
|
-
<p class="help-text">This file contains your profile information (name, title, experience).</p>
|
|
188
|
-
|
|
189
|
-
${profile ? html`
|
|
190
|
-
<div class="current-file">
|
|
191
|
-
<strong>Current:</strong> profile.json (${profile.size} bytes)
|
|
192
|
-
<button class="btn-danger" @click=${() => this.handleDelete('profile.json')}>Delete</button>
|
|
193
|
-
</div>
|
|
194
|
-
` : ''}
|
|
195
|
-
|
|
196
|
-
<input type="file" id="profileFile" accept=".json" />
|
|
197
|
-
<button class="btn-primary" @click=${() => {
|
|
198
|
-
const input = this.shadowRoot?.querySelector('#profileFile') as HTMLInputElement;
|
|
199
|
-
if (input.files?.[0]) this.handleUpload('profile.json', input.files[0]);
|
|
200
|
-
}}>Upload profile.json</button>
|
|
201
|
-
</div>
|
|
202
|
-
`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
renderAboutMeSection() {
|
|
206
|
-
const aboutMe = this.getContent('about-me.md');
|
|
207
|
-
return html`
|
|
208
|
-
<div class="section">
|
|
209
|
-
<h3>About Me Page <span class="required-badge">Required</span></h3>
|
|
210
|
-
<p class="help-text">Content for your About Me page. Supports Markdown with frontmatter.</p>
|
|
211
|
-
|
|
212
|
-
${aboutMe ? html`
|
|
213
|
-
<div class="current-file">
|
|
214
|
-
<strong>Current:</strong> about-me.md (${aboutMe.size} bytes)
|
|
215
|
-
<button class="btn-danger" @click=${() => this.handleDelete('about-me.md')}>Delete</button>
|
|
216
|
-
</div>
|
|
217
|
-
` : ''}
|
|
218
|
-
|
|
219
|
-
<input type="file" id="aboutFile" accept=".md" />
|
|
220
|
-
<button class="btn-primary" @click=${() => {
|
|
221
|
-
const input = this.shadowRoot?.querySelector('#aboutFile') as HTMLInputElement;
|
|
222
|
-
if (input.files?.[0]) this.handleUpload('about-me.md', input.files[0]);
|
|
223
|
-
}}>Upload about-me.md</button>
|
|
224
|
-
</div>
|
|
225
|
-
`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
renderBlogsSection() {
|
|
229
|
-
const blogs = this.getSectionFiles('blogs/').filter(b => b.key.endsWith('.json'));
|
|
230
|
-
return html`
|
|
231
|
-
<div class="section">
|
|
232
|
-
<h3>Blog Posts</h3>
|
|
233
|
-
<p class="help-text">Each blog needs 2 files: a JSON (metadata) and MD (content) file.</p>
|
|
234
|
-
|
|
235
|
-
<h4>Upload New Blog</h4>
|
|
236
|
-
<input type="file" id="blogMetaFile" accept=".json" />
|
|
237
|
-
<input type="file" id="blogContentFile" accept=".md" />
|
|
238
|
-
<input type="text" id="blogSlug" placeholder="Slug (e.g., my-new-post)" class="mt-1" />
|
|
239
|
-
<button class="btn-primary" @click=${() => {
|
|
240
|
-
const metaInput = this.shadowRoot?.querySelector('#blogMetaFile') as HTMLInputElement;
|
|
241
|
-
const contentInput = this.shadowRoot?.querySelector('#blogContentFile') as HTMLInputElement;
|
|
242
|
-
const slugInput = this.shadowRoot?.querySelector('#blogSlug') as HTMLInputElement;
|
|
243
|
-
if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
|
|
244
|
-
this.handleUpload(`blogs/${slugInput.value}.json`, metaInput.files[0]);
|
|
245
|
-
this.handleUpload(`blogs/${slugInput.value}.md`, contentInput.files[0]);
|
|
246
|
-
}
|
|
247
|
-
}}>Upload Blog (JSON + MD)</button>
|
|
248
|
-
|
|
249
|
-
<div class="file-list">
|
|
250
|
-
<h4>Current Blogs (${blogs.length})</h4>
|
|
251
|
-
${blogs.map(b => html`
|
|
252
|
-
<div class="file-item">
|
|
253
|
-
<span>${b.key.replace('.json', '')}</span>
|
|
254
|
-
<button class="btn-danger" @click=${() => {
|
|
255
|
-
const slug = b.key.replace('blogs/', '').replace('.json', '');
|
|
256
|
-
this.handleDelete(`blogs/${slug}.json`);
|
|
257
|
-
this.handleDelete(`blogs/${slug}.md`);
|
|
258
|
-
}}>Delete</button>
|
|
259
|
-
</div>
|
|
260
|
-
`)}
|
|
261
|
-
${blogs.length === 0 ? html`<p>No blogs yet.</p>` : ''}
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
renderStoriesSection() {
|
|
268
|
-
const stories = this.getSectionFiles('stories/').filter(s => s.key.endsWith('.json'));
|
|
269
|
-
return html`
|
|
270
|
-
<div class="section">
|
|
271
|
-
<h3>Stories</h3>
|
|
272
|
-
<p class="help-text">Each story needs 2 files: a JSON (metadata) and MD (content) file.</p>
|
|
273
|
-
|
|
274
|
-
<h4>Upload New Story</h4>
|
|
275
|
-
<input type="file" id="storyMetaFile" accept=".json" />
|
|
276
|
-
<input type="file" id="storyContentFile" accept=".md" />
|
|
277
|
-
<input type="text" id="storySlug" placeholder="Slug (e.g., my-story)" class="mt-1" />
|
|
278
|
-
<button class="btn-primary" @click=${() => {
|
|
279
|
-
const metaInput = this.shadowRoot?.querySelector('#storyMetaFile') as HTMLInputElement;
|
|
280
|
-
const contentInput = this.shadowRoot?.querySelector('#storyContentFile') as HTMLInputElement;
|
|
281
|
-
const slugInput = this.shadowRoot?.querySelector('#storySlug') as HTMLInputElement;
|
|
282
|
-
if (metaInput.files?.[0] && contentInput.files?.[0] && slugInput.value) {
|
|
283
|
-
this.handleUpload(`stories/${slugInput.value}.json`, metaInput.files[0]);
|
|
284
|
-
this.handleUpload(`stories/${slugInput.value}.md`, contentInput.files[0]);
|
|
285
|
-
}
|
|
286
|
-
}}>Upload Story (JSON + MD)</button>
|
|
287
|
-
|
|
288
|
-
<div class="file-list">
|
|
289
|
-
<h4>Current Stories (${stories.length})</h4>
|
|
290
|
-
${stories.map(s => html`
|
|
291
|
-
<div class="file-item">
|
|
292
|
-
<span>${s.key.replace('.json', '')}</span>
|
|
293
|
-
<button class="btn-danger" @click=${() => {
|
|
294
|
-
const slug = s.key.replace('stories/', '').replace('.json', '');
|
|
295
|
-
this.handleDelete(`stories/${slug}.json`);
|
|
296
|
-
this.handleDelete(`stories/${slug}.md`);
|
|
297
|
-
}}>Delete</button>
|
|
298
|
-
</div>
|
|
299
|
-
`)}
|
|
300
|
-
${stories.length === 0 ? html`<p>No stories yet.</p>` : ''}
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
`;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
renderImagesSection() {
|
|
307
|
-
const images = this.getSectionFiles('images/');
|
|
308
|
-
return html`
|
|
309
|
-
<div class="section">
|
|
310
|
-
<h3>Images</h3>
|
|
311
|
-
<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>
|
|
312
|
-
|
|
313
|
-
<input type="file" id="imageFile" accept="image/*" />
|
|
314
|
-
<input type="text" id="imagePath" placeholder="Image name (e.g., profile-photo.jpg)" class="mt-1" />
|
|
315
|
-
<button class="btn-primary" @click=${() => {
|
|
316
|
-
const fileInput = this.shadowRoot?.querySelector('#imageFile') as HTMLInputElement;
|
|
317
|
-
const pathInput = this.shadowRoot?.querySelector('#imagePath') as HTMLInputElement;
|
|
318
|
-
if (fileInput.files?.[0] && pathInput.value) {
|
|
319
|
-
this.handleUpload(`images/${pathInput.value}`, fileInput.files[0]);
|
|
320
|
-
}
|
|
321
|
-
}}>Upload to images/</button>
|
|
322
|
-
|
|
323
|
-
<div class="file-list">
|
|
324
|
-
<h4>Current Images (${images.length})</h4>
|
|
325
|
-
${images.map(img => html`
|
|
326
|
-
<div class="file-item">
|
|
327
|
-
<span>${img.key} (${img.size} bytes)</span>
|
|
328
|
-
<button class="btn-danger" @click=${() => this.handleDelete(img.key)}>Delete</button>
|
|
329
|
-
</div>
|
|
330
|
-
`)}
|
|
331
|
-
${images.length === 0 ? html`<p>No images yet.</p>` : ''}
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
`;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
renderLogoSection() {
|
|
338
|
-
const logo = this.getContent('logo.svg');
|
|
339
|
-
return html`
|
|
340
|
-
<div class="section">
|
|
341
|
-
<h3>Site Logo</h3>
|
|
342
|
-
<p class="help-text">Upload your site logo (SVG format recommended). This appears in the header of your site.</p>
|
|
343
|
-
|
|
344
|
-
${logo ? html`
|
|
345
|
-
<div class="current-file">
|
|
346
|
-
<strong>Current:</strong> logo.svg (${logo.size} bytes)
|
|
347
|
-
<button class="btn-danger" @click=${() => this.handleDelete('logo.svg')}>Delete</button>
|
|
348
|
-
</div>
|
|
349
|
-
` : ''}
|
|
350
|
-
|
|
351
|
-
<input type="file" id="logoFile" accept=".svg,image/svg+xml" />
|
|
352
|
-
<button class="btn-primary mt-1 mb-1" @click=${() => {
|
|
353
|
-
const input = this.shadowRoot?.querySelector('#logoFile') as HTMLInputElement;
|
|
354
|
-
if (input.files?.[0]) this.handleUpload('logo.svg', input.files[0]);
|
|
355
|
-
}}>Upload logo.svg</button>
|
|
356
|
-
|
|
357
|
-
<div class="info-box">
|
|
358
|
-
<strong>Tip:</strong> Use an SVG with transparent background. The logo will automatically adapt to light/dark themes.
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
render() {
|
|
365
|
-
if (!this.isAuthenticated) {
|
|
366
|
-
return html`
|
|
367
|
-
<div class="container">
|
|
368
|
-
<div class="login-box">
|
|
369
|
-
<h2>Admin Login</h2>
|
|
370
|
-
<p>Enter your API key to manage content</p>
|
|
371
|
-
<form @submit=${this.handleLogin}>
|
|
372
|
-
<input type="password" id="apiKey" placeholder="API Key" />
|
|
373
|
-
<button type="submit" class="btn-primary">Login</button>
|
|
374
|
-
</form>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
`;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return html`
|
|
381
|
-
<div class="container">
|
|
382
|
-
<div class="header">
|
|
383
|
-
<h1>Content Manager</h1>
|
|
384
|
-
<button class="btn-secondary" @click=${() => this.handleClearCache()}>Clear Cache</button>
|
|
385
|
-
</div>
|
|
386
|
-
|
|
387
|
-
<div class="nav-tabs">
|
|
388
|
-
<button class="nav-tab ${this.activeSection === 'home' ? 'active' : ''}"
|
|
389
|
-
@click=${() => this.activeSection = 'home'}>Home</button>
|
|
390
|
-
<button class="nav-tab ${this.activeSection === 'profile' ? 'active' : ''}"
|
|
391
|
-
@click=${() => this.activeSection = 'profile'}>Profile</button>
|
|
392
|
-
<button class="nav-tab ${this.activeSection === 'aboutme' ? 'active' : ''}"
|
|
393
|
-
@click=${() => this.activeSection = 'aboutme'}>About Me</button>
|
|
394
|
-
<button class="nav-tab ${this.activeSection === 'blogs' ? 'active' : ''}"
|
|
395
|
-
@click=${() => this.activeSection = 'blogs'}>Blogs</button>
|
|
396
|
-
<button class="nav-tab ${this.activeSection === 'stories' ? 'active' : ''}"
|
|
397
|
-
@click=${() => this.activeSection = 'stories'}>Stories</button>
|
|
398
|
-
<button class="nav-tab ${this.activeSection === 'images' ? 'active' : ''}"
|
|
399
|
-
@click=${() => this.activeSection = 'images'}>Images</button>
|
|
400
|
-
<button class="nav-tab ${this.activeSection === 'logo' ? 'active' : ''}"
|
|
401
|
-
@click=${() => this.activeSection = 'logo'}>Logo</button>
|
|
402
|
-
<button class="nav-tab ${this.activeSection === 'static' ? 'active' : ''}"
|
|
403
|
-
@click=${() => {
|
|
404
|
-
this.activeSection = 'static';
|
|
405
|
-
this.fetchStaticDetails();
|
|
406
|
-
}}>Site Settings</button>
|
|
407
|
-
</div>
|
|
408
|
-
|
|
409
|
-
${this.statusMessage ? html`
|
|
410
|
-
<div class="status-message ${this.statusMessage.includes('successful') || this.statusMessage.includes('cleared') ? 'success' : this.statusMessage.includes('failed') || this.statusMessage.includes('Error') ? 'error' : ''}">
|
|
411
|
-
${this.statusMessage}
|
|
412
|
-
</div>
|
|
413
|
-
` : ''}
|
|
414
|
-
|
|
415
|
-
${this.activeSection === 'home' ? this.renderHomeSection() : ''}
|
|
416
|
-
${this.activeSection === 'profile' ? this.renderProfileSection() : ''}
|
|
417
|
-
${this.activeSection === 'aboutme' ? this.renderAboutMeSection() : ''}
|
|
418
|
-
${this.activeSection === 'blogs' ? this.renderBlogsSection() : ''}
|
|
419
|
-
${this.activeSection === 'stories' ? this.renderStoriesSection() : ''}
|
|
420
|
-
${this.activeSection === 'images' ? this.renderImagesSection() : ''}
|
|
421
|
-
${this.activeSection === 'logo' ? this.renderLogoSection() : ''}
|
|
422
|
-
${this.activeSection === 'static' ? this.renderStaticSection() : ''}
|
|
423
|
-
</div>
|
|
424
|
-
`;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
renderStaticSection() {
|
|
428
|
-
return html`
|
|
429
|
-
<div class="section">
|
|
430
|
-
<h3>Site Settings</h3>
|
|
431
|
-
<p class="help-text">Manage your site's static details like title, footer links, etc.</p>
|
|
432
|
-
|
|
433
|
-
<div class="mb-1">
|
|
434
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">Site Title</label>
|
|
435
|
-
<input type="text" id="siteTitle" .value=${this.staticDetails?.siteTitle || ''} />
|
|
436
|
-
</div>
|
|
437
|
-
|
|
438
|
-
<div class="mb-1">
|
|
439
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">Copyright Text</label>
|
|
440
|
-
<input type="text" id="copyright" .value=${this.staticDetails?.copyright || ''} />
|
|
441
|
-
</div>
|
|
442
|
-
|
|
443
|
-
<div class="mb-1">
|
|
444
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">LinkedIn URL</label>
|
|
445
|
-
<input type="text" id="linkedin" .value=${this.staticDetails?.linkedin || ''} />
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
<div class="mb-1">
|
|
449
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">GitHub URL</label>
|
|
450
|
-
<input type="text" id="github" .value=${this.staticDetails?.github || ''} />
|
|
451
|
-
</div>
|
|
452
|
-
|
|
453
|
-
<div class="mb-1">
|
|
454
|
-
<label style="display:block;margin-bottom:4px;font-weight:500">Email</label>
|
|
455
|
-
<input type="text" id="email" .value=${this.staticDetails?.email || ''} />
|
|
456
|
-
</div>
|
|
457
|
-
|
|
458
|
-
<button class="btn-primary" @click=${async () => {
|
|
459
|
-
const siteTitle = (this.shadowRoot?.querySelector('#siteTitle') as HTMLInputElement)?.value;
|
|
460
|
-
const copyright = (this.shadowRoot?.querySelector('#copyright') as HTMLInputElement)?.value;
|
|
461
|
-
const linkedin = (this.shadowRoot?.querySelector('#linkedin') as HTMLInputElement)?.value;
|
|
462
|
-
const github = (this.shadowRoot?.querySelector('#github') as HTMLInputElement)?.value;
|
|
463
|
-
const email = (this.shadowRoot?.querySelector('#email') as HTMLInputElement)?.value;
|
|
464
|
-
|
|
465
|
-
const data: Record<string, string> = {};
|
|
466
|
-
if (siteTitle) data.siteTitle = siteTitle;
|
|
467
|
-
if (copyright) data.copyright = copyright;
|
|
468
|
-
if (linkedin) data.linkedin = linkedin;
|
|
469
|
-
if (github) data.github = github;
|
|
470
|
-
if (email) data.email = email;
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
const url = `${this.apiUrl}/content/staticdetails.json`;
|
|
474
|
-
const res = await fetch(url, {
|
|
475
|
-
method: 'PUT',
|
|
476
|
-
headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' },
|
|
477
|
-
body: JSON.stringify(data)
|
|
478
|
-
});
|
|
479
|
-
if (res.ok) {
|
|
480
|
-
this.statusMessage = 'Settings saved!';
|
|
481
|
-
this.fetchContent();
|
|
482
|
-
} else {
|
|
483
|
-
this.statusMessage = 'Failed to save settings.';
|
|
484
|
-
}
|
|
485
|
-
} catch (e) {
|
|
486
|
-
this.statusMessage = 'Error saving settings.';
|
|
487
|
-
}
|
|
488
|
-
}}>Save Settings</button>
|
|
489
|
-
</div>
|
|
490
|
-
`;
|
|
491
|
-
}
|
|
492
|
-
}
|