@leadertechie/personal-site-kit 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +94 -17
- package/dist/api/content-utils.d.ts +27 -0
- package/dist/api/content-utils.d.ts.map +1 -0
- package/dist/api/handlers/about-me.d.ts.map +1 -1
- 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/static-details.d.ts +1 -1
- package/dist/api/handlers/static-details.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/website-api.d.ts +1 -1
- package/dist/api/website-api.d.ts.map +1 -1
- package/dist/api.js +17 -2
- package/dist/assets/logo-placeholder.svg +21 -0
- package/dist/chunks/index-C1krnvU3.js +211 -0
- package/dist/chunks/index-DsRjL9Uy.js +2727 -0
- package/dist/chunks/site-store-CGV9c2DI.js +89 -0
- package/dist/chunks/{template-gGTkeOcA.js → template-Lmx7Dxoc.js} +132 -91
- package/dist/chunks/website-api-CFRUPu0X.js +958 -0
- package/dist/index.js +42 -14
- package/dist/prerender/data-fetcher.d.ts +19 -0
- package/dist/prerender/data-fetcher.d.ts.map +1 -0
- package/dist/prerender/page-content.d.ts.map +1 -1
- 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 +25 -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/template.d.ts +3 -1
- package/dist/prerender/template.d.ts.map +1 -1
- package/dist/prerender/website-prerender.d.ts +6 -0
- package/dist/prerender/website-prerender.d.ts.map +1 -1
- package/dist/prerender.js +291 -145
- package/dist/shared/config/index.d.ts +1 -0
- package/dist/shared/config/index.d.ts.map +1 -1
- package/dist/shared/core/site-store.d.ts +1 -0
- package/dist/shared/core/site-store.d.ts.map +1 -1
- package/dist/shared/core/theme-toggle.d.ts.map +1 -1
- package/dist/shared/page-content.d.ts.map +1 -1
- package/dist/shared/router.d.ts +9 -3
- package/dist/shared/router.d.ts.map +1 -1
- package/dist/shared/website-ui.d.ts +23 -0
- package/dist/shared/website-ui.d.ts.map +1 -1
- package/dist/shared.js +6 -4
- package/dist/ui/about-me/index.d.ts +2 -10
- package/dist/ui/about-me/index.d.ts.map +1 -1
- package/dist/ui/about-me/styles.d.ts.map +1 -1
- 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/index.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/ui/footer/styles.d.ts.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- 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.js +15 -3
- package/package.json +37 -13
- package/public/assets/logo-placeholder.svg +21 -0
- package/dist/chunks/index-BqixlS-2.js +0 -1157
- package/dist/chunks/website-api-CVsi-OLc.js +0 -596
- package/dist/ui/about-me/renderer.d.ts +0 -5
- package/dist/ui/about-me/renderer.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/about-me.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/static-details.ts +0 -48
- package/src/api/index.ts +0 -7
- package/src/api/utils.ts +0 -16
- package/src/api/website-api.ts +0 -124
- package/src/index.ts +0 -4
- package/src/prerender/__tests__/page-content.test.ts +0 -54
- package/src/prerender/__tests__/template.test.ts +0 -54
- package/src/prerender/index.ts +0 -7
- package/src/prerender/page-content.ts +0 -263
- package/src/prerender/prerender.ts +0 -25
- package/src/prerender/template.ts +0 -65
- package/src/prerender/website-prerender.ts +0 -152
- 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 -17
- package/src/shared/interfaces/ifooter-link.ts +0 -4
- package/src/shared/interfaces/iroute.ts +0 -4
- package/src/shared/models/theme-variables.css +0 -25
- package/src/shared/page-content.ts +0 -210
- package/src/shared/router.ts +0 -241
- package/src/shared/runtime.ts +0 -11
- package/src/shared/template.ts +0 -35
- package/src/shared/website-ui.ts +0 -92
- package/src/styles/markdown.css +0 -129
- package/src/ui/about-me/api.ts +0 -12
- package/src/ui/about-me/index.ts +0 -155
- package/src/ui/about-me/renderer.ts +0 -7
- package/src/ui/about-me/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/ui/index.ts +0 -4
- /package/{src/shared → dist}/styles/markdown.css +0 -0
- /package/{src → dist}/styles/theme.css +0 -0
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { IFooterLink } from './interfaces/ifooter-link';
|
|
2
|
-
import { IRoute } from './interfaces/iroute';
|
|
3
|
-
export type { IFooterLink, IRoute };
|
|
4
|
-
import { MarkdownPipeline } from '@leadertechie/md2html';
|
|
5
|
-
|
|
6
|
-
export interface PageContent {
|
|
7
|
-
title: string;
|
|
8
|
-
description: string;
|
|
9
|
-
canonicalUrl: string;
|
|
10
|
-
content: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ContentMetadata {
|
|
14
|
-
slug: string;
|
|
15
|
-
title: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
summary?: string;
|
|
18
|
-
date: string;
|
|
19
|
-
imageUrl?: string;
|
|
20
|
-
tags?: string[];
|
|
21
|
-
author?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const pipeline = new MarkdownPipeline({
|
|
25
|
-
imagePathPrefix: 'images/',
|
|
26
|
-
styleOptions: {
|
|
27
|
-
classPrefix: 'md-',
|
|
28
|
-
addHeadingIds: true
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export const renderMarkdown = (content: string): string => {
|
|
33
|
-
if (!content) return '';
|
|
34
|
-
return pipeline.renderMarkdown(content);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const generatePageContent = (
|
|
38
|
-
pathname: string,
|
|
39
|
-
routes: IRoute[],
|
|
40
|
-
footerLinks: IFooterLink[],
|
|
41
|
-
data?: {
|
|
42
|
-
blogs?: ContentMetadata[];
|
|
43
|
-
stories?: ContentMetadata[];
|
|
44
|
-
profile?: any;
|
|
45
|
-
content?: string;
|
|
46
|
-
title?: string;
|
|
47
|
-
description?: string;
|
|
48
|
-
date?: string;
|
|
49
|
-
slug?: string;
|
|
50
|
-
siteTitle?: string;
|
|
51
|
-
siteDescription?: string;
|
|
52
|
-
copyright?: string;
|
|
53
|
-
apiUrl?: string;
|
|
54
|
-
baseUrl?: string;
|
|
55
|
-
}
|
|
56
|
-
): PageContent => {
|
|
57
|
-
const logo = "/api/logo";
|
|
58
|
-
const siteTitle = data?.siteTitle || "My Personal Website";
|
|
59
|
-
const siteDescription = data?.siteDescription || "Welcome to my professional portfolio and blog.";
|
|
60
|
-
const copyright = data?.copyright || "2026 All Rights Reserved";
|
|
61
|
-
const baseUrl = data?.baseUrl || "";
|
|
62
|
-
const canonicalUrl = baseUrl ? new URL(pathname, baseUrl).toString() : pathname;
|
|
63
|
-
|
|
64
|
-
const navLinks = routes
|
|
65
|
-
.map(
|
|
66
|
-
(route) =>
|
|
67
|
-
`<a href="${route.link}" class="nav-link" data-route="${route.link === "/" ? "home" : route.text.toLowerCase()}">${route.text}</a>`,
|
|
68
|
-
)
|
|
69
|
-
.join("");
|
|
70
|
-
|
|
71
|
-
const bannerTemplate = `
|
|
72
|
-
<my-banner header="${siteTitle}" logo="${logo}">
|
|
73
|
-
<theme-toggle slot="theme-switcher"></theme-toggle>
|
|
74
|
-
<nav slot="nav-links">
|
|
75
|
-
${navLinks}
|
|
76
|
-
</nav>
|
|
77
|
-
</my-banner>`;
|
|
78
|
-
|
|
79
|
-
const footerTemplate = `
|
|
80
|
-
<my-footer
|
|
81
|
-
copyright="${copyright}"
|
|
82
|
-
footerlinks='\${JSON.stringify(footerLinks)}'>
|
|
83
|
-
</my-footer>`;
|
|
84
|
-
|
|
85
|
-
const renderContentGists = (items: ContentMetadata[] = [], title: string, type: 'blogs' | 'stories') => {
|
|
86
|
-
const listHtml = items.length > 0
|
|
87
|
-
? items.map(item => `
|
|
88
|
-
<div class="gist-card">
|
|
89
|
-
<a href="${type}/${item.slug}" data-route="${type}-${item.slug}"><h4>${item.title}</h4></a>
|
|
90
|
-
<p>${item.summary || item.description || ''}</p>
|
|
91
|
-
<small>${new Date(item.date).toLocaleDateString()}</small>
|
|
92
|
-
</div>
|
|
93
|
-
`).join('')
|
|
94
|
-
: `<p>No ${type} available yet.</p>`;
|
|
95
|
-
|
|
96
|
-
return `
|
|
97
|
-
<div class="recent-content-section ${title.includes('Stories') ? 'mt-2' : ''}">
|
|
98
|
-
<h3 class="border-bottom pb-05">${title}</h3>
|
|
99
|
-
${listHtml}
|
|
100
|
-
</div>
|
|
101
|
-
`;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (pathname === "/" || pathname === "") {
|
|
105
|
-
const homeHtml = data?.content || `<h1>Welcome to ${siteTitle}</h1><p>Start by configuring your content in the admin portal.</p>`;
|
|
106
|
-
|
|
107
|
-
const mainContent = `
|
|
108
|
-
${bannerTemplate}
|
|
109
|
-
<main class="container container-wide column-layout row-layout">
|
|
110
|
-
<div class="home-main-content main-column text-left">
|
|
111
|
-
${homeHtml}
|
|
112
|
-
</div>
|
|
113
|
-
<aside class="home-side-content sidebar-column">
|
|
114
|
-
${renderContentGists(data?.blogs, 'Recent Blogs', 'blogs')}
|
|
115
|
-
${renderContentGists(data?.stories, 'Recent Stories', 'stories')}
|
|
116
|
-
</aside>
|
|
117
|
-
</main>
|
|
118
|
-
${footerTemplate}`;
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
title: `${siteTitle} - Home`,
|
|
122
|
-
description: siteDescription,
|
|
123
|
-
canonicalUrl,
|
|
124
|
-
content: mainContent
|
|
125
|
-
};
|
|
126
|
-
} else if (pathname === "/blogs" || pathname === "/stories" || pathname.startsWith("/blogs/") || pathname.startsWith("/stories/")) {
|
|
127
|
-
const isBlog = pathname.includes("blogs");
|
|
128
|
-
const type = isBlog ? "blogs" : "stories";
|
|
129
|
-
const items = isBlog ? data?.blogs : data?.stories;
|
|
130
|
-
const title = type.charAt(0).toUpperCase() + type.slice(1);
|
|
131
|
-
const currentSlug = data?.slug;
|
|
132
|
-
|
|
133
|
-
const mainContent = `
|
|
134
|
-
${bannerTemplate}
|
|
135
|
-
<main class="container container-wide">
|
|
136
|
-
<div class="mb-2">
|
|
137
|
-
<input type="text" id="content-search" placeholder="Search ${type}..." class="search-input search-input-lg">
|
|
138
|
-
</div>
|
|
139
|
-
<div class="column-layout">
|
|
140
|
-
<aside class="sidebar-nav sidebar-column">
|
|
141
|
-
<div id="content-list">
|
|
142
|
-
${items?.map(item => {
|
|
143
|
-
const searchTerms = [
|
|
144
|
-
item.title,
|
|
145
|
-
...(item.tags || []),
|
|
146
|
-
item.summary || item.description || '',
|
|
147
|
-
item.slug
|
|
148
|
-
].join(' ').toLowerCase();
|
|
149
|
-
|
|
150
|
-
return `
|
|
151
|
-
<div class="list-item sidebar-item ${item.slug === currentSlug ? 'active' : ''}"
|
|
152
|
-
data-search="${searchTerms}">
|
|
153
|
-
<a href="/${type}/${item.slug}" data-route="${type}-${item.slug}" class="sidebar-item sidebar-item-link">
|
|
154
|
-
<h4>${item.title}</h4>
|
|
155
|
-
<small>${new Date(item.date).toLocaleDateString()}</small>
|
|
156
|
-
</a>
|
|
157
|
-
</div>
|
|
158
|
-
`}).join('') || `<p>No ${type} available yet.</p>`}
|
|
159
|
-
</div>
|
|
160
|
-
</aside>
|
|
161
|
-
<div class="wide-main-column text-left">
|
|
162
|
-
<div id="content-viewer">
|
|
163
|
-
${currentSlug
|
|
164
|
-
? (isBlog ? `<my-blog-viewer slug="${currentSlug}"></my-blog-viewer>` : `<my-story-viewer slug="${currentSlug}"></my-story-viewer>`)
|
|
165
|
-
: (items && items.length > 0 ? `<p>Select a ${type.slice(0, -1)} to read.</p>` : '')}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</main>
|
|
170
|
-
${footerTemplate}`;
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
title: `${title} - ${siteTitle}`,
|
|
174
|
-
description: `Read the latest ${type} from ${siteTitle}.`,
|
|
175
|
-
canonicalUrl,
|
|
176
|
-
content: mainContent
|
|
177
|
-
};
|
|
178
|
-
} else if (pathname === "/about-me") {
|
|
179
|
-
const apiBaseUrl = data?.apiUrl || "";
|
|
180
|
-
const mainContent = `
|
|
181
|
-
${bannerTemplate}
|
|
182
|
-
<main class="container container-narrow">
|
|
183
|
-
<my-aboutme base-url="${apiBaseUrl}"></my-aboutme>
|
|
184
|
-
</main>
|
|
185
|
-
${footerTemplate}`;
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
title: `About - ${siteTitle}`,
|
|
189
|
-
description: `Learn more about ${siteTitle}.`,
|
|
190
|
-
canonicalUrl,
|
|
191
|
-
content: mainContent
|
|
192
|
-
};
|
|
193
|
-
} else {
|
|
194
|
-
const mainContent = `
|
|
195
|
-
${bannerTemplate}
|
|
196
|
-
<main class="container container-narrow text-center page-content">
|
|
197
|
-
<h1 class="page-title">Page Not Found</h1>
|
|
198
|
-
<p>The page you're looking for doesn't exist.</p>
|
|
199
|
-
<p><a href="/">Return to home</a></p>
|
|
200
|
-
</main>
|
|
201
|
-
${footerTemplate}`;
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
title: `404 Not Found - ${siteTitle}`,
|
|
205
|
-
description: "The page you requested could not be found.",
|
|
206
|
-
canonicalUrl,
|
|
207
|
-
content: mainContent
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
};
|
package/src/shared/router.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { generatePageContent, ContentMetadata, IFooterLink } from './page-content';
|
|
2
|
-
import { IRoute } from './interfaces/iroute';
|
|
3
|
-
import { WebsiteUI } from './website-ui';
|
|
4
|
-
|
|
5
|
-
export class Router {
|
|
6
|
-
private routes: IRoute[];
|
|
7
|
-
private siteTitle: string;
|
|
8
|
-
private copyright: string;
|
|
9
|
-
private footerLinks: IFooterLink[];
|
|
10
|
-
private apiUrl: string;
|
|
11
|
-
private logo: string = '/api/logo';
|
|
12
|
-
private appElement: HTMLElement | null = null;
|
|
13
|
-
|
|
14
|
-
constructor(private ui: WebsiteUI) {
|
|
15
|
-
const store = ui.getStore();
|
|
16
|
-
const config = store.getConfig();
|
|
17
|
-
|
|
18
|
-
// Default routes if not provided in bootstrap
|
|
19
|
-
this.routes = [
|
|
20
|
-
{ link: '/', text: 'Home' },
|
|
21
|
-
{ link: '/blogs', text: 'Blogs' },
|
|
22
|
-
{ link: '/stories', text: 'Stories' },
|
|
23
|
-
{ link: '/about-me', text: 'About Me' },
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
this.siteTitle = config.siteTitle;
|
|
27
|
-
this.copyright = config.copyright;
|
|
28
|
-
this.apiUrl = config.apiUrl;
|
|
29
|
-
|
|
30
|
-
const normalizeUrl = (url?: string) => {
|
|
31
|
-
if (!url) return '';
|
|
32
|
-
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
33
|
-
if (url.startsWith('www.')) return `https://${url}`;
|
|
34
|
-
return url;
|
|
35
|
-
};
|
|
36
|
-
|
|
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 !== '');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public init(appElementId: string = 'app') {
|
|
45
|
-
this.appElement = document.getElementById(appElementId);
|
|
46
|
-
if (!this.appElement) {
|
|
47
|
-
console.error(`App element with id ${appElementId} not found`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
this.setupEventListeners();
|
|
52
|
-
this.navigate(window.location.pathname);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private setupEventListeners() {
|
|
56
|
-
document.body.addEventListener('click', (event) => {
|
|
57
|
-
const target = (event.target as HTMLElement).closest('a[data-route]');
|
|
58
|
-
if (target) {
|
|
59
|
-
event.preventDefault();
|
|
60
|
-
const route = target.getAttribute('href');
|
|
61
|
-
if (route && route !== window.location.pathname) {
|
|
62
|
-
window.history.pushState({}, '', route);
|
|
63
|
-
this.navigate(route);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
window.addEventListener('popstate', () => {
|
|
69
|
-
this.navigate(window.location.pathname);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Search input handler
|
|
73
|
-
document.body.addEventListener('input', (event) => {
|
|
74
|
-
const target = event.target as HTMLInputElement;
|
|
75
|
-
if (target.id === 'content-search') {
|
|
76
|
-
const query = target.value.toLowerCase().trim();
|
|
77
|
-
const items = document.querySelectorAll('#content-list .list-item');
|
|
78
|
-
items.forEach(item => {
|
|
79
|
-
const searchText = item.getAttribute('data-search') || '';
|
|
80
|
-
if (searchText.includes(query)) {
|
|
81
|
-
item.classList.remove('hidden');
|
|
82
|
-
} else {
|
|
83
|
-
item.classList.add('hidden');
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public async navigate(path: string) {
|
|
91
|
-
if (path.startsWith('/blogs/') && path.length > 7) {
|
|
92
|
-
await this.renderContentDetailPage(path);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (path.startsWith('/stories/') && path.length > 9) {
|
|
96
|
-
await this.renderContentDetailPage(path);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
switch (path) {
|
|
101
|
-
case '/':
|
|
102
|
-
await this.renderHomePage();
|
|
103
|
-
break;
|
|
104
|
-
case '/about-me':
|
|
105
|
-
this.renderAboutMePage();
|
|
106
|
-
break;
|
|
107
|
-
case '/blogs':
|
|
108
|
-
case '/stories':
|
|
109
|
-
await this.renderContentListPage(path);
|
|
110
|
-
break;
|
|
111
|
-
case '/admin':
|
|
112
|
-
await this.renderAdminPage();
|
|
113
|
-
break;
|
|
114
|
-
default:
|
|
115
|
-
await this.renderHomePage(); // Fallback to home for now
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private setPageMeta(title: string, description: string, url: string) {
|
|
120
|
-
document.title = title;
|
|
121
|
-
const metaTags = [
|
|
122
|
-
{ name: 'description', content: description },
|
|
123
|
-
{ property: 'og:title', content: title },
|
|
124
|
-
{ property: 'og:description', content: description },
|
|
125
|
-
{ property: 'og:url', content: url }
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
metaTags.forEach(tag => {
|
|
129
|
-
let element = tag.name
|
|
130
|
-
? document.querySelector(`meta[name="${tag.name}"]`)
|
|
131
|
-
: document.querySelector(`meta[property="${tag.property}"]`);
|
|
132
|
-
|
|
133
|
-
if (!element) {
|
|
134
|
-
element = document.createElement('meta');
|
|
135
|
-
if (tag.name) element.setAttribute('name', tag.name);
|
|
136
|
-
if (tag.property) element.setAttribute('property', tag.property);
|
|
137
|
-
document.head.appendChild(element);
|
|
138
|
-
}
|
|
139
|
-
element.setAttribute('content', tag.content);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
let canonicalLink = document.querySelector('link[rel="canonical"]');
|
|
143
|
-
if (!canonicalLink) {
|
|
144
|
-
canonicalLink = document.createElement('link');
|
|
145
|
-
canonicalLink.setAttribute('rel', 'canonical');
|
|
146
|
-
document.head.appendChild(canonicalLink);
|
|
147
|
-
}
|
|
148
|
-
canonicalLink.setAttribute('href', url);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private async renderHomePage() {
|
|
152
|
-
let blogs: ContentMetadata[] = [];
|
|
153
|
-
let stories: ContentMetadata[] = [];
|
|
154
|
-
let homeContent = '';
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const [blogsRes, storiesRes, homeRes] = await Promise.all([
|
|
158
|
-
fetch(`${this.apiUrl}/api/blogs`),
|
|
159
|
-
fetch(`${this.apiUrl}/api/stories`),
|
|
160
|
-
fetch(`${this.apiUrl}/api/home`)
|
|
161
|
-
]);
|
|
162
|
-
if (blogsRes.ok) blogs = (await blogsRes.json()).slice(0, 3);
|
|
163
|
-
if (storiesRes.ok) stories = (await storiesRes.json()).slice(0, 3);
|
|
164
|
-
if (homeRes.ok) homeContent = (await homeRes.json()).content;
|
|
165
|
-
} catch (e) {
|
|
166
|
-
console.error('Failed to fetch home content', e);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const pageContent = generatePageContent('/', this.routes, this.footerLinks, {
|
|
170
|
-
blogs, stories, content: homeContent, siteTitle: this.siteTitle, copyright: this.copyright
|
|
171
|
-
});
|
|
172
|
-
if (this.appElement) this.appElement.innerHTML = pageContent.content;
|
|
173
|
-
this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private renderAboutMePage() {
|
|
177
|
-
const pageContent = generatePageContent('/about-me', this.routes, this.footerLinks, {
|
|
178
|
-
siteTitle: this.siteTitle, copyright: this.copyright, apiUrl: this.apiUrl
|
|
179
|
-
});
|
|
180
|
-
if (this.appElement) this.appElement.innerHTML = pageContent.content;
|
|
181
|
-
this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private async renderContentListPage(pathname: string) {
|
|
185
|
-
const type = pathname === '/blogs' ? 'blogs' : 'stories';
|
|
186
|
-
let items: ContentMetadata[] = [];
|
|
187
|
-
try {
|
|
188
|
-
const res = await fetch(`${this.apiUrl}/api/${type}`);
|
|
189
|
-
if (res.ok) items = await res.json();
|
|
190
|
-
} catch (e) {}
|
|
191
|
-
|
|
192
|
-
const latestSlug = items.length > 0 ? items[0].slug : undefined;
|
|
193
|
-
const data = type === 'blogs' ? { blogs: items, slug: latestSlug } : { stories: items, slug: latestSlug };
|
|
194
|
-
const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
|
|
195
|
-
...data, siteTitle: this.siteTitle, copyright: this.copyright
|
|
196
|
-
});
|
|
197
|
-
if (this.appElement) this.appElement.innerHTML = pageContent.content;
|
|
198
|
-
this.setPageMeta(pageContent.title, pageContent.description, pageContent.canonicalUrl);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private async renderContentDetailPage(pathname: string) {
|
|
202
|
-
const isBlog = pathname.startsWith('/blogs/');
|
|
203
|
-
const type = isBlog ? 'blogs' : 'stories';
|
|
204
|
-
const slug = pathname.replace(`/${type}/`, '').split('/')[0];
|
|
205
|
-
|
|
206
|
-
let items: ContentMetadata[] = [];
|
|
207
|
-
try {
|
|
208
|
-
const res = await fetch(`${this.apiUrl}/api/${type}`);
|
|
209
|
-
if (res.ok) items = await res.json();
|
|
210
|
-
} catch (e) {}
|
|
211
|
-
|
|
212
|
-
const data = type === 'blogs' ? { blogs: items, slug } : { stories: items, slug };
|
|
213
|
-
const pageContent = generatePageContent(pathname, this.routes, this.footerLinks, {
|
|
214
|
-
...data, siteTitle: this.siteTitle, copyright: this.copyright
|
|
215
|
-
});
|
|
216
|
-
if (this.appElement) this.appElement.innerHTML = pageContent.content;
|
|
217
|
-
this.setPageMeta(`${slug.replace(/-/g, ' ')} - ${this.siteTitle}`, 'Read more content', window.location.href);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private async renderAdminPage() {
|
|
221
|
-
// Admin portal should be lazy loaded or integrated
|
|
222
|
-
const pageContent = generatePageContent('/admin', this.routes, this.footerLinks, {
|
|
223
|
-
siteTitle: this.siteTitle, copyright: this.copyright
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (this.appElement) {
|
|
227
|
-
this.appElement.innerHTML = `
|
|
228
|
-
<my-banner header="${this.siteTitle}" logo="${this.logo}">
|
|
229
|
-
<theme-toggle slot="theme-switcher"></theme-toggle>
|
|
230
|
-
<nav slot="nav-links">
|
|
231
|
-
<a href="/" class="nav-link" data-route="home">Home</a>
|
|
232
|
-
</nav>
|
|
233
|
-
</my-banner>
|
|
234
|
-
<main class="container container-medium">
|
|
235
|
-
<admin-portal></admin-portal>
|
|
236
|
-
</main>
|
|
237
|
-
<my-footer copyright="${this.copyright}" footerlinks='[]'></my-footer>
|
|
238
|
-
`;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
package/src/shared/runtime.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Runtime-only exports for Cloudflare Workers (no DOM/web components)
|
|
2
|
-
export * from './interfaces/iroute';
|
|
3
|
-
export * from './interfaces/ifooter-link';
|
|
4
|
-
export * from './page-content';
|
|
5
|
-
|
|
6
|
-
// Core utilities and types (DOM-free)
|
|
7
|
-
export type { IRoute } from './interfaces/iroute';
|
|
8
|
-
export type { IFooterLink } from './interfaces/ifooter-link';
|
|
9
|
-
|
|
10
|
-
// Page content generation utilities
|
|
11
|
-
export { generatePageContent } from './page-content';
|
package/src/shared/template.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export interface TemplateProps {
|
|
2
|
-
title: string;
|
|
3
|
-
description: string;
|
|
4
|
-
canonicalUrl: string;
|
|
5
|
-
content: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const createHtmlTemplate = ({
|
|
9
|
-
title,
|
|
10
|
-
description,
|
|
11
|
-
canonicalUrl,
|
|
12
|
-
content
|
|
13
|
-
}: TemplateProps): string => {
|
|
14
|
-
return `<!doctype html>
|
|
15
|
-
<html lang="en" data-theme="light">
|
|
16
|
-
<head>
|
|
17
|
-
<meta charset="UTF-8" />
|
|
18
|
-
<link rel="icon" type="image/svg+xml" href="/api/logo" />
|
|
19
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
-
<title>${title}</title>
|
|
21
|
-
<meta name="description" content="${description}" />
|
|
22
|
-
<meta property="og:title" content="${title}" />
|
|
23
|
-
<meta property="og:description" content="${description}" />
|
|
24
|
-
<meta property="og:url" content="${canonicalUrl}" />
|
|
25
|
-
<link rel="canonical" href="${canonicalUrl}" />
|
|
26
|
-
<link rel="stylesheet" href="/src/styles.css" />
|
|
27
|
-
<script type="module" src="/src/index.ts"></script>
|
|
28
|
-
</head>
|
|
29
|
-
<body>
|
|
30
|
-
<div id="app">
|
|
31
|
-
${content}
|
|
32
|
-
</div>
|
|
33
|
-
</body>
|
|
34
|
-
</html>`;
|
|
35
|
-
};
|
package/src/shared/website-ui.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { SiteStore } from './core/site-store';
|
|
2
|
-
import { IRoute } from './interfaces/iroute';
|
|
3
|
-
import { Router } from './router';
|
|
4
|
-
|
|
5
|
-
export interface UIConfig {
|
|
6
|
-
apiUrl?: string;
|
|
7
|
-
baseUrl?: string;
|
|
8
|
-
routes?: IRoute[];
|
|
9
|
-
appElementId?: string;
|
|
10
|
-
theme?: {
|
|
11
|
-
primaryColor?: string;
|
|
12
|
-
backgroundColor?: string;
|
|
13
|
-
textColor?: string;
|
|
14
|
-
customCss?: string;
|
|
15
|
-
};
|
|
16
|
-
onBootstrap?: (ui: WebsiteUI) => void | Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class WebsiteUI {
|
|
20
|
-
private static instance: WebsiteUI;
|
|
21
|
-
private store: SiteStore;
|
|
22
|
-
private config: UIConfig;
|
|
23
|
-
private router: Router | null = null;
|
|
24
|
-
|
|
25
|
-
private constructor(config: UIConfig = {}) {
|
|
26
|
-
this.store = SiteStore.getInstance();
|
|
27
|
-
this.config = config;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public static getInstance(config?: UIConfig): WebsiteUI {
|
|
31
|
-
if (!WebsiteUI.instance) {
|
|
32
|
-
WebsiteUI.instance = new WebsiteUI(config);
|
|
33
|
-
} else if (config) {
|
|
34
|
-
// Merge config if provided on existing instance
|
|
35
|
-
WebsiteUI.instance.config = { ...WebsiteUI.instance.config, ...config };
|
|
36
|
-
}
|
|
37
|
-
return WebsiteUI.instance;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public async bootstrap() {
|
|
41
|
-
// 1. Initialize SiteStore with infra config
|
|
42
|
-
await this.store.init({
|
|
43
|
-
apiUrl: this.config.apiUrl,
|
|
44
|
-
baseUrl: this.config.baseUrl
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// 2. Apply theme overrides if any
|
|
48
|
-
this.applyTheme();
|
|
49
|
-
|
|
50
|
-
// 3. Setup Router
|
|
51
|
-
this.router = new Router(this);
|
|
52
|
-
this.router.init(this.config.appElementId || 'app');
|
|
53
|
-
|
|
54
|
-
// 4. Run bootstrap hook
|
|
55
|
-
if (this.config.onBootstrap) {
|
|
56
|
-
await this.config.onBootstrap(this);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log('WebsiteUI bootstrapped');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private applyTheme() {
|
|
63
|
-
if (!this.config.theme) return;
|
|
64
|
-
|
|
65
|
-
const { theme } = this.config;
|
|
66
|
-
const root = document.documentElement;
|
|
67
|
-
|
|
68
|
-
if (theme.primaryColor) root.style.setProperty('--link-color', theme.primaryColor);
|
|
69
|
-
if (theme.backgroundColor) root.style.setProperty('--background-color', theme.backgroundColor);
|
|
70
|
-
if (theme.textColor) root.style.setProperty('--text-color', theme.textColor);
|
|
71
|
-
|
|
72
|
-
if (theme.customCss) {
|
|
73
|
-
const style = document.createElement('style');
|
|
74
|
-
style.textContent = theme.customCss;
|
|
75
|
-
document.head.appendChild(style);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public getStore() {
|
|
80
|
-
return this.store;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public getConfig() {
|
|
84
|
-
return this.config;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public getRouter() {
|
|
88
|
-
return this.router;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export const bootstrap = (config?: UIConfig) => WebsiteUI.getInstance(config).bootstrap();
|