@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,77 @@
1
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
2
+
3
+ export interface StaticDetails {
4
+ siteTitle?: string;
5
+ copyright?: string;
6
+ linkedin?: string;
7
+ github?: string;
8
+ email?: string;
9
+ }
10
+
11
+ export interface BasePageData {
12
+ routes: IRoute[];
13
+ footerLinks: IFooterLink[];
14
+ staticDetails: StaticDetails;
15
+ apiUrl: string;
16
+ baseUrl: string;
17
+ pathname: string;
18
+ name?: string;
19
+ title?: string;
20
+ }
21
+
22
+ export class BasePageGenerator {
23
+ protected generateBanner(routes: IRoute[], siteTitle: string, logo: string): string {
24
+ const navLinks = routes
25
+ .map(r => `<a href="${r.link}" class="nav-link" data-route="${r.link === "/" ? "home" : r.text.toLowerCase()}">${r.text}</a>`)
26
+ .join("");
27
+
28
+ return `
29
+ <my-banner header="${siteTitle}" logo="${logo}">
30
+ <theme-toggle slot="theme-switcher"></theme-toggle>
31
+ <nav slot="nav-links">
32
+ ${navLinks}
33
+ </nav>
34
+ </my-banner>`;
35
+ }
36
+
37
+ protected generateFooter(footerLinks: IFooterLink[], copyright: string): string {
38
+ return `
39
+ <my-footer
40
+ copyright="${copyright}"
41
+ footerlinks='${JSON.stringify(footerLinks)}'>
42
+ </my-footer>`;
43
+ }
44
+
45
+ protected generateMeta(title: string, description: string, canonicalUrl: string): void {
46
+ // This would be handled in the main function
47
+ }
48
+
49
+ protected wrapContent(banner: string, mainContent: string, footer: string): string {
50
+ return `${banner}${mainContent}${footer}`;
51
+ }
52
+
53
+ public generatePage(
54
+ pathname: string,
55
+ routes: IRoute[],
56
+ footerLinks: IFooterLink[],
57
+ staticDetails: StaticDetails,
58
+ apiUrl: string,
59
+ baseUrl: string,
60
+ mainContent: string,
61
+ title: string,
62
+ description: string
63
+ ): PageContent {
64
+ const logo = "/api/logo";
65
+ const banner = this.generateBanner(routes, staticDetails.siteTitle || "My Personal Website", logo);
66
+ const footer = this.generateFooter(footerLinks, staticDetails.copyright || "2026 My Personal Website");
67
+ const canonicalUrl = new URL(pathname, baseUrl).toString();
68
+ const content = this.wrapContent(banner, mainContent, footer);
69
+
70
+ return {
71
+ title,
72
+ description,
73
+ canonicalUrl,
74
+ content
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,35 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+
4
+ export interface BlogDetailPageData {
5
+ routes: IRoute[];
6
+ footerLinks: IFooterLink[];
7
+ staticDetails: StaticDetails;
8
+ apiUrl: string;
9
+ baseUrl: string;
10
+ pathname: string;
11
+ slug: string;
12
+ }
13
+
14
+ export class BlogDetailPageGenerator extends BasePageGenerator {
15
+ public generate(data: BlogDetailPageData): PageContent {
16
+ const { slug, staticDetails, ...baseData } = data;
17
+
18
+ const mainContent = `
19
+ <main class="container container-narrow">
20
+ <my-blog-viewer slug="${slug}"></my-blog-viewer>
21
+ </main>`;
22
+
23
+ return this.generatePage(
24
+ baseData.pathname,
25
+ baseData.routes,
26
+ baseData.footerLinks,
27
+ staticDetails,
28
+ baseData.apiUrl,
29
+ baseData.baseUrl,
30
+ mainContent,
31
+ `Blog: ${slug}`,
32
+ "Blog post"
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,43 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+ import { BlogMeta } from '../data-fetcher';
4
+
5
+ export interface BlogsListPageData {
6
+ routes: IRoute[];
7
+ footerLinks: IFooterLink[];
8
+ staticDetails: StaticDetails;
9
+ apiUrl: string;
10
+ baseUrl: string;
11
+ pathname: string;
12
+ latestBlogs: BlogMeta[];
13
+ name: string;
14
+ }
15
+
16
+ export class BlogsListPageGenerator extends BasePageGenerator {
17
+ public generate(data: BlogsListPageData): PageContent {
18
+ const { latestBlogs, name, staticDetails, ...baseData } = data;
19
+
20
+ const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
21
+
22
+ const mainContent = `
23
+ <main class="container container-wide">
24
+ <h1>Blogs</h1>
25
+ <input type="text" placeholder="Search blogs..." class="search-input" />
26
+ <div class="blog-list">
27
+ ${blogGists || "<p>No blogs yet.</p>"}
28
+ </div>
29
+ </main>`;
30
+
31
+ return this.generatePage(
32
+ baseData.pathname,
33
+ baseData.routes,
34
+ baseData.footerLinks,
35
+ staticDetails,
36
+ baseData.apiUrl,
37
+ baseData.baseUrl,
38
+ mainContent,
39
+ `Blogs – ${name}`,
40
+ "Read the latest blog posts."
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,54 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+ import { Profile, BlogMeta } from '../data-fetcher';
4
+
5
+ export interface HomePageData {
6
+ routes: IRoute[];
7
+ footerLinks: IFooterLink[];
8
+ staticDetails: StaticDetails;
9
+ apiUrl: string;
10
+ baseUrl: string;
11
+ pathname: string;
12
+ profile: Profile | null;
13
+ homeContent: string;
14
+ latestBlogs: BlogMeta[];
15
+ latestStories: BlogMeta[];
16
+ }
17
+
18
+ export class HomePageGenerator extends BasePageGenerator {
19
+ public generate(data: HomePageData): PageContent {
20
+ const { profile, homeContent, latestBlogs, latestStories, staticDetails, ...baseData } = data;
21
+
22
+ const name = profile?.name || "User";
23
+ const title = profile?.title || "Professional";
24
+
25
+ const homeHtml = homeContent || `<h1>Welcome to ${name}</h1><p>Upload home.md to customize this page.</p>`;
26
+ const blogGists = latestBlogs.map(b => `<div class="gist-card"><a href="/blogs/${b.slug}"><h4>${b.title}</h4></a><p>${b.summary}</p><small>${b.date}</small></div>`).join("");
27
+ const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
28
+
29
+ const mainContent = `
30
+ <main class="container container-wide column-layout">
31
+ <div class="main-column">
32
+ ${homeHtml}
33
+ </div>
34
+ <div class="sidebar-column">
35
+ <h3>Recent Blogs</h3>
36
+ ${blogGists || "<p>No blogs yet.</p>"}
37
+ <h3 class="mt-2">Recent Stories</h3>
38
+ ${storyGists || "<p>No stories yet.</p>"}
39
+ </div>
40
+ </main>`;
41
+
42
+ return this.generatePage(
43
+ baseData.pathname,
44
+ baseData.routes,
45
+ baseData.footerLinks,
46
+ staticDetails,
47
+ baseData.apiUrl,
48
+ baseData.baseUrl,
49
+ mainContent,
50
+ `${name} – ${title}`,
51
+ `Welcome to ${name}'s personal website. Professional portfolio and content.`
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,8 @@
1
+ export * from './base';
2
+ export * from './home';
3
+ export * from './about';
4
+ export * from './blogs-list';
5
+ export * from './stories-list';
6
+ export * from './blog-detail';
7
+ export * from './story-detail';
8
+ export * from './not-found';
@@ -0,0 +1,36 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+
4
+ export interface NotFoundPageData {
5
+ routes: IRoute[];
6
+ footerLinks: IFooterLink[];
7
+ staticDetails: StaticDetails;
8
+ apiUrl: string;
9
+ baseUrl: string;
10
+ pathname: string;
11
+ }
12
+
13
+ export class NotFoundPageGenerator extends BasePageGenerator {
14
+ public generate(data: NotFoundPageData): PageContent {
15
+ const { staticDetails, ...baseData } = data;
16
+
17
+ const mainContent = `
18
+ <main class="container container-narrow text-center">
19
+ <h1>Page Not Found</h1>
20
+ <p>The page you're looking for doesn't exist.</p>
21
+ <p><a href="/">Return to home</a></p>
22
+ </main>`;
23
+
24
+ return this.generatePage(
25
+ baseData.pathname,
26
+ baseData.routes,
27
+ baseData.footerLinks,
28
+ staticDetails,
29
+ baseData.apiUrl,
30
+ baseData.baseUrl,
31
+ mainContent,
32
+ "404 Not Found",
33
+ "The page you requested could not be found."
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,43 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+ import { BlogMeta } from '../data-fetcher';
4
+
5
+ export interface StoriesListPageData {
6
+ routes: IRoute[];
7
+ footerLinks: IFooterLink[];
8
+ staticDetails: StaticDetails;
9
+ apiUrl: string;
10
+ baseUrl: string;
11
+ pathname: string;
12
+ latestStories: BlogMeta[];
13
+ name: string;
14
+ }
15
+
16
+ export class StoriesListPageGenerator extends BasePageGenerator {
17
+ public generate(data: StoriesListPageData): PageContent {
18
+ const { latestStories, name, staticDetails, ...baseData } = data;
19
+
20
+ const storyGists = latestStories.map(s => `<div class="gist-card"><a href="/stories/${s.slug}"><h4>${s.title}</h4></a><p>${s.summary}</p><small>${s.date}</small></div>`).join("");
21
+
22
+ const mainContent = `
23
+ <main class="container container-wide">
24
+ <h1>Stories</h1>
25
+ <input type="text" placeholder="Search stories..." class="search-input" />
26
+ <div class="story-list">
27
+ ${storyGists || "<p>No stories yet.</p>"}
28
+ </div>
29
+ </main>`;
30
+
31
+ return this.generatePage(
32
+ baseData.pathname,
33
+ baseData.routes,
34
+ baseData.footerLinks,
35
+ staticDetails,
36
+ baseData.apiUrl,
37
+ baseData.baseUrl,
38
+ mainContent,
39
+ `Stories – ${name}`,
40
+ "Read the latest stories."
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ import { BasePageGenerator, StaticDetails } from './base';
2
+ import { IRoute, IFooterLink, PageContent } from '../page-content';
3
+
4
+ export interface StoryDetailPageData {
5
+ routes: IRoute[];
6
+ footerLinks: IFooterLink[];
7
+ staticDetails: StaticDetails;
8
+ apiUrl: string;
9
+ baseUrl: string;
10
+ pathname: string;
11
+ slug: string;
12
+ }
13
+
14
+ export class StoryDetailPageGenerator extends BasePageGenerator {
15
+ public generate(data: StoryDetailPageData): PageContent {
16
+ const { slug, staticDetails, ...baseData } = data;
17
+
18
+ const mainContent = `
19
+ <main class="container container-narrow">
20
+ <my-story-viewer slug="${slug}"></my-story-viewer>
21
+ </main>`;
22
+
23
+ return this.generatePage(
24
+ baseData.pathname,
25
+ baseData.routes,
26
+ baseData.footerLinks,
27
+ staticDetails,
28
+ baseData.apiUrl,
29
+ baseData.baseUrl,
30
+ mainContent,
31
+ `Story: ${slug}`,
32
+ "Story post"
33
+ );
34
+ }
35
+ }
@@ -4,7 +4,7 @@ export * from './types';
4
4
 
5
5
  const DEFAULT_INFRA: InfrastructureConfig = {
6
6
  baseUrl: typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5173',
7
- apiUrl: (typeof window !== 'undefined' && (window as any).__VITE_API_URL__) || 'http://localhost:8787'
7
+ apiUrl: (typeof window !== 'undefined' && ((window as any).__VITE_API_URL__ || (import.meta as any).env?.VITE_API_URL)) || 'http://localhost:8788'
8
8
  };
9
9
 
10
10
  const DEFAULT_STATIC: StaticDetails = {
@@ -79,7 +79,7 @@ export const generatePageContent = (
79
79
  const footerTemplate = `
80
80
  <my-footer
81
81
  copyright="${copyright}"
82
- footerlinks='\${JSON.stringify(footerLinks)}'>
82
+ footerLinks='${JSON.stringify(footerLinks)}'>
83
83
  </my-footer>`;
84
84
 
85
85
  const renderContentGists = (items: ContentMetadata[] = [], title: string, type: 'blogs' | 'stories') => {
@@ -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,28 +34,24 @@ 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
45
  this.loadContent();
64
46
  }
65
47
 
66
48
  updated(changedProperties: Map<string, any>) {
67
49
  super.updated(changedProperties);
68
50
 
69
- // Only load content from API if we haven't already loaded via hydration data
70
- if (this.loading === false && this.profile && this.contentNodes.length > 0) {
51
+ if (this.loading === false && this.profile && this.htmlContent) {
71
52
  return;
72
53
  }
73
54
 
74
- // If baseUrl changed and we have a valid baseUrl, load content
75
55
  if (changedProperties.has('baseUrl') || changedProperties.has('base-url')) {
76
56
  this.loadContent();
77
57
  }
@@ -87,42 +67,26 @@ export class MyAboutme extends LitElement {
87
67
 
88
68
  try {
89
69
  this.loading = true;
90
-
91
- // Fetch content from API using the injectable fetcher.
92
- // Use the apiBaseUrl getter to get either property or attribute
93
- const url = this.apiBaseUrl;
94
70
  const data = await this.fetcher(url);
95
71
 
96
72
  this.profile = data.profile;
97
- this.contentNodes = data.contentNodes;
73
+ this.htmlContent = (data as any).processedMarkdown;
98
74
  this.loading = false;
99
- // Wait for the render to complete
100
75
  await this.updateComplete;
101
76
  } catch (error) {
102
77
  this.loading = false;
103
- // Set fallback content
104
78
  this.setFallbackContent();
105
- // Wait for the render to complete
106
79
  await this.updateComplete;
107
80
  }
108
81
  }
109
82
 
110
83
  private setFallbackContent() {
111
- this.profile = null; // No profile data
112
- this.contentNodes = [
113
- {
114
- type: 'heading',
115
- content: 'Profile Not Found'
116
- },
117
- {
118
- type: 'paragraph',
119
- content: 'Your about-me profile has not been initialized yet. Please run the seed command to get started:'
120
- },
121
- {
122
- type: 'paragraph',
123
- content: 'npm run seed -- <username> <password>'
124
- }
125
- ];
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
+ `;
126
90
  }
127
91
 
128
92
  render() {
@@ -130,15 +94,10 @@ export class MyAboutme extends LitElement {
130
94
  return html`<div class="aboutme"><div class="loading">Loading...</div></div>`;
131
95
  }
132
96
 
133
- // If profile is not available, check if fallback content is present.
134
97
  if (!this.profile) {
135
- if (this.contentNodes && this.contentNodes.length > 0) {
136
- return html`<div class="aboutme">${this.renderer.renderContent(this.contentNodes)}</div>`;
137
- }
138
- 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>`;
139
99
  }
140
100
 
141
- // Render profile header + content
142
101
  const profileImageUrl = this.profile.profileImageUrl
143
102
  ? (this.profile.profileImageUrl.startsWith('http') || this.profile.profileImageUrl.startsWith('/')
144
103
  ? this.profile.profileImageUrl
@@ -155,10 +114,8 @@ export class MyAboutme extends LitElement {
155
114
  <h1>${this.profile.name}</h1>
156
115
  <p class="profile-title">${this.profile.title} • ${this.profile.experience}</p>
157
116
  </div>
158
- <div class="content-section">
159
- ${this.renderer.renderContent(this.contentNodes)}
160
- </div>
117
+ <div class="content-section" .innerHTML="${this.htmlContent}"></div>
161
118
  </div>
162
119
  `;
163
120
  }
164
- }
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
+ }