diff --git a/src/routes/api/blog.json/BlogController.test.ts b/src/lib/blog/BlogController.test.ts similarity index 69% rename from src/routes/api/blog.json/BlogController.test.ts rename to src/lib/blog/BlogController.test.ts index df2a1f5..811a28f 100644 --- a/src/routes/api/blog.json/BlogController.test.ts +++ b/src/lib/blog/BlogController.test.ts @@ -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(); }); }); }); diff --git a/src/lib/blog/BlogController.ts b/src/lib/blog/BlogController.ts new file mode 100644 index 0000000..c10ff7f --- /dev/null +++ b/src/lib/blog/BlogController.ts @@ -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 { + const markdownRepository = await MarkdownRepository.fromViteGlobImport( + blogPostMetaGlobImport, + bookReviewsMetaGlobImport + ); + return new BlogController(markdownRepository); + } + + constructor(private readonly markdownRepository: MarkdownRepository) {} + + async getAllBlogPosts(): Promise> { + 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)); + } +} diff --git a/src/lib/blog/BookReview.test.ts b/src/lib/blog/BookReview.test.ts new file mode 100644 index 0000000..9508b6a --- /dev/null +++ b/src/lib/blog/BookReview.test.ts @@ -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); + }); +}); diff --git a/src/lib/blog/BookReview.ts b/src/lib/blog/BookReview.ts new file mode 100644 index 0000000..eb764ca --- /dev/null +++ b/src/lib/blog/BookReview.ts @@ -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; + } +} diff --git a/src/lib/blog/BookReviewSet.test.ts b/src/lib/blog/BookReviewSet.test.ts new file mode 100644 index 0000000..361be1e --- /dev/null +++ b/src/lib/blog/BookReviewSet.test.ts @@ -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]); + }); +}); diff --git a/src/lib/blog/BookReviewSet.ts b/src/lib/blog/BookReviewSet.ts new file mode 100644 index 0000000..2d35fec --- /dev/null +++ b/src/lib/blog/BookReviewSet.ts @@ -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; + } +} diff --git a/src/lib/blog/markdown-repository.test.ts b/src/lib/blog/markdown-repository.test.ts index 3832103..9a34670 100644 --- a/src/lib/blog/markdown-repository.test.ts +++ b/src/lib/blog/markdown-repository.test.ts @@ -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); }); }); diff --git a/src/lib/blog/markdown-repository.ts b/src/lib/blog/markdown-repository.ts index b0ff206..6931f89 100644 --- a/src/lib/blog/markdown-repository.ts +++ b/src/lib/blog/markdown-repository.ts @@ -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 { - let fileImports: MarkdownFile[] = []; + public static async fromViteGlobImport(blogGlobImport, bookReviewGlobImport): Promise { + let fileImports: MarkdownFile[] = []; let blogPosts: BlogPost[] = []; - const allFiles = Object.entries(globImport); + let bookReviews: BookReview[] = []; - for (const entry of allFiles) { - const [filename, module] = entry as [string, () => Promise]; + const blogPostFiles = Object.entries(blogGlobImport); + + for (const blogPostFile of blogPostFiles) { + const [filename, module] = blogPostFile as [string, () => Promise]; try { const fileContent = await module(); - const markdownFile = new MarkdownFile({ fileName: filename, content: fileContent }); + const markdownFile = new MarkdownFile({ + 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]; + try { + const fileContent = await module(); - getMarkdownFileForFileName(fileName: string): MarkdownFile | null { - return this.markdownFiles.find((file) => file.fileName === fileName) ?? null; - } + const markdownFile = new MarkdownFile({ + fileName: filename, + content: fileContent, + }); - getBlogPostWithTitle(title: string): BlogPost | null { - return this.blogPosts.getBlogPostWithTitle(title); + 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, + }); + } + } + + return new MarkdownRepository(blogPosts, bookReviews); } } diff --git a/src/lib/blog/test-builders/book-review-builder.ts b/src/lib/blog/test-builders/book-review-builder.ts new file mode 100644 index 0000000..0be6135 --- /dev/null +++ b/src/lib/blog/test-builders/book-review-builder.ts @@ -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(); +} diff --git a/src/lib/blog/test-fixtures/2023-02-01-test.md b/src/lib/blog/test-fixtures/blog-2023-02-01-test.md similarity index 100% rename from src/lib/blog/test-fixtures/2023-02-01-test.md rename to src/lib/blog/test-fixtures/blog-2023-02-01-test.md diff --git a/src/lib/blog/test-fixtures/book-review-test.md b/src/lib/blog/test-fixtures/book-review-test.md new file mode 100644 index 0000000..58ccab7 --- /dev/null +++ b/src/lib/blog/test-fixtures/book-review-test.md @@ -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. \ No newline at end of file diff --git a/src/routes/api/blog.json/+server.ts b/src/routes/api/blog.json/+server.ts index 70bdf0b..8b4a221 100644 --- a/src/routes/api/blog.json/+server.ts +++ b/src/routes/api/blog.json/+server.ts @@ -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 { diff --git a/src/routes/api/blog.json/BlogController.ts b/src/routes/api/blog.json/BlogController.ts deleted file mode 100644 index b67d06a..0000000 --- a/src/routes/api/blog.json/BlogController.ts +++ /dev/null @@ -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 { - const markdownRepository = await MarkdownRepository.fromViteGlobImport(blogPostMetaGlobImport); - return new BlogController(markdownRepository); - } - - constructor(private readonly markdownRepository: MarkdownRepository) {} - - async getAllBlogPosts(): Promise { - 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)); - } -} diff --git a/src/routes/api/blog/[slug].json/+server.ts b/src/routes/api/blog/[slug].json/+server.ts index 6efb077..599a237 100644 --- a/src/routes/api/blog/[slug].json/+server.ts +++ b/src/routes/api/blog/[slug].json/+server.ts @@ -1,14 +1,16 @@ 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 { slug } = params; + // const controller = await BlogController.singleton(); + const { slug } = params; - const post = await fetchBlogPostBySlug(slug); + const post = await fetchBlogPostBySlug(slug); - if (!post) { - throw error(404, `Could not find blog post with slug '${slug}'`); - } + if (!post) { + throw error(404, `Could not find blog post with slug '${slug}'`); + } - return json({ post }); + return json({ post }); }; diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index 4be36d0..1020e7f 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -73,9 +73,18 @@ aria-setsize={posts.length} > - {#if post.book_review} πŸ“š {/if} -
{post.title}
-
{post.preview}...
+
+ {#if post.book_review} πŸ“š {/if}{post.title} +
+ +
+ {#if post.preview} + {post.preview}... + {:else} + No preview available ): Click to read the full post. + {/if} +
+