feat: combine client and server note management
This commit is contained in:
parent
bcf443c5e3
commit
1b8a9e1752
2
drizzle/0002_tired_ghost_rider.sql
Normal file
2
drizzle/0002_tired_ghost_rider.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "notes" RENAME COLUMN "creationTime" TO "createdAt";--> statement-breakpoint
|
||||||
|
ALTER TABLE "notes" RENAME COLUMN "lastEdited" TO "updatedAt";
|
187
drizzle/meta/0002_snapshot.json
Normal file
187
drizzle/meta/0002_snapshot.json
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"id": "87053763-7633-4589-a8fb-ad1b9f822314",
|
||||||
|
"prevId": "b75921d7-7437-4ff8-980f-8450655b4b64",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.blocks": {
|
||||||
|
"name": "blocks",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"name": "tag",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"lines": {
|
||||||
|
"name": "lines",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'[\"\",\"\",\"\",\"\"]'::json"
|
||||||
|
},
|
||||||
|
"isLocked": {
|
||||||
|
"name": "isLocked",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"noteId": {
|
||||||
|
"name": "noteId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"blocks_noteId_notes_id_fk": {
|
||||||
|
"name": "blocks_noteId_notes_id_fk",
|
||||||
|
"tableFrom": "blocks",
|
||||||
|
"tableTo": "notes",
|
||||||
|
"columnsFrom": [
|
||||||
|
"noteId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'Untitled'"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"authorId": {
|
||||||
|
"name": "authorId",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"notes_authorId_users_id_fk": {
|
||||||
|
"name": "notes_authorId_users_id_fk",
|
||||||
|
"tableFrom": "notes",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"authorId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,13 @@
|
|||||||
"when": 1752591486103,
|
"when": 1752591486103,
|
||||||
"tag": "0001_complex_dreadnoughts",
|
"tag": "0001_complex_dreadnoughts",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1752847652742,
|
||||||
|
"tag": "0002_tired_ghost_rider",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { blocksTable, IBlock, notesTable } from "@/lib/db/schema";
|
import { blocksTable, notesTable } from "@/lib/db/schema";
|
||||||
|
import { IBlock } from "@/lib/db/types";
|
||||||
import { requireAuth } from "./auth";
|
import { requireAuth } from "./auth";
|
||||||
import { assertNoteOwner } from "./notes";
|
import { assertNoteOwner } from "./notes";
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export async function assertBlockOwner(blockId: string, authorId: string) {
|
|||||||
async function updateLastEdited(tx: Transaction, noteId: string) {
|
async function updateLastEdited(tx: Transaction, noteId: string) {
|
||||||
await tx
|
await tx
|
||||||
.update(notesTable)
|
.update(notesTable)
|
||||||
.set({ lastEdited: new Date() })
|
.set({ updatedAt: new Date() })
|
||||||
.where(eq(notesTable.id, noteId));
|
.where(eq(notesTable.id, noteId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,7 @@ export async function getBlockIfOwned(blockId: string): Promise<IBlock> {
|
|||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBlock(formData: FormData) {
|
export async function createBlock(noteId: string): Promise<IBlock> {
|
||||||
const noteId = formData.get("noteId") as string;
|
|
||||||
|
|
||||||
const user = await requireAuth();
|
const user = await requireAuth();
|
||||||
await assertNoteOwner(noteId, user.id);
|
await assertNoteOwner(noteId, user.id);
|
||||||
|
|
||||||
@ -50,14 +48,14 @@ export async function createBlock(formData: FormData) {
|
|||||||
const lastBlock = blocks.pop();
|
const lastBlock = blocks.pop();
|
||||||
const order = lastBlock === undefined ? 1 : lastBlock.order + 1;
|
const order = lastBlock === undefined ? 1 : lastBlock.order + 1;
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
return db.transaction(async (tx) => {
|
||||||
await tx
|
const [newBlock] = await tx
|
||||||
.insert(blocksTable)
|
.insert(blocksTable)
|
||||||
.values({ noteId, order });
|
.values({ noteId, order })
|
||||||
|
.returning();
|
||||||
await updateLastEdited(tx, noteId);
|
await updateLastEdited(tx, noteId);
|
||||||
|
return newBlock;
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBlocks(noteId: string): Promise<IBlock[]> {
|
export async function getBlocks(noteId: string): Promise<IBlock[]> {
|
||||||
@ -71,16 +69,11 @@ export async function getBlocks(noteId: string): Promise<IBlock[]> {
|
|||||||
.orderBy(blocksTable.order);
|
.orderBy(blocksTable.order);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBlock(formData: FormData) {
|
export async function deleteBlock(blockId: string) {
|
||||||
const blockId = formData.get("blockId") as string;
|
|
||||||
|
|
||||||
const user = await requireAuth();
|
const user = await requireAuth();
|
||||||
await assertBlockOwner(blockId, user.id);
|
await assertBlockOwner(blockId, user.id);
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
const block = await getBlockIfOwned(blockId);
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
@ -88,75 +81,6 @@ export async function deleteBlock(formData: FormData) {
|
|||||||
.where(eq(blocksTable.id, blockId));
|
.where(eq(blocksTable.id, blockId));
|
||||||
await updateLastEdited(tx, block.noteId);
|
await updateLastEdited(tx, block.noteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function changeLock(formData: FormData) {
|
|
||||||
const blockId = formData.get("blockId") as string;
|
|
||||||
const isLocked = formData.get("isLocked") === "true";
|
|
||||||
|
|
||||||
const user = await requireAuth();
|
|
||||||
await assertBlockOwner(blockId, user.id);
|
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
|
||||||
if (!block) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
await tx
|
|
||||||
.update(blocksTable)
|
|
||||||
.set({ isLocked })
|
|
||||||
.where(eq(blocksTable.id, blockId));
|
|
||||||
await updateLastEdited(tx, block.noteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addLine(formData: FormData) {
|
|
||||||
const blockId = formData.get("blockId") as string;
|
|
||||||
|
|
||||||
const user = await requireAuth();
|
|
||||||
await assertBlockOwner(blockId, user.id);
|
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
await tx
|
|
||||||
.update(blocksTable)
|
|
||||||
.set({ lines: [...block.lines, ""] })
|
|
||||||
.where(eq(blocksTable.id, blockId));
|
|
||||||
await updateLastEdited(tx, block.noteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteLine(formData: FormData) {
|
|
||||||
const blockId = formData.get("blockId") as string;
|
|
||||||
|
|
||||||
const user = await requireAuth();
|
|
||||||
await assertBlockOwner(blockId, user.id);
|
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
await tx
|
|
||||||
.update(blocksTable)
|
|
||||||
.set({ lines: block.lines.slice(0, -1) })
|
|
||||||
.where(eq(blocksTable.id, blockId));
|
|
||||||
await updateLastEdited(tx, block.noteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveUp(formData: FormData) {
|
export async function moveUp(formData: FormData) {
|
||||||
@ -166,9 +90,6 @@ export async function moveUp(formData: FormData) {
|
|||||||
await assertBlockOwner(blockId, user.id);
|
await assertBlockOwner(blockId, user.id);
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
const block = await getBlockIfOwned(blockId);
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblings = await getBlocks(block.noteId);
|
const siblings = await getBlocks(block.noteId);
|
||||||
const currentIndex = siblings.findIndex((b) => b.id === block.id);
|
const currentIndex = siblings.findIndex((b) => b.id === block.id);
|
||||||
@ -192,8 +113,6 @@ export async function moveUp(formData: FormData) {
|
|||||||
.where(eq(blocksTable.id, prevBlock.id));
|
.where(eq(blocksTable.id, prevBlock.id));
|
||||||
await updateLastEdited(tx, block.noteId);
|
await updateLastEdited(tx, block.noteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveDown(formData: FormData) {
|
export async function moveDown(formData: FormData) {
|
||||||
@ -203,9 +122,6 @@ export async function moveDown(formData: FormData) {
|
|||||||
await assertBlockOwner(blockId, user.id);
|
await assertBlockOwner(blockId, user.id);
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
const block = await getBlockIfOwned(blockId);
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblings = await getBlocks(block.noteId);
|
const siblings = await getBlocks(block.noteId);
|
||||||
const currentIndex = siblings.findIndex((b) => b.id === block.id);
|
const currentIndex = siblings.findIndex((b) => b.id === block.id);
|
||||||
@ -229,42 +145,22 @@ export async function moveDown(formData: FormData) {
|
|||||||
.where(eq(blocksTable.id, nextBlock.id));
|
.where(eq(blocksTable.id, nextBlock.id));
|
||||||
await updateLastEdited(tx, block.noteId);
|
await updateLastEdited(tx, block.noteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setLines(blockId: string, lines: string[]) {
|
export async function updateBlock(block: IBlock) {
|
||||||
const user = await requireAuth();
|
const user = await requireAuth();
|
||||||
await assertBlockOwner(blockId, user.id);
|
await assertBlockOwner(block.id, user.id);
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
.update(blocksTable)
|
.update(blocksTable)
|
||||||
.set({ lines })
|
.set({
|
||||||
.where(eq(blocksTable.id, blockId));
|
tag: block.tag,
|
||||||
await updateLastEdited(tx, block.noteId);
|
lines: block.lines,
|
||||||
});
|
isLocked: block.isLocked,
|
||||||
}
|
order: block.order,
|
||||||
|
})
|
||||||
export async function setTag(blockId: string, tag: string) {
|
.where(eq(blocksTable.id, block.id));
|
||||||
const user = await requireAuth();
|
|
||||||
await assertBlockOwner(blockId, user.id);
|
|
||||||
|
|
||||||
const block = await getBlockIfOwned(blockId);
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
|
||||||
await tx
|
|
||||||
.update(blocksTable)
|
|
||||||
.set({ tag })
|
|
||||||
.where(eq(blocksTable.id, blockId));
|
|
||||||
await updateLastEdited(tx, block.noteId);
|
await updateLastEdited(tx, block.noteId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
import { desc, eq, and } from "drizzle-orm";
|
import { desc, eq, and } from "drizzle-orm";
|
||||||
import { blocksTable, INote, notesTable, usersTable } from "@/lib/db/schema";
|
import { blocksTable, notesTable, usersTable } from "@/lib/db/schema";
|
||||||
import { requireAuth } from "./auth";
|
import { INote, INoteWithBlocks } from "@/lib/db/types";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
|
import { requireAuth } from "./auth";
|
||||||
|
|
||||||
export async function assertNoteOwner(noteId: string, userId: string) {
|
export async function assertNoteOwner(noteId: string, userId: string) {
|
||||||
const note = await db.query.notesTable.findFirst({
|
const note = await db.query.notesTable.findFirst({
|
||||||
@ -16,11 +17,12 @@ export async function assertNoteOwner(noteId: string, userId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNoteIfOwned(noteId: string): Promise<INote> {
|
export async function getNoteIfOwned(noteId: string): Promise<INoteWithBlocks> {
|
||||||
const user = await requireAuth();
|
const user = await requireAuth();
|
||||||
|
|
||||||
const note = await db.query.notesTable.findFirst({
|
const note = await db.query.notesTable.findFirst({
|
||||||
where: eq(notesTable.id, noteId),
|
where: eq(notesTable.id, noteId),
|
||||||
|
with: { blocks: { orderBy: blocksTable.order } },
|
||||||
});
|
});
|
||||||
if (!note || note.authorId !== user.id) {
|
if (!note || note.authorId !== user.id) {
|
||||||
notFound();
|
notFound();
|
||||||
@ -46,7 +48,7 @@ export async function getNotes(): Promise<INote[]> {
|
|||||||
.select()
|
.select()
|
||||||
.from(notesTable)
|
.from(notesTable)
|
||||||
.where(eq(notesTable.authorId, user.id))
|
.where(eq(notesTable.authorId, user.id))
|
||||||
.orderBy(desc(notesTable.lastEdited));
|
.orderBy(desc(notesTable.updatedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteNote(formData: FormData) {
|
export async function deleteNote(formData: FormData) {
|
||||||
@ -58,7 +60,6 @@ export async function deleteNote(formData: FormData) {
|
|||||||
await db
|
await db
|
||||||
.delete(notesTable)
|
.delete(notesTable)
|
||||||
.where(and(eq(notesTable.id, noteId), eq(notesTable.authorId, user.id)));
|
.where(and(eq(notesTable.id, noteId), eq(notesTable.authorId, user.id)));
|
||||||
|
|
||||||
revalidatePath("/notes");
|
revalidatePath("/notes");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +73,6 @@ export async function setTitle(noteId: string, title: string) {
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(notesTable)
|
.update(notesTable)
|
||||||
.set({ title, lastEdited: new Date() })
|
.set({ title, updatedAt: new Date() })
|
||||||
.where(eq(notesTable.id, noteId));
|
.where(eq(notesTable.id, noteId));
|
||||||
|
|
||||||
revalidatePath("/notes/[id]", "page");
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Metadata } from "next";
|
|||||||
import AuthForm from "@/components/forms/AuthForm";
|
import AuthForm from "@/components/forms/AuthForm";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Authentication - Rhyme",
|
title: "Authenticate - Rhyme",
|
||||||
description: "Register or log into Rhyme account to save, show and load notes",
|
description: "Register or log into Rhyme account to save, show and load notes",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getNoteIfOwned } from "@/app/actions/notes";
|
import { getNoteIfOwned } from "@/app/actions/notes";
|
||||||
import { getBlocks } from "@/app/actions/blocks";
|
import Editor from "@/components/editor/NoteEditor";
|
||||||
import Editor from "@/components/editor/Editor";
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@ -12,11 +11,10 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|||||||
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const note = await getNoteIfOwned(id);
|
const note = await getNoteIfOwned(id);
|
||||||
const blocks = await getBlocks(note.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Editor note={note} blocks={blocks} />
|
<Editor note={note} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAuth } from "@/app/actions/auth";
|
import { getAuth } from "@/app/actions/auth";
|
||||||
import Editor from "@/components/editor/Editor";
|
import NoteEditor from "@/components/editor/NoteEditor";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const auth = await getAuth();
|
const auth = await getAuth();
|
||||||
@ -11,7 +11,7 @@ export default async function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-8">
|
<div className="flex flex-col items-center gap-8">
|
||||||
<Editor />
|
<NoteEditor isUnauthorized />
|
||||||
<i className="text-center text-sm text-neutral-400">
|
<i className="text-center text-sm text-neutral-400">
|
||||||
Changes are not saved!<br />
|
Changes are not saved!<br />
|
||||||
<Link href="/auth" className="font-bold hover:underline">Log in</Link> to save your notes.
|
<Link href="/auth" className="font-bold hover:underline">Log in</Link> to save your notes.
|
||||||
|
@ -1,29 +1,40 @@
|
|||||||
import { ChangeEvent, useState } from "react";
|
import { ChangeEvent, Dispatch } from "react";
|
||||||
import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react";
|
import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react";
|
||||||
import { addLine, changeLock, deleteBlock, deleteLine, moveDown, moveUp, setLines, setTag } from "@/app/actions/blocks";
|
import { deleteBlock, updateBlock } from "@/app/actions/blocks";
|
||||||
|
import { NoteAction } from "@/lib/reducers/noteReducer";
|
||||||
import { useDebounce } from "@/lib/hooks/useDebounce";
|
import { useDebounce } from "@/lib/hooks/useDebounce";
|
||||||
import { IBlock } from "@/lib/db/schema";
|
import { IBlock } from "@/lib/db/types";
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
import LineInput from "./LineInput";
|
import LineInput from "./LineInput";
|
||||||
|
|
||||||
export default function Block({ block }: { block: IBlock }) {
|
export default function Block({
|
||||||
const [lines, setLinesState] = useState(block.lines);
|
block,
|
||||||
const [tag, setTagState] = useState(block.tag);
|
isUnauthorized = false,
|
||||||
|
dispatch,
|
||||||
useDebounce(() => {
|
}: {
|
||||||
setLines(block.id, lines);
|
block: IBlock;
|
||||||
}, [lines]);
|
isUnauthorized?: boolean;
|
||||||
|
dispatch: Dispatch<NoteAction>;
|
||||||
useDebounce(() => {
|
}) {
|
||||||
setTag(block.id, tag);
|
const lineChangeHandler = (e: ChangeEvent<HTMLInputElement>, i: number) => {
|
||||||
}, [tag]);
|
const newLines = [...block.lines];
|
||||||
|
|
||||||
const lineChangeHandler = (i: number, e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newLines = [...lines];
|
|
||||||
newLines[i] = e.target.value;
|
newLines[i] = e.target.value;
|
||||||
setLinesState(newLines);
|
dispatch({ type: "UPDATE_BLOCK_LINES", blockId: block.id, lines: newLines });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletBlockHandler = () => {
|
||||||
|
dispatch({ type: "DELETE_BLOCK", blockId: block.id });
|
||||||
|
if (!isUnauthorized) {
|
||||||
|
deleteBlock(block.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useDebounce(() => {
|
||||||
|
if (!isUnauthorized) {
|
||||||
|
updateBlock(block);
|
||||||
|
}
|
||||||
|
}, [block]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<div className="border-2 border-neutral-800 rounded-lg p-3 w-full">
|
<div className="border-2 border-neutral-800 rounded-lg p-3 w-full">
|
||||||
@ -31,8 +42,8 @@ export default function Block({ block }: { block: IBlock }) {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="enter tag..."
|
placeholder="enter tag..."
|
||||||
className="w-full focus:outline-none"
|
className="w-full focus:outline-none"
|
||||||
onChange={(e) => setTagState(e.target.value)}
|
onChange={(e) => dispatch({ type: "UPDATE_BLOCK_TAG", blockId: block.id, tag: e.target.value })}
|
||||||
value={tag}
|
value={block.tag}
|
||||||
disabled={block.isLocked}
|
disabled={block.isLocked}
|
||||||
/>
|
/>
|
||||||
{block.lines.map((line, i) => (
|
{block.lines.map((line, i) => (
|
||||||
@ -40,44 +51,39 @@ export default function Block({ block }: { block: IBlock }) {
|
|||||||
key={i}
|
key={i}
|
||||||
defaultValue={line}
|
defaultValue={line}
|
||||||
disabled={block.isLocked}
|
disabled={block.isLocked}
|
||||||
onChange={(e) => lineChangeHandler(i, e)}
|
onChange={(e) => lineChangeHandler(e, i)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="flex items-center mx-2 mt-2">
|
<div className="flex items-center mx-2 mt-2">
|
||||||
<div className="flex gap-1 mr-4">
|
<div className="flex gap-1 mr-4">
|
||||||
<form action={addLine}>
|
<IconOnlyButton
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
onClick={() => dispatch({ type: "ADD_BLOCK_LINE", blockId: block.id })}
|
||||||
<IconOnlyButton type="submit" icon={<Plus size={18} />} disabled={block.isLocked} />
|
icon={<Plus size={18} />}
|
||||||
</form>
|
disabled={block.isLocked}
|
||||||
<form action={deleteLine}>
|
/>
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
<IconOnlyButton
|
||||||
<IconOnlyButton type="submit" icon={<Minus size={18} />} disabled={block.isLocked} />
|
onClick={() => dispatch({ type: "DELETE_BLOCK_LINE", blockId: block.id })}
|
||||||
</form>
|
icon={<Minus size={18} />}
|
||||||
|
disabled={block.isLocked}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<form action={moveUp}>
|
<IconOnlyButton
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
onClick={() => dispatch({ type: "MOVE_BLOCK_UP", blockId: block.id })}
|
||||||
<IconOnlyButton type="submit" icon={<ArrowUp size={18} />} />
|
icon={<ArrowUp size={18} />}
|
||||||
</form>
|
/>
|
||||||
<form action={moveDown}>
|
<IconOnlyButton
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
onClick={() => dispatch({ type: "MOVE_BLOCK_DOWN", blockId: block.id })}
|
||||||
<IconOnlyButton type="submit" icon={<ArrowDown size={18} />} />
|
icon={<ArrowDown size={18} />}
|
||||||
</form>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="flex gap-2 ml-auto">
|
||||||
<form action={changeLock}>
|
<IconOnlyButton
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
onClick={() => dispatch({ type: "CHANGE_BLOCK_LOCK", blockId: block.id })}
|
||||||
{!block.isLocked && <input type="hidden" name="isLocked" value="true" />}
|
alwaysOn={block.isLocked}
|
||||||
<IconOnlyButton
|
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
||||||
type="submit"
|
/>
|
||||||
alwaysOn={block.isLocked}
|
<IconOnlyButton onClick={deletBlockHandler} icon={<X size={18} />} />
|
||||||
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<form action={deleteBlock}>
|
|
||||||
<input type="hidden" name="blockId" value={block.id} />
|
|
||||||
<IconOnlyButton type="submit" icon={<X size={18} />} />
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Copy, Plus } from "lucide-react";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { IBlock, INote } from "@/lib/db/schema";
|
|
||||||
import { createBlock } from "@/app/actions/blocks";
|
|
||||||
import { setTitle } from "@/app/actions/notes";
|
|
||||||
import { useDebounce } from "@/lib/hooks/useDebounce";
|
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
|
||||||
import Block from "./Block";
|
|
||||||
|
|
||||||
const defaultNoteId = uuidv4();
|
|
||||||
|
|
||||||
const defaultNote: INote = {
|
|
||||||
id: defaultNoteId,
|
|
||||||
title: "Untitled",
|
|
||||||
creationTime: new Date(),
|
|
||||||
lastEdited: new Date(),
|
|
||||||
authorId: uuidv4(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultBlocks: IBlock[] = [
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
tag: "",
|
|
||||||
lines: ["", "", "", ""],
|
|
||||||
isLocked: false,
|
|
||||||
order: 1,
|
|
||||||
noteId: defaultNoteId,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Editor({
|
|
||||||
note = defaultNote,
|
|
||||||
blocks = defaultBlocks,
|
|
||||||
}: {
|
|
||||||
note?: INote;
|
|
||||||
blocks?: IBlock[];
|
|
||||||
}) {
|
|
||||||
const [title, setTitleState] = useState(note.title);
|
|
||||||
|
|
||||||
const copyHandler = () => {
|
|
||||||
let copyText = "";
|
|
||||||
blocks.forEach((block) => {
|
|
||||||
if (block.tag !== "") {
|
|
||||||
copyText += `[${block.tag}]`;
|
|
||||||
}
|
|
||||||
copyText += block.lines.join("\n");
|
|
||||||
});
|
|
||||||
navigator.clipboard.writeText(copyText);
|
|
||||||
}
|
|
||||||
|
|
||||||
useDebounce(() => {
|
|
||||||
setTitle(note.id, title);
|
|
||||||
}, [title]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center max-w-2xl w-full gap-4">
|
|
||||||
<input
|
|
||||||
className="font-bold text-xl w-full text-center focus:outline-none"
|
|
||||||
onChange={(e) => setTitleState(e.target.value)}
|
|
||||||
value={title}
|
|
||||||
/>
|
|
||||||
{blocks.map((block) => <Block key={block.id} block={block} /> )}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<form action={createBlock}>
|
|
||||||
<input type="hidden" name="noteId" value={note.id} />
|
|
||||||
<IconOnlyButton icon={<Plus size={24} />} type="submit" />
|
|
||||||
</form>
|
|
||||||
<IconOnlyButton icon={<Copy size={24} />} onClick={copyHandler} title="Copy note to clipboard" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
95
src/components/editor/NoteEditor.tsx
Normal file
95
src/components/editor/NoteEditor.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useReducer } from "react";
|
||||||
|
import { Copy, Plus } from "lucide-react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { createBlock } from "@/app/actions/blocks";
|
||||||
|
import { setTitle } from "@/app/actions/notes";
|
||||||
|
import { useDebounce } from "@/lib/hooks/useDebounce";
|
||||||
|
import { noteReducer } from "@/lib/reducers/noteReducer";
|
||||||
|
import { INoteWithBlocks } from "@/lib/db/types";
|
||||||
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
|
import Block from "./Block";
|
||||||
|
|
||||||
|
const defaultNote: INoteWithBlocks = {
|
||||||
|
id: "",
|
||||||
|
title: "Untitled",
|
||||||
|
authorId: "",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
noteId: "",
|
||||||
|
tag: "",
|
||||||
|
lines: ["", "", "", ""],
|
||||||
|
isLocked: false,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NoteEditor({
|
||||||
|
note = defaultNote,
|
||||||
|
isUnauthorized = false,
|
||||||
|
}: {
|
||||||
|
note?: INoteWithBlocks;
|
||||||
|
isUnauthorized?: boolean;
|
||||||
|
}) {
|
||||||
|
const [state, dispatch] = useReducer(noteReducer, note);
|
||||||
|
|
||||||
|
const copyHandler = () => {
|
||||||
|
let copyText = "";
|
||||||
|
state.blocks.forEach((block) => {
|
||||||
|
if (block.tag !== "") {
|
||||||
|
copyText += `[${block.tag}]\n`;
|
||||||
|
}
|
||||||
|
copyText += block.lines.join("\n") + "\n\n";
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(copyText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBlockHandler = () => {
|
||||||
|
const tempBlockId = uuidv4();
|
||||||
|
dispatch({ type: "CREATE_BLOCK", noteId: note.id, blockId: tempBlockId });
|
||||||
|
if (!isUnauthorized) {
|
||||||
|
createBlock(note.id).then((block) => {
|
||||||
|
dispatch({ type: "CHANGE_BLOCK_ID", blockId: tempBlockId, newBlockId: block.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useDebounce(() => {
|
||||||
|
if (!isUnauthorized) {
|
||||||
|
setTitle(note.id, state.title);
|
||||||
|
}
|
||||||
|
}, [state.title]);
|
||||||
|
|
||||||
|
const sortedBlocks = state.blocks.sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center max-w-2xl w-full gap-4">
|
||||||
|
<input
|
||||||
|
className="font-bold text-xl w-full text-center focus:outline-none"
|
||||||
|
onChange={(e) => dispatch({ type: "UPDATE_TITLE", title: e.target.value })}
|
||||||
|
value={state.title}
|
||||||
|
/>
|
||||||
|
{sortedBlocks.map((block) => (
|
||||||
|
<Block
|
||||||
|
key={block.id}
|
||||||
|
block={block}
|
||||||
|
dispatch={dispatch}
|
||||||
|
isUnauthorized={isUnauthorized}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<IconOnlyButton onClick={createBlockHandler} icon={<Plus size={24} />} />
|
||||||
|
<IconOnlyButton
|
||||||
|
onClick={copyHandler}
|
||||||
|
icon={<Copy size={24} />}
|
||||||
|
title="Copy note to clipboard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { INote } from "@/lib/db/schema";
|
import { INote } from "@/lib/db/types";
|
||||||
import { deleteNote } from "@/app/actions/notes";
|
import { deleteNote } from "@/app/actions/notes";
|
||||||
import IconOnlyButton from "./IconOnlyButton";
|
import IconOnlyButton from "./IconOnlyButton";
|
||||||
|
|
||||||
@ -18,8 +18,8 @@ export default function NoteCard({ note }: { note: INote }) {
|
|||||||
<div className="flex items-center mb-3 gap-4">
|
<div className="flex items-center mb-3 gap-4">
|
||||||
<Link href={`/notes/${note.id}`} className="flex flex-col border border-neutral-500 py-3 px-4 rounded-lg hover:bg-neutral-800 w-full">
|
<Link href={`/notes/${note.id}`} className="flex flex-col border border-neutral-500 py-3 px-4 rounded-lg hover:bg-neutral-800 w-full">
|
||||||
<h1 className="font-bold">{note.title}</h1>
|
<h1 className="font-bold">{note.title}</h1>
|
||||||
<i className="text-neutral-400 text-sm">Last time edited: {makeTimestamp(note.lastEdited)}</i>
|
<i className="text-neutral-400 text-sm">Last time edited: {makeTimestamp(note.updatedAt)}</i>
|
||||||
<i className="text-neutral-400 text-sm">Creation date: {makeTimestamp(note.creationTime)}</i>
|
<i className="text-neutral-400 text-sm">Creation date: {makeTimestamp(note.createdAt)}</i>
|
||||||
</Link>
|
</Link>
|
||||||
<form action={deleteNote}>
|
<form action={deleteNote}>
|
||||||
<input type="hidden" name="noteId" value={note.id} />
|
<input type="hidden" name="noteId" value={note.id} />
|
||||||
|
@ -14,8 +14,8 @@ export const usersRelations = relations(usersTable, ({ many }) => ({
|
|||||||
export const notesTable = pgTable("notes", {
|
export const notesTable = pgTable("notes", {
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
title: varchar({ length: 50 }).notNull().default("Untitled"),
|
title: varchar({ length: 50 }).notNull().default("Untitled"),
|
||||||
creationTime: timestamp().notNull().defaultNow(),
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
lastEdited: timestamp().notNull().defaultNow(),
|
updatedAt: timestamp().notNull().defaultNow(),
|
||||||
authorId: uuid().notNull().references(() => usersTable.id, { onDelete: "cascade" }),
|
authorId: uuid().notNull().references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export const notesRelations = relations(notesTable, ({ one, many }) => ({
|
|||||||
fields: [notesTable.authorId],
|
fields: [notesTable.authorId],
|
||||||
references: [usersTable.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
block: many(blocksTable),
|
blocks: many(blocksTable),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const blocksTable = pgTable("blocks", {
|
export const blocksTable = pgTable("blocks", {
|
||||||
@ -46,7 +46,3 @@ export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
|||||||
references: [blocksTable.id],
|
references: [blocksTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type IUser = typeof usersTable.$inferSelect;
|
|
||||||
export type INote = typeof notesTable.$inferSelect;
|
|
||||||
export type IBlock = typeof blocksTable.$inferSelect;
|
|
||||||
|
9
src/lib/db/types.ts
Normal file
9
src/lib/db/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { blocksTable, notesTable, usersTable } from "./schema";
|
||||||
|
|
||||||
|
export type IUser = typeof usersTable.$inferSelect;
|
||||||
|
export type INote = typeof notesTable.$inferSelect;
|
||||||
|
export type IBlock = typeof blocksTable.$inferSelect;
|
||||||
|
|
||||||
|
export interface INoteWithBlocks extends INote {
|
||||||
|
blocks: IBlock[];
|
||||||
|
}
|
@ -8,7 +8,7 @@ export function useDebounce(callback: () => void, deps: DependencyList) {
|
|||||||
if (isFirstRun.current) {
|
if (isFirstRun.current) {
|
||||||
isFirstRun.current = false;
|
isFirstRun.current = false;
|
||||||
} else {
|
} else {
|
||||||
const timeout = setTimeout(callback, 1000);
|
const timeout = setTimeout(callback, 400);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}, deps);
|
}, deps);
|
||||||
|
186
src/lib/reducers/noteReducer.ts
Normal file
186
src/lib/reducers/noteReducer.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { INoteWithBlocks } from "@/lib/db/types";
|
||||||
|
|
||||||
|
export type NoteAction =
|
||||||
|
| { type: "UPDATE_TITLE"; title: string }
|
||||||
|
| { type: "CREATE_BLOCK", noteId: string, blockId: string }
|
||||||
|
| { type: "DELETE_BLOCK"; blockId: string }
|
||||||
|
| { type: "ADD_BLOCK_LINE"; blockId: string }
|
||||||
|
| { type: "DELETE_BLOCK_LINE"; blockId: string }
|
||||||
|
| { type: "UPDATE_BLOCK_TAG"; blockId: string, tag: string }
|
||||||
|
| { type: "UPDATE_BLOCK_LINES"; blockId: string, lines: string[] }
|
||||||
|
| { type: "MOVE_BLOCK_UP"; blockId: string }
|
||||||
|
| { type: "MOVE_BLOCK_DOWN"; blockId: string }
|
||||||
|
| { type: "CHANGE_BLOCK_LOCK"; blockId: string }
|
||||||
|
| { type: "CHANGE_BLOCK_ID"; blockId: string, newBlockId: string };
|
||||||
|
|
||||||
|
export function noteReducer(state: INoteWithBlocks, action: NoteAction): INoteWithBlocks {
|
||||||
|
switch (action.type) {
|
||||||
|
case "UPDATE_TITLE":
|
||||||
|
document.title = action.title;
|
||||||
|
return { ...state, title: action.title };
|
||||||
|
|
||||||
|
case "CREATE_BLOCK":
|
||||||
|
const maxOrder = Math.max(0, ...state.blocks.map(b => b.order));
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: [
|
||||||
|
...state.blocks,
|
||||||
|
{
|
||||||
|
id: action.blockId,
|
||||||
|
tag: "",
|
||||||
|
lines: ["", "", "", ""],
|
||||||
|
order: maxOrder + 1,
|
||||||
|
isLocked: false,
|
||||||
|
noteId: action.noteId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
case "CHANGE_BLOCK_ID":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
id: action.newBlockId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_BLOCK_TAG":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
tag: action.tag,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_BLOCK_LINES":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
lines: action.lines,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DELETE_BLOCK":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.filter((b) => b.id !== action.blockId),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "MOVE_BLOCK_UP": {
|
||||||
|
const sortedBlocks = state.blocks.sort((a, b) => a.order - b.order);
|
||||||
|
const blockIndex = sortedBlocks.findIndex((b) => b.id === action.blockId);
|
||||||
|
const prevBlockIndex = blockIndex - 1;
|
||||||
|
if (prevBlockIndex < 0) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBlocks = [...sortedBlocks];
|
||||||
|
newBlocks[blockIndex] = {
|
||||||
|
...newBlocks[blockIndex],
|
||||||
|
order: newBlocks[prevBlockIndex].order,
|
||||||
|
};
|
||||||
|
newBlocks[prevBlockIndex] = {
|
||||||
|
...newBlocks[prevBlockIndex],
|
||||||
|
order: newBlocks[blockIndex].order + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: newBlocks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "MOVE_BLOCK_DOWN": {
|
||||||
|
const sortedBlocks = state.blocks.sort((a, b) => a.order - b.order);
|
||||||
|
const blockIndex = sortedBlocks.findIndex((b) => b.id === action.blockId);
|
||||||
|
const nextBlockIndex = blockIndex + 1;
|
||||||
|
if (nextBlockIndex >= sortedBlocks.length) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBlocks = [...sortedBlocks];
|
||||||
|
newBlocks[blockIndex] = {
|
||||||
|
...newBlocks[blockIndex],
|
||||||
|
order: newBlocks[nextBlockIndex].order,
|
||||||
|
};
|
||||||
|
newBlocks[nextBlockIndex] = {
|
||||||
|
...newBlocks[nextBlockIndex],
|
||||||
|
order: newBlocks[blockIndex].order - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: newBlocks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ADD_BLOCK_LINE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
lines: [...b.lines, ""],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DELETE_BLOCK_LINE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
lines: b.lines.slice(0, -1),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "CHANGE_BLOCK_LOCK":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
blocks: state.blocks.map((b) => {
|
||||||
|
if (b.id === action.blockId) {
|
||||||
|
return {
|
||||||
|
...b,
|
||||||
|
isLocked: !b.isLocked,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user