@leadertechie/personal-site-kit 0.1.0-alpha.2 → 0.1.0-alpha.4

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 (42) hide show
  1. package/dist/assets/logo-placeholder.svg +21 -0
  2. package/dist/chunks/index-VimKeB5W.js +1927 -0
  3. package/dist/chunks/{template-gGTkeOcA.js → template-MawmknFQ.js} +13 -4
  4. package/dist/index.js +5 -3
  5. package/dist/shared/router.d.ts.map +1 -1
  6. package/dist/shared.js +1 -1
  7. package/dist/ui/about-me/styles.d.ts.map +1 -1
  8. package/dist/ui/admin/styles.d.ts.map +1 -1
  9. package/dist/ui/banner/styles.d.ts.map +1 -1
  10. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts +2 -0
  11. package/dist/ui/blog-viewer/__tests__/blogviewer.test.d.ts.map +1 -0
  12. package/dist/ui/blog-viewer/index.d.ts +25 -0
  13. package/dist/ui/blog-viewer/index.d.ts.map +1 -0
  14. package/dist/ui/blog-viewer/styles.d.ts +2 -0
  15. package/dist/ui/blog-viewer/styles.d.ts.map +1 -0
  16. package/dist/ui/footer/styles.d.ts.map +1 -1
  17. package/dist/ui/index.d.ts +2 -0
  18. package/dist/ui/index.d.ts.map +1 -1
  19. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts +2 -0
  20. package/dist/ui/story-viewer/__tests__/storyviewer.test.d.ts.map +1 -0
  21. package/dist/ui/story-viewer/index.d.ts +25 -0
  22. package/dist/ui/story-viewer/index.d.ts.map +1 -0
  23. package/dist/ui/story-viewer/styles.d.ts +2 -0
  24. package/dist/ui/story-viewer/styles.d.ts.map +1 -0
  25. package/dist/ui.js +4 -2
  26. package/package.json +23 -7
  27. package/public/assets/logo-placeholder.svg +21 -0
  28. package/src/shared/config/index.ts +1 -1
  29. package/src/shared/router.ts +12 -3
  30. package/src/ui/about-me/styles.ts +81 -6
  31. package/src/ui/admin/styles.ts +0 -47
  32. package/src/ui/banner/styles.ts +89 -4
  33. package/src/ui/blog-viewer/__tests__/blogviewer.test.ts +7 -0
  34. package/src/ui/blog-viewer/index.ts +124 -0
  35. package/src/ui/blog-viewer/styles.ts +23 -0
  36. package/src/ui/footer/index.ts +1 -1
  37. package/src/ui/footer/styles.ts +43 -2
  38. package/src/ui/index.ts +2 -0
  39. package/src/ui/story-viewer/__tests__/storyviewer.test.ts +7 -0
  40. package/src/ui/story-viewer/index.ts +120 -0
  41. package/src/ui/story-viewer/styles.ts +54 -0
  42. package/dist/chunks/index-BqixlS-2.js +0 -1157
@@ -0,0 +1,124 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
+ import { MarkdownPipeline, ContentNode } from '@leadertechie/md2html';
5
+
6
+ import { blogviewerStyles } from './styles';
7
+
8
+ const pipeline = new MarkdownPipeline({
9
+ imagePathPrefix: 'images/',
10
+ styleOptions: {
11
+ classPrefix: 'md-',
12
+ addHeadingIds: true
13
+ }
14
+ });
15
+
16
+ interface BlogPost {
17
+ title: string;
18
+ description: string;
19
+ date: string;
20
+ tags: string[];
21
+ author: string;
22
+ imageUrl: string;
23
+ content: string;
24
+ }
25
+
26
+ @customElement('my-blog-viewer')
27
+ export class BlogViewer extends LitElement {
28
+ static styles = blogviewerStyles;
29
+
30
+ @property({ type: String })
31
+ accessor slug = '';
32
+
33
+ @state()
34
+ accessor blogPost: BlogPost | null = null;
35
+
36
+ @state()
37
+ accessor loading = true;
38
+
39
+ @state()
40
+ accessor error = '';
41
+
42
+ private get apiBaseUrl(): string {
43
+ return 'https://api.techieleader.com';
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ if (this.slug) {
49
+ this.loadBlog();
50
+ }
51
+ }
52
+
53
+ updated(changedProperties: Map<string, unknown>) {
54
+ if (changedProperties.has('slug') && this.slug) {
55
+ this.loadBlog();
56
+ }
57
+ }
58
+
59
+ loadBlog() {
60
+ this.loading = true;
61
+ this.error = '';
62
+ console.log('[BlogViewer] Loading blog:', this.slug);
63
+
64
+ fetch(`${this.apiBaseUrl}/api/blogs/${this.slug}`)
65
+ .then(res => {
66
+ console.log('[BlogViewer] Response status:', res.status);
67
+ if (res.ok) return res.json();
68
+ throw new Error('Failed to load blog');
69
+ })
70
+ .then(blog => {
71
+ console.log('[BlogViewer] Found blog:', blog?.title, 'slug:', blog?.slug);
72
+ if (blog && blog.content) {
73
+ this.blogPost = blog;
74
+ } else {
75
+ this.error = 'Blog content not found';
76
+ }
77
+ this.loading = false;
78
+ })
79
+ .catch(e => {
80
+ console.error('[BlogViewer] Failed to load blog:', e);
81
+ this.error = 'Failed to load blog';
82
+ this.loading = false;
83
+ });
84
+ }
85
+
86
+ render() {
87
+ if (this.loading) {
88
+ return html`<div class="blog-viewer"><div class="loading">Loading...</div></div>`;
89
+ }
90
+
91
+ if (this.error) {
92
+ return html`<div class="blog-viewer"><div class="error">${this.error}</div></div>`;
93
+ }
94
+
95
+ if (!this.blogPost) {
96
+ return html`<div class="blog-viewer"><div class="error">Blog not found</div></div>`;
97
+ }
98
+
99
+ const { title, date, tags, content } = this.blogPost;
100
+
101
+ return html`
102
+ <article class="blog-viewer">
103
+ <h1>${title}</h1>
104
+ <div class="meta">
105
+ <span>${date}</span>
106
+ </div>
107
+ ${tags?.length ? html`
108
+ <div class="tags">
109
+ ${tags.map(tag => html`<span class="tag">${tag}</span>`)}
110
+ </div>
111
+ ` : ''}
112
+ <div class="content">
113
+ ${unsafeHTML(this.renderMarkdown(content))}
114
+ </div>
115
+ </article>
116
+ `;
117
+ }
118
+
119
+ renderMarkdown(content: string): string {
120
+ if (!content) return '';
121
+ const nodes = pipeline.parse(content);
122
+ return pipeline.render(nodes);
123
+ }
124
+ }
@@ -0,0 +1,23 @@
1
+ import { css } from 'lit';
2
+
3
+ export const blogviewerStyles = css`
4
+ :host {
5
+ display: block;
6
+ }
7
+
8
+ .tags {
9
+ display: flex;
10
+ gap: 0.5rem;
11
+ flex-wrap: wrap;
12
+ margin-top: 1rem;
13
+ }
14
+
15
+ .tag {
16
+ background: var(--nav-link-hover-bg, #f0f0f0);
17
+ color: var(--text-color, #333);
18
+ padding: 0.25rem 0.5rem;
19
+ border-radius: 4px;
20
+ font-size: 0.875rem;
21
+ border: 1px solid var(--border-color, transparent);
22
+ }
23
+ `;
@@ -20,7 +20,7 @@ export class FooterComponent extends LitElement {
20
20
  <div class="footer-content">
21
21
  <span>&copy; ${this.copyright}</span>
22
22
  <span class="links">
23
- ${this.footerLinks.map(
23
+ ${(this.footerLinks || []).map(
24
24
  (link) => {
25
25
  const isExternal = link.link.startsWith('http') || link.link.startsWith('mailto:');
26
26
  return html`<a
@@ -1,9 +1,50 @@
1
1
  import { css } from 'lit';
2
2
 
3
3
  export const footerStyles = css`
4
+ :host {
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ padding: 1rem 2rem;
9
+ background: var(--background-color);
10
+ color: var(--text-color);
11
+ border-top: 1px solid var(--nav-link-hover-bg);
12
+ flex-wrap: wrap;
13
+ }
14
+
15
+ .footer-content {
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: flex-start;
19
+ }
20
+
21
+ .links {
22
+ display: flex;
23
+ gap: 1rem;
24
+ margin-top: 0.5rem;
25
+ }
26
+
27
+ .links a {
28
+ color: inherit;
29
+ text-decoration: none;
30
+ padding: 0.25rem 0.5rem;
31
+ border-radius: 4px;
32
+ transition: background-color 0.3s ease;
33
+ }
34
+
35
+ .links a:hover {
36
+ background-color: rgba(0, 0, 0, 0.1);
37
+ }
38
+
39
+ @media (min-width: 768px) {
4
40
  .footer-content {
5
- padding: 1rem;
6
- display: flex;
41
+ flex-direction: row;
42
+ align-items: center;
43
+ width: 100%;
7
44
  justify-content: space-between;
8
45
  }
46
+ .links {
47
+ margin-top: 0;
48
+ }
49
+ }
9
50
  `;
package/src/ui/index.ts CHANGED
@@ -2,3 +2,5 @@ export * from './about-me';
2
2
  export * from './admin';
3
3
  export * from './banner';
4
4
  export * from './footer';
5
+ export * from './blog-viewer';
6
+ export * from './story-viewer';
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ describe('StoryViewer Package', () => {
4
+ it('should have valid exports', () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,120 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
+ import { MarkdownPipeline } from '@leadertechie/md2html';
5
+
6
+ import { storyviewerStyles } from './styles';
7
+
8
+ const pipeline = new MarkdownPipeline({
9
+ imagePathPrefix: 'images/',
10
+ styleOptions: {
11
+ classPrefix: 'md-',
12
+ addHeadingIds: true
13
+ }
14
+ });
15
+
16
+ interface StoryPost {
17
+ title: string;
18
+ description: string;
19
+ date: string;
20
+ tags: string[];
21
+ author: string;
22
+ imageUrl: string;
23
+ content: string;
24
+ }
25
+
26
+ @customElement('my-story-viewer')
27
+ export class StoryViewer extends LitElement {
28
+ static styles = storyviewerStyles;
29
+
30
+ @property({ type: String })
31
+ accessor slug = '';
32
+
33
+ @state()
34
+ accessor storyPost: StoryPost | null = null;
35
+
36
+ @state()
37
+ accessor loading = true;
38
+
39
+ @state()
40
+ accessor error = '';
41
+
42
+ private get apiBaseUrl(): string {
43
+ return 'https://api.techieleader.com';
44
+ }
45
+
46
+ async connectedCallback() {
47
+ super.connectedCallback();
48
+ if (this.slug) {
49
+ await this.loadStory();
50
+ }
51
+ }
52
+
53
+ updated(changedProperties: Map<string, any>) {
54
+ if (changedProperties.has('slug') && this.slug) {
55
+ this.loadStory();
56
+ }
57
+ }
58
+
59
+ async loadStory() {
60
+ this.loading = true;
61
+ this.error = '';
62
+
63
+ try {
64
+ const res = await fetch(`${this.apiBaseUrl}/api/stories/${this.slug}`);
65
+ if (res.ok) {
66
+ const story = await res.json();
67
+ if (story && story.content) {
68
+ this.storyPost = story;
69
+ } else {
70
+ this.error = 'Story content not found';
71
+ }
72
+ } else {
73
+ this.error = 'Story not found';
74
+ }
75
+ } catch (e) {
76
+ this.error = 'Failed to load story';
77
+ }
78
+
79
+ this.loading = false;
80
+ }
81
+
82
+ render() {
83
+ if (this.loading) {
84
+ return html`<div class="loading">Loading...</div>`;
85
+ }
86
+
87
+ if (this.error) {
88
+ return html`<div class="error">${this.error}</div>`;
89
+ }
90
+
91
+ if (!this.storyPost) {
92
+ return html`<div class="error">Story not found</div>`;
93
+ }
94
+
95
+ const { title, date, tags, content } = this.storyPost;
96
+
97
+ return html`
98
+ <article>
99
+ <h1>${title}</h1>
100
+ <div class="meta">
101
+ <span>${date}</span>
102
+ </div>
103
+ ${tags?.length ? html`
104
+ <div class="tags">
105
+ ${tags.map(tag => html`<span class="tag">${tag}</span>`)}
106
+ </div>
107
+ ` : ''}
108
+ <div class="content">
109
+ ${unsafeHTML(this.renderMarkdown(content))}
110
+ </div>
111
+ </article>
112
+ `;
113
+ }
114
+
115
+ private renderMarkdown(content: string): string {
116
+ if (!content) return '';
117
+ const nodes = pipeline.parse(content);
118
+ return pipeline.render(nodes);
119
+ }
120
+ }
@@ -0,0 +1,54 @@
1
+ import { css } from 'lit';
2
+
3
+ export const storyviewerStyles = css`
4
+ :host {
5
+ display: block;
6
+ max-width: 800px;
7
+ margin: 0 auto;
8
+ }
9
+
10
+ .loading {
11
+ text-align: center;
12
+ padding: 2rem;
13
+ }
14
+
15
+ .error {
16
+ color: red;
17
+ text-align: center;
18
+ padding: 2rem;
19
+ }
20
+
21
+ h1 {
22
+ margin-bottom: 0.5rem;
23
+ }
24
+
25
+ .meta {
26
+ color: #666;
27
+ margin-bottom: 2rem;
28
+ }
29
+
30
+ .tags {
31
+ display: flex;
32
+ gap: 0.5rem;
33
+ flex-wrap: wrap;
34
+ margin-top: 1rem;
35
+ }
36
+
37
+ .tag {
38
+ background: var(--nav-link-hover-bg, #f0f0f0);
39
+ color: var(--text-color, #333);
40
+ padding: 0.25rem 0.5rem;
41
+ border-radius: 4px;
42
+ font-size: 0.875rem;
43
+ border: 1px solid var(--border-color, transparent);
44
+ }
45
+
46
+ .content {
47
+ line-height: 1.8;
48
+ text-align: left;
49
+ }
50
+
51
+ .content h1, .content h2, .content h3 {
52
+ margin-top: 1.5rem;
53
+ }
54
+ `;