refactor: remove references to SnoutStreetStudio posts
This commit is contained in:
parent
eb1e9d7b67
commit
d5dc6d0565
19 changed files with 632 additions and 872 deletions
|
|
@ -1,7 +1,18 @@
|
||||||
import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest';
|
import {
|
||||||
import { BlogController } from './BlogController.js';
|
describe,
|
||||||
import { MarkdownRepository } from './markdown-repository.js';
|
it,
|
||||||
import { exampleMarkdown, exampleMarkdownFrontmatter } from './test-fixtures/example-markdown.js';
|
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`, () => {
|
describe(`BlogController`, () => {
|
||||||
let controller: BlogController;
|
let controller: BlogController;
|
||||||
|
|
@ -16,23 +27,27 @@ describe(`BlogController`, () => {
|
||||||
const blogPosts = await controller.getAllBlogPosts();
|
const blogPosts = await controller.getAllBlogPosts();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
|
const aKnownBlogPost = blogPosts.find(
|
||||||
const aKnownBookReview = blogPosts.find((post) => post.title === 'After');
|
(post) => post.title === "Vibe Check #10",
|
||||||
const aKnownSnoutStreetStudiosPost = blogPosts.find((post) => post.title === 'Cinnamon Dust Linen Shirt');
|
);
|
||||||
const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post');
|
const aKnownBookReview = blogPosts.find((post) => post.title === "After");
|
||||||
|
const aMadeUpBlogPost = blogPosts.find(
|
||||||
|
(post) => post.title === "Some made up blog post",
|
||||||
|
);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(aMadeUpBlogPost).toBeUndefined();
|
expect(aMadeUpBlogPost).toBeUndefined();
|
||||||
expect(aKnownBlogPost).not.toBeUndefined();
|
expect(aKnownBlogPost).not.toBeUndefined();
|
||||||
expect(aKnownBookReview).not.toBeUndefined();
|
expect(aKnownBookReview).not.toBeUndefined();
|
||||||
expect(aKnownSnoutStreetStudiosPost).not.toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`getBlogPostBySlug`, () => {
|
describe(`getBlogPostBySlug`, () => {
|
||||||
it(`should return null when the post doesn't exist`, async () => {
|
it(`should return null when the post doesn't exist`, async () => {
|
||||||
// When
|
// When
|
||||||
const shouldBeNull = await controller.getBlogPostBySlug('some-made-up-blog-post');
|
const shouldBeNull = await controller.getBlogPostBySlug(
|
||||||
|
"some-made-up-blog-post",
|
||||||
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(shouldBeNull).toBeNull();
|
expect(shouldBeNull).toBeNull();
|
||||||
|
|
@ -40,23 +55,26 @@ describe(`BlogController`, () => {
|
||||||
|
|
||||||
it(`should return the blog post when it exists`, async () => {
|
it(`should return the blog post when it exists`, async () => {
|
||||||
// When
|
// When
|
||||||
const blogPost = await controller.getBlogPostBySlug('2023-02-03-vibe-check-10');
|
const blogPost = await controller.getBlogPostBySlug(
|
||||||
|
"2023-02-03-vibe-check-10",
|
||||||
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(blogPost).not.toBeNull();
|
expect(blogPost).not.toBeNull();
|
||||||
expect(blogPost.title).toBe('Vibe Check #10');
|
expect(blogPost.title).toBe("Vibe Check #10");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Finding content by slug`, () => {
|
describe(`Finding content by slug`, () => {
|
||||||
describe(`Finding a blog post`, () => {
|
describe(`Finding a blog post`, () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const slugForRealBlogPost = '2023-02-03-vibe-check-10';
|
const slugForRealBlogPost = "2023-02-03-vibe-check-10";
|
||||||
const slugForFakeBlogPost = 'some-made-up-blog-post';
|
const slugForFakeBlogPost = "some-made-up-blog-post";
|
||||||
|
|
||||||
it(`should return null if there's no blog post with the slug`, async () => {
|
it(`should return null if there's no blog post with the slug`, async () => {
|
||||||
// WHEN
|
// WHEN
|
||||||
const blogPost = await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
|
const blogPost =
|
||||||
|
await controller.getAnyKindOfContentBySlug(slugForFakeBlogPost);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(blogPost).toBeNull();
|
expect(blogPost).toBeNull();
|
||||||
|
|
@ -64,17 +82,18 @@ describe(`BlogController`, () => {
|
||||||
|
|
||||||
it(`should return the blog post if it exists`, async () => {
|
it(`should return the blog post if it exists`, async () => {
|
||||||
// WHEN
|
// WHEN
|
||||||
const blogPost = await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
|
const blogPost =
|
||||||
|
await controller.getAnyKindOfContentBySlug(slugForRealBlogPost);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(blogPost).not.toBeNull();
|
expect(blogPost).not.toBeNull();
|
||||||
expect(blogPost.title).toBe('Vibe Check #10');
|
expect(blogPost.title).toBe("Vibe Check #10");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Finding a book review`, () => {
|
describe(`Finding a book review`, () => {
|
||||||
const realSlug = 'after';
|
const realSlug = "after";
|
||||||
const fakeSlug = 'some-made-up-book-review';
|
const fakeSlug = "some-made-up-book-review";
|
||||||
|
|
||||||
it(`should return null if there's no book review with the slug`, async () => {
|
it(`should return null if there's no book review with the slug`, async () => {
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
@ -90,39 +109,17 @@ describe(`BlogController`, () => {
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(bookReview).not.toBeNull();
|
expect(bookReview).not.toBeNull();
|
||||||
expect(bookReview.title).toBe('After');
|
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`, () => {
|
describe(`Creating a new blog post as a file`, () => {
|
||||||
const thisDirectory = import.meta.url
|
const thisDirectory = import.meta.url
|
||||||
.replace('file://', '')
|
.replace("file://", "")
|
||||||
.split('/')
|
.split("/")
|
||||||
.filter((part) => part !== 'BlogController.test.ts')
|
.filter((part) => part !== "BlogController.test.ts")
|
||||||
.join('/');
|
.join("/");
|
||||||
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
|
const fileName = `${thisDirectory}/test-fixtures/test-blog-controller.md`;
|
||||||
let controller: BlogController;
|
let controller: BlogController;
|
||||||
|
|
||||||
|
|
@ -139,7 +136,10 @@ describe(`BlogController`, () => {
|
||||||
const markdownContent = exampleMarkdown;
|
const markdownContent = exampleMarkdown;
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const blogPost = await controller.createBlogPost(fileName, markdownContent);
|
const blogPost = await controller.createBlogPost(
|
||||||
|
fileName,
|
||||||
|
markdownContent,
|
||||||
|
);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(blogPost).not.toBeNull();
|
expect(blogPost).not.toBeNull();
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
import type { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js';
|
import type { BlogPost } from "./BlogPost.js";
|
||||||
import type { BlogPost } from './BlogPost.js';
|
import type { BookReview } from "./BookReview.js";
|
||||||
import type { BookReview } from './BookReview.js';
|
import { MarkdownRepository } from "./markdown-repository.js";
|
||||||
import { MarkdownRepository } from './markdown-repository.js';
|
|
||||||
|
|
||||||
interface BlogItem {
|
export interface BlogItem {
|
||||||
title: string;
|
title: string;
|
||||||
date: string;
|
date: string;
|
||||||
content: string;
|
content: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
content_type: 'blog' | 'book_review' | 'snout_street_studios';
|
content_type: "blog" | "book_review" | "snout_street_studios";
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlogPostListItem extends BlogItem {
|
export interface BlogPostListItem extends BlogItem {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
|
@ -21,7 +20,7 @@ interface BlogPostListItem extends BlogItem {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookReviewListItem extends BlogItem {
|
export interface BookReviewListItem extends BlogItem {
|
||||||
book_review: true;
|
book_review: true;
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
|
@ -31,12 +30,6 @@ interface BookReviewListItem extends BlogItem {
|
||||||
finished: string;
|
finished: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnoutStreetStudiosPostListItem extends BlogItem {
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BlogController {
|
export class BlogController {
|
||||||
private _markdownRepository: MarkdownRepository;
|
private _markdownRepository: MarkdownRepository;
|
||||||
|
|
||||||
|
|
@ -53,38 +46,39 @@ export class BlogController {
|
||||||
return this._markdownRepository;
|
return this._markdownRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBlogPost(resolvedFileName: string, markdownContent: string): Promise<BlogPost> {
|
async createBlogPost(
|
||||||
const createdBlogPost = await this._markdownRepository.createBlogPostMarkdownFile(
|
resolvedFileName: string,
|
||||||
|
markdownContent: string,
|
||||||
|
): Promise<BlogPost> {
|
||||||
|
const createdBlogPost =
|
||||||
|
await this._markdownRepository.createBlogPostMarkdownFile(
|
||||||
resolvedFileName,
|
resolvedFileName,
|
||||||
markdownContent
|
markdownContent,
|
||||||
);
|
);
|
||||||
this._markdownRepository = await MarkdownRepository.singleton(true);
|
this._markdownRepository = await MarkdownRepository.singleton(true);
|
||||||
return createdBlogPost;
|
return createdBlogPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllBlogPosts(
|
async getAllBlogPosts(
|
||||||
pageSize?: number
|
pageSize?: number,
|
||||||
): Promise<Array<BlogPostListItem | BookReviewListItem | SnoutStreetStudiosPostListItem>> {
|
): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
||||||
const blogPosts = this._markdownRepository.blogPosts;
|
const blogPosts = this._markdownRepository.blogPosts;
|
||||||
|
|
||||||
const bookReviews = this._markdownRepository.bookReviews;
|
const bookReviews = this._markdownRepository.bookReviews;
|
||||||
|
|
||||||
const snoutStreetStudiosPosts = this._markdownRepository.snoutStreetStudiosPosts;
|
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map(
|
||||||
|
(blogPost) => {
|
||||||
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
|
|
||||||
return this.blogPostToBlogPostListItem(blogPost);
|
return this.blogPostToBlogPostListItem(blogPost);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const bookReviewListItems: BookReviewListItem[] = bookReviews.bookReviews.map((bookReview) => {
|
const bookReviewListItems: BookReviewListItem[] =
|
||||||
|
bookReviews.bookReviews.map((bookReview) => {
|
||||||
return this.bookReviewToBookReviewListItem(bookReview);
|
return this.bookReviewToBookReviewListItem(bookReview);
|
||||||
});
|
});
|
||||||
|
|
||||||
const snoutStreetStudiosPostListItems: SnoutStreetStudiosPostListItem[] = snoutStreetStudiosPosts.posts.map(
|
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems].sort(
|
||||||
(post) => this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(post)
|
(a, b) => (a.date > b.date ? -1 : 1),
|
||||||
);
|
|
||||||
|
|
||||||
const allBlogPosts = [...blogPostListItems, ...bookReviewListItems, ...snoutStreetStudiosPostListItems].sort(
|
|
||||||
(a, b) => (a.date > b.date ? -1 : 1)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pageSize === undefined) {
|
if (pageSize === undefined) {
|
||||||
|
|
@ -95,7 +89,7 @@ export class BlogController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBlogPostBySlug(slug: string): Promise<BlogPostListItem | null> {
|
async getBlogPostBySlug(slug: string): Promise<BlogPostListItem | null> {
|
||||||
const blogPost = await this._markdownRepository.getBlogPostBySlug(slug);
|
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||||
if (blogPost) {
|
if (blogPost) {
|
||||||
return this.blogPostToBlogPostListItem(blogPost);
|
return this.blogPostToBlogPostListItem(blogPost);
|
||||||
}
|
}
|
||||||
|
|
@ -105,35 +99,35 @@ export class BlogController {
|
||||||
|
|
||||||
async getBlogPostsByTags(tags: string[]): Promise<BlogPostListItem[]> {
|
async getBlogPostsByTags(tags: string[]): Promise<BlogPostListItem[]> {
|
||||||
const posts = await this.getAllBlogPosts();
|
const posts = await this.getAllBlogPosts();
|
||||||
const blogPosts = posts.filter((post) => post.content_type === 'blog') as BlogPostListItem[];
|
const blogPosts = posts.filter(
|
||||||
|
(post) => post.content_type === "blog",
|
||||||
|
) as BlogPostListItem[];
|
||||||
return blogPosts
|
return blogPosts
|
||||||
.filter((post: BlogPostListItem) => post['tags']?.length > 0)
|
.filter((post: BlogPostListItem) => post["tags"]?.length > 0)
|
||||||
.filter((post: BlogPostListItem) => (post.tags as string[]).some((tag) => tags.includes(tag)));
|
.filter((post: BlogPostListItem) =>
|
||||||
|
(post.tags as string[]).some((tag) => tags.includes(tag)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAnyKindOfContentBySlug(
|
async getAnyKindOfContentBySlug(
|
||||||
slug: string
|
slug: string,
|
||||||
): Promise<BookReviewListItem | BlogPostListItem | SnoutStreetStudiosPostListItem | null> {
|
): Promise<BookReviewListItem | BlogPostListItem | null> {
|
||||||
const blogPost = await this._markdownRepository.getBlogPostBySlug(slug);
|
const blogPost = this._markdownRepository.getBlogPostBySlug(slug);
|
||||||
if (blogPost) {
|
if (blogPost) {
|
||||||
return this.blogPostToBlogPostListItem(blogPost);
|
return this.blogPostToBlogPostListItem(blogPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookReview = await this._markdownRepository.getBookReviewBySlug(slug);
|
const bookReview = this._markdownRepository.getBookReviewBySlug(slug);
|
||||||
if (bookReview) {
|
if (bookReview) {
|
||||||
return this.bookReviewToBookReviewListItem(bookReview);
|
return this.bookReviewToBookReviewListItem(bookReview);
|
||||||
}
|
}
|
||||||
|
|
||||||
const snoutStreetStudiosPost = await this._markdownRepository.getSnoutStreetStudiosPostBySlug(slug);
|
|
||||||
|
|
||||||
if (snoutStreetStudiosPost) {
|
|
||||||
return this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(snoutStreetStudiosPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem {
|
private bookReviewToBookReviewListItem(
|
||||||
|
bookReview: BookReview,
|
||||||
|
): BookReviewListItem {
|
||||||
return {
|
return {
|
||||||
book_review: true,
|
book_review: true,
|
||||||
title: bookReview.title,
|
title: bookReview.title,
|
||||||
|
|
@ -144,7 +138,7 @@ export class BlogController {
|
||||||
score: bookReview.score,
|
score: bookReview.score,
|
||||||
slug: bookReview.slug,
|
slug: bookReview.slug,
|
||||||
content: bookReview.html,
|
content: bookReview.html,
|
||||||
content_type: 'book_review',
|
content_type: "book_review",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,20 +151,8 @@ export class BlogController {
|
||||||
date: blogPost.date.toISOString(),
|
date: blogPost.date.toISOString(),
|
||||||
preview: blogPost.excerpt,
|
preview: blogPost.excerpt,
|
||||||
slug: blogPost.slug,
|
slug: blogPost.slug,
|
||||||
content_type: 'blog',
|
content_type: "blog",
|
||||||
tags: blogPost.tags,
|
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeAll, beforeEach } from "vitest";
|
||||||
import { MarkdownRepository } from './markdown-repository.js';
|
import { MarkdownRepository } from "./markdown-repository.js";
|
||||||
import { resolve, dirname } from 'path';
|
import { resolve, dirname } from "path";
|
||||||
|
|
||||||
import { aBlogPost } from './test-builders/blog-post-builder.js';
|
import { aBlogPost } from "./test-builders/blog-post-builder.js";
|
||||||
import { aSnoutStreetStudiosPost } from './test-builders/snout-street-studios-post-builder.js';
|
|
||||||
|
|
||||||
const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, { as: 'raw' });
|
const blogPostImport = import.meta.glob(`./test-fixtures/blog-*.md`, {
|
||||||
const bookReviewImport = import.meta.glob(`./test-fixtures/book-review-*.md`, { as: 'raw' });
|
as: "raw",
|
||||||
const snoutStreetPostImport = import.meta.glob(`./test-fixtures/snout-street-studio-*.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 = `<p>This is a blog post written in markdown.</p>
|
const expectedHtml = `<p>This is a blog post written in markdown.</p>
|
||||||
<p>This is a <a href="http://www.bbc.co.uk">link</a></p>`;
|
<p>This is a <a href="http://www.bbc.co.uk">link</a></p>`;
|
||||||
|
|
@ -19,38 +25,28 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
repository = await MarkdownRepository.fromViteGlobImport(
|
repository = await MarkdownRepository.fromViteGlobImport(
|
||||||
blogPostImport,
|
blogPostImport,
|
||||||
bookReviewImport,
|
bookReviewImport,
|
||||||
snoutStreetPostImport
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should load`, async () => {
|
it(`should load`, async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const expectedBlogPost = await aBlogPost()
|
const expectedBlogPost = aBlogPost()
|
||||||
.withAuthor('Thomas Wilson')
|
.withAuthor("Thomas Wilson")
|
||||||
.withDate(new Date('2023-02-01T08:00:00Z'))
|
.withDate(new Date("2023-02-01T08:00:00Z"))
|
||||||
.withSlug('2023-02-01-test')
|
.withSlug("2023-02-01-test")
|
||||||
.withTitle('Test Blog Post')
|
.withTitle("Test Blog Post")
|
||||||
.withExcerpt('This is a blog post written in markdown. This is a link')
|
.withExcerpt("This is a blog post written in markdown. This is a link")
|
||||||
.withHtml(expectedHtml)
|
.withHtml(expectedHtml)
|
||||||
.withFileName('blog-2023-02-01-test.md')
|
.withFileName("blog-2023-02-01-test.md")
|
||||||
.build();
|
|
||||||
|
|
||||||
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('<p>This is a test post.</p>')
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const blogPost = repository.blogPosts.getBlogPostWithTitle('Test Blog Post');
|
const blogPost =
|
||||||
const snoutStreetPosts = repository.snoutStreetStudiosPosts.posts;
|
repository.blogPosts.getBlogPostWithTitle("Test Blog Post");
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(repository).toBeDefined();
|
expect(repository).toBeDefined();
|
||||||
expect(blogPost).toStrictEqual(expectedBlogPost);
|
expect(blogPost).toStrictEqual(expectedBlogPost);
|
||||||
expect(snoutStreetPosts).toStrictEqual([expectedSnoutStreetPost]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should automatically build all the blog posts and book reviews`, async () => {
|
it(`should automatically build all the blog posts and book reviews`, async () => {
|
||||||
|
|
@ -62,7 +58,7 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
describe(`Finding by Slug`, () => {
|
describe(`Finding by Slug`, () => {
|
||||||
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
|
it(`should return null if there's neither a blog post nor a book review with the slug`, async () => {
|
||||||
// WHEN
|
// WHEN
|
||||||
const markdownFile = repository.getBlogPostBySlug('non-existent-slug');
|
const markdownFile = repository.getBlogPostBySlug("non-existent-slug");
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(markdownFile).toBeNull();
|
expect(markdownFile).toBeNull();
|
||||||
|
|
@ -71,27 +67,29 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
|
|
||||||
describe(`Deleting markdown files`, () => {
|
describe(`Deleting markdown files`, () => {
|
||||||
let repository: MarkdownRepository;
|
let repository: MarkdownRepository;
|
||||||
const currentDirectory = dirname(import.meta.url.replace('file://', ''));
|
const currentDirectory = dirname(import.meta.url.replace("file://", ""));
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
repository = await MarkdownRepository.fromViteGlobImport(
|
repository = await MarkdownRepository.fromViteGlobImport(
|
||||||
blogPostImport,
|
blogPostImport,
|
||||||
bookReviewImport,
|
bookReviewImport,
|
||||||
snoutStreetPostImport
|
snoutStreetPostImport,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`);
|
const resolvedPath = resolve(
|
||||||
|
`${currentDirectory}/test-fixtures/test-file.md`,
|
||||||
|
);
|
||||||
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
|
await repository.createBlogPostMarkdownFile(resolvedPath, expectedHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
|
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const theFileName = 'non-existent-file.md';
|
const theFileName = "non-existent-file.md";
|
||||||
|
|
||||||
// WHEN/THEN
|
// WHEN/THEN
|
||||||
expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError(
|
expect(async () =>
|
||||||
`File 'non-existent-file.md' not found.`
|
repository.deleteBlogPostMarkdownFile(theFileName),
|
||||||
);
|
).rejects.toThrowError(`File 'non-existent-file.md' not found.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should successfully delete a file when it does exist`, async () => {
|
it(`should successfully delete a file when it does exist`, async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
import { writeFile, unlink, existsSync } from 'fs';
|
import { writeFile, unlink, existsSync } from "fs";
|
||||||
|
|
||||||
import { BlogPost } from './BlogPost.js';
|
import { BlogPost } from "./BlogPost.js";
|
||||||
import { MarkdownFile } from './MarkdownFile.js';
|
import { MarkdownFile } from "./MarkdownFile.js";
|
||||||
import { BlogPostSet } from './BlogPostSet.js';
|
import { BlogPostSet } from "./BlogPostSet.js";
|
||||||
import { BookReviewSet } from './BookReviewSet.js';
|
import { BookReviewSet } from "./BookReviewSet.js";
|
||||||
import { BookReview } from './BookReview.js';
|
import { BookReview } from "./BookReview.js";
|
||||||
import { SnoutStreetStudiosPost } from '$lib/snout-street-studios/SnoutStreetStudiosPost.js';
|
|
||||||
import { SnoutStreetStudiosPostSet } from './SnoutStreetStudiosPostSet.js';
|
|
||||||
|
|
||||||
// We have to duplicate the `../..` here because import.meta must have a static string,
|
// We have to duplicate the `../..` here because import.meta must have a static string,
|
||||||
// and it (rightfully) cannot have dynamic locations
|
// and it (rightfully) cannot have dynamic locations
|
||||||
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, { as: 'raw' });
|
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, {
|
||||||
const bookReviewsMetaGlobImport = import.meta.glob(`../../content/book-reviews/*.md`, { as: 'raw' });
|
as: "raw",
|
||||||
const snoutStreetStudiosPostMetaGlobImport = import.meta.glob('../../content/snout-street-studios/*.md', { as: 'raw' });
|
});
|
||||||
|
const bookReviewsMetaGlobImport = import.meta.glob(
|
||||||
|
`../../content/book-reviews/*.md`,
|
||||||
|
{ as: "raw" },
|
||||||
|
);
|
||||||
|
|
||||||
interface BlogPostFrontmatterValues {
|
interface BlogPostFrontmatterValues {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -32,35 +34,26 @@ interface BookReviewFrontmatterValues {
|
||||||
image: string;
|
image: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnoutStreetStudiosPostFrontmatterValues {
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MarkdownRepository {
|
export class MarkdownRepository {
|
||||||
readonly blogPosts: BlogPostSet;
|
readonly blogPosts: BlogPostSet;
|
||||||
readonly bookReviews: BookReviewSet;
|
readonly bookReviews: BookReviewSet;
|
||||||
readonly snoutStreetStudiosPosts: SnoutStreetStudiosPostSet;
|
|
||||||
private static _singleton: MarkdownRepository;
|
private static _singleton: MarkdownRepository;
|
||||||
|
|
||||||
private constructor(
|
private constructor(blogPosts: BlogPost[], bookReviews: BookReview[]) {
|
||||||
blogPosts: BlogPost[],
|
|
||||||
bookReviews: BookReview[],
|
|
||||||
snoutStreetStudiosPosts: SnoutStreetStudiosPost[]
|
|
||||||
) {
|
|
||||||
this.blogPosts = new BlogPostSet(blogPosts);
|
this.blogPosts = new BlogPostSet(blogPosts);
|
||||||
this.bookReviews = new BookReviewSet(bookReviews);
|
this.bookReviews = new BookReviewSet(bookReviews);
|
||||||
this.snoutStreetStudiosPosts = new SnoutStreetStudiosPostSet(snoutStreetStudiosPosts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async singleton(forceRefresh = false): Promise<MarkdownRepository> {
|
public static async singleton(
|
||||||
|
forceRefresh = false,
|
||||||
|
): Promise<MarkdownRepository> {
|
||||||
if (forceRefresh || !this._singleton) {
|
if (forceRefresh || !this._singleton) {
|
||||||
console.log(`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`);
|
console.log(
|
||||||
|
`[MarkdownRepository::singleton] Building MarkdownRepository singleton.`,
|
||||||
|
);
|
||||||
this._singleton = await MarkdownRepository.fromViteGlobImport(
|
this._singleton = await MarkdownRepository.fromViteGlobImport(
|
||||||
blogPostMetaGlobImport,
|
blogPostMetaGlobImport,
|
||||||
bookReviewsMetaGlobImport,
|
bookReviewsMetaGlobImport,
|
||||||
snoutStreetStudiosPostMetaGlobImport
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,19 +63,24 @@ export class MarkdownRepository {
|
||||||
public static async fromViteGlobImport(
|
public static async fromViteGlobImport(
|
||||||
blogGlobImport: any,
|
blogGlobImport: any,
|
||||||
bookReviewGlobImport: any,
|
bookReviewGlobImport: any,
|
||||||
snoutStreetPostGlobImport: any
|
|
||||||
): Promise<MarkdownRepository> {
|
): Promise<MarkdownRepository> {
|
||||||
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
|
let fileImports: MarkdownFile<BlogPostFrontmatterValues>[] = [];
|
||||||
let blogPosts: BlogPost[] = [];
|
let blogPosts: BlogPost[] = [];
|
||||||
let bookReviews: BookReview[] = [];
|
let bookReviews: BookReview[] = [];
|
||||||
let snoutStreetPosts: SnoutStreetStudiosPost[] = [];
|
|
||||||
|
|
||||||
const blogPostFiles = Object.entries(blogGlobImport);
|
const blogPostFiles = Object.entries(blogGlobImport);
|
||||||
|
|
||||||
for (const blogPostFile of blogPostFiles) {
|
for (const blogPostFile of blogPostFiles) {
|
||||||
const [filename, module] = blogPostFile as [string, () => Promise<string>];
|
const [filename, module] = blogPostFile as [
|
||||||
|
string,
|
||||||
|
() => Promise<string>,
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(filename, await module());
|
const markdownFile =
|
||||||
|
await MarkdownFile.build<BlogPostFrontmatterValues>(
|
||||||
|
filename,
|
||||||
|
await module(),
|
||||||
|
);
|
||||||
|
|
||||||
const blogPost = new BlogPost({
|
const blogPost = new BlogPost({
|
||||||
excerpt: markdownFile.excerpt,
|
excerpt: markdownFile.excerpt,
|
||||||
|
|
@ -92,7 +90,7 @@ export class MarkdownRepository {
|
||||||
author: markdownFile.frontmatter.author,
|
author: markdownFile.frontmatter.author,
|
||||||
date: markdownFile.frontmatter.date,
|
date: markdownFile.frontmatter.date,
|
||||||
fileName: filename,
|
fileName: filename,
|
||||||
tags: markdownFile.frontmatter.tags,
|
tags: markdownFile.frontmatter.tags ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
fileImports = [...fileImports, markdownFile];
|
fileImports = [...fileImports, markdownFile];
|
||||||
|
|
@ -106,9 +104,16 @@ export class MarkdownRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
|
for (const bookReviewFile of Object.entries(bookReviewGlobImport)) {
|
||||||
const [filename, module] = bookReviewFile as [string, () => Promise<string>];
|
const [filename, module] = bookReviewFile as [
|
||||||
|
string,
|
||||||
|
() => Promise<string>,
|
||||||
|
];
|
||||||
try {
|
try {
|
||||||
const markdownFile = await MarkdownFile.build<BookReviewFrontmatterValues>(filename, await module());
|
const markdownFile =
|
||||||
|
await MarkdownFile.build<BookReviewFrontmatterValues>(
|
||||||
|
filename,
|
||||||
|
await module(),
|
||||||
|
);
|
||||||
|
|
||||||
const bookReview = new BookReview({
|
const bookReview = new BookReview({
|
||||||
author: markdownFile.frontmatter.author,
|
author: markdownFile.frontmatter.author,
|
||||||
|
|
@ -131,50 +136,33 @@ export class MarkdownRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const snoutStreetPostFile of Object.entries(snoutStreetPostGlobImport)) {
|
console.log(
|
||||||
const [filename, module] = snoutStreetPostFile as [string, () => Promise<string>];
|
`[MarkdownRepository::fromViteGlobImport] Loaded ${fileImports.length} files.`,
|
||||||
try {
|
|
||||||
const markdownFile = await MarkdownFile.build<SnoutStreetStudiosPostFrontmatterValues>(
|
|
||||||
filename,
|
|
||||||
await module()
|
|
||||||
);
|
);
|
||||||
|
const repository = new MarkdownRepository(blogPosts, bookReviews);
|
||||||
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.`);
|
console.log(`[MarkdownRepository::fromViteGlobImport] Built all posts.`);
|
||||||
return repository;
|
return repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlogPostBySlug(slug: string): BlogPost | null {
|
getBlogPostBySlug(slug: string): BlogPost | null {
|
||||||
return this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ?? null;
|
return (
|
||||||
|
this.blogPosts.blogPosts.find((blogPost) => blogPost.slug === slug) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBookReviewBySlug(slug: string): BookReview | null {
|
getBookReviewBySlug(slug: string): BookReview | null {
|
||||||
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
return (
|
||||||
|
this.bookReviews.bookReviews.find(
|
||||||
|
(bookReview) => bookReview.slug === slug,
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnoutStreetStudiosPostBySlug(slug: string): SnoutStreetStudiosPost | null {
|
async createBlogPostMarkdownFile(
|
||||||
return this.snoutStreetStudiosPosts.posts.find((post) => post.slug === slug) ?? null;
|
resolvedPath: string,
|
||||||
}
|
contents: string,
|
||||||
|
): Promise<BlogPost> {
|
||||||
async createBlogPostMarkdownFile(resolvedPath: string, contents: string): Promise<BlogPost> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
writeFile(resolvedPath, contents, (err) => {
|
writeFile(resolvedPath, contents, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -189,7 +177,10 @@ export class MarkdownRepository {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(resolvedPath, contents);
|
const markdownFile = await MarkdownFile.build<BlogPostFrontmatterValues>(
|
||||||
|
resolvedPath,
|
||||||
|
contents,
|
||||||
|
);
|
||||||
|
|
||||||
const blogPost = new BlogPost({
|
const blogPost = new BlogPost({
|
||||||
html: markdownFile.html,
|
html: markdownFile.html,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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<SnoutStreetStudiosPost | null> {
|
|
||||||
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.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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<typeof SnoutStreetStudiosPostProps> & {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export interface SnoutStreetStudiosPostDto {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
html: string;
|
|
||||||
date: Date;
|
|
||||||
excerpt: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export type { SnoutStreetStudiosPostDto } from './SnoutStreetStudiosPostDto.js';
|
|
||||||
export { SnoutStreetStudiosPost } from './SnoutStreetStudiosPost.js';
|
|
||||||
export { SnoutStreetStudiosApiGateway } from './ApiGateway.js';
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
import { BlogController } from '$lib/blog/BlogController.js';
|
import {
|
||||||
import type { Load } from '@sveltejs/kit';
|
BlogController,
|
||||||
import { differenceInCalendarDays, getYear } from 'date-fns';
|
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;
|
export const prerender = true;
|
||||||
|
|
||||||
|
type PostsGroupedByMonth = Array<{
|
||||||
|
yearDate: string;
|
||||||
|
posts: (BlogPostListItem | BookReviewListItem)[];
|
||||||
|
}>;
|
||||||
|
|
||||||
export const load: Load = async ({}) => {
|
export const load: Load = async ({}) => {
|
||||||
const controller = await BlogController.singleton();
|
const controller = await BlogController.singleton();
|
||||||
const posts = await controller.getAllBlogPosts();
|
const posts = await controller.getAllBlogPosts();
|
||||||
|
|
@ -13,11 +24,29 @@ export const load: Load = async ({}) => {
|
||||||
const numberOfPosts = posts.length;
|
const numberOfPosts = posts.length;
|
||||||
const firstPost = posts[numberOfPosts - 1];
|
const firstPost = posts[numberOfPosts - 1];
|
||||||
const numberOfBlogPostsThisYear: number = posts.filter(
|
const numberOfBlogPostsThisYear: number = posts.filter(
|
||||||
(post) => getYear(new Date(post.date)) === currentYear
|
(post) => getYear(new Date(post.date)) === currentYear,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
posts,
|
posts,
|
||||||
|
postsGroupedByMonth,
|
||||||
firstPost,
|
firstPost,
|
||||||
numberOfPosts,
|
numberOfPosts,
|
||||||
numberOfBlogPostsThisYear,
|
numberOfBlogPostsThisYear,
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,22 @@
|
||||||
|
|
||||||
const { data }: Props = $props();
|
const { data }: Props = $props();
|
||||||
|
|
||||||
const { numberOfBlogPostsThisYear, numberOfPosts, posts } = data;
|
const {
|
||||||
|
numberOfBlogPostsThisYear,
|
||||||
|
numberOfPosts,
|
||||||
|
posts,
|
||||||
|
postsGroupedByMonth,
|
||||||
|
} = data;
|
||||||
|
|
||||||
const daysSinceFirstPost = $derived(
|
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(
|
const daysSinceLastPublish = $derived(
|
||||||
differenceInCalendarDays(new Date(), new Date(posts[0].date))
|
differenceInCalendarDays(new Date(), new Date(posts[0].date)),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -40,8 +48,10 @@
|
||||||
/>
|
/>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>All Writing</h2>
|
<h2>All Writing</h2>
|
||||||
|
{#each postsGroupedByMonth as month}
|
||||||
|
<h3>{month.yearDate}</h3>
|
||||||
<ul class="posts">
|
<ul class="posts">
|
||||||
{#each posts as post, index}
|
{#each month.posts as post, index}
|
||||||
<BlogPostListItem
|
<BlogPostListItem
|
||||||
{index}
|
{index}
|
||||||
content_type={post.content_type}
|
content_type={post.content_type}
|
||||||
|
|
@ -54,10 +64,11 @@
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style lang="scss">
|
<style>
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -82,9 +93,10 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
padding-bottom: 24px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100%;
|
grid-template-columns: 100%;
|
||||||
gap: var(--spacing-xl);
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
interface Props {
|
|
||||||
children?: import('svelte').Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { children }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>Snout St. Studios</h1>
|
|
||||||
|
|
||||||
{@render children?.()}
|
|
||||||
|
|
@ -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 {};
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue