AI Implementation feature(869): Admin Area Blog and Blog Page (#58)
This commit was merged in pull request #58.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MarkdownService } from '../../core/services/markdown.service';
|
||||
import { PublicBlogPost } from '../../core/services/blog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blog-presenter',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule],
|
||||
styles: [`
|
||||
.blog-item { display: block; margin: 0 0 16px 0; }
|
||||
.blog-title { margin: 0 0 4px 0; font-size: 18px; }
|
||||
.blog-date { display: block; font-size: 12px; opacity: 0.7; margin-bottom: 8px; }
|
||||
.blog-body { font-size: 14px; }
|
||||
`],
|
||||
template: `
|
||||
<article class="blog-item" [attr.data-testid]="'blog-presenter-' + post().id">
|
||||
<h2 class="blog-title" [attr.data-testid]="'blog-presenter-title-' + post().id">
|
||||
{{ post().title }}
|
||||
</h2>
|
||||
<time class="blog-date" [attr.data-testid]="'blog-presenter-date-' + post().id">
|
||||
{{ post().publishedAt | date: 'medium' }}
|
||||
</time>
|
||||
<div
|
||||
class="blog-body"
|
||||
[attr.data-testid]="'blog-presenter-body-' + post().id"
|
||||
[innerHTML]="renderedHtml()"
|
||||
></div>
|
||||
</article>
|
||||
`,
|
||||
})
|
||||
export class BlogPresenterComponent {
|
||||
readonly post = input.required<PublicBlogPost>();
|
||||
private readonly markdown = new MarkdownService();
|
||||
|
||||
renderedHtml(): string {
|
||||
return this.markdown.render(this.post()?.bodyMd ?? '');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,60 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BlogApiService, PublicBlogPost } from '../../core/services/blog.service';
|
||||
import { BlogPresenterComponent } from './blog-presenter.component';
|
||||
import { deriveBlogListState } from './blog.pure';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blog-page',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, BlogPresenterComponent],
|
||||
styles: [`
|
||||
.blog-page { display: flex; flex-direction: column; gap: 12px; }
|
||||
.blog-loading, .blog-error { padding: 8px; }
|
||||
.blog-error { color: var(--color-danger, #f00); }
|
||||
.blog-empty { opacity: 0.7; }
|
||||
.blog-list { display: flex; flex-direction: column; }
|
||||
`],
|
||||
template: `
|
||||
<section class="page-blog">
|
||||
<section class="blog-page" data-testid="blog-page">
|
||||
<h2>Blog</h2>
|
||||
<p>Blog posts will appear here.</p>
|
||||
|
||||
@if (state().kind === 'loading') {
|
||||
<p class="blog-loading" data-testid="blog-loading">Loading blog posts…</p>
|
||||
} @else if (state().kind === 'error') {
|
||||
<p class="blog-error" data-testid="blog-error">{{ state().message }}</p>
|
||||
} @else if (state().kind === 'empty') {
|
||||
<p class="blog-empty" data-testid="blog-empty">No announcements yet.</p>
|
||||
} @else {
|
||||
<div class="blog-list" data-testid="blog-list">
|
||||
@for (post of posts(); track post.id) {
|
||||
<app-blog-presenter [post]="post" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
})
|
||||
export class BlogPage {}
|
||||
export class BlogPage implements OnInit {
|
||||
private readonly blog = inject(BlogApiService);
|
||||
|
||||
readonly posts = signal<PublicBlogPost[]>([]);
|
||||
readonly loading = signal(true);
|
||||
readonly error = signal<string | null>(null);
|
||||
|
||||
readonly state = computed(() =>
|
||||
deriveBlogListState({ loading: this.loading(), error: this.error(), count: this.posts().length }),
|
||||
);
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const list = await this.blog.listPublished();
|
||||
this.posts.set(list);
|
||||
} catch (e: any) {
|
||||
this.error.set(e?.error?.message ?? e?.message ?? 'Failed to load blog posts');
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export type BlogListKind = 'loading' | 'error' | 'empty' | 'list';
|
||||
|
||||
export interface BlogListState {
|
||||
kind: BlogListKind;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function deriveBlogListState(input: {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
count: number;
|
||||
}): BlogListState {
|
||||
if (input.loading) return { kind: 'loading' };
|
||||
if (input.error) return { kind: 'error', message: input.error };
|
||||
if (input.count === 0) return { kind: 'empty' };
|
||||
return { kind: 'list' };
|
||||
}
|
||||
Reference in New Issue
Block a user