diff --git a/drizzle/0000_cold_odin.sql b/drizzle/0000_cold_odin.sql new file mode 100644 index 0000000..40ba045 --- /dev/null +++ b/drizzle/0000_cold_odin.sql @@ -0,0 +1,26 @@ +CREATE TABLE "blocks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tag" varchar(100) DEFAULT '' NOT NULL, + "lines" json DEFAULT '["","","",""]'::json NOT NULL, + "isLocked" boolean DEFAULT false NOT NULL, + "order" integer DEFAULT 1 NOT NULL, + "noteId" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" varchar(50) DEFAULT 'Untitled' NOT NULL, + "creationTime" timestamp DEFAULT now() NOT NULL, + "lastEdited" timestamp DEFAULT now() NOT NULL, + "authorId" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "username" varchar(50) NOT NULL, + "password" varchar NOT NULL, + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +ALTER TABLE "blocks" ADD CONSTRAINT "blocks_noteId_notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "public"."notes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notes" ADD CONSTRAINT "notes_authorId_users_id_fk" FOREIGN KEY ("authorId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..b6b6271 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,188 @@ +{ + "id": "20af197f-8cc5-49a7-9066-ee214573f6ce", + "prevId": "00000000-0000-0000-0000-000000000000", + "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, + "default": 1 + }, + "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'" + }, + "creationTime": { + "name": "creationTime", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastEdited": { + "name": "lastEdited", + "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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index eaa8fcf..bbd262f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,5 +1,13 @@ { "version": "7", "dialect": "postgresql", - "entries": [] + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1752532763005, + "tag": "0000_cold_odin", + "breakpoints": true + } + ] } \ No newline at end of file diff --git a/src/app/actions/blocks.ts b/src/app/actions/blocks.ts new file mode 100644 index 0000000..0ff79f8 --- /dev/null +++ b/src/app/actions/blocks.ts @@ -0,0 +1,39 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { blocksTable, IBlock } from "@/lib/db/schema"; + +export async function createBlock(formData: FormData) { + const noteId = formData.get("noteId") as string; + await db + .insert(blocksTable) + .values({ noteId }); + revalidatePath("/notes/[id]"); +} + +export async function getBlocks(noteId: string): Promise { + return db + .select() + .from(blocksTable) + .where(eq(blocksTable.noteId, noteId)); +} + +export async function deleteBlock(formData: FormData) { + const blockId = formData.get("blockId") as string; + await db + .delete(blocksTable) + .where(eq(blocksTable.id, blockId)); + revalidatePath("/notes/[id]"); +} + +export async function changeLock(formData: FormData) { + const blockId = formData.get("blockId") as string; + const isLocked = formData.get("isLocked") === null ? false : true; + await db + .update(blocksTable) + .set({ isLocked }) + .where(eq(blocksTable.id, blockId)); + revalidatePath("/notes/[id]"); +} diff --git a/src/app/actions/notes.ts b/src/app/actions/notes.ts index f590d36..404e957 100644 --- a/src/app/actions/notes.ts +++ b/src/app/actions/notes.ts @@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { desc, eq, and } from "drizzle-orm"; -import { INote, notesTable, usersTable } from "@/lib/db/schema"; +import { blocksTable, INote, notesTable, usersTable } from "@/lib/db/schema"; import { db } from "@/lib/db"; import { requireAuth } from "./auth"; @@ -13,8 +13,19 @@ export async function createNote() { .insert(notesTable) .values({ authorId: user.id }) .returning({ id: usersTable.id }); - const id = result[0].id; - redirect(`/notes/${id}`); + const noteId = result[0].id; + + await db.insert(blocksTable).values({ noteId }); + redirect(`/notes/${noteId}`); +} + +export async function getNote(noteId: string): Promise { + const user = await requireAuth(); + const note = await db + .select() + .from(notesTable) + .where(and(eq(notesTable.id, noteId), eq(notesTable.authorId, user.id))); + return note.length > 0 ? note[0] : null; } export async function getNotes(): Promise { diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index 2bc2011..90591c0 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -1,18 +1,29 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { getNote } from "@/app/actions/notes"; +import { getBlocks } from "@/app/actions/blocks"; import Editor from "@/components/editor/Editor"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; - return { title: id }; + const note = await getNote(id); + if (!note) { + notFound(); + } + return { title: note.title }; } export default async function Note({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; + const note = await getNote(id); + if (!note) { + notFound(); + } + const blocks = await getBlocks(note.id); return (
- {id} - +
); } diff --git a/src/components/editor/Block.tsx b/src/components/editor/Block.tsx index 0b1a166..f49679d 100644 --- a/src/components/editor/Block.tsx +++ b/src/components/editor/Block.tsx @@ -1,4 +1,5 @@ import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react"; +import { changeLock, deleteBlock } from "@/app/actions/blocks"; import { IBlock } from "@/lib/db/schema"; import IconOnlyButton from "../ui/IconOnlyButton"; import LineInput from "./LineInput"; @@ -12,23 +13,33 @@ export default function Block({ block }: { block: IBlock }) { placeholder="enter tag..." className="w-full focus:outline-none" defaultValue={block.tag} + disabled={block.isLocked} /> - {block.lines.map((line, i) => )} + {block.lines.map((line, i) => ( + + ))}
- } /> - } /> + } disabled={block.isLocked} /> + } disabled={block.isLocked} />
} /> } />
- } /> - : } - /> +
+ + } type="submit" /> + +
+ + {!block.isLocked && } + : } + /> +
diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx index d8f53c6..6827086 100644 --- a/src/components/editor/Editor.tsx +++ b/src/components/editor/Editor.tsx @@ -1,6 +1,7 @@ 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 IconOnlyButton from "../ui/IconOnlyButton"; import Block from "./Block"; @@ -34,10 +35,16 @@ export default function Editor({ }) { return (
- + {blocks.map((block) => )}
- } /> +
+ + } type="submit" /> + } title="Copy note to clipboard" />
diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 90b8da4..1c8a038 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -16,14 +16,15 @@ export const notesTable = pgTable("notes", { title: varchar({ length: 50 }).notNull().default("Untitled"), creationTime: timestamp().notNull().defaultNow(), lastEdited: timestamp().notNull().defaultNow(), - authorId: uuid().notNull(), + authorId: uuid().notNull().references(() => usersTable.id, { onDelete: "cascade" }), }); -export const notesRelations = relations(notesTable, ({ one }) => ({ +export const notesRelations = relations(notesTable, ({ one, many }) => ({ author: one(usersTable, { fields: [notesTable.authorId], references: [usersTable.id], }), + block: many(blocksTable), })); export const blocksTable = pgTable("blocks", { @@ -32,7 +33,7 @@ export const blocksTable = pgTable("blocks", { lines: json().notNull().default(["", "", "", ""]).$type(), isLocked: boolean().notNull().default(false), order: integer().notNull().default(1), - noteId: uuid().notNull(), + noteId: uuid().notNull().references(() => notesTable.id, { onDelete: "cascade" }), }); export const blocksRelations = relations(blocksTable, ({ one }) => ({ @@ -40,6 +41,10 @@ export const blocksRelations = relations(blocksTable, ({ one }) => ({ fields: [blocksTable.noteId], references: [notesTable.id], }), + block: one(blocksTable, { + fields: [blocksTable.id], + references: [blocksTable.id], + }), })); export type IUser = typeof usersTable.$inferSelect;