BlogEngine: Create Blog Posts locally with endpoint
This commit is contained in:
parent
57dd0a017e
commit
3e2053e884
10 changed files with 305 additions and 36 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, it, beforeEach, beforeAll, expect, afterEach } from 'vitest';
|
import { describe, it, beforeEach, afterAll, beforeAll, expect, afterEach } from 'vitest';
|
||||||
import { BlogController } from './BlogController.js';
|
import { BlogController } from './BlogController.js';
|
||||||
import { MarkdownRepository } from './markdown-repository.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;
|
||||||
|
|
@ -74,16 +75,32 @@ describe(`BlogController`, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Creating a new blog post as a file`, () => {
|
describe(`Creating a new blog post as a file`, () => {
|
||||||
let fileName: string;
|
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;
|
let controller: BlogController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fileName = 'some-made-up-blog-post.md';
|
|
||||||
controller = await BlogController.singleton();
|
controller = await BlogController.singleton();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterAll(async () => {
|
||||||
await controller.markdownRepository.deleteBlogPostFile(fileName);
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ interface BookReviewListItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlogController {
|
export class BlogController {
|
||||||
private readonly _markdownRepository: MarkdownRepository;
|
private _markdownRepository: MarkdownRepository;
|
||||||
|
|
||||||
static async singleton(): Promise<BlogController> {
|
static async singleton(): Promise<BlogController> {
|
||||||
const markdownRepository = await MarkdownRepository.singleton();
|
const markdownRepository = await MarkdownRepository.singleton();
|
||||||
|
|
@ -43,6 +43,15 @@ export class BlogController {
|
||||||
return this._markdownRepository;
|
return this._markdownRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createBlogPost(resolvedFileName: string, markdownContent: string): Promise<BlogPost> {
|
||||||
|
const createdBlogPost = await this._markdownRepository.createBlogPostMarkdownFile(
|
||||||
|
resolvedFileName,
|
||||||
|
markdownContent
|
||||||
|
);
|
||||||
|
this._markdownRepository = await MarkdownRepository.singleton();
|
||||||
|
return createdBlogPost;
|
||||||
|
}
|
||||||
|
|
||||||
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
||||||
const blogPosts = await this._markdownRepository.blogPosts;
|
const blogPosts = await this._markdownRepository.blogPosts;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ describe('BlogPost', () => {
|
||||||
date: new Date('2022-01-01T00:00Z'),
|
date: new Date('2022-01-01T00:00Z'),
|
||||||
slug: 'test-slug',
|
slug: 'test-slug',
|
||||||
markdownContent: 'Test Content',
|
markdownContent: 'Test Content',
|
||||||
|
fileName: `the-file-name.md`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
@ -41,6 +42,7 @@ describe('BlogPost', () => {
|
||||||
.withDate(new Date('2022-01-01T00:00Z'))
|
.withDate(new Date('2022-01-01T00:00Z'))
|
||||||
.withSlug('test-slug')
|
.withSlug('test-slug')
|
||||||
.withMarkdownContent('Test Content')
|
.withMarkdownContent('Test Content')
|
||||||
|
.withFileName(`the-file-name.md`)
|
||||||
.constructAndThenBuild();
|
.constructAndThenBuild();
|
||||||
|
|
||||||
expect(blogPost).toStrictEqual(expectedBlogPost);
|
expect(blogPost).toStrictEqual(expectedBlogPost);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ interface BlogPostParams {
|
||||||
author: string;
|
author: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
markdownContent: string;
|
markdownContent: string;
|
||||||
|
fileName: string; // excluding any leading `..`
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlogPost {
|
export class BlogPost {
|
||||||
|
|
@ -23,6 +24,7 @@ export class BlogPost {
|
||||||
readonly author: string;
|
readonly author: string;
|
||||||
readonly slug: string;
|
readonly slug: string;
|
||||||
readonly markdownContent: string;
|
readonly markdownContent: string;
|
||||||
|
readonly fileName: string;
|
||||||
|
|
||||||
private _html: string | null = null;
|
private _html: string | null = null;
|
||||||
private _excerpt: string | null = null;
|
private _excerpt: string | null = null;
|
||||||
|
|
@ -33,6 +35,7 @@ export class BlogPost {
|
||||||
this.author = params.author;
|
this.author = params.author;
|
||||||
this.slug = params.slug;
|
this.slug = params.slug;
|
||||||
this.markdownContent = params.markdownContent;
|
this.markdownContent = params.markdownContent;
|
||||||
|
this.fileName = params.fileName.split(`/`)[-1];
|
||||||
}
|
}
|
||||||
|
|
||||||
get html(): string | null {
|
get html(): string | null {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, it, expect, beforeAll } from 'vitest';
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
import { MarkdownRepository } from './markdown-repository.js';
|
import { MarkdownRepository } from './markdown-repository.js';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
|
||||||
import { MarkdownFile } from './MarkdownFile.js';
|
import { MarkdownFile } from './MarkdownFile.js';
|
||||||
import { aBlogPost } from './test-builders/blog-post-builder.js';
|
import { aBlogPost } from './test-builders/blog-post-builder.js';
|
||||||
|
|
@ -31,6 +32,7 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
.withSlug('2023-02-01-test')
|
.withSlug('2023-02-01-test')
|
||||||
.withTitle('Test Blog Post')
|
.withTitle('Test Blog Post')
|
||||||
.withMarkdownContent(testMarkdownContent)
|
.withMarkdownContent(testMarkdownContent)
|
||||||
|
.withFileName('blog-2023-02-01-test.md')
|
||||||
.constructAndThenBuild();
|
.constructAndThenBuild();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
@ -68,9 +70,13 @@ 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://', ''));
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
|
repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
|
||||||
|
|
||||||
|
const resolvedPath = resolve(`${currentDirectory}/test-fixtures/test-file.md`);
|
||||||
|
await repository.createBlogPostMarkdownFile(resolvedPath, testMarkdownContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
|
|
@ -78,9 +84,17 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
const theFileName = 'non-existent-file.md';
|
const theFileName = 'non-existent-file.md';
|
||||||
|
|
||||||
// WHEN/THEN
|
// WHEN/THEN
|
||||||
expect(() => repository.deleteBlogPostMarkdownFile(theFileName)).toThrowError(
|
expect(async () => repository.deleteBlogPostMarkdownFile(theFileName)).rejects.toThrowError(
|
||||||
`Cannot delete file ${theFileName} as it does not exist`
|
`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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { open } 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';
|
||||||
|
|
@ -65,6 +65,7 @@ export class MarkdownRepository {
|
||||||
slug: markdownFile.frontmatter.slug,
|
slug: markdownFile.frontmatter.slug,
|
||||||
author: markdownFile.frontmatter.author,
|
author: markdownFile.frontmatter.author,
|
||||||
date: markdownFile.frontmatter.date,
|
date: markdownFile.frontmatter.date,
|
||||||
|
fileName: filename,
|
||||||
});
|
});
|
||||||
|
|
||||||
fileImports = [...fileImports, markdownFile];
|
fileImports = [...fileImports, markdownFile];
|
||||||
|
|
@ -126,10 +127,64 @@ export class MarkdownRepository {
|
||||||
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteBlogPostMarkdownFile(fileName: string): Promise<void> {
|
async createBlogPostMarkdownFile(resolvdePath: string, contents: string): Promise<BlogPost> {
|
||||||
const file = this.blogPosts.blogPosts.find((blogPost) => blogPost.fileName === fileName);
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (file) {
|
writeFile(resolvdePath, contents, (err) => {
|
||||||
const file = resolve(blogPostMarkdownDirectory, fileName);
|
if (err) {
|
||||||
|
console.error({
|
||||||
|
message: `createBlogPostMarkdownFile: Caught error while writing file ${resolvdePath}`,
|
||||||
|
err,
|
||||||
|
error: JSON.stringify(err),
|
||||||
|
});
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const markdownFile = new MarkdownFile<BlogPostFrontmatterValues>({
|
||||||
|
fileName: resolvdePath,
|
||||||
|
content: contents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blogPost = new BlogPost({
|
||||||
|
markdownContent: markdownFile.content,
|
||||||
|
title: markdownFile.frontmatter.title,
|
||||||
|
slug: markdownFile.frontmatter.slug,
|
||||||
|
author: markdownFile.frontmatter.author,
|
||||||
|
date: markdownFile.frontmatter.date,
|
||||||
|
fileName: resolvdePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return blogPost;
|
||||||
|
})
|
||||||
|
.then(async (blogPost: BlogPost) => {
|
||||||
|
blogPost.build();
|
||||||
|
return blogPost;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBlogPostMarkdownFile(resolvedFilePath: string): Promise<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class BlogPostBuilder {
|
||||||
private _author = 'default author';
|
private _author = 'default author';
|
||||||
private _date = new Date('2022-01-01T00:00Z');
|
private _date = new Date('2022-01-01T00:00Z');
|
||||||
private _slug = 'default-slug';
|
private _slug = 'default-slug';
|
||||||
|
private _fileName = 'default-file-name.md';
|
||||||
|
|
||||||
private _markdownContent = 'default markdown content';
|
private _markdownContent = 'default markdown content';
|
||||||
|
|
||||||
|
|
@ -18,6 +19,11 @@ class BlogPostBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withFileName(fileName: string): BlogPostBuilder {
|
||||||
|
this._fileName = fileName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withAuthor(author: string): BlogPostBuilder {
|
withAuthor(author: string): BlogPostBuilder {
|
||||||
this._author = author;
|
this._author = author;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -46,6 +52,7 @@ class BlogPostBuilder {
|
||||||
author: this._author,
|
author: this._author,
|
||||||
date: this._date,
|
date: this._date,
|
||||||
slug: this._slug,
|
slug: this._slug,
|
||||||
|
fileName: this._fileName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/lib/blog/test-fixtures/example-markdown.ts
Normal file
20
src/lib/blog/test-fixtures/example-markdown.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export const exampleMarkdown = `---
|
||||||
|
title: "Test Blog Post"
|
||||||
|
author: "Thomas Wilson"
|
||||||
|
date: 2023-02-01T08:00:00Z
|
||||||
|
slug: "2023-02-01-test"
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a blog post written in markdown.
|
||||||
|
|
||||||
|
This is a [link](http://www.bbc.co.uk)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const exampleMarkdownFrontmatter = {
|
||||||
|
title: 'Test Blog Post',
|
||||||
|
author: 'Thomas Wilson',
|
||||||
|
date: new Date('2023-02-01T08:00:00Z'),
|
||||||
|
slug: '2023-02-01-test',
|
||||||
|
draft: false,
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,56 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { resolve } from 'path';
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from '@sveltejs/kit';
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
import { BlogController } from '../../../../lib/blog/BlogController.js';
|
||||||
|
import { dump as dumpYaml } from 'js-yaml';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ getClientAddress }) => {
|
const thisDirectory = import.meta.url
|
||||||
|
.replace('file://', '')
|
||||||
|
.split('/')
|
||||||
|
.filter((part) => part !== '+server.ts')
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ getClientAddress, request }) => {
|
||||||
const address = await getClientAddress();
|
const address = await getClientAddress();
|
||||||
|
let fileName: string;
|
||||||
|
let markdownContent: string;
|
||||||
|
let title: string;
|
||||||
|
let date: string;
|
||||||
|
let slug: string;
|
||||||
|
let author: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBody = await request.json();
|
||||||
|
fileName = requestBody.fileName;
|
||||||
|
markdownContent = requestBody.markdownContent;
|
||||||
|
title = requestBody.title;
|
||||||
|
date = requestBody.date;
|
||||||
|
slug = requestBody.slug;
|
||||||
|
author = requestBody.author;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`Caught error destructuring request body`);
|
||||||
|
console.error(e);
|
||||||
|
throw error(400, 'Error in request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([fileName, markdownContent, title, date, slug, author].includes(undefined)) {
|
||||||
|
throw error(400, `Missing parameters.`);
|
||||||
|
} else if (address !== '127.0.0.1') {
|
||||||
|
throw error(403, `Forbidden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = await BlogController.singleton();
|
||||||
|
|
||||||
|
const worryinglyManualFrontMatter = [`---`, dumpYaml({ title, date: new Date(date), slug, author }), `---`].join(
|
||||||
|
`\n`
|
||||||
|
);
|
||||||
|
const escapedMarkdown = markdownContent.replaceAll(/\\n/g, '\n');
|
||||||
|
|
||||||
|
const contentWithFrontmatter = [worryinglyManualFrontMatter, escapedMarkdown].join(`\n`);
|
||||||
|
|
||||||
|
const resolvedFileName = resolve(thisDirectory, `../../../../content/blog/${fileName}`);
|
||||||
|
|
||||||
|
await controller.createBlogPost(resolvedFileName, contentWithFrontmatter);
|
||||||
console.log({ address });
|
console.log({ address });
|
||||||
return json({ address });
|
return json({ address });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,120 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { format as formatDate } from "date-fns";
|
import { format as formatDate } from "date-fns";
|
||||||
|
import { BlogPost } from "$lib/blog/BlogPost.js";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
let title = "";
|
let title = "";
|
||||||
let author = "Thomas Wilson";
|
let author = "Thomas Wilson";
|
||||||
let date = formatDate(new Date(), "yyyy-MM-dd");
|
let date = new Date();
|
||||||
let content = "";
|
let content = "";
|
||||||
|
let slug = "";
|
||||||
|
let blogPost: BlogPost | null = null;
|
||||||
|
|
||||||
function onCreate() {}
|
$: safeContent = JSON.stringify(content);
|
||||||
|
|
||||||
|
function slugifyString(originalString: string): string {
|
||||||
|
return originalString
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(/ /g, "-")
|
||||||
|
.replaceAll(/[^a-zA-Z0-9-]+/g, "")
|
||||||
|
.replaceAll(/-+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTitleChange() {
|
||||||
|
const dateAsString = formatDate(date, "yyyy-MM-dd");
|
||||||
|
const slugifiedTitle = slugifyString(title);
|
||||||
|
slug = `${dateAsString}-${slugifiedTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreatePreviewBlogPost() {
|
||||||
|
const _blogPost = new BlogPost({
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
markdownContent: content,
|
||||||
|
slug,
|
||||||
|
date,
|
||||||
|
fileName: `${slug}.md`
|
||||||
|
});
|
||||||
|
|
||||||
|
await _blogPost.build();
|
||||||
|
blogPost = _blogPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreate() {
|
||||||
|
const requestBody = {
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
slug,
|
||||||
|
markdownContent: JSON.stringify(content),
|
||||||
|
fileName: `${slug}.md`,
|
||||||
|
date: date.toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("/api/blog/new.json", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
return await goto(`blog/${slug}`);
|
||||||
|
} else {
|
||||||
|
alert("Something went wrong");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="new-blog-post">
|
<section class="new-blog-post">
|
||||||
<h1>New Blog Post</h1>
|
<h1>New Blog Post</h1>
|
||||||
<div class="field">
|
<form on:submit|preventDefault={onCreate}>
|
||||||
<label class="field__label" for="title">Title</label>
|
<div class="field">
|
||||||
<input type="text" id="title" bind:value={title} />
|
<label class="field__label" for="title">Title</label>
|
||||||
</div>
|
<input
|
||||||
<div class="field">
|
type="text"
|
||||||
<label class="field__label" for="author">Author</label>
|
id="title"
|
||||||
<input type="text" id="author" bind:value={author} />
|
required
|
||||||
</div>
|
bind:value={title}
|
||||||
|
on:change={handleTitleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="field__label" for="author">Author</label>
|
||||||
|
<input type="text" id="author" required bind:value={author} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field__label" for="date">Date</label>
|
<label class="field__label" for="slug">Slug</label>
|
||||||
<input type="text" id="date" bind:value={date} />
|
<input type="text" id="slug" required bind:value={slug} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field__label" for="content">Content</label>
|
<label class="field__label" for="content">Content</label>
|
||||||
<textarea id="content" rows="10" cols="50" bind:value={content} />
|
<textarea id="content" rows="10" cols="50" bind:value={content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="submit">
|
<div class="submit">
|
||||||
<button class="create-button" on:click={onCreate}>Create</button>
|
<button
|
||||||
</div>
|
class="preview-button"
|
||||||
|
type="button"
|
||||||
|
on:click|preventDefault={onCreatePreviewBlogPost}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
<button class="create-button">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if blogPost}
|
||||||
|
<section class="preview">
|
||||||
|
<h2>Preview</h2>
|
||||||
|
<article>
|
||||||
|
{@html blogPost.html}
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
--gap: 8px;
|
--gap: 8px;
|
||||||
|
|
@ -72,4 +153,17 @@
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
grid-template-rows: min-content 1fr;
|
||||||
|
gap: var(--gap);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview article {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 65ch;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue