BlogEngine: Re-add Book Reviews to the Blog Index page

This commit is contained in:
Thomas 2023-02-05 22:31:58 +00:00
parent 9fca4a6867
commit a8fc9a2691
15 changed files with 344 additions and 87 deletions

View file

@ -1,9 +1,8 @@
import type { BlogPost } from '$lib/blog/BlogPost.js';
import { describe, it, beforeEach, expect } from 'vitest';
import { BlogController } from './BlogController.js';
describe(`BlogController`, () => {
describe(`Getting all blog posts`, () => {
describe(`Getting all blog posts and book reviews`, () => {
let controller: BlogController;
beforeEach(async () => {
@ -16,11 +15,13 @@ describe(`BlogController`, () => {
// WHEN
const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
const aKnownBookReview = blogPosts.find((post) => post.title === 'After');
const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post');
// then
expect(aMadeUpBlogPost).toBeNull();
expect(aKnownBlogPost).not.toBeNull();
expect(aMadeUpBlogPost).toBeUndefined();
expect(aKnownBlogPost).not.toBeUndefined();
expect(aKnownBookReview).not.toBeUndefined();
});
});
});

View file

@ -0,0 +1,70 @@
import { MarkdownRepository } from './markdown-repository.js';
const blogPostMetaGlobImport = import.meta.glob('../../content/blog/*.md', { as: 'raw' });
const bookReviewsMetaGlobImport = import.meta.glob('../../content/book-reviews/*.md', { as: 'raw' });
interface BlogPostListItem {
title: string;
author: string;
date: string;
book_review: boolean;
preview: string;
content: string;
slug: string;
}
interface BookReviewListItem {
book_review: true;
title: string;
author: string;
image: string;
slug: string;
score: number;
finished: string;
date: string;
}
export class BlogController {
static async singleton(): Promise<BlogController> {
const markdownRepository = await MarkdownRepository.fromViteGlobImport(
blogPostMetaGlobImport,
bookReviewsMetaGlobImport
);
return new BlogController(markdownRepository);
}
constructor(private readonly markdownRepository: MarkdownRepository) {}
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem>> {
const blogPosts = await this.markdownRepository.blogPosts;
const bookReviews = await this.markdownRepository.bookReviews;
await blogPosts.buildAllBlogPosts();
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
return {
title: blogPost.title,
author: blogPost.author,
book_review: false,
content: blogPost.html,
date: blogPost.date.toISOString(),
preview: blogPost.excerpt,
slug: blogPost.slug,
};
});
const bookReviewListItems: BookReviewListItem[] = bookReviews.bookReviews.map((bookReview) => {
return {
book_review: true,
title: bookReview.title,
author: bookReview.author,
date: bookReview.date.toISOString(),
finished: bookReview.finished.toISOString(),
image: bookReview.image,
score: bookReview.score,
slug: bookReview.slug,
};
});
return [...blogPostListItems, ...bookReviewListItems].sort((a, b) => (a.date > b.date ? -1 : 1));
}
}

View file

@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { BookReview } from './BookReview.js';
import { aBookReview } from './test-builders/book-review-builder.js';
describe(`BookReview`, () => {
it(`should construct`, () => {
// GIVEN
const bookReview = new BookReview({
title: 'After',
author: 'Dr Bruce Greyson',
score: 3.5,
image: 'after',
slug: 'after',
date: new Date('2021-05-05'),
finished: new Date('2021-04-20'),
draft: false,
});
// WHEN
const expectedBookReview = aBookReview()
.withTitle('After')
.withAuthor('Dr Bruce Greyson')
.withScore(3.5)
.withImage('after')
.withSlug('after')
.withDate(new Date('2021-05-05'))
.withFinished(new Date('2021-04-20'))
.build();
// THEN
expect(bookReview).toEqual(expectedBookReview);
});
});

View file

@ -0,0 +1,30 @@
interface BookReviewProps {
title: string;
author: string;
score: number;
image: string;
slug: string;
date: Date;
finished: Date;
draft: boolean;
}
export class BookReview {
readonly title: string;
readonly author: string;
readonly score: number;
readonly image: string;
readonly slug: string;
readonly date: Date;
readonly finished: Date;
constructor(props: BookReviewProps) {
this.title = props.title;
this.author = props.author;
this.score = props.score;
this.image = props.image;
this.slug = props.slug;
this.date = props.date;
this.finished = props.finished;
}
}

View file

@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import { BookReviewSet } from './BookReviewSet.js';
import { aBookReview } from './test-builders/book-review-builder.js';
describe(`BookReviewSet`, () => {
it(`should construct`, () => {
// GIVEN
const bookReview = aBookReview().withTitle(`The title`).build();
// WHEN
const bookReviewSet = new BookReviewSet([bookReview]);
// THEN
expect(bookReviewSet.bookReviews).toStrictEqual([bookReview]);
});
});

View file

@ -0,0 +1,13 @@
import { BookReview } from './BookReview.js';
export class BookReviewSet {
private _bookReviews: BookReview[] = [];
constructor(bookReviews: BookReview[]) {
this._bookReviews = bookReviews;
}
get bookReviews(): BookReview[] {
return this._bookReviews;
}
}

View file

@ -2,10 +2,10 @@ import { describe, it, expect } from 'vitest';
import { MarkdownRepository } from './markdown-repository.js';
import { MarkdownFile } from './MarkdownFile.js';
import { BlogPost } from './BlogPost.js';
import { aBlogPost } from './test-builders/blog-post-builder.js';
const globImport = import.meta.glob(`./test-fixtures/*.md`, { as: 'raw' });
const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, { as: 'raw' });
const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, { as: 'raw' });
const testMarkdownContent = `---
title: "Test Blog Post"
@ -23,12 +23,7 @@ This is a [link](http://www.bbc.co.uk)
describe(`Blog MarkdownRepository`, () => {
it(`should load`, async () => {
// GIVEN
const repository = await MarkdownRepository.fromViteGlobImport(globImport);
const expectedFile = new MarkdownFile({
fileName: './test-fixtures/2023-02-01-test.md',
content: testMarkdownContent,
});
const repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
const expectedBlogPost = aBlogPost()
.withAuthor('Thomas Wilson')
@ -39,12 +34,10 @@ describe(`Blog MarkdownRepository`, () => {
.build();
// WHEN
const file = repository.getMarkdownFileForFileName('./test-fixtures/2023-02-01-test.md');
const blogPost = repository.getBlogPostWithTitle('Test Blog Post');
const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post');
// THEN
expect(repository).toBeDefined();
expect(file).toStrictEqual(expectedFile);
expect(blogPost).toStrictEqual(expectedBlogPost);
});
});

View file

@ -1,35 +1,51 @@
import { BlogPost } from './BlogPost.js';
import { MarkdownFile } from './MarkdownFile.js';
import { BlogPostSet } from './BlogPostSet.js';
import { BookReviewSet } from './BookReviewSet.js';
import { BookReview } from './BookReview.js';
interface FrontmatterValues {
interface BlogPostFrontmatterValues {
title: string;
slug: string;
date: Date;
author: string;
}
export class MarkdownRepository {
readonly markdownFiles: MarkdownFile[];
readonly blogPosts: BlogPostSet;
interface BookReviewFrontmatterValues {
title: string;
author: string; // Author of the book, not the review
slug: string;
date: Date;
finished: Date;
score: number;
image: string;
}
private constructor(files: MarkdownFile[], blogPosts: BlogPost[]) {
this.blogPosts = new BlogPostSet([]);
this.markdownFiles = files;
export class MarkdownRepository {
readonly blogPosts: BlogPostSet;
readonly bookReviews: BookReviewSet;
private constructor(blogPosts: BlogPost[], bookReviews: BookReview[]) {
this.blogPosts = new BlogPostSet(blogPosts);
this.bookReviews = new BookReviewSet(bookReviews);
}
public static async fromViteGlobImport(globImport): Promise<MarkdownRepository> {
let fileImports: MarkdownFile<FrontmatterValues>[] = [];
public static async fromViteGlobImport(blogGlobImport, bookReviewGlobImport): Promise<MarkdownRepository> {
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
let blogPosts: BlogPost[] = [];
const allFiles = Object.entries(globImport);
let bookReviews: BookReview[] = [];
for (const entry of allFiles) {
const [filename, module] = entry as [string, () => Promise<string>];
const blogPostFiles = Object.entries(blogGlobImport);
for (const blogPostFile of blogPostFiles) {
const [filename, module] = blogPostFile as [string, () => Promise<string>];
try {
const fileContent = await module();
const markdownFile = new MarkdownFile<FrontmatterValues>({ fileName: filename, content: fileContent });
const markdownFile = new MarkdownFile<BlogPostFrontmatterValues>({
fileName: filename,
content: fileContent,
});
const blogPost = new BlogPost({
markdownContent: markdownFile.content,
title: markdownFile.frontmatter.title,
@ -48,14 +64,36 @@ export class MarkdownRepository {
}
}
return new MarkdownRepository(fileImports, blogPosts);
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
const [filename, module] = bookReviewFile as [string, () => Promise<string>];
try {
const fileContent = await module();
const markdownFile = new MarkdownFile<BookReviewFrontmatterValues>({
fileName: filename,
content: fileContent,
});
const bookReview = new BookReview({
author: markdownFile.frontmatter.author,
title: markdownFile.frontmatter.title,
slug: markdownFile.frontmatter.slug,
date: markdownFile.frontmatter.date,
draft: false,
finished: markdownFile.frontmatter.finished,
image: markdownFile.frontmatter.image,
score: markdownFile.frontmatter.score,
});
bookReviews = [...bookReviews, bookReview];
} catch (e) {
console.error({
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
error: e,
});
}
}
getMarkdownFileForFileName(fileName: string): MarkdownFile | null {
return this.markdownFiles.find((file) => file.fileName === fileName) ?? null;
}
getBlogPostWithTitle(title: string): BlogPost | null {
return this.blogPosts.getBlogPostWithTitle(title);
return new MarkdownRepository(blogPosts, bookReviews);
}
}

View file

@ -0,0 +1,69 @@
import { BookReview } from '../BookReview.js';
class BookReviewBuilder {
private title = 'default title';
private author = 'default author';
private date = new Date();
private draft = false;
private finished = new Date();
private image = 'default image';
private score = 0;
private slug = 'default slug';
withTitle(title: string): BookReviewBuilder {
this.title = title;
return this;
}
withAuthor(author: string): BookReviewBuilder {
this.author = author;
return this;
}
withDate(date: Date): BookReviewBuilder {
this.date = date;
return this;
}
withDraft(draft: boolean): BookReviewBuilder {
this.draft = draft;
return this;
}
withFinished(finished: Date): BookReviewBuilder {
this.finished = finished;
return this;
}
withImage(image: string): BookReviewBuilder {
this.image = image;
return this;
}
withScore(score: number): BookReviewBuilder {
this.score = score;
return this;
}
withSlug(slug: string): BookReviewBuilder {
this.slug = slug;
return this;
}
build(): BookReview {
return new BookReview({
title: this.title,
author: this.author,
date: this.date,
draft: this.draft,
finished: this.finished,
image: this.image,
score: this.score,
slug: this.slug,
});
}
}
export function aBookReview(): BookReviewBuilder {
return new BookReviewBuilder();
}

View file

@ -0,0 +1,23 @@
---
title: "After"
author: "Dr Bruce Greyson"
score: 3.5
image: "after"
slug: "after"
book_review: true
date: 2021-05-05
finished: 2021-04-20
draft: false
tags:
- non-fiction
- death
links:
- country: "🇬🇧"
store_name: "Hive"
link: "https://www.hive.co.uk/Product/MD-Dr-Bruce-Greyson/After--A-Doctor-Explores-What-Near-Death-Experiences-Reve/25523446"
- country: "🇺🇸"
store_name: "bookshop.org"
link: "https://bookshop.org/books/after-a-doctor-explores-what-near-death-experiences-reveal-about-life-and-beyond/9781250263032"
---
This is some test content.

View file

@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { BlogController } from './BlogController.js';
import { BlogController } from '../../../lib/blog/BlogController';
export const GET = async () => {
try {

View file

@ -1,42 +0,0 @@
import type { BlogPostSet } from '../../../lib/blog/BlogPostSet.js';
import { MarkdownRepository } from '../../../lib/blog/markdown-repository.js';
const blogPostMetaGlobImport = import.meta.glob('../../../content/blog/*.md', { as: 'raw' });
interface BlogPostListItem {
title: string;
author: string;
date: string;
book_review: boolean;
preview: string;
content: string;
slug: string;
}
export class BlogController {
static async singleton(): Promise<BlogController> {
const markdownRepository = await MarkdownRepository.fromViteGlobImport(blogPostMetaGlobImport);
return new BlogController(markdownRepository);
}
constructor(private readonly markdownRepository: MarkdownRepository) {}
async getAllBlogPosts(): Promise<BlogPostListItem[]> {
const blogPosts = await this.markdownRepository.blogPosts;
await blogPosts.buildAllBlogPosts();
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
return {
title: blogPost.title,
author: blogPost.author,
book_review: false,
content: blogPost.html,
date: blogPost.date.toISOString(),
preview: blogPost.excerpt,
slug: blogPost.slug,
};
});
return blogPostListItems.sort((a, b) => (a.date > b.date ? -1 : 1));
}
}

View file

@ -1,7 +1,9 @@
import { json, type LoadEvent, error } from '@sveltejs/kit';
import { fetchBlogPostBySlug } from '$lib';
import { BlogController } from '../../../../lib/blog/BlogController.js';
export const GET = async ({ params }: LoadEvent) => {
// const controller = await BlogController.singleton();
const { slug } = params;
const post = await fetchBlogPostBySlug(slug);

View file

@ -73,9 +73,18 @@
aria-setsize={posts.length}
>
<a href={`/blog/${post.slug}`}>
{#if post.book_review} 📚 {/if}
<div class="post-title">{post.title}</div>
<div class="post-preview">{post.preview}...</div>
<div class="post-title">
{#if post.book_review} 📚 {/if}{post.title}
</div>
<div class="post-preview">
{#if post.preview}
{post.preview}...
{:else}
No preview available ): Click to read the full post.
{/if}
</div>
<div class="post-date">
{intlFormat(
new Date(post.date),