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",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"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 { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { desc, eq, and } from "drizzle-orm";
|
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 { db } from "@/lib/db";
|
||||||
import { requireAuth } from "./auth";
|
import { requireAuth } from "./auth";
|
||||||
|
|
||||||
@ -13,8 +13,19 @@ export async function createNote() {
|
|||||||
.insert(notesTable)
|
.insert(notesTable)
|
||||||
.values({ authorId: user.id })
|
.values({ authorId: user.id })
|
||||||
.returning({ id: usersTable.id });
|
.returning({ id: usersTable.id });
|
||||||
const id = result[0].id;
|
const noteId = result[0].id;
|
||||||
redirect(`/notes/${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[]> {
|
export async function getNotes(): Promise<INote[]> {
|
||||||
|
@ -1,18 +1,29 @@
|
|||||||
import { Metadata } from "next";
|
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";
|
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;
|
||||||
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 }> }) {
|
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const note = await getNote(id);
|
||||||
|
if (!note) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
const blocks = await getBlocks(note.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{id}
|
<Editor note={note} blocks={blocks} />
|
||||||
<Editor />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ArrowDown, ArrowUp, LockOpen, Lock, Minus, Plus, X } from "lucide-react";
|
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 { IBlock } from "@/lib/db/schema";
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
import LineInput from "./LineInput";
|
import LineInput from "./LineInput";
|
||||||
@ -12,23 +13,33 @@ export default function Block({ block }: { block: IBlock }) {
|
|||||||
placeholder="enter tag..."
|
placeholder="enter tag..."
|
||||||
className="w-full focus:outline-none"
|
className="w-full focus:outline-none"
|
||||||
defaultValue={block.tag}
|
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 items-center mx-2 mt-2">
|
||||||
<div className="flex gap-1 mr-4">
|
<div className="flex gap-1 mr-4">
|
||||||
<IconOnlyButton icon={<Plus size={18} />} />
|
<IconOnlyButton icon={<Plus size={18} />} disabled={block.isLocked} />
|
||||||
<IconOnlyButton icon={<Minus size={18} />} />
|
<IconOnlyButton icon={<Minus size={18} />} disabled={block.isLocked} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<IconOnlyButton icon={<ArrowUp size={18} />} />
|
<IconOnlyButton icon={<ArrowUp size={18} />} />
|
||||||
<IconOnlyButton icon={<ArrowDown size={18} />} />
|
<IconOnlyButton icon={<ArrowDown size={18} />} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="flex gap-2 ml-auto">
|
||||||
<IconOnlyButton icon={<X size={18} />} />
|
<form action={deleteBlock}>
|
||||||
<IconOnlyButton
|
<input type="hidden" name="blockId" value={block.id} />
|
||||||
alwaysOn={block.isLocked}
|
<IconOnlyButton icon={<X size={18} />} type="submit" />
|
||||||
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Copy, Plus } from "lucide-react";
|
import { Copy, Plus } from "lucide-react";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { IBlock, INote } from "@/lib/db/schema";
|
import { IBlock, INote } from "@/lib/db/schema";
|
||||||
|
import { createBlock } from "@/app/actions/blocks";
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
import Block from "./Block";
|
import Block from "./Block";
|
||||||
|
|
||||||
@ -34,10 +35,16 @@ export default function Editor({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center max-w-2xl w-full gap-4">
|
<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} /> )}
|
{blocks.map((block) => <Block key={block.id} block={block} /> )}
|
||||||
<div className="flex gap-2">
|
<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" />
|
<IconOnlyButton icon={<Copy size={24} />} title="Copy note to clipboard" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,14 +16,15 @@ export const notesTable = pgTable("notes", {
|
|||||||
title: varchar({ length: 50 }).notNull().default("Untitled"),
|
title: varchar({ length: 50 }).notNull().default("Untitled"),
|
||||||
creationTime: timestamp().notNull().defaultNow(),
|
creationTime: timestamp().notNull().defaultNow(),
|
||||||
lastEdited: 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, {
|
author: one(usersTable, {
|
||||||
fields: [notesTable.authorId],
|
fields: [notesTable.authorId],
|
||||||
references: [usersTable.id],
|
references: [usersTable.id],
|
||||||
}),
|
}),
|
||||||
|
block: many(blocksTable),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const blocksTable = pgTable("blocks", {
|
export const blocksTable = pgTable("blocks", {
|
||||||
@ -32,7 +33,7 @@ export const blocksTable = pgTable("blocks", {
|
|||||||
lines: json().notNull().default(["", "", "", ""]).$type<string[]>(),
|
lines: json().notNull().default(["", "", "", ""]).$type<string[]>(),
|
||||||
isLocked: boolean().notNull().default(false),
|
isLocked: boolean().notNull().default(false),
|
||||||
order: integer().notNull().default(1),
|
order: integer().notNull().default(1),
|
||||||
noteId: uuid().notNull(),
|
noteId: uuid().notNull().references(() => notesTable.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
||||||
@ -40,6 +41,10 @@ export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
|||||||
fields: [blocksTable.noteId],
|
fields: [blocksTable.noteId],
|
||||||
references: [notesTable.id],
|
references: [notesTable.id],
|
||||||
}),
|
}),
|
||||||
|
block: one(blocksTable, {
|
||||||
|
fields: [blocksTable.id],
|
||||||
|
references: [blocksTable.id],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type IUser = typeof usersTable.$inferSelect;
|
export type IUser = typeof usersTable.$inferSelect;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user