feat: add block functionality
This commit is contained in:
parent
646bbaaeb9
commit
32b33ae4c4
26
drizzle/0000_cold_odin.sql
Normal file
26
drizzle/0000_cold_odin.sql
Normal file
@ -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;
|
188
drizzle/meta/0000_snapshot.json
Normal file
188
drizzle/meta/0000_snapshot.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": []
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1752532763005,
|
||||
"tag": "0000_cold_odin",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
39
src/app/actions/blocks.ts
Normal file
39
src/app/actions/blocks.ts
Normal file
@ -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<IBlock[]> {
|
||||
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]");
|
||||
}
|
@ -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<INote | null> {
|
||||
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<INote[]> {
|
||||
|
@ -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<Metadata> {
|
||||
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 (
|
||||
<div className="flex justify-center">
|
||||
{id}
|
||||
<Editor />
|
||||
<Editor note={note} blocks={blocks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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) => <LineInput key={i} defaultValue={line} /> )}
|
||||
{block.lines.map((line, i) => (
|
||||
<LineInput key={i} defaultValue={line} disabled={block.isLocked} />
|
||||
))}
|
||||
<div className="flex items-center mx-2 mt-2">
|
||||
<div className="flex gap-1 mr-4">
|
||||
<IconOnlyButton icon={<Plus size={18} />} />
|
||||
<IconOnlyButton icon={<Minus size={18} />} />
|
||||
<IconOnlyButton icon={<Plus size={18} />} disabled={block.isLocked} />
|
||||
<IconOnlyButton icon={<Minus size={18} />} disabled={block.isLocked} />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<IconOnlyButton icon={<ArrowUp size={18} />} />
|
||||
<IconOnlyButton icon={<ArrowDown size={18} />} />
|
||||
</div>
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<IconOnlyButton icon={<X size={18} />} />
|
||||
<IconOnlyButton
|
||||
alwaysOn={block.isLocked}
|
||||
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
||||
/>
|
||||
<form action={deleteBlock}>
|
||||
<input type="hidden" name="blockId" value={block.id} />
|
||||
<IconOnlyButton icon={<X size={18} />} type="submit" />
|
||||
</form>
|
||||
<form action={changeLock}>
|
||||
<input type="hidden" name="blockId" value={block.id} />
|
||||
{!block.isLocked && <input type="hidden" name="isLocked" />}
|
||||
<IconOnlyButton
|
||||
alwaysOn={block.isLocked}
|
||||
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<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" defaultValue={note.title} />
|
||||
<input
|
||||
className="font-bold text-xl w-full text-center focus:outline-none"
|
||||
defaultValue={note.title}
|
||||
/>
|
||||
{blocks.map((block) => <Block key={block.id} block={block} /> )}
|
||||
<div className="flex gap-2">
|
||||
<IconOnlyButton icon={<Plus size={24} />} />
|
||||
<form action={createBlock}>
|
||||
<input type="hidden" name="noteId" value={note.id} />
|
||||
<IconOnlyButton icon={<Plus size={24} />} type="submit" />
|
||||
</form>
|
||||
<IconOnlyButton icon={<Copy size={24} />} title="Copy note to clipboard" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<string[]>(),
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user