diff --git a/src/client/captcha/index.ts b/src/client/captcha/index.ts new file mode 100644 index 0000000..0ffdd02 --- /dev/null +++ b/src/client/captcha/index.ts @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/src/client/editor/menu.ts b/src/client/editor/menu.ts index 6822025..848c5a6 100644 --- a/src/client/editor/menu.ts +++ b/src/client/editor/menu.ts @@ -2,8 +2,8 @@ import { setBlockType, toggleMark, wrapIn } from "prosemirror-commands" import type { EditorView } from "prosemirror-view"; import type { Command } from "prosemirror-state"; import { Plugin } from "prosemirror-state"; -import { view } from "./index.ts"; import { schema } from "prosemirror-markdown"; +import { update_post } from "./sync"; interface MenuButtonOptions { @@ -26,6 +26,10 @@ function heading(level: number) { } } +async function save_editor(view: EditorView) { + await update_post(view.state) +} + class MenuView { items: MenuButtonOptions[]; editorView: EditorView; @@ -43,7 +47,7 @@ class MenuView { e.preventDefault() editorView.focus() items.forEach(({ command, dom }) => { - if (dom.contains(e.target)) command(editorView.state, editorView.dispatch, editorView); + if (dom.contains(e.target as any)) command(editorView.state, editorView.dispatch, editorView); }) }) } @@ -73,5 +77,14 @@ export const menu = menuPlugin([ { command: toggleMark(schema.marks.em!), dom: icon("i", "em") }, { command: setBlockType(schema.nodes.paragraph!), dom: icon("p", "paragraph") }, heading(1), heading(2), heading(3), - { command: wrapIn(schema.nodes.blockquote!), dom: icon(">", "blockquote") } + { command: wrapIn(schema.nodes.blockquote!), dom: icon(">", "blockquote") }, + { + dom: icon(` + +`, "save"), + command: (state, dispatch, view) => { + if (dispatch && view) update_post(view.state); + return true; // don't hide + } + } ]); \ No newline at end of file diff --git a/src/client/editor/sync.ts b/src/client/editor/sync.ts index 66d2aa0..9d65905 100644 --- a/src/client/editor/sync.ts +++ b/src/client/editor/sync.ts @@ -1,5 +1,8 @@ +import { defaultMarkdownSerializer } from "prosemirror-markdown"; // @ts-ignore, doesn't matter. import default_md from "./default.md" with {type: "text"}; +import type { EditorState } from "prosemirror-state"; +import type { Post } from "@/lib/types"; var data_element = document.querySelector('script#data'); // JSON {content: "# Markdown\n bla bla bla"} var editor_container = document.querySelector("#editor-container") as Element; @@ -13,3 +16,29 @@ if (!data_element) { export const initial_md = data_element ? JSON.parse(data_element?.textContent).content : default_md; +export async function update_post(state: EditorState) { + const update = { + content: defaultMarkdownSerializer.serialize(state.doc), + } + const target = new URL(window.location.href).pathname; + let result = await fetch(target, { + method: "POST", + body: JSON.stringify(update), + headers: { + "Content-type": "application/json; charset=UTF-8" + } + }); + let response = await result.json() as { + success: false, + errors: any[] + } | { + success: true; + post: Post; + }; + + if (!response.success) { + response.errors.forEach(element => { + alert(element) + }); + } +} \ No newline at end of file diff --git a/src/db/posts.ts b/src/db/posts.ts index 40c01a1..e985077 100644 --- a/src/db/posts.ts +++ b/src/db/posts.ts @@ -38,13 +38,14 @@ export async function update_post(post: Post): Promise { } /** - * Updates the content of a post. Verifies the user owns it. Might error with `PostNotFoundError`. + * Updates the content of a post. Verifies the user owns it. Might error with `PostNotFoundError`. No need to pass the `updated_at` field. * @param {number} user_id User's ID. * @param {string} post_uuid Post's UUID. * @param {Partial} data The post itself. * @returns {Promise} Post. */ export async function update_post_check(user_id: number, post_uuid: string, data: Partial): Promise { + data.updated_at = new Date(); const [existing] = await db .select({ post_uuid: schema.posts.uuid, owner_id: schema.blogs.user_id }) .from(schema.posts) diff --git a/src/routes/write.ts b/src/routes/write.ts index c56d29e..305d1b7 100644 --- a/src/routes/write.ts +++ b/src/routes/write.ts @@ -8,8 +8,9 @@ import { post_template } from "@/lib/post_template"; import eta from "@/lib/eta"; import { UUID_REGEX } from "@/lib/constants"; import type { Blog, Post } from "@/lib/types"; -import { signup_schema } from "@/lib/zod"; +import { update_post_schema } from "@/lib/zod"; import { zValidator } from "@hono/zod-validator"; +import * as z from "zod"; const write = new Hono(); @@ -36,15 +37,28 @@ write.get(`/:uuid{${UUID_REGEX.source}}`, async (c) => { if (e instanceof PostNotFoundError) return c.notFound(); console.error(e); } -}).post(zValidator("json", signup_schema), async (c) => { +}).post(zValidator("json", update_post_schema, (val, c) => { + if (!val.success) { + const error = z.flattenError(val.error) + return c.json({ + success: false, + errors: error.fieldErrors.content + }, 400) + } +}), async (c) => { let [user, uuid, body] = [c.get('user'), c.req.param('uuid'), c.req.valid("json")]; if (!user || !uuid || !body) return c.text("Bad Request.", 400); try { - const result = await update_post_check(user.id!, uuid, body); - delete result.id; - return c.json(result); + const post = await update_post_check(user.id!, uuid, body as Partial); + return c.json({ + success: true, + post + }); } catch (e: any) { - return c.json(e); + return c.json({ + success: false, + error: e, + }); } })