-
Notifications
You must be signed in to change notification settings - Fork 0
Fix white screen, note creation button, and add auto-create feature #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||
| /* eslint-disable eslint-plugin-import/no-nodejs-modules */ | ||||||
| import { readFile, readdir, writeFile } from "node:fs/promises"; | ||||||
| import { basename, join } from "node:path"; | ||||||
| import type { Context } from "hono"; | ||||||
|
|
||||||
| const DATA_DIR = join(process.cwd(), "data"); | ||||||
| const DEFAULT_LIMIT = 5; | ||||||
| const NOT_FOUND_INDEX = -1; | ||||||
| const START_INDEX_OFFSET = 1; | ||||||
| const STATUS_BAD_REQUEST = 400; | ||||||
| const STATUS_INTERNAL_ERROR = 500; | ||||||
|
|
||||||
| // Helper to get notes sorted by date (descending) | ||||||
| const getNotes = async () => { | ||||||
| try { | ||||||
| const files = await readdir(DATA_DIR); | ||||||
| const notes = files | ||||||
| .filter((file) => /^\d{4}-\d{2}-\d{2}\.md$/.test(file)) | ||||||
| .map((file) => ({ | ||||||
| date: basename(file, ".md"), | ||||||
| filename: file, | ||||||
| })); | ||||||
| // eslint-disable-next-line eslint-plugin-unicorn/no-array-sort | ||||||
| return [...notes].sort((a, b) => b.date.localeCompare(a.date)); | ||||||
| } catch (error) { | ||||||
| console.error("Error reading notes directory:", error); | ||||||
| return []; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| // eslint-disable-next-line max-statements | ||||||
| const getNotesHandler = async (c: Context) => { | ||||||
| const limit = Number(c.req.query("limit")) || DEFAULT_LIMIT; | ||||||
| const cursor = c.req.query("cursor"); | ||||||
|
|
||||||
| const allNotes = await getNotes(); | ||||||
|
|
||||||
| let startIndex = 0; | ||||||
| // eslint-disable-next-line no-undefined | ||||||
| if (cursor !== undefined && cursor !== "") { | ||||||
| const cursorIndex = allNotes.findIndex((note) => note.date === cursor); | ||||||
| if (cursorIndex !== NOT_FOUND_INDEX) { | ||||||
| startIndex = cursorIndex + START_INDEX_OFFSET; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const slicedNotes = allNotes.slice(startIndex, startIndex + limit); | ||||||
|
|
||||||
| const notesWithContent = await Promise.all( | ||||||
| slicedNotes.map(async (note) => { | ||||||
| const content = await readFile(join(DATA_DIR, note.filename), "utf8"); | ||||||
| return { | ||||||
| date: note.date, | ||||||
| content, | ||||||
| }; | ||||||
| }) | ||||||
| ); | ||||||
|
|
||||||
| // eslint-disable-next-line eslint-plugin-unicorn/prefer-at, no-undefined, no-magic-numbers, eslint-plugin-unicorn/prefer-ternary, no-ternary | ||||||
| const nextCursor = slicedNotes.length > 0 ? slicedNotes[slicedNotes.length - 1].date : undefined; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line can be simplified by using the
Suggested change
|
||||||
|
|
||||||
| return c.json({ | ||||||
| notes: notesWithContent, | ||||||
| nextCursor, | ||||||
| hasMore: startIndex + limit < allNotes.length, | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| const getNoteHandler = async (c: Context) => { | ||||||
| const date = c.req.param("date"); | ||||||
| if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { | ||||||
| return c.json({ error: "Invalid date format" }, STATUS_BAD_REQUEST); | ||||||
| } | ||||||
|
|
||||||
| const filepath = join(DATA_DIR, `${date}.md`); | ||||||
| try { | ||||||
| const content = await readFile(filepath, "utf8"); | ||||||
| return c.json({ date, content }); | ||||||
| } catch (error: unknown) { | ||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any | ||||||
| if ((error as any).code === "ENOENT") { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type assertion
Suggested change
|
||||||
| return c.json({ date, content: "" }); | ||||||
| } | ||||||
| return c.json({ error: "Failed to read note" }, STATUS_INTERNAL_ERROR); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| // eslint-disable-next-line max-statements | ||||||
| const saveNoteHandler = async (c: Context) => { | ||||||
| const date = c.req.param("date"); | ||||||
| if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { | ||||||
| return c.json({ error: "Invalid date format" }, STATUS_BAD_REQUEST); | ||||||
| } | ||||||
|
|
||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||||||
| const body: { content?: string } = await c.req.json(); | ||||||
| const content = body.content ?? ""; | ||||||
|
|
||||||
| const filepath = join(DATA_DIR, `${date}.md`); | ||||||
| try { | ||||||
| await writeFile(filepath, content, "utf8"); | ||||||
| return c.json({ success: true, date, content }); | ||||||
| } catch (error) { | ||||||
| console.error("Error writing note:", error); | ||||||
| return c.json({ error: "Failed to save note" }, STATUS_INTERNAL_ERROR); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| export default { getNotesHandler, getNoteHandler, saveNoteHandler }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,74 +1,72 @@ | ||
| import { describe, it, expect, beforeAll, afterAll } from "vitest"; | ||
| import app from "./index"; | ||
| import { writeFile, unlink, readdir } from "node:fs/promises"; | ||
| /* eslint-disable eslint-plugin-import/no-nodejs-modules */ | ||
| import { spawn } from "node:child_process"; | ||
| import { access, rm, stat } from "node:fs/promises"; | ||
| import { join } from "node:path"; | ||
| import { subDays } from "date-fns"; | ||
| import { beforeAll, describe, expect, it } from "vitest"; | ||
|
|
||
| const DATA_DIR = join(process.cwd(), "data"); | ||
| const DATE_PADDING = 2; | ||
| const MONTH_OFFSET = 1; | ||
| const TEST_TIMEOUT = 10_000; | ||
| const SERVER_STARTUP_TIME = 3000; | ||
| const DAYS_TO_CHECK = 3; | ||
| const INCREMENT_STEP = 1; | ||
| const EMPTY_SIZE = 0; | ||
|
|
||
| describe("Note API", () => { | ||
| // Helper to format date as yyyy-MM-dd | ||
| const formatDate = (date: Date) => { | ||
| const yyyy = date.getFullYear(); | ||
| const mm = String(date.getMonth() + MONTH_OFFSET).padStart(DATE_PADDING, "0"); | ||
| const dd = String(date.getDate()).padStart(DATE_PADDING, "0"); | ||
| return `${yyyy}-${mm}-${dd}`; | ||
| }; | ||
|
|
||
| const verifyNoteExists = async (date: Date) => { | ||
| const filename = `${formatDate(date)}.md`; | ||
| const filepath = join(DATA_DIR, filename); | ||
|
|
||
| console.log(`Checking for file: ${filepath}`); | ||
|
|
||
| try { | ||
| await access(filepath); | ||
| const fileStat = await stat(filepath); | ||
| expect(fileStat.size).toBe(EMPTY_SIZE); | ||
| } catch (error) { | ||
| throw new Error(`File ${filename} was not created. Error: ${String(error)}`, { cause: error }); | ||
| } | ||
| }; | ||
|
|
||
| describe("Server Auto Create", () => { | ||
| // Clean up data directory before test | ||
| beforeAll(async () => { | ||
| // Clean up | ||
| try { | ||
| const files = await readdir(DATA_DIR); | ||
| for (const file of files) { | ||
| if (file.endsWith(".md")) { | ||
| await unlink(join(DATA_DIR, file)); | ||
| } | ||
| } | ||
| } catch {} | ||
| await rm(DATA_DIR, { recursive: true, force: true }); | ||
| } catch { | ||
| // Ignore cleanup error | ||
| } | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| // Clean up | ||
| try { | ||
| const files = await readdir(DATA_DIR); | ||
| for (const file of files) { | ||
| if (file.endsWith(".md")) { | ||
| await unlink(join(DATA_DIR, file)); | ||
| } | ||
| } | ||
| } catch {} | ||
| }); | ||
| it("creates past 3 days notes when TEST_AUTO_CREATE is set", async () => { | ||
| console.log("Starting server process with TEST_AUTO_CREATE=true"); | ||
| const serverProcess = spawn("bun", ["src/index.ts"], { | ||
| cwd: process.cwd(), | ||
| env: { ...process.env, TEST_AUTO_CREATE: "true", PORT: "3002" }, | ||
| stdio: "inherit", | ||
| }); | ||
|
|
||
| it("should create a note", async () => { | ||
| const res = await app.fetch( | ||
| new Request("http://localhost/api/notes/2023-01-01", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ content: "Hello" }), | ||
| }) | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| const data = await res.json(); | ||
| expect(data).toEqual({ success: true, date: "2023-01-01", content: "Hello" }); | ||
| }); | ||
|
|
||
| it("should retrieve a note", async () => { | ||
| const res = await app.fetch( | ||
| new Request("http://localhost/api/notes/2023-01-01") | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| const data = await res.json(); | ||
| expect(data).toEqual({ date: "2023-01-01", content: "Hello" }); | ||
| }); | ||
| // Wait for server to start and process env | ||
| // eslint-disable-next-line eslint-plugin-promise/avoid-new | ||
| await new Promise((resolve) => { setTimeout(resolve, SERVER_STARTUP_TIME); }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a fixed |
||
|
|
||
| it("should list notes", async () => { | ||
| // Add another note | ||
| await app.fetch( | ||
| new Request("http://localhost/api/notes/2023-01-02", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ content: "World" }), | ||
| }) | ||
| ); | ||
| serverProcess.kill(); | ||
|
|
||
| const res = await app.fetch( | ||
| new Request("http://localhost/api/notes") | ||
| const today = new Date(); | ||
| // eslint-disable-next-line @typescript-eslint/promise-function-async | ||
| const checks = Array.from({ length: DAYS_TO_CHECK }, (_, i) => i + INCREMENT_STEP).map((i) => | ||
| verifyNoteExists(subDays(today, i)) | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| const data = await res.json(); | ||
| expect(data.notes).toHaveLength(2); | ||
| expect(data.notes[0].date).toBe("2023-01-02"); // Descending | ||
| expect(data.notes[1].date).toBe("2023-01-01"); | ||
| }); | ||
|
|
||
| await Promise.all(checks); | ||
| }, TEST_TIMEOUT); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
[...notes]creates a shallow copy before sorting. Sincenotesis a new array created within this function's scope, creating another copy is redundant. You can directly sort thenotesarray to simplify the code.