@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.
Files changed (197) hide show
  1. package/dist/api/__tests__/info.test.d.ts +2 -0
  2. package/dist/api/__tests__/info.test.d.ts.map +1 -0
  3. package/dist/api/__tests__/utils.test.d.ts +2 -0
  4. package/dist/api/__tests__/utils.test.d.ts.map +1 -0
  5. package/dist/api/content-utils.d.ts +27 -0
  6. package/dist/api/content-utils.d.ts.map +1 -0
  7. package/dist/api/handlers/{aboutme.d.ts → about-me.d.ts} +1 -1
  8. package/dist/api/handlers/about-me.d.ts.map +1 -0
  9. package/dist/api/handlers/auth-handler.d.ts +2 -0
  10. package/dist/api/handlers/auth-handler.d.ts.map +1 -0
  11. package/dist/api/handlers/auth.d.ts +23 -0
  12. package/dist/api/handlers/auth.d.ts.map +1 -0
  13. package/dist/api/handlers/content-api.d.ts +0 -1
  14. package/dist/api/handlers/content-api.d.ts.map +1 -1
  15. package/dist/api/handlers/content.d.ts.map +1 -1
  16. package/dist/api/handlers/home.d.ts.map +1 -1
  17. package/dist/api/handlers/{staticdetails.d.ts → static-details.d.ts} +1 -1
  18. package/dist/api/handlers/static-details.d.ts.map +1 -0
  19. package/dist/api/index.d.ts +7 -8
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/api/website-api.d.ts +10 -0
  22. package/dist/api/website-api.d.ts.map +1 -0
  23. package/dist/api.d.ts +2 -0
  24. package/dist/api.js +19 -589
  25. package/dist/assets/logo-placeholder.svg +21 -0
  26. package/dist/chunks/index-CGvOrVf8.js +213 -0
  27. package/dist/chunks/index-_AMi6ort.js +2690 -0
  28. package/dist/chunks/site-store-Vqmjjz9c.js +86 -0
  29. package/dist/chunks/template-C1tMqlPY.js +597 -0
  30. package/dist/chunks/website-api-CuyeBej-.js +920 -0
  31. package/dist/index.d.ts +5 -2
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +48 -352
  34. package/dist/prerender/__tests__/page-content.test.d.ts +2 -0
  35. package/dist/prerender/__tests__/page-content.test.d.ts.map +1 -0
  36. package/dist/prerender/__tests__/template.test.d.ts +2 -0
  37. package/dist/prerender/__tests__/template.test.d.ts.map +1 -0
  38. package/dist/prerender/data-fetcher.d.ts +19 -0
  39. package/dist/prerender/data-fetcher.d.ts.map +1 -0
  40. package/dist/prerender/index.d.ts +5 -4
  41. package/dist/prerender/index.d.ts.map +1 -1
  42. package/dist/prerender/{pageContent.d.ts → page-content.d.ts} +1 -1
  43. package/dist/prerender/page-content.d.ts.map +1 -0
  44. package/dist/prerender/page-generators/about.d.ts +16 -0
  45. package/dist/prerender/page-generators/about.d.ts.map +1 -0
  46. package/dist/prerender/page-generators/base.d.ts +26 -0
  47. package/dist/prerender/page-generators/base.d.ts.map +1 -0
  48. package/dist/prerender/page-generators/blog-detail.d.ts +15 -0
  49. package/dist/prerender/page-generators/blog-detail.d.ts.map +1 -0
  50. package/dist/prerender/page-generators/blogs-list.d.ts +17 -0
  51. package/dist/prerender/page-generators/blogs-list.d.ts.map +1 -0
  52. package/dist/prerender/page-generators/home.d.ts +19 -0
  53. package/dist/prerender/page-generators/home.d.ts.map +1 -0
  54. package/dist/prerender/page-generators/index.d.ts +9 -0
  55. package/dist/prerender/page-generators/index.d.ts.map +1 -0
  56. package/dist/prerender/page-generators/not-found.d.ts +14 -0
  57. package/dist/prerender/page-generators/not-found.d.ts.map +1 -0
  58. package/dist/prerender/page-generators/stories-list.d.ts +17 -0
  59. package/dist/prerender/page-generators/stories-list.d.ts.map +1 -0
  60. package/dist/prerender/page-generators/story-detail.d.ts +15 -0
  61. package/dist/prerender/page-generators/story-detail.d.ts.map +1 -0
  62. package/dist/prerender/website-prerender.d.ts +22 -0
  63. package/dist/prerender/website-prerender.d.ts.map +1 -0
  64. package/dist/prerender.d.ts +2 -0
  65. package/dist/prerender.js +163 -151
  66. package/dist/shared/config/index.d.ts +1 -0
  67. package/dist/shared/config/index.d.ts.map +1 -1
  68. package/dist/shared/core/__tests__/theme-toggle.test.d.ts +2 -0
  69. package/dist/shared/core/__tests__/theme-toggle.test.d.ts.map +1 -0
  70. package/dist/shared/core/site-store.d.ts +1 -0
  71. package/dist/shared/core/site-store.d.ts.map +1 -1
  72. package/dist/shared/index.d.ts +5 -3
  73. package/dist/shared/index.d.ts.map +1 -1
  74. package/dist/shared/interfaces/{iFooterLink.d.ts → ifooter-link.d.ts} +1 -1
  75. package/dist/shared/interfaces/ifooter-link.d.ts.map +1 -0
  76. package/dist/shared/interfaces/{iRoute.d.ts → iroute.d.ts} +1 -1
  77. package/dist/shared/interfaces/iroute.d.ts.map +1 -0
  78. package/dist/shared/{pageContent.d.ts → page-content.d.ts} +4 -3
  79. package/dist/shared/page-content.d.ts.map +1 -0
  80. package/dist/shared/router.d.ts +23 -0
  81. package/dist/shared/router.d.ts.map +1 -0
  82. package/dist/shared/runtime.d.ts +6 -6
  83. package/dist/shared/runtime.d.ts.map +1 -1
  84. package/dist/shared/website-ui.d.ts +32 -0
  85. package/dist/shared/website-ui.d.ts.map +1 -0
  86. package/dist/shared.js +13 -8
  87. package/dist/ui/about-me/api.d.ts.map +1 -0
  88. package/dist/ui/{aboutme → about-me}/index.d.ts +2 -10
  89. package/dist/ui/about-me/index.d.ts.map +1 -0
  90. package/dist/ui/about-me/styles.d.ts.map +1 -0
  91. package/dist/ui/admin/api.d.ts +16 -0
  92. package/dist/ui/admin/api.d.ts.map +1 -0
  93. package/dist/ui/admin/components/AboutMeSection.d.ts +7 -0
  94. package/dist/ui/admin/components/AboutMeSection.d.ts.map +1 -0
  95. package/dist/ui/admin/components/AdminSection.d.ts +13 -0
  96. package/dist/ui/admin/components/AdminSection.d.ts.map +1 -0
  97. package/dist/ui/admin/components/BlogsSection.d.ts +7 -0
  98. package/dist/ui/admin/components/BlogsSection.d.ts.map +1 -0
  99. package/dist/ui/admin/components/HomeSection.d.ts +7 -0
  100. package/dist/ui/admin/components/HomeSection.d.ts.map +1 -0
  101. package/dist/ui/admin/components/ImagesSection.d.ts +7 -0
  102. package/dist/ui/admin/components/ImagesSection.d.ts.map +1 -0
  103. package/dist/ui/admin/components/LoginForm.d.ts +9 -0
  104. package/dist/ui/admin/components/LoginForm.d.ts.map +1 -0
  105. package/dist/ui/admin/components/LogoSection.d.ts +7 -0
  106. package/dist/ui/admin/components/LogoSection.d.ts.map +1 -0
  107. package/dist/ui/admin/components/ProfileSection.d.ts +7 -0
  108. package/dist/ui/admin/components/ProfileSection.d.ts.map +1 -0
  109. package/dist/ui/admin/components/StaticSection.d.ts +9 -0
  110. package/dist/ui/admin/components/StaticSection.d.ts.map +1 -0
  111. package/dist/ui/admin/components/StoriesSection.d.ts +7 -0
  112. package/dist/ui/admin/components/StoriesSection.d.ts.map +1 -0
  113. package/dist/ui/admin/components/index.d.ts +11 -0
  114. package/dist/ui/admin/components/index.d.ts.map +1 -0
  115. package/dist/ui/admin/index.d.ts +27 -26
  116. package/dist/ui/admin/index.d.ts.map +1 -1
  117. package/dist/ui/admin/styles.d.ts.map +1 -1
  118. package/dist/ui/admin/types.d.ts +24 -0
  119. package/dist/ui/admin/types.d.ts.map +1 -0
  120. package/dist/ui/banner/styles.d.ts.map +1 -1
  121. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts +2 -0
  122. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts.map +1 -0
  123. package/dist/ui/blog-viewer/index.d.ts +25 -0
  124. package/dist/ui/blog-viewer/index.d.ts.map +1 -0
  125. package/dist/ui/blog-viewer/styles.d.ts +2 -0
  126. package/dist/ui/blog-viewer/styles.d.ts.map +1 -0
  127. package/dist/ui/footer/index.d.ts +1 -1
  128. package/dist/ui/footer/index.d.ts.map +1 -1
  129. package/dist/ui/footer/styles.d.ts.map +1 -1
  130. package/dist/ui/index.d.ts +7 -0
  131. package/dist/ui/index.d.ts.map +1 -0
  132. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts +2 -0
  133. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts.map +1 -0
  134. package/dist/ui/story-viewer/index.d.ts +25 -0
  135. package/dist/ui/story-viewer/index.d.ts.map +1 -0
  136. package/dist/ui/story-viewer/styles.d.ts +2 -0
  137. package/dist/ui/story-viewer/styles.d.ts.map +1 -0
  138. package/dist/ui.d.ts +1 -1
  139. package/dist/ui.js +17 -818
  140. package/package.json +35 -12
  141. package/public/assets/logo-placeholder.svg +21 -0
  142. package/dist/api/handlers/aboutme.d.ts.map +0 -1
  143. package/dist/api/handlers/staticdetails.d.ts.map +0 -1
  144. package/dist/prerender/pageContent.d.ts.map +0 -1
  145. package/dist/shared/interfaces/iFooterLink.d.ts.map +0 -1
  146. package/dist/shared/interfaces/iRoute.d.ts.map +0 -1
  147. package/dist/shared/pageContent.d.ts.map +0 -1
  148. package/dist/ui/aboutme/api.d.ts.map +0 -1
  149. package/dist/ui/aboutme/index.d.ts.map +0 -1
  150. package/dist/ui/aboutme/renderer.d.ts +0 -5
  151. package/dist/ui/aboutme/renderer.d.ts.map +0 -1
  152. package/dist/ui/aboutme/styles.d.ts.map +0 -1
  153. package/src/api/__tests__/info.test.ts +0 -44
  154. package/src/api/__tests__/utils.test.ts +0 -78
  155. package/src/api/handlers/aboutme.ts +0 -99
  156. package/src/api/handlers/content-api.ts +0 -268
  157. package/src/api/handlers/content.ts +0 -72
  158. package/src/api/handlers/home.ts +0 -79
  159. package/src/api/handlers/info.ts +0 -12
  160. package/src/api/handlers/logo.ts +0 -55
  161. package/src/api/handlers/staticdetails.ts +0 -48
  162. package/src/api/index.ts +0 -125
  163. package/src/api/utils.ts +0 -16
  164. package/src/prerender/__tests__/pageContent.test.ts +0 -54
  165. package/src/prerender/__tests__/template.test.ts +0 -54
  166. package/src/prerender/index.ts +0 -138
  167. package/src/prerender/pageContent.ts +0 -263
  168. package/src/prerender/prerender.ts +0 -25
  169. package/src/prerender/template.ts +0 -65
  170. package/src/shared/config/api.ts +0 -16
  171. package/src/shared/config/index.ts +0 -41
  172. package/src/shared/config/types.ts +0 -16
  173. package/src/shared/core/__tests__/theme-toggle.test.ts +0 -204
  174. package/src/shared/core/site-store.ts +0 -38
  175. package/src/shared/core/theme-toggle.ts +0 -118
  176. package/src/shared/index.ts +0 -15
  177. package/src/shared/interfaces/iFooterLink.ts +0 -4
  178. package/src/shared/interfaces/iRoute.ts +0 -4
  179. package/src/shared/models/theme-variables.css +0 -25
  180. package/src/shared/pageContent.ts +0 -209
  181. package/src/shared/runtime.ts +0 -11
  182. package/src/shared/template.ts +0 -35
  183. package/src/styles/markdown.css +0 -129
  184. package/src/ui/aboutme/api.ts +0 -12
  185. package/src/ui/aboutme/index.ts +0 -155
  186. package/src/ui/aboutme/renderer.ts +0 -7
  187. package/src/ui/aboutme/styles.ts +0 -10
  188. package/src/ui/admin/index.ts +0 -492
  189. package/src/ui/admin/styles.ts +0 -317
  190. package/src/ui/banner/index.ts +0 -38
  191. package/src/ui/banner/styles.ts +0 -10
  192. package/src/ui/footer/index.ts +0 -37
  193. package/src/ui/footer/styles.ts +0 -9
  194. /package/{src/shared → dist}/styles/markdown.css +0 -0
  195. /package/{src → dist}/styles/theme.css +0 -0
  196. /package/dist/ui/{aboutme → about-me}/api.d.ts +0 -0
  197. /package/dist/ui/{aboutme → about-me}/styles.d.ts +0 -0
@@ -0,0 +1,86 @@
1
+ const __vite_import_meta_env__ = {};
2
+ const DEFAULT_INFRA = {
3
+ baseUrl: typeof window !== "undefined" ? window.location.origin : "http://localhost:5173",
4
+ apiUrl: typeof window !== "undefined" && (window.__VITE_API_URL__ || __vite_import_meta_env__?.VITE_API_URL) || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8787")
5
+ };
6
+ const DEFAULT_STATIC = {
7
+ siteTitle: "My Personal Website",
8
+ siteDescription: "My Personal Website",
9
+ copyright: "2026 My Personal Website",
10
+ linkedin: "https://linkedin.com/in/yourname",
11
+ github: "https://github.com/yourname",
12
+ email: "yourname@domain.com"
13
+ };
14
+ let activeConfig = { ...DEFAULT_INFRA, ...DEFAULT_STATIC };
15
+ async function initializeConfig(infra) {
16
+ if (infra) {
17
+ if (infra.baseUrl) activeConfig.baseUrl = infra.baseUrl;
18
+ if (infra.apiUrl) activeConfig.apiUrl = infra.apiUrl;
19
+ }
20
+ try {
21
+ const res = await fetch(`${activeConfig.apiUrl}/api/static`);
22
+ if (res.ok) {
23
+ const remoteStatic = await res.json().catch(() => ({}));
24
+ activeConfig = { ...activeConfig, ...remoteStatic };
25
+ }
26
+ } catch (e) {
27
+ console.warn("Failed to load static details from R2, using defaults.");
28
+ }
29
+ return activeConfig;
30
+ }
31
+ async function refreshConfig() {
32
+ try {
33
+ const res = await fetch(`${activeConfig.apiUrl}/api/static`);
34
+ if (res.ok) {
35
+ const remoteStatic = await res.json().catch(() => ({}));
36
+ activeConfig = { ...activeConfig, ...remoteStatic };
37
+ }
38
+ } catch (e) {
39
+ console.warn("Failed to refresh static details.");
40
+ }
41
+ return activeConfig;
42
+ }
43
+ function getConfig() {
44
+ return activeConfig;
45
+ }
46
+ class SiteStore {
47
+ constructor() {
48
+ this.config = null;
49
+ this.listeners = /* @__PURE__ */ new Set();
50
+ }
51
+ static getInstance() {
52
+ if (!SiteStore.instance) {
53
+ SiteStore.instance = new SiteStore();
54
+ }
55
+ return SiteStore.instance;
56
+ }
57
+ async init(infra) {
58
+ this.config = await initializeConfig(infra);
59
+ this.notify();
60
+ return this.config;
61
+ }
62
+ subscribe(listener) {
63
+ this.listeners.add(listener);
64
+ if (this.config) listener(this.config);
65
+ return () => this.listeners.delete(listener);
66
+ }
67
+ notify() {
68
+ if (this.config) {
69
+ this.listeners.forEach((l) => l(this.config));
70
+ }
71
+ }
72
+ async refresh() {
73
+ await refreshConfig();
74
+ this.config = getConfig();
75
+ this.notify();
76
+ }
77
+ getConfig() {
78
+ return this.config || getConfig();
79
+ }
80
+ }
81
+ export {
82
+ SiteStore as S,
83
+ getConfig as g,
84
+ initializeConfig as i,
85
+ refreshConfig as r
86
+ };
@@ -0,0 +1,597 @@
1
+ import { S as SiteStore } from "./site-store-Vqmjjz9c.js";
2
+ import { MarkdownPipeline } from "@leadertechie/r2tohtml";
3
+ const pipeline = new MarkdownPipeline({
4
+ imagePathPrefix: "images/",
5
+ styleOptions: {
6
+ classPrefix: "md-",
7
+ addHeadingIds: true
8
+ }
9
+ });
10
+ const renderMarkdown = (content) => {
11
+ if (!content) return "";
12
+ return pipeline.renderMarkdown(content);
13
+ };
14
+ const generatePageContent = (pathname, routes, footerLinks, data) => {
15
+ const logo = "/api/logo";
16
+ const siteTitle = data?.siteTitle || "My Personal Website";
17
+ const siteDescription = data?.siteDescription || "Welcome to my professional portfolio and blog.";
18
+ const copyright = data?.copyright || "2026 All Rights Reserved";
19
+ const baseUrl = data?.baseUrl || "";
20
+ const canonicalUrl = baseUrl ? new URL(pathname, baseUrl).toString() : pathname;
21
+ const navLinks = routes.map(
22
+ (route) => `<a href="${route.link}" class="nav-link" data-route="${route.link === "/" ? "home" : route.text.toLowerCase()}">${route.text}</a>`
23
+ ).join("");
24
+ const bannerTemplate = `
25
+ <my-banner header="${siteTitle}" logo="${logo}">
26
+ <theme-toggle slot="theme-switcher"></theme-toggle>
27
+ <nav slot="nav-links">
28
+ ${navLinks}
29
+ </nav>
30
+ </my-banner>`;
31
+ const footerTemplate = `
32
+ <my-footer
33
+ copyright="${copyright}"
34
+ footer-links='${JSON.stringify(footerLinks)}'>
35
+ </my-footer>`;
36
+ const renderContentGists = (items = [], title, type) => {
37
+ const listHtml = items.length > 0 ? items.map((item) => `
38
+ <div class="gist-card">
39
+ <a href="/${type}/${item.slug}" data-route="${type}-${item.slug}"><h4>${item.title}</h4></a>
40
+ <p>${item.summary || item.description || ""}</p>
41
+ <small>${new Date(item.date).toLocaleDateString()}</small>
42
+ </div>
43
+ `).join("") : `<p>No ${type} available yet.</p>`;
44
+ return `
45
+ <div class="recent-content-section ${title.includes("Stories") ? "mt-2" : ""}">
46
+ <h3 class="border-bottom pb-05">${title}</h3>
47
+ ${listHtml}
48
+ </div>
49
+ `;
50
+ };
51
+ if (pathname === "/" || pathname === "") {
52
+ const homeHtml = data?.content || `<h1>Welcome to ${siteTitle}</h1><p>Start by configuring your content in the admin portal.</p>`;
53
+ const mainContent = `
54
+ ${bannerTemplate}
55
+ <main class="container container-wide column-layout row-layout">
56
+ <div class="home-main-content main-column text-left">
57
+ ${homeHtml}
58
+ </div>
59
+ <aside class="home-side-content sidebar-column">
60
+ ${renderContentGists(data?.blogs, "Recent Blogs", "blogs")}
61
+ ${renderContentGists(data?.stories, "Recent Stories", "stories")}
62
+ </aside>
63
+ </main>
64
+ ${footerTemplate}`;
65
+ return {
66
+ title: `${siteTitle} - Home`,
67
+ description: siteDescription,
68
+ canonicalUrl,
69
+ content: mainContent
70
+ };
71
+ } else if (pathname === "/blogs" || pathname === "/stories" || pathname.startsWith("/blogs/") || pathname.startsWith("/stories/")) {
72
+ const isBlog = pathname.includes("blogs");
73
+ const type = isBlog ? "blogs" : "stories";
74
+ const items = isBlog ? data?.blogs : data?.stories;
75
+ const title = type.charAt(0).toUpperCase() + type.slice(1);
76
+ const currentSlug = data?.slug;
77
+ const mainContent = `
78
+ ${bannerTemplate}
79
+ <main class="container container-wide">
80
+ <div class="mb-2">
81
+ <input type="text" id="content-search" placeholder="Search ${type}..." class="search-input search-input-lg">
82
+ </div>
83
+ <div class="column-layout">
84
+ <aside class="sidebar-nav sidebar-column">
85
+ <div id="content-list">
86
+ ${items?.map((item) => {
87
+ const searchTerms = [
88
+ item.title,
89
+ ...item.tags || [],
90
+ item.summary || item.description || "",
91
+ item.slug
92
+ ].join(" ").toLowerCase();
93
+ return `
94
+ <div class="list-item sidebar-item ${item.slug === currentSlug ? "active" : ""}"
95
+ data-search="${searchTerms}">
96
+ <a href="/${type}/${item.slug}" data-route="${type}-${item.slug}" class="sidebar-item sidebar-item-link">
97
+ <h4>${item.title}</h4>
98
+ <small>${new Date(item.date).toLocaleDateString()}</small>
99
+ </a>
100
+ </div>
101
+ `;
102
+ }).join("") || `<p>No ${type} available yet.</p>`}
103
+ </div>
104
+ </aside>
105
+ <div class="wide-main-column text-left">
106
+ <div id="content-viewer">
107
+ ${currentSlug ? isBlog ? `<my-blog-viewer slug="${currentSlug}" api-url="${data?.apiUrl || ""}"></my-blog-viewer>` : `<my-story-viewer slug="${currentSlug}" api-url="${data?.apiUrl || ""}"></my-story-viewer>` : items && items.length > 0 ? `<p>Select a ${type.slice(0, -1)} to read.</p>` : ""}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </main>
112
+ ${footerTemplate}`;
113
+ return {
114
+ title: `${title} - ${siteTitle}`,
115
+ description: `Read the latest ${type} from ${siteTitle}.`,
116
+ canonicalUrl,
117
+ content: mainContent
118
+ };
119
+ } else if (pathname === "/about-me") {
120
+ const apiBaseUrl = data?.apiUrl || "";
121
+ const mainContent = `
122
+ ${bannerTemplate}
123
+ <main class="container container-narrow">
124
+ <my-aboutme base-url="${apiBaseUrl}"></my-aboutme>
125
+ </main>
126
+ ${footerTemplate}`;
127
+ return {
128
+ title: `About - ${siteTitle}`,
129
+ description: `Learn more about ${siteTitle}.`,
130
+ canonicalUrl,
131
+ content: mainContent
132
+ };
133
+ } else {
134
+ const mainContent = `
135
+ ${bannerTemplate}
136
+ <main class="container container-narrow text-center page-content">
137
+ <h1 class="page-title">Page Not Found</h1>
138
+ <p>The page you're looking for doesn't exist.</p>
139
+ <p><a href="/">Return to home</a></p>
140
+ </main>
141
+ ${footerTemplate}`;
142
+ return {
143
+ title: `404 Not Found - ${siteTitle}`,
144
+ description: "The page you requested could not be found.",
145
+ canonicalUrl,
146
+ content: mainContent
147
+ };
148
+ }
149
+ };
150
+ class Router {
151
+ constructor(ui) {
152
+ this.ui = ui;
153
+ this.logo = "/api/logo";
154
+ this.appElement = null;
155
+ const store = ui.getStore();
156
+ const config = store.getConfig();
157
+ this.routes = [
158
+ { link: "/", text: "Home" },
159
+ { link: "/blogs", text: "Blogs" },
160
+ { link: "/stories", text: "Stories" },
161
+ { link: "/about-me", text: "About Me" }
162
+ ];
163
+ this.apiUrl = config.apiUrl;
164
+ }
165
+ get config() {
166
+ return this.ui.getStore().getConfig();
167
+ }
168
+ get siteTitle() {
169
+ return this.config.siteTitle;
170
+ }
171
+ get copyright() {
172
+ return this.config.copyright;
173
+ }
174
+ get footerLinks() {
175
+ const normalizeUrl = (url) => {
176
+ if (!url) return "";
177
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
178
+ if (url.startsWith("www.")) return `https://${url}`;
179
+ return url;
180
+ };
181
+ return [
182
+ { text: "LinkedIn", link: normalizeUrl(this.config.linkedin) || "https://linkedin.com" },
183
+ { text: "GitHub", link: normalizeUrl(this.config.github) || "https://github.com" },
184
+ { text: "Email", link: this.config.email ? `mailto:${this.config.email}` : "mailto:hello@example.com" }
185
+ ];
186
+ }
187
+ init(appElementId = "app") {
188
+ this.appElement = document.getElementById(appElementId);
189
+ if (!this.appElement) {
190
+ console.error(`App element with id ${appElementId} not found`);
191
+ return;
192
+ }
193
+ this.setupEventListeners();
194
+ this.navigate(window.location.pathname);
195
+ }
196
+ setupEventListeners() {
197
+ document.body.addEventListener("click", (event) => {
198
+ const target = event.target.closest("a[data-route]");
199
+ if (target) {
200
+ event.preventDefault();
201
+ const route = target.getAttribute("href");
202
+ if (route && route !== window.location.pathname) {
203
+ window.history.pushState({}, "", route);
204
+ this.navigate(route);
205
+ }
206
+ }
207
+ });
208
+ window.addEventListener("popstate", () => {
209
+ this.navigate(window.location.pathname);
210
+ });
211
+ document.body.addEventListener("input", (event) => {
212
+ const target = event.target;
213
+ if (target.id === "content-search") {
214
+ const query = target.value.toLowerCase().trim();
215
+ const items = document.querySelectorAll("#content-list .list-item");
216
+ items.forEach((item) => {
217
+ const searchText = item.getAttribute("data-search") || "";
218
+ if (searchText.includes(query)) {
219
+ item.classList.remove("hidden");
220
+ } else {
221
+ item.classList.add("hidden");
222
+ }
223
+ });
224
+ }
225
+ });
226
+ }
227
+ async navigate(path) {
228
+ if (path.startsWith("/blogs/") && path.length > 7) {
229
+ await this.renderContentDetailPage(path);
230
+ return;
231
+ }
232
+ if (path.startsWith("/stories/") && path.length > 9) {
233
+ await this.renderContentDetailPage(path);
234
+ return;
235
+ }
236
+ switch (path) {
237
+ case "/":
238
+ await this.renderHomePage();
239
+ break;
240
+ case "/about-me":
241
+ this.renderAboutMePage();
242
+ break;
243
+ case "/blogs":
244
+ case "/stories":
245
+ await this.renderContentListPage(path);
246
+ break;
247
+ case "/admin":
248
+ await this.renderAdminPage();
249
+ break;
250
+ default:
251
+ await this.renderHomePage();
252
+ }
253
+ }
254
+ setPageMeta(title, description, url) {
255
+ document.title = title;
256
+ const metaTags = [
257
+ { name: "description", content: description },
258
+ { property: "og:title", content: title },
259
+ { property: "og:description", content: description },
260
+ { property: "og:url", content: url }
261
+ ];
262
+ metaTags.forEach((tag) => {
263
+ let element = tag.name ? document.querySelector(`meta[name="${tag.name}"]`) : document.querySelector(`meta[property="${tag.property}"]`);
264
+ if (!element) {
265
+ element = document.createElement("meta");
266
+ if (tag.name) element.setAttribute("name", tag.name);
267
+ if (tag.property) element.setAttribute("property", tag.property);
268
+ document.head.appendChild(element);
269
+ }
270
+ element.setAttribute("content", tag.content);
271
+ });
272
+ let canonicalLink = document.querySelector('link[rel="canonical"]');
273
+ if (!canonicalLink) {
274
+ canonicalLink = document.createElement("link");
275
+ canonicalLink.setAttribute("rel", "canonical");
276
+ document.head.appendChild(canonicalLink);
277
+ }
278
+ canonicalLink.setAttribute("href", url);
279
+ }
280
+ async renderHomePage() {
281
+ let blogs = [];
282
+ let stories = [];
283
+ let homeContent = "";
284
+ try {
285
+ const [blogsRes, storiesRes, homeRes] = await Promise.all([
286
+ fetch(`${this.apiUrl}/api/blogs`),
287
+ fetch(`${this.apiUrl}/api/stories`),
288
+ fetch(`${this.apiUrl}/api/home`)
289
+ ]);
290
+ if (blogsRes.ok) {
291
+ const data = await blogsRes.json().catch(() => []);
292
+ blogs = Array.isArray(data) ? data.slice(0, 3) : [];
293
+ }
294
+ if (storiesRes.ok) {
295
+ const data = await storiesRes.json().catch(() => []);
296
+ stories = Array.isArray(data) ? data.slice(0, 3) : [];
297
+ }
298
+ if (homeRes.ok) {
299
+ const data = await homeRes.json().catch(() => ({}));
300
+ homeContent = data.content || "";
301
+ }
302
+ } catch (e) {
303
+ console.error("Failed to fetch home content", e);
304
+ }
305
+ const pageContent = generatePageContent("/", this.routes, this.footerLinks, {
306
+ blogs,
307
+ stories,
308
+ content: homeContent,
309
+ siteTitle: this.siteTitle,
310
+ copyright: this.copyright
311
+ });
312
+ if (this.appElement) this.appElement.innerHTML = pageContent.content;
313
+ this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
314
+ }
315
+ renderAboutMePage() {
316
+ const pageContent = generatePageContent("/about-me", this.routes, this.footerLinks, {
317
+ siteTitle: this.siteTitle,
318
+ copyright: this.copyright,
319
+ apiUrl: this.apiUrl
320
+ });
321
+ if (this.appElement) this.appElement.innerHTML = pageContent.content;
322
+ this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
323
+ }
324
+ async renderContentListPage(pathname) {
325
+ const type = pathname === "/blogs" ? "blogs" : "stories";
326
+ let items = [];
327
+ try {
328
+ const res = await fetch(`${this.apiUrl}/api/${type}`);
329
+ if (res.ok) items = await res.json();
330
+ } catch (e) {
331
+ }
332
+ const latestSlug = items.length > 0 ? items[0].slug : void 0;
333
+ const data = type === "blogs" ? { blogs: items, slug: latestSlug, apiUrl: this.apiUrl } : { stories: items, slug: latestSlug, apiUrl: this.apiUrl };
334
+ const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
335
+ ...data,
336
+ siteTitle: this.siteTitle,
337
+ copyright: this.copyright
338
+ });
339
+ if (this.appElement) this.appElement.innerHTML = pageContent.content;
340
+ this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
341
+ }
342
+ async renderContentDetailPage(pathname) {
343
+ const isBlog = pathname.startsWith("/blogs/");
344
+ const type = isBlog ? "blogs" : "stories";
345
+ const slug = pathname.replace(`/${type}/`, "").split("/")[0];
346
+ let items = [];
347
+ try {
348
+ const res = await fetch(`${this.apiUrl}/api/${type}`);
349
+ if (res.ok) items = await res.json();
350
+ } catch (e) {
351
+ }
352
+ const data = type === "blogs" ? { blogs: items, slug, apiUrl: this.apiUrl } : { stories: items, slug, apiUrl: this.apiUrl };
353
+ const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
354
+ ...data,
355
+ siteTitle: this.siteTitle,
356
+ copyright: this.copyright
357
+ });
358
+ if (this.appElement) this.appElement.innerHTML = pageContent.content;
359
+ this.setPageMeta(`${slug.replace(/-/g, " ")} - ${this.siteTitle}`, "Read more content", window.location.href);
360
+ }
361
+ async renderAdminPage() {
362
+ generatePageContent("/admin", this.routes, this.footerLinks, {
363
+ siteTitle: this.siteTitle,
364
+ copyright: this.copyright
365
+ });
366
+ if (this.appElement) {
367
+ this.appElement.innerHTML = `
368
+ <my-banner header="${this.siteTitle}" logo="${this.logo}">
369
+ <theme-toggle slot="theme-switcher"></theme-toggle>
370
+ <nav slot="nav-links">
371
+ <a href="/" class="nav-link" data-route="home">Home</a>
372
+ </nav>
373
+ </my-banner>
374
+ <main class="container container-medium">
375
+ <admin-portal></admin-portal>
376
+ </main>
377
+ <my-footer copyright="${this.copyright}" footerLinks='${JSON.stringify(this.footerLinks)}'></my-footer>
378
+ `;
379
+ }
380
+ }
381
+ }
382
+ class WebsiteUI {
383
+ constructor(config = {}) {
384
+ this.router = null;
385
+ this.store = SiteStore.getInstance();
386
+ this.config = config;
387
+ }
388
+ static getInstance(config) {
389
+ if (!WebsiteUI.instance) {
390
+ WebsiteUI.instance = new WebsiteUI(config);
391
+ } else if (config) {
392
+ WebsiteUI.instance.config = { ...WebsiteUI.instance.config, ...config };
393
+ }
394
+ return WebsiteUI.instance;
395
+ }
396
+ async bootstrap() {
397
+ await this.store.init({
398
+ apiUrl: this.config.apiUrl,
399
+ baseUrl: this.config.baseUrl
400
+ });
401
+ this.store.subscribe(() => {
402
+ this.applyTheme();
403
+ this.updateFavicon();
404
+ if (this.router && window.location.pathname !== "/admin") {
405
+ this.router.navigate(window.location.pathname);
406
+ }
407
+ });
408
+ this.applyTheme();
409
+ this.updateFavicon();
410
+ this.router = new Router(this);
411
+ this.router.init(this.config.appElementId || "app");
412
+ if (this.config.onBootstrap) {
413
+ await this.config.onBootstrap(this);
414
+ }
415
+ console.log("WebsiteUI bootstrapped");
416
+ }
417
+ applyTheme() {
418
+ if (!this.config.theme) return;
419
+ const { theme } = this.config;
420
+ const root = document.documentElement;
421
+ if (theme.primaryColor) root.style.setProperty("--link-color", theme.primaryColor);
422
+ if (theme.backgroundColor) root.style.setProperty("--background-color", theme.backgroundColor);
423
+ if (theme.textColor) root.style.setProperty("--text-color", theme.textColor);
424
+ if (theme.customCss) {
425
+ const style = document.createElement("style");
426
+ style.textContent = theme.customCss;
427
+ document.head.appendChild(style);
428
+ }
429
+ }
430
+ updateFavicon() {
431
+ const favicon = document.querySelector('link[rel="icon"]');
432
+ const logoUrl = `/api/logo?t=${Date.now()}`;
433
+ if (favicon) {
434
+ favicon.setAttribute("href", logoUrl);
435
+ } else {
436
+ const newFavicon = document.createElement("link");
437
+ newFavicon.rel = "icon";
438
+ newFavicon.href = logoUrl;
439
+ document.head.appendChild(newFavicon);
440
+ }
441
+ }
442
+ getStore() {
443
+ return this.store;
444
+ }
445
+ getConfig() {
446
+ return this.config;
447
+ }
448
+ getRouter() {
449
+ return this.router;
450
+ }
451
+ }
452
+ const bootstrap = (config) => WebsiteUI.getInstance(config).bootstrap();
453
+ class ThemeToggle extends HTMLElement {
454
+ constructor() {
455
+ super();
456
+ this.toggleTheme = () => {
457
+ const currentTheme = document.documentElement.getAttribute("data-theme");
458
+ const newTheme = currentTheme === "dark" ? "light" : "dark";
459
+ document.documentElement.setAttribute("data-theme", newTheme);
460
+ localStorage.setItem("theme", newTheme);
461
+ this.updateToggleButton(newTheme);
462
+ this.dispatchThemeChangeEvent(newTheme);
463
+ };
464
+ this.attachShadow({ mode: "open" });
465
+ this.render();
466
+ this.applyThemeFromLocalStorage();
467
+ }
468
+ connectedCallback() {
469
+ this.shadowRoot?.querySelector("button")?.addEventListener("click", this.toggleTheme);
470
+ }
471
+ disconnectedCallback() {
472
+ this.shadowRoot?.querySelector("button")?.removeEventListener("click", this.toggleTheme);
473
+ }
474
+ render() {
475
+ if (this.shadowRoot) {
476
+ this.shadowRoot.innerHTML = `
477
+ <style>
478
+ :host {
479
+ display: inline-block;
480
+ }
481
+ button {
482
+ background: none;
483
+ border: none;
484
+ cursor: pointer;
485
+ font-size: 1.5rem;
486
+ padding: 0.5rem;
487
+ color: var(--text-color, #213547); /* Default for light mode */
488
+ transition: color 0.3s ease;
489
+ }
490
+ button:hover {
491
+ color: var(--primary-color, #747bff); /* Hover color */
492
+ }
493
+ /* Dark mode specific styles for the button */
494
+ html[data-theme='dark'] button {
495
+ color: var(--dark-mode-text-color, rgba(255, 255, 255, 0.87));
496
+ }
497
+ html[data-theme='dark'] button:hover {
498
+ color: var(--dark-mode-primary-color, #646cff);
499
+ }
500
+ </style>
501
+ <button id="theme-toggle" aria-label="Toggle theme">
502
+ <span class="icon">${this.getSunIcon()}</span> <!-- Default to sun for light mode -->
503
+ </button>
504
+ `;
505
+ }
506
+ }
507
+ applyThemeFromLocalStorage() {
508
+ const savedTheme = localStorage.getItem("theme");
509
+ if (savedTheme) {
510
+ document.documentElement.setAttribute("data-theme", savedTheme);
511
+ this.updateToggleButton(savedTheme);
512
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
513
+ document.documentElement.setAttribute("data-theme", "dark");
514
+ this.updateToggleButton("dark");
515
+ } else {
516
+ document.documentElement.setAttribute("data-theme", "light");
517
+ this.updateToggleButton("light");
518
+ }
519
+ }
520
+ updateToggleButton(theme) {
521
+ const button = this.shadowRoot?.querySelector("#theme-toggle");
522
+ if (button) {
523
+ const iconSpan = button.querySelector(".icon");
524
+ if (iconSpan) {
525
+ iconSpan.innerHTML = theme === "dark" ? this.getMoonIcon() : this.getSunIcon();
526
+ }
527
+ }
528
+ }
529
+ getSunIcon() {
530
+ return `
531
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
532
+ <circle cx="12" cy="12" r="5"/>
533
+ <line x1="12" y1="1" x2="12" y2="3"/>
534
+ <line x1="12" y1="21" x2="12" y2="23"/>
535
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
536
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
537
+ <line x1="1" y1="12" x2="3" y2="12"/>
538
+ <line x1="21" y1="12" x2="23" y2="12"/>
539
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
540
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
541
+ </svg>
542
+ `;
543
+ }
544
+ getMoonIcon() {
545
+ return `
546
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
547
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
548
+ </svg>
549
+ `;
550
+ }
551
+ dispatchThemeChangeEvent(theme) {
552
+ const event = new CustomEvent("theme-changed", {
553
+ detail: { theme },
554
+ bubbles: true,
555
+ composed: true
556
+ });
557
+ this.dispatchEvent(event);
558
+ }
559
+ }
560
+ customElements.define("theme-toggle", ThemeToggle);
561
+ const createHtmlTemplate = ({
562
+ title,
563
+ description,
564
+ canonicalUrl,
565
+ content
566
+ }) => {
567
+ return `<!doctype html>
568
+ <html lang="en" data-theme="light">
569
+ <head>
570
+ <meta charset="UTF-8" />
571
+ <link rel="icon" type="image/svg+xml" href="/api/logo" />
572
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
573
+ <title>${title}</title>
574
+ <meta name="description" content="${description}" />
575
+ <meta property="og:title" content="${title}" />
576
+ <meta property="og:description" content="${description}" />
577
+ <meta property="og:url" content="${canonicalUrl}" />
578
+ <link rel="canonical" href="${canonicalUrl}" />
579
+ <link rel="stylesheet" href="/src/styles.css" />
580
+ <script type="module" src="/src/index.ts"><\/script>
581
+ </head>
582
+ <body>
583
+ <div id="app">
584
+ ${content}
585
+ </div>
586
+ </body>
587
+ </html>`;
588
+ };
589
+ export {
590
+ Router as R,
591
+ ThemeToggle as T,
592
+ WebsiteUI as W,
593
+ bootstrap as b,
594
+ createHtmlTemplate as c,
595
+ generatePageContent as g,
596
+ renderMarkdown as r
597
+ };