diff --git a/src/lib/blog/BlogController.test.ts b/src/lib/blog/BlogController.test.ts index 3cf4da7..1f66f18 100644 --- a/src/lib/blog/BlogController.test.ts +++ b/src/lib/blog/BlogController.test.ts @@ -1,149 +1,149 @@ -import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest'; -import { BlogController } from './BlogController.js'; -import { MarkdownRepository } from './markdown-repository.js'; -import { exampleMarkdown, exampleMarkdownFrontmatter } from './test-fixtures/example-markdown.js'; +import { + describe, + it, + beforeEach, + afterAll, + beforeAll, + expect, + afterEach, +} from "vitest"; +import { BlogController } from "./BlogController.js"; +import { MarkdownRepository } from "./markdown-repository.js"; +import { + exampleMarkdown, + exampleMarkdownFrontmatter, +} from "./test-fixtures/example-markdown.js"; describe(`BlogController`, () => { + let controller: BlogController; + + beforeEach(async () => { + controller = await BlogController.singleton(); + }); + + describe(`Getting all posts which show up on the /blog page`, () => { + it(`should load blogs from the content folder`, async () => { + // GIVEN + const blogPosts = await controller.getAllBlogPosts(); + + // 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).toBeUndefined(); + expect(aKnownBlogPost).not.toBeUndefined(); + expect(aKnownBookReview).not.toBeUndefined(); + }); + }); + + describe(`getBlogPostBySlug`, () => { + it(`should return null when the post doesn't exist`, async () => { + // When + const shouldBeNull = await controller.getBlogPostBySlug( + "some-made-up-blog-post", + ); + + // Then + expect(shouldBeNull).toBeNull(); + }); + + it(`should return the blog post when it exists`, async () => { + // When + const blogPost = await controller.getBlogPostBySlug( + "2023-02-03-vibe-check-10", + ); + + // Then + expect(blogPost).not.toBeNull(); + expect(blogPost.title).toBe("Vibe Check #10"); + }); + }); + + describe(`Finding content by slug`, () => { + describe(`Finding a blog post`, () => { + // GIVEN + const slugForRealBlogPost = "2023-02-03-vibe-check-10"; + const slugForFakeBlogPost = "some-made-up-blog-post"; + + it(`should return null if there's no blog post with the slug`, async () => { + // WHEN + const blogPost = + await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost); + + // THEN + expect(blogPost).toBeNull(); + }); + + it(`should return the blog post if it exists`, async () => { + // WHEN + const blogPost = + await controller.getAnyKindOfContentBySlug(slugForRealBlogPost); + + // THEN + expect(blogPost).not.toBeNull(); + expect(blogPost.title).toBe("Vibe Check #10"); + }); + }); + + describe(`Finding a book review`, () => { + const realSlug = "after"; + const fakeSlug = "some-made-up-book-review"; + + it(`should return null if there's no book review with the slug`, async () => { + // WHEN + const bookReview = await controller.getAnyKindOfContentBySlug(fakeSlug); + + // THEN + expect(bookReview).toBeNull(); + }); + + it(`should return the book review if it exists`, async () => { + // WHEN + const bookReview = await controller.getAnyKindOfContentBySlug(realSlug); + + // THEN + expect(bookReview).not.toBeNull(); + expect(bookReview.title).toBe("After"); + }); + }); + }); + + describe(`Creating a new blog post as a file`, () => { + const thisDirectory = import.meta.url + .replace("file://", "") + .split("/") + .filter((part) => part !== "BlogController.test.ts") + .join("/"); + const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`; let controller: BlogController; beforeEach(async () => { - controller = await BlogController.singleton(); + controller = await BlogController.singleton(); }); - describe(`Getting all posts which show up on the /blog page`, () => { - it(`should load blogs from the content folder`, async () => { - // GIVEN - const blogPosts = await controller.getAllBlogPosts(); - - // WHEN - const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10'); - const aKnownBookReview = blogPosts.find((post) => post.title === 'After'); - const aKnownSnoutStreetStudiosPost = blogPosts.find((post) => post.title === 'Cinnamon Dust Linen Shirt'); - const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post'); - - // then - expect(aMadeUpBlogPost).toBeUndefined(); - expect(aKnownBlogPost).not.toBeUndefined(); - expect(aKnownBookReview).not.toBeUndefined(); - expect(aKnownSnoutStreetStudiosPost).not.toBeUndefined(); - }); + afterAll(async () => { + await controller.markdownRepository.deleteBlogPostMarkdownFile(fileName); }); - describe(`getBlogPostBySlug`, () => { - it(`should return null when the post doesn't exist`, async () => { - // When - const shouldBeNull = await controller.getBlogPostBySlug('some-made-up-blog-post'); + it(`should create a new file in the content folder`, async () => { + // GIVEN + const markdownContent = exampleMarkdown; - // Then - expect(shouldBeNull).toBeNull(); - }); + // WHEN + const blogPost = await controller.createBlogPost( + fileName, + markdownContent, + ); - it(`should return the blog post when it exists`, async () => { - // When - const blogPost = await controller.getBlogPostBySlug('2023-02-03-vibe-check-10'); - - // Then - expect(blogPost).not.toBeNull(); - expect(blogPost.title).toBe('Vibe Check #10'); - }); - }); - - describe(`Finding content by slug`, () => { - describe(`Finding a blog post`, () => { - // GIVEN - const slugForRealBlogPost = '2023-02-03-vibe-check-10'; - const slugForFakeBlogPost = 'some-made-up-blog-post'; - - it(`should return null if there's no blog post with the slug`, async () => { - // WHEN - const blogPost = await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost); - - // THEN - expect(blogPost).toBeNull(); - }); - - it(`should return the blog post if it exists`, async () => { - // WHEN - const blogPost = await controller.getAnyKindOfContentBySlug(slugForRealBlogPost); - - // THEN - expect(blogPost).not.toBeNull(); - expect(blogPost.title).toBe('Vibe Check #10'); - }); - }); - - describe(`Finding a book review`, () => { - const realSlug = 'after'; - const fakeSlug = 'some-made-up-book-review'; - - it(`should return null if there's no book review with the slug`, async () => { - // WHEN - const bookReview = await controller.getAnyKindOfContentBySlug(fakeSlug); - - // THEN - expect(bookReview).toBeNull(); - }); - - it(`should return the book review if it exists`, async () => { - // WHEN - const bookReview = await controller.getAnyKindOfContentBySlug(realSlug); - - // THEN - expect(bookReview).not.toBeNull(); - expect(bookReview.title).toBe('After'); - }); - }); - - describe(`Finding a Snout Street Studios post`, () => { - const realSlug = '2023-08-cinnamon-dust-linen-shirt'; - const fakeSlug = 'some-made-up-snout-street-studios-post'; - - it(`should return null if there's no Snout Street Studios post with the slug`, async () => { - // WHEN - const snoutStreetStudiosPost = await controller.getAnyKindOfContentBySlug(fakeSlug); - - // THEN - expect(snoutStreetStudiosPost).toBeNull(); - }); - - it(`should return the Snout Street Studios post if it exists`, async () => { - // WHEN - const snoutStreetStudiosPost = await controller.getAnyKindOfContentBySlug(realSlug); - - // THEN - expect(snoutStreetStudiosPost).not.toBeNull(); - expect(snoutStreetStudiosPost.title).toBe('Cinnamon Dust Linen Shirt'); - }); - }); - }); - - describe(`Creating a new blog post as a file`, () => { - const thisDirectory = import.meta.url - .replace('file://', '') - .split('/') - .filter((part) => part !== 'BlogController.test.ts') - .join('/'); - const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`; - let controller: BlogController; - - beforeEach(async () => { - controller = await BlogController.singleton(); - }); - - afterAll(async () => { - await controller.markdownRepository.deleteBlogPostMarkdownFile(fileName); - }); - - it(`should create a new file in the content folder`, async () => { - // GIVEN - const markdownContent = exampleMarkdown; - - // WHEN - const blogPost = await controller.createBlogPost(fileName, markdownContent); - - // THEN - expect(blogPost).not.toBeNull(); - expect(blogPost.html).not.toBeNull(); - }); + // THEN + expect(blogPost).not.toBeNull(); + expect(blogPost.html).not.toBeNull(); }); + }); }); diff --git a/src/lib/blog/BlogController.ts b/src/lib/blog/BlogController.ts index 9cea446..09c6ddf 100644 --- a/src/lib/blog/BlogController.ts +++ b/src/lib/blog/BlogController.ts @@ -1,176 +1,158 @@ -import type { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js'; -import type { BlogPost } from './BlogPost.js'; -import type { BookReview } from './BookReview.js'; -import { MarkdownRepository } from './markdown-repository.js'; +import type { BlogPost } from "./BlogPost.js"; +import type { BookReview } from "./BookReview.js"; +import { MarkdownRepository } from "./markdown-repository.js"; -interface BlogItem { - title: string; - date: string; - content: string; - slug: string; - content_type: 'blog' | 'book_review' | 'snout_street_studios'; - tags?: string[]; +export interface BlogItem { + title: string; + date: string; + content: string; + slug: string; + content_type: "blog" | "book_review" | "snout_street_studios"; + tags?: string[]; } -interface BlogPostListItem extends BlogItem { - title: string; - author: string; - date: string; - book_review: boolean; - preview: string; - tags: string[]; +export interface BlogPostListItem extends BlogItem { + title: string; + author: string; + date: string; + book_review: boolean; + preview: string; + tags: string[]; } -interface BookReviewListItem extends BlogItem { - book_review: true; - title: string; - author: string; - image: string; - slug: string; - score: number; - finished: string; -} - -interface SnoutStreetStudiosPostListItem extends BlogItem { - title: string; - slug: string; - date: string; +export interface BookReviewListItem extends BlogItem { + book_review: true; + title: string; + author: string; + image: string; + slug: string; + score: number; + finished: string; } export class BlogController { - private _markdownRepository: MarkdownRepository; + private _markdownRepository: MarkdownRepository; - static async singleton(): Promise { - const markdownRepository = await MarkdownRepository.singleton(); - return new BlogController(markdownRepository); + static async singleton(): Promise { + const markdownRepository = await MarkdownRepository.singleton(); + return new BlogController(markdownRepository); + } + + constructor(markdownRepository: MarkdownRepository) { + this._markdownRepository = markdownRepository; + } + + get markdownRepository(): MarkdownRepository { + return this._markdownRepository; + } + + async createBlogPost( + resolvedFileName: string, + markdownContent: string, + ): Promise { + const createdBlogPost = + await this._markdownRepository.createBlogPostMarkdownFile( + resolvedFileName, + markdownContent, + ); + this._markdownRepository = await MarkdownRepository.singleton(true); + return createdBlogPost; + } + + async getAllBlogPosts( + pageSize?: number, + ): Promise> { + const blogPosts = this._markdownRepository.blogPosts; + + const bookReviews = this._markdownRepository.bookReviews; + + const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map( + (blogPost) => { + return this.blogPostToBlogPostListItem(blogPost); + }, + ); + + const bookReviewListItems: BookReviewListItem[] = + bookReviews.bookReviews.map((bookReview) => { + return this.bookReviewToBookReviewListItem(bookReview); + }); + + const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort( + (a, b) => (a.date > b.date ? -1 : 1), + ); + + if (pageSize === undefined) { + return allBlogPosts; } - constructor(markdownRepository: MarkdownRepository) { - this._markdownRepository = markdownRepository; + return allBlogPosts.slice(0, pageSize); + } + + async getBlogPostBySlug(slug: string): Promise { + const blogPost = this._markdownRepository.getBlogPostBySlug(slug); + if (blogPost) { + return this.blogPostToBlogPostListItem(blogPost); } - get markdownRepository(): MarkdownRepository { - return this._markdownRepository; + return null; + } + + async getBlogPostsByTags(tags: string[]): Promise { + const posts = await this.getAllBlogPosts(); + const blogPosts = posts.filter( + (post) => post.content_type === "blog", + ) as BlogPostListItem[]; + return blogPosts + .filter((post: BlogPostListItem) => post["tags"]?.length > 0) + .filter((post: BlogPostListItem) => + (post.tags as string[]).some((tag) => tags.includes(tag)), + ); + } + + async getAnyKindOfContentBySlug( + slug: string, + ): Promise { + const blogPost = this._markdownRepository.getBlogPostBySlug(slug); + if (blogPost) { + return this.blogPostToBlogPostListItem(blogPost); } - async createBlogPost(resolvedFileName: string, markdownContent: string): Promise { - const createdBlogPost = await this._markdownRepository.createBlogPostMarkdownFile( - resolvedFileName, - markdownContent - ); - this._markdownRepository = await MarkdownRepository.singleton(true); - return createdBlogPost; + const bookReview = this._markdownRepository.getBookReviewBySlug(slug); + if (bookReview) { + return this.bookReviewToBookReviewListItem(bookReview); } - async getAllBlogPosts( - pageSize?: number - ): Promise> { - const blogPosts = this._markdownRepository.blogPosts; + return null; + } - const bookReviews = this._markdownRepository.bookReviews; + private bookReviewToBookReviewListItem( + bookReview: BookReview, + ): BookReviewListItem { + 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, + content: bookReview.html, + content_type: "book_review", + }; + } - const snoutStreetStudiosPosts = this._markdownRepository.snoutStreetStudiosPosts; - - const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => { - return this.blogPostToBlogPostListItem(blogPost); - }); - - const bookReviewListItems: BookReviewListItem[] = bookReviews.bookReviews.map((bookReview) => { - return this.bookReviewToBookReviewListItem(bookReview); - }); - - const snoutStreetStudiosPostListItems: SnoutStreetStudiosPostListItem[] = snoutStreetStudiosPosts.posts.map( - (post) => this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(post) - ); - - const allBlogPosts = [...blogPostListItems, ...bookReviewListItems, ...snoutStreetStudiosPostListItems].sort( - (a, b) => (a.date > b.date ? -1 : 1) - ); - - if (pageSize === undefined) { - return allBlogPosts; - } - - return allBlogPosts.slice(0, pageSize); - } - - async getBlogPostBySlug(slug: string): Promise { - const blogPost = await this._markdownRepository.getBlogPostBySlug(slug); - if (blogPost) { - return this.blogPostToBlogPostListItem(blogPost); - } - - return null; - } - - async getBlogPostsByTags(tags: string[]): Promise { - const posts = await this.getAllBlogPosts(); - const blogPosts = posts.filter((post) => post.content_type === 'blog') as BlogPostListItem[]; - return blogPosts - .filter((post: BlogPostListItem) => post['tags']?.length > 0) - .filter((post: BlogPostListItem) => (post.tags as string[]).some((tag) => tags.includes(tag))); - } - - async getAnyKindOfContentBySlug( - slug: string - ): Promise { - const blogPost = await this._markdownRepository.getBlogPostBySlug(slug); - if (blogPost) { - return this.blogPostToBlogPostListItem(blogPost); - } - - const bookReview = await this._markdownRepository.getBookReviewBySlug(slug); - if (bookReview) { - return this.bookReviewToBookReviewListItem(bookReview); - } - - const snoutStreetStudiosPost = await this._markdownRepository.getSnoutStreetStudiosPostBySlug(slug); - - if (snoutStreetStudiosPost) { - return this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(snoutStreetStudiosPost); - } - - return null; - } - - private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem { - 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, - content: bookReview.html, - content_type: 'book_review', - }; - } - - private blogPostToBlogPostListItem(blogPost: BlogPost): BlogPostListItem { - return { - title: blogPost.title, - author: blogPost.author, - book_review: false, - content: blogPost.html, - date: blogPost.date.toISOString(), - preview: blogPost.excerpt, - slug: blogPost.slug, - content_type: 'blog', - tags: blogPost.tags, - }; - } - - private snoutStreetStudiosPostToSnoutStreetStudiosPostListItem( - post: SnoutStreetStudiosPost - ): SnoutStreetStudiosPostListItem { - return { - title: post.title, - slug: post.slug, - date: post.date.toISOString(), - content_type: 'snout_street_studios', - content: post.html, - }; - } + private blogPostToBlogPostListItem(blogPost: BlogPost): BlogPostListItem { + return { + title: blogPost.title, + author: blogPost.author, + book_review: false, + content: blogPost.html, + date: blogPost.date.toISOString(), + preview: blogPost.excerpt, + slug: blogPost.slug, + content_type: "blog", + tags: blogPost.tags, + }; + } } diff --git a/src/lib/blog/SnoutStreetStudiosPostSet.test.ts b/src/lib/blog/SnoutStreetStudiosPostSet.test.ts deleted file mode 100644 index 2e3d707..0000000 --- a/src/lib/blog/SnoutStreetStudiosPostSet.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { aSnoutStreetStudiosPost } from './test-builders/snout-street-studios-post-builder.js'; -import { SnoutStreetStudiosPostSet } from './SnoutStreetStudiosPostSet.js'; - -describe(`SnoutStreetStudiosBlogPostSet`, () => { - it(`Should contain a list of posts`, () => { - // GIVEN - const postOne = aSnoutStreetStudiosPost().withTitle('Post One').build(); - const postTwo = aSnoutStreetStudiosPost().withTitle('Post Two').build(); - - // WHEN - const postSet = new SnoutStreetStudiosPostSet([postOne, postTwo]); - - // THEN - expect(postSet.posts).toStrictEqual([postOne, postTwo]); - }); -}); diff --git a/src/lib/blog/SnoutStreetStudiosPostSet.ts b/src/lib/blog/SnoutStreetStudiosPostSet.ts deleted file mode 100644 index e854e78..0000000 --- a/src/lib/blog/SnoutStreetStudiosPostSet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js'; - -export class SnoutStreetStudiosPostSet { - private readonly _posts: SnoutStreetStudiosPost[] = []; - - constructor(posts: SnoutStreetStudiosPost[]) { - this._posts = posts; - } - - public get posts(): SnoutStreetStudiosPost[] { - return this._posts; - } -} diff --git a/src/lib/blog/markdown-repository.test.ts b/src/lib/blog/markdown-repository.test.ts index c766a7a..ed393da 100644 --- a/src/lib/blog/markdown-repository.test.ts +++ b/src/lib/blog/markdown-repository.test.ts @@ -1,105 +1,103 @@ -import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; -import { MarkdownRepository } from './markdown-repository.js'; -import { resolve, dirname } from 'path'; +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { MarkdownRepository } from "./markdown-repository.js"; +import { resolve, dirname } from "path"; -import { aBlogPost } from './test-builders/blog-post-builder.js'; -import { aSnoutStreetStudiosPost } from './test-builders/snout-street-studios-post-builder.js'; +import { aBlogPost } from "./test-builders/blog-post-builder.js"; -const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, { as: 'raw' }); -const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, { as: 'raw' }); -const snoutStreetPostImport = import.meta.glob(`./test-fixtures/snout-street-studio-*.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 snoutStreetPostImport = import.meta.glob( + `./test-fixtures/snout-street-studio-*.md`, + { as: "raw" }, +); const expectedHtml = `

This is a blog post written in markdown.

This is a link

`; describe(`Blog MarkdownRepository`, () => { + let repository: MarkdownRepository; + + beforeEach(async () => { + repository = await MarkdownRepository.fromViteGlobImport( + blogPostImport, + bookReviewImport, + ); + }); + + it(`should load`, async () => { + // GIVEN + const expectedBlogPost = aBlogPost() + .withAuthor("Thomas Wilson") + .withDate(new Date("2023-02-01T08:00:00Z")) + .withSlug("2023-02-01-test") + .withTitle("Test Blog Post") + .withExcerpt("This is a blog post written in markdown. This is a link") + .withHtml(expectedHtml) + .withFileName("blog-2023-02-01-test.md") + .build(); + + // WHEN + const blogPost = + repository.blogPosts.getBlogPostWithTitle("Test Blog Post"); + + // THEN + expect(repository).toBeDefined(); + expect(blogPost).toStrictEqual(expectedBlogPost); + }); + + it(`should automatically build all the blog posts and book reviews`, async () => { + // WHEN/THEN + expect(repository.blogPosts.blogPosts[0].html).not.toBeNull(); + expect(repository.bookReviews.bookReviews[0].html).not.toBeNull(); + }); + + describe(`Finding by Slug`, () => { + it(`should return null if there's neither a blog post nor a book review with the slug`, async () => { + // WHEN + const markdownFile = repository.getBlogPostBySlug("non-existent-slug"); + + // THEN + expect(markdownFile).toBeNull(); + }); + }); + + describe(`Deleting markdown files`, () => { let repository: MarkdownRepository; + const currentDirectory = dirname(import.meta.url.replace("file://", "")); - beforeEach(async () => { - repository = await MarkdownRepository.fromViteGlobImport( - blogPostImport, - bookReviewImport, - snoutStreetPostImport - ); + beforeAll(async () => { + repository = await MarkdownRepository.fromViteGlobImport( + blogPostImport, + bookReviewImport, + snoutStreetPostImport, + ); + + const resolvedPath = resolve( + `${currentDirectory}/test-fixtures/test-file.md`, + ); + await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml); }); - it(`should load`, async () => { - // GIVEN - const expectedBlogPost = await aBlogPost() - .withAuthor('Thomas Wilson') - .withDate(new Date('2023-02-01T08:00:00Z')) - .withSlug('2023-02-01-test') - .withTitle('Test Blog Post') - .withExcerpt('This is a blog post written in markdown. This is a link') - .withHtml(expectedHtml) - .withFileName('blog-2023-02-01-test.md') - .build(); + it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => { + // GIVEN + const theFileName = "non-existent-file.md"; - const expectedSnoutStreetPost = aSnoutStreetStudiosPost() - .withSlug('the-test-slug') - .withTitle('Test Post') - .withDate(new Date('2023-09-02T06:40:00.000Z')) - .withExcerpt('This is a test post.') - .withHtml('

This is a test post.

') - .build(); - - // WHEN - const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post'); - const snoutStreetPosts = repository.snoutStreetStudiosPosts.posts; - - // THEN - expect(repository).toBeDefined(); - expect(blogPost).toStrictEqual(expectedBlogPost); - expect(snoutStreetPosts).toStrictEqual([expectedSnoutStreetPost]); + // WHEN/THEN + expect(async () => + repository.deleteBlogPostMarkdownFile(theFileName), + ).rejects.toThrowError(`File 'non-existent-file.md' not found.`); }); - it(`should automatically build all the blog posts and book reviews`, async () => { - // WHEN/THEN - expect(repository.blogPosts.blogPosts[0].html).not.toBeNull(); - expect(repository.bookReviews.bookReviews[0].html).not.toBeNull(); - }); - - describe(`Finding by Slug`, () => { - it(`should return null if there's neither a blog post nor a book review with the slug`, async () => { - // WHEN - const markdownFile = repository.getBlogPostBySlug('non-existent-slug'); - - // THEN - expect(markdownFile).toBeNull(); - }); - }); - - describe(`Deleting markdown files`, () => { - let repository: MarkdownRepository; - const currentDirectory = dirname(import.meta.url.replace('file://', '')); - - beforeAll(async () => { - repository = await MarkdownRepository.fromViteGlobImport( - blogPostImport, - bookReviewImport, - snoutStreetPostImport - ); - - const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`); - await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml); - }); - - it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => { - // GIVEN - const theFileName = 'non-existent-file.md'; - - // WHEN/THEN - expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError( - `File 'non-existent-file.md' not found.` - ); - }); - - it(`should successfully delete a file when it does exist`, async () => { - // GIVEN - const fileName = `${currentDirectory}/test-fixtures/test-file.md`; - - // WHEN - await repository.deleteBlogPostMarkdownFile(fileName); - }); + it(`should successfully delete a file when it does exist`, async () => { + // GIVEN + const fileName = `${currentDirectory}/test-fixtures/test-file.md`; + + // WHEN + await repository.deleteBlogPostMarkdownFile(fileName); }); + }); }); diff --git a/src/lib/blog/markdown-repository.ts b/src/lib/blog/markdown-repository.ts index f1cddee..8e0e0cf 100644 --- a/src/lib/blog/markdown-repository.ts +++ b/src/lib/blog/markdown-repository.ts @@ -1,231 +1,222 @@ -import { writeFile, unlink, existsSync } from 'fs'; +import { writeFile, unlink, existsSync } from "fs"; -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'; -import { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js'; -import { SnoutStreetStudiosPostSet } from './SnoutStreetStudiosPostSet.js'; +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"; // We have to duplicate the `../..` here because import.meta must have a static string, // and it (rightfully) cannot have dynamic locations -const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, { as: 'raw' }); -const bookReviewsMetaGlobImport = import.meta.glob(`../../content/book-reviews/*.md`, { as: 'raw' }); -const snoutStreetStudiosPostMetaGlobImport = import.meta.glob('../../content/snout-street-studios/*.md', { as: 'raw' }); +const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, { + as: "raw", +}); +const bookReviewsMetaGlobImport = import.meta.glob( + `../../content/book-reviews/*.md`, + { as: "raw" }, +); interface BlogPostFrontmatterValues { - title: string; - slug: string; - date: Date; - author: string; - tags?: string[]; + title: string; + slug: string; + date: Date; + author: string; + tags?: string[]; } interface BookReviewFrontmatterValues { - title: string; - author: string; // Author of the book, not the review - slug: string; - date: Date; - finished: Date; - score: number; - image: string; -} - -interface SnoutStreetStudiosPostFrontmatterValues { - title: string; - slug: string; - date: string; + title: string; + author: string; // Author of the book, not the review + slug: string; + date: Date; + finished: Date; + score: number; + image: string; } export class MarkdownRepository { - readonly blogPosts: BlogPostSet; - readonly bookReviews: BookReviewSet; - readonly snoutStreetStudiosPosts: SnoutStreetStudiosPostSet; - private static _singleton: MarkdownRepository; + readonly blogPosts: BlogPostSet; + readonly bookReviews: BookReviewSet; + private static _singleton: MarkdownRepository; - private constructor( - blogPosts: BlogPost[], - bookReviews: BookReview[], - snoutStreetStudiosPosts: SnoutStreetStudiosPost[] - ) { - this.blogPosts = new BlogPostSet(blogPosts); - this.bookReviews = new BookReviewSet(bookReviews); - this.snoutStreetStudiosPosts = new SnoutStreetStudiosPostSet(snoutStreetStudiosPosts); + private constructor(blogPosts: BlogPost[], bookReviews: BookReview[]) { + this.blogPosts = new BlogPostSet(blogPosts); + this.bookReviews = new BookReviewSet(bookReviews); + } + + public static async singleton( + forceRefresh = false, + ): Promise { + if (forceRefresh || !this._singleton) { + console.log( + `[MarkdownRepository::singleton] Building MarkdownRepository singleton.`, + ); + this._singleton = await MarkdownRepository.fromViteGlobImport( + blogPostMetaGlobImport, + bookReviewsMetaGlobImport, + ); } - public static async singleton(forceRefresh = false): Promise { - if (forceRefresh || !this._singleton) { - console.log(`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`); - this._singleton = await MarkdownRepository.fromViteGlobImport( - blogPostMetaGlobImport, - bookReviewsMetaGlobImport, - snoutStreetStudiosPostMetaGlobImport - ); - } + return this._singleton; + } - return this._singleton; - } + public static async fromViteGlobImport( + blogGlobImport: any, + bookReviewGlobImport: any, + ): Promise { + let fileImports: MarkdownFile[] = []; + let blogPosts: BlogPost[] = []; + let bookReviews: BookReview[] = []; - public static async fromViteGlobImport( - blogGlobImport: any, - bookReviewGlobImport: any, - snoutStreetPostGlobImport: any - ): Promise { - let fileImports: MarkdownFile[] = []; - let blogPosts: BlogPost[] = []; - let bookReviews: BookReview[] = []; - let snoutStreetPosts: SnoutStreetStudiosPost[] = []; + const blogPostFiles = Object.entries(blogGlobImport); - const blogPostFiles = Object.entries(blogGlobImport); + for (const blogPostFile of blogPostFiles) { + const [filename, module] = blogPostFile as [ + string, + () => Promise, + ]; + try { + const markdownFile = + await MarkdownFile.build( + filename, + await module(), + ); - for (const blogPostFile of blogPostFiles) { - const [filename, module] = blogPostFile as [string, () => Promise]; - try { - const markdownFile = await MarkdownFile.build(filename, await module()); - - const blogPost = new BlogPost({ - excerpt: markdownFile.excerpt, - html: markdownFile.html, - title: markdownFile.frontmatter.title, - slug: markdownFile.frontmatter.slug, - author: markdownFile.frontmatter.author, - date: markdownFile.frontmatter.date, - fileName: filename, - tags: markdownFile.frontmatter.tags, - }); - - fileImports = [...fileImports, markdownFile]; - blogPosts = [...blogPosts, blogPost]; - } catch (e) { - console.error({ - message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`, - error: e, - }); - } - } - - for (const bookReviewFile of Object.entries(bookReviewGlobImport)) { - const [filename, module] = bookReviewFile as [string, () => Promise]; - try { - const markdownFile = await MarkdownFile.build(filename, await module()); - - 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, - html: markdownFile.html, - }); - - bookReviews = [...bookReviews, bookReview]; - } catch (e) { - console.error({ - message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`, - error: e, - }); - } - } - - for (const snoutStreetPostFile of Object.entries(snoutStreetPostGlobImport)) { - const [filename, module] = snoutStreetPostFile as [string, () => Promise]; - try { - const markdownFile = await MarkdownFile.build( - filename, - await module() - ); - - const snoutStreetPost = new SnoutStreetStudiosPost({ - title: markdownFile.frontmatter.title, - slug: markdownFile.frontmatter.slug, - date: new Date(markdownFile.frontmatter.date), - html: markdownFile.html, - excerpt: markdownFile.excerpt, - }); - - snoutStreetPosts = [...snoutStreetPosts, snoutStreetPost]; - } catch (e: any) { - console.error({ - message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`, - error: e, - }); - } - } - - console.log(`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`); - const repository = new MarkdownRepository(blogPosts, bookReviews, snoutStreetPosts); - console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`); - return repository; - } - - getBlogPostBySlug(slug: string): BlogPost | null { - return this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? null; - } - - getBookReviewBySlug(slug: string): BookReview | null { - return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null; - } - - getSnoutStreetStudiosPostBySlug(slug: string): SnoutStreetStudiosPost | null { - return this.snoutStreetStudiosPosts.posts.find((post) => post.slug === slug) ?? null; - } - - async createBlogPostMarkdownFile(resolvedPath: string, contents: string): Promise { - return new Promise((resolve, reject) => { - writeFile(resolvedPath, contents, (err) => { - if (err) { - console.error({ - message: `createBlogPostMarkdownFile: Caught error while writing file ${resolvedPath}`, - err, - error: JSON.stringify(err), - }); - reject(err); - } - - resolve(); - }); - }).then(async () => { - const markdownFile = await MarkdownFile.build(resolvedPath, contents); - - const blogPost = new BlogPost({ - html: markdownFile.html, - excerpt: markdownFile.excerpt, - title: markdownFile.frontmatter?.title ?? undefined, - slug: markdownFile.frontmatter?.slug ?? undefined, - author: markdownFile.frontmatter?.author ?? undefined, - date: markdownFile.frontmatter?.date ?? undefined, - fileName: resolvedPath, - tags: [], - }); - - return blogPost; + const blogPost = new BlogPost({ + excerpt: markdownFile.excerpt, + html: markdownFile.html, + title: markdownFile.frontmatter.title, + slug: markdownFile.frontmatter.slug, + author: markdownFile.frontmatter.author, + date: markdownFile.frontmatter.date, + fileName: filename, + tags: markdownFile.frontmatter.tags ?? [], }); + + fileImports = [...fileImports, markdownFile]; + blogPosts = [...blogPosts, blogPost]; + } catch (e) { + console.error({ + message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`, + error: e, + }); + } } - async deleteBlogPostMarkdownFile(resolvedFilePath: string): Promise { - const isPresent = existsSync(resolvedFilePath); + for (const bookReviewFile of Object.entries(bookReviewGlobImport)) { + const [filename, module] = bookReviewFile as [ + string, + () => Promise, + ]; + try { + const markdownFile = + await MarkdownFile.build( + filename, + await module(), + ); - if (!isPresent) { - throw `Sausages File '${resolvedFilePath}' not found.`; + 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, + html: markdownFile.html, + }); + + bookReviews = [...bookReviews, bookReview]; + } catch (e) { + console.error({ + message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`, + error: e, + }); + } + } + + console.log( + `[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`, + ); + const repository = new MarkdownRepository(blogPosts, bookReviews); + console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`); + return repository; + } + + getBlogPostBySlug(slug: string): BlogPost | null { + return ( + this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? + null + ); + } + + getBookReviewBySlug(slug: string): BookReview | null { + return ( + this.bookReviews.bookReviews.find( + (bookReview) => bookReview.slug === slug, + ) ?? null + ); + } + + async createBlogPostMarkdownFile( + resolvedPath: string, + contents: string, + ): Promise { + return new Promise((resolve, reject) => { + writeFile(resolvedPath, contents, (err) => { + if (err) { + console.error({ + message: `createBlogPostMarkdownFile: Caught error while writing file ${resolvedPath}`, + err, + error: JSON.stringify(err), + }); + reject(err); } - return new Promise((resolve, reject) => { - unlink(resolvedFilePath, (err) => { - if (err) { - console.error({ - message: `deleteBlogPostMarkdownFile: Caught error while deleting file ${resolvedFilePath}`, - err, - error: JSON.stringify(err), - }); - reject(err); - } + resolve(); + }); + }).then(async () => { + const markdownFile = await MarkdownFile.build( + resolvedPath, + contents, + ); - resolve(); - }); - }); + const blogPost = new BlogPost({ + html: markdownFile.html, + excerpt: markdownFile.excerpt, + title: markdownFile.frontmatter?.title ?? undefined, + slug: markdownFile.frontmatter?.slug ?? undefined, + author: markdownFile.frontmatter?.author ?? undefined, + date: markdownFile.frontmatter?.date ?? undefined, + fileName: resolvedPath, + tags: [], + }); + + return blogPost; + }); + } + + async deleteBlogPostMarkdownFile(resolvedFilePath: string): Promise { + const isPresent = existsSync(resolvedFilePath); + + if (!isPresent) { + throw `Sausages File '${resolvedFilePath}' not found.`; } + + return new Promise((resolve, reject) => { + unlink(resolvedFilePath, (err) => { + if (err) { + console.error({ + message: `deleteBlogPostMarkdownFile: Caught error while deleting file ${resolvedFilePath}`, + err, + error: JSON.stringify(err), + }); + reject(err); + } + + resolve(); + }); + }); + } } diff --git a/src/lib/blog/test-builders/snout-street-studios-post-builder.ts b/src/lib/blog/test-builders/snout-street-studios-post-builder.ts deleted file mode 100644 index d45a971..0000000 --- a/src/lib/blog/test-builders/snout-street-studios-post-builder.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js'; - -class SnoutStreetStudiosPostBuilder { - private slug = 'the-default-slug'; - private title = 'the-default-title'; - private date = new Date('2000-01-01'); - private html = 'the-default-html'; - private excerpt = 'the-default-excerpt'; - - public withSlug(slug: string): SnoutStreetStudiosPostBuilder { - this.slug = slug; - return this; - } - - public withTitle(title: string): SnoutStreetStudiosPostBuilder { - this.title = title; - return this; - } - - public withDate(date: Date): SnoutStreetStudiosPostBuilder { - this.date = date; - return this; - } - - public withHtml(html: string): SnoutStreetStudiosPostBuilder { - this.html = html; - return this; - } - - public withExcerpt(excerpt: string): SnoutStreetStudiosPostBuilder { - this.excerpt = excerpt; - return this; - } - - build(): SnoutStreetStudiosPost { - return new SnoutStreetStudiosPost({ - slug: this.slug, - title: this.title, - date: this.date, - html: this.html, - excerpt: this.excerpt, - }); - } -} - -export function aSnoutStreetStudiosPost(): SnoutStreetStudiosPostBuilder { - return new SnoutStreetStudiosPostBuilder(); -} diff --git a/src/lib/blog/test-fixtures/snout-street-studio-post-test.md b/src/lib/blog/test-fixtures/snout-street-studio-post-test.md deleted file mode 100644 index 059c80c..0000000 --- a/src/lib/blog/test-fixtures/snout-street-studio-post-test.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: 'Test Post' -post_type: 'finished_project' -date: 2023-09-02T06:40:00.000Z -garment_birthday: 2023-09-01 -slug: the-test-slug -labour_hours: '100' -elapsed_time: '5 weeks' -cloth_description: 'test cloth description' -cloth_link: 'https://www.example.com' -pattern_description: 'test pattern description' -pattern_link: 'https://www.pattern.com' -author: Thomas Wilson -images: - - cinnamon-dust-linen-shirt/2023-08-14-cinnamon-shirt.jpeg - ---- - -This is a test post. \ No newline at end of file diff --git a/src/lib/snout-street-studios/ApiGateway.ts b/src/lib/snout-street-studios/ApiGateway.ts deleted file mode 100644 index 9939ffe..0000000 --- a/src/lib/snout-street-studios/ApiGateway.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { SnoutStreetStudiosPost } from './SnoutStreetStudiosPost.js'; -import type { BlogController } from '$lib/blog/BlogController.js'; - -export class SnoutStreetStudiosApiGateway { - constructor(private readonly controller: BlogController) {} - - async getPostBySlug(slug: string): Promise { - const post = await this.controller.getAnyKindOfContentBySlug(slug); - - if (!post) { - return null; - } - - return { - date: new Date(post.date), - slug: post.slug, - title: post.title, - html: post.content, - toJson: () => JSON.stringify(post), - excerpt: post. - }; - } -} diff --git a/src/lib/snout-street-studios/SnoutStreetStudiosPost.test.ts b/src/lib/snout-street-studios/SnoutStreetStudiosPost.test.ts deleted file mode 100644 index b5ba4e2..0000000 --- a/src/lib/snout-street-studios/SnoutStreetStudiosPost.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { SnoutStreetStudiosPost } from './SnoutStreetStudiosPost.js'; -import { aSnoutStreetStudiosPost } from '$lib/blog/test-builders/snout-street-studios-post-builder.js'; - -describe('SnoutStreetStudiosPost', () => { - it(`should construct`, () => { - // WHEN - const post = new SnoutStreetStudiosPost({ - title: 'the title', - slug: 'the-slug', - date: new Date('2023-09-02T06:58:00Z'), - html: 'the html', - }); - - // THEN - expect(post).toStrictEqual( - aSnoutStreetStudiosPost() - .withTitle('the title') - .withSlug('the-slug') - .withDate(new Date('2023-09-02T06:58:00Z')) - .withHtml('the html') - .withExcerpt(undefined) - .build() - ); - }); -}); diff --git a/src/lib/snout-street-studios/SnoutStreetStudiosPost.ts b/src/lib/snout-street-studios/SnoutStreetStudiosPost.ts deleted file mode 100644 index 562f1f9..0000000 --- a/src/lib/snout-street-studios/SnoutStreetStudiosPost.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { z } from 'zod'; -import type { SnoutStreetStudiosPostDto } from './SnoutStreetStudiosPostDto.js'; - -const SnoutStreetStudiosPostProps = z.object({ - slug: z.string(), - title: z.string(), - date: z.date(), - html: z.string(), -}); - -// Make a props type from the zod schema, where the values are non-optional. -type Props = z.infer & { - excerpt: string; - html: string; -}; - -export class SnoutStreetStudiosPost { - public readonly slug: string; - public readonly title: string; - public readonly date: Date; - public readonly html: string; - public readonly excerpt: string; - - constructor(props: Props) { - try { - const { slug, title, date, html } = SnoutStreetStudiosPostProps.parse(props); - this.slug = slug; - this.title = title; - this.date = date; - this.html = html; - this.excerpt = props.excerpt; - } catch (error) { - SnoutStreetStudiosPost.logAndThenThrowError(`Failed to construct post`, 'constructor', { props, error }); - throw error; - } - } - - private static logAndThenThrowError(errorMessage: string, contextName: string, ...args: any) { - console.error({ - info: `Caught error in SnoutStreetStudiosPost::${contextName}`, - errorMessage, - ...args, - }); - - throw new Error(errorMessage); - } - - public toJson(): string { - const dto: SnoutStreetStudiosPostDto = { - slug: this.slug, - title: this.title, - date: this.date, - html: this.html, - excerpt: this.excerpt, - }; - - return JSON.stringify(dto); - } - - public static fromJson(json: string): SnoutStreetStudiosPost { - try { - JSON.parse(json); - } catch { - this.logAndThenThrowError('Failed to parse JSON', 'fromJson', { json }); - } - - try { - const dto: SnoutStreetStudiosPostDto = JSON.parse(json); - return new SnoutStreetStudiosPost(dto); - } catch { - this.logAndThenThrowError(`Failed to construct post from JSON`, 'fromJson', { json }); - } - } -} diff --git a/src/lib/snout-street-studios/SnoutStreetStudiosPostDto.ts b/src/lib/snout-street-studios/SnoutStreetStudiosPostDto.ts deleted file mode 100644 index 40e138c..0000000 --- a/src/lib/snout-street-studios/SnoutStreetStudiosPostDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface SnoutStreetStudiosPostDto { - slug: string; - title: string; - html: string; - date: Date; - excerpt: string; -} diff --git a/src/lib/snout-street-studios/index.ts b/src/lib/snout-street-studios/index.ts deleted file mode 100644 index ec3bd93..0000000 --- a/src/lib/snout-street-studios/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { SnoutStreetStudiosPostDto } from './SnoutStreetStudiosPostDto.js'; -export { SnoutStreetStudiosPost } from './SnoutStreetStudiosPost.js'; -export { SnoutStreetStudiosApiGateway } from './ApiGateway.js'; diff --git a/src/routes/blog/+page.server.ts b/src/routes/blog/+page.server.ts index 347aa76..d4c69b4 100644 --- a/src/routes/blog/+page.server.ts +++ b/src/routes/blog/+page.server.ts @@ -1,25 +1,54 @@ -import { BlogController } from '$lib/blog/BlogController.js'; -import type { Load } from '@sveltejs/kit'; -import { differenceInCalendarDays, getYear } from 'date-fns'; +import { + BlogController, + type BlogItem, + type BlogPostListItem, + type BookReviewListItem, +} from "$lib/blog/BlogController.js"; +import type { BookReview } from "$lib/blog/BookReview.js"; +import type { Load } from "@sveltejs/kit"; +import { differenceInCalendarDays, getYear } from "date-fns"; export const prerender = true; +type PostsGroupedByMonth = Array<{ + yearDate: string; + posts: (BlogPostListItem | BookReviewListItem)[]; +}>; + export const load: Load = async ({}) => { - const controller = await BlogController.singleton(); - const posts = await controller.getAllBlogPosts(); + const controller = await BlogController.singleton(); + const posts = await controller.getAllBlogPosts(); - const currentYear = getYear(new Date()); + const currentYear = getYear(new Date()); - const numberOfPosts = posts.length; - const firstPost = posts[numberOfPosts - 1]; - const numberOfBlogPostsThisYear: number = posts.filter( - (post) => getYear(new Date(post.date)) === currentYear - ).length; + const numberOfPosts = posts.length; + const firstPost = posts[numberOfPosts - 1]; + const numberOfBlogPostsThisYear: number = posts.filter( + (post) => getYear(new Date(post.date)) === currentYear, + ).length; - return { - posts, - firstPost, - numberOfPosts, - numberOfBlogPostsThisYear, - }; + const postsGroupedByMonth = posts.reduce((grouped, post) => { + const yearDate = Intl.DateTimeFormat("en-gb", { + year: "numeric", + month: "long", + }).format(new Date(post.date)); + + const index = grouped.findIndex((entry) => entry.yearDate === yearDate); + + if (index === -1) { + grouped.push({ yearDate, posts: [post] }); + } else { + grouped[index].posts.push(post); + } + + return grouped; + }, [] as PostsGroupedByMonth); + + return { + posts, + postsGroupedByMonth, + firstPost, + numberOfPosts, + numberOfBlogPostsThisYear, + }; }; diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index f1d6afc..69210b2 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -12,14 +12,22 @@ const { data }: Props = $props(); - const { numberOfBlogPostsThisYear, numberOfPosts, posts } = data; + const { + numberOfBlogPostsThisYear, + numberOfPosts, + posts, + postsGroupedByMonth, + } = data; const daysSinceFirstPost = $derived( - differenceInCalendarDays(new Date(), new Date(posts[posts.length - 1].date)) + differenceInCalendarDays( + new Date(), + new Date(posts[posts.length - 1].date), + ), ); const daysSinceLastPublish = $derived( - differenceInCalendarDays(new Date(), new Date(posts[0].date)) + differenceInCalendarDays(new Date(), new Date(posts[0].date)), ); @@ -40,24 +48,27 @@ />

All Writing

-
    - {#each posts as post, index} - - {/each} -
+ {#each postsGroupedByMonth as month} +

{month.yearDate}

+
    + {#each month.posts as post, index} + + {/each} +
+ {/each}
- diff --git a/src/routes/snout-street-studios/[slug]/+layout.svelte b/src/routes/snout-street-studios/[slug]/+layout.svelte deleted file mode 100644 index 37da23c..0000000 --- a/src/routes/snout-street-studios/[slug]/+layout.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

Snout St. Studios

- -{@render children?.()} diff --git a/src/routes/snout-street-studios/[slug]/+page.svelte b/src/routes/snout-street-studios/[slug]/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/snout-street-studios/new/+page.svelte b/src/routes/snout-street-studios/new/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/snout-street-studios/new/+page.ts b/src/routes/snout-street-studios/new/+page.ts deleted file mode 100644 index 9a45289..0000000 --- a/src/routes/snout-street-studios/new/+page.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LoadEvent } from '@sveltejs/kit'; -import { error } from '@sveltejs/kit'; - -export async function load({ url }: LoadEvent) { - if (url.hostname !== 'localhost') { - return error(404, 'Not found'); - } - - return {}; -}