feat: start editor database integration
This commit is contained in:
parent
2ad220e525
commit
917e460a96
@ -1,23 +1,42 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
import { revalidatePath } from "next/cache";
|
||||||
import { blocksTable } from "@/lib/db/schema";
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { blocksTable, IBlock, INote } from "@/lib/db/schema";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
|
|
||||||
export async function updateBlock({
|
export async function createBlock(note: INote) {
|
||||||
id,
|
await db
|
||||||
tag,
|
.insert(blocksTable)
|
||||||
lines,
|
.values({ noteId: note.id });
|
||||||
}: {
|
|
||||||
id: string;
|
revalidatePath("/blocks/[id]");
|
||||||
tag?: string;
|
}
|
||||||
lines?: string[];
|
|
||||||
}) {
|
export async function getBlocks(note: INote) {
|
||||||
|
return db.select()
|
||||||
|
.from(blocksTable)
|
||||||
|
.where(and(eq(blocksTable.noteId, note.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBlockTag(block: IBlock, newTag: string) {
|
||||||
await db
|
await db
|
||||||
.update(blocksTable)
|
.update(blocksTable)
|
||||||
.set({
|
.set({ tag: newTag })
|
||||||
...(tag !== undefined ? { tag } : {}),
|
.where(eq(blocksTable.id, block.id));
|
||||||
...(lines !== undefined ? { lines } : {}),
|
}
|
||||||
})
|
|
||||||
.where(eq(blocksTable.id, id));
|
export async function deleteBlock(block: IBlock) {
|
||||||
|
await db.delete(blocksTable)
|
||||||
|
.where(and(eq(blocksTable.id, block.id)));
|
||||||
|
|
||||||
|
revalidatePath("/blocks/[id]");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchLock(block: IBlock) {
|
||||||
|
await db.update(blocksTable)
|
||||||
|
.set({ isLocked: !block.isLocked })
|
||||||
|
.where(and(eq(blocksTable.id, block.id)));
|
||||||
|
|
||||||
|
revalidatePath("/blocks/[id]");
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
import { desc, eq, and } from "drizzle-orm";
|
"use server";
|
||||||
|
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
import { validate as uuidValidate } from "uuid";
|
import { validate as uuidValidate } from "uuid";
|
||||||
import { notesTable } from "./db/schema";
|
import { notesTable } from "@/lib/db/schema";
|
||||||
import { getAuth } from "./auth";
|
import { getAuth } from "@/lib/auth";
|
||||||
import { db } from "./db";
|
import { eq, and, desc } from "drizzle-orm";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
|
||||||
|
export async function createNote() {
|
||||||
|
const auth = await getAuth();
|
||||||
|
if (!auth) {
|
||||||
|
redirect("/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = await db
|
||||||
|
.insert(notesTable)
|
||||||
|
.values({ authorId: auth.id })
|
||||||
|
.returning({ id: notesTable.id });
|
||||||
|
|
||||||
|
redirect(`/notes/${notes[0].id}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNotes(authorId: string) {
|
export async function getNotes(authorId: string) {
|
||||||
return db.select()
|
return db.select()
|
||||||
@ -32,20 +50,6 @@ export async function getNote(noteId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createNote() {
|
|
||||||
const auth = await getAuth();
|
|
||||||
if (!auth) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const note = await db
|
|
||||||
.insert(notesTable)
|
|
||||||
.values({ authorId: auth.id })
|
|
||||||
.returning({ id: notesTable.id });
|
|
||||||
|
|
||||||
return note[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteNote(noteId: string) {
|
export async function deleteNote(noteId: string) {
|
||||||
if (!uuidValidate(noteId)) {
|
if (!uuidValidate(noteId)) {
|
||||||
return null;
|
return null;
|
||||||
@ -57,4 +61,12 @@ export async function deleteNote(noteId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(notesTable).where(eq(notesTable.id, noteId));
|
await db.delete(notesTable).where(eq(notesTable.id, noteId));
|
||||||
|
revalidatePath("/notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTitle(noteId: string, title: string) {
|
||||||
|
await db
|
||||||
|
.update(notesTable)
|
||||||
|
.set({ title })
|
||||||
|
.where(eq(notesTable.id, noteId));
|
||||||
}
|
}
|
8
src/app/not-found.tsx
Normal file
8
src/app/not-found.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="font-bold text-2xl mb-2">404</h1>
|
||||||
|
<p>Page not found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getNote } from "@/lib/notes";
|
import { getBlocks } from "@/app/actions/blocks";
|
||||||
|
import { getNote } from "@/app/actions/notes";
|
||||||
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> {
|
||||||
@ -10,7 +11,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title: note.name };
|
return { title: note.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
||||||
@ -20,9 +21,14 @@ export default async function Note({ params }: { params: Promise<{ id: string }>
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blocks = await getBlocks(note);
|
||||||
|
if (!blocks) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Editor defaultTitle={note.name} />
|
<Editor note={note} blocks={blocks} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
import { createNote } from "@/lib/notes";
|
|
||||||
|
|
||||||
export default async function NewNote() {
|
|
||||||
const noteId = await createNote();
|
|
||||||
|
|
||||||
if (noteId) {
|
|
||||||
redirect(`/notes/${noteId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-center mt-4">
|
|
||||||
Failed to create a new note!<br />
|
|
||||||
Please, try again.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getNotes } from "@/lib/notes";
|
import { getNotes } from "@/app/actions/notes";
|
||||||
import { getAuth } from "@/lib/auth";
|
import { getAuth } from "@/lib/auth";
|
||||||
import NoteCard from "@/components/ui/NoteCard";
|
import NoteCard from "@/components/ui/NoteCard";
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CircleQuestionMark, List, Plus, UserRound, UserRoundMinus } from "lucide-react";
|
import { CircleQuestionMark, List, Plus, UserRound, UserRoundMinus } from "lucide-react";
|
||||||
|
import { createNote } from "@/app/actions/notes";
|
||||||
import { logOut } from "@/app/actions/auth";
|
import { logOut } from "@/app/actions/auth";
|
||||||
import { getAuth } from "@/lib/auth";
|
import { getAuth } from "@/lib/auth";
|
||||||
import HeaderButton from "./ui/HeaderButton";
|
import HeaderButton from "./ui/HeaderButton";
|
||||||
@ -15,9 +16,7 @@ export default async function Header() {
|
|||||||
|
|
||||||
{auth && (
|
{auth && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link href="/notes/new">
|
<HeaderButton onClick={createNote} title="new" icon={<Plus size={20} />} />
|
||||||
<HeaderButton title="new" icon={<Plus size={20} />} />
|
|
||||||
</Link>
|
|
||||||
<Link href="/notes">
|
<Link href="/notes">
|
||||||
<HeaderButton title="list" icon={<List size={20} />} />
|
<HeaderButton title="list" icon={<List size={20} />} />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -2,47 +2,34 @@
|
|||||||
|
|
||||||
import { ChangeEvent } from "react";
|
import { ChangeEvent } from "react";
|
||||||
import { ArrowDown, ArrowUp, Lock, LockOpen, Minus, Plus, X } from "lucide-react";
|
import { ArrowDown, ArrowUp, Lock, LockOpen, Minus, Plus, X } from "lucide-react";
|
||||||
import { Action, IBlock, ILine } from "@/lib/editorReducer";
|
import { IBlock } from "@/lib/db/schema";
|
||||||
|
import { switchLock, deleteBlock } from "@/app/actions/blocks";
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
import LineInput from "./LineInput";
|
import LineInput from "./LineInput";
|
||||||
|
|
||||||
export default function Block({
|
export default function Block({ block }: { block: IBlock }) {
|
||||||
block,
|
|
||||||
dispatch,
|
|
||||||
}: {
|
|
||||||
block: IBlock;
|
|
||||||
dispatch: React.Dispatch<Action>;
|
|
||||||
}) {
|
|
||||||
const handleAddLine = () => {
|
const handleAddLine = () => {
|
||||||
dispatch({ type: "add_line", blockId: block.id });
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteLine = () => {
|
const handleDeleteLine = () => {
|
||||||
dispatch({ type: "delete_line", blockId: block.id });
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTagUpdate = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleTagUpdate = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
dispatch({ type: "update_tag", blockId: block.id, tag: e.target.value });
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLineUpdate = (e: ChangeEvent<HTMLInputElement>, line: ILine) => {
|
const handleLineUpdate = (e: ChangeEvent<HTMLInputElement>, line: string) => {
|
||||||
dispatch({ type: "update_line_text", blockId: block.id, lineId: line.id, text: e.target.value });
|
// TODO
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteBlock = () => {
|
|
||||||
dispatch({ type: "delete_block", blockId: block.id });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBlockUp = () => {
|
const handleBlockUp = () => {
|
||||||
dispatch({ type: "move_block_up", blockId: block.id });
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBlockDown = () => {
|
const handleBlockDown = () => {
|
||||||
dispatch({ type: "move_block_down", blockId: block.id });
|
// TODO
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleLock = () => {
|
|
||||||
dispatch({ type: "toggle_lock", blockId: block.id });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -52,15 +39,15 @@ export default function Block({
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="enter tag..."
|
placeholder="enter tag..."
|
||||||
className="w-full focus:outline-none"
|
className="w-full focus:outline-none"
|
||||||
value={block.tag}
|
defaultValue={block.tag}
|
||||||
disabled={block.locked}
|
disabled={block.isLocked}
|
||||||
onChange={handleTagUpdate}
|
onChange={handleTagUpdate}
|
||||||
/>
|
/>
|
||||||
{block.lines.map((line) => (
|
{block.lines.map((line, i) => (
|
||||||
<LineInput
|
<LineInput
|
||||||
key={line.id}
|
key={i}
|
||||||
value={line.text}
|
defaultValue={line}
|
||||||
disabled={block.locked}
|
disabled={block.isLocked}
|
||||||
onChange={(e) => handleLineUpdate(e, line)}
|
onChange={(e) => handleLineUpdate(e, line)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -74,8 +61,12 @@ export default function Block({
|
|||||||
<IconOnlyButton onClick={handleBlockDown} icon={<ArrowDown size={18} />} />
|
<IconOnlyButton onClick={handleBlockDown} icon={<ArrowDown size={18} />} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="flex gap-2 ml-auto">
|
||||||
<IconOnlyButton onClick={handleDeleteBlock} icon={<X size={18} />} />
|
<IconOnlyButton onClick={() => deleteBlock(block)} icon={<X size={18} />} />
|
||||||
<IconOnlyButton onClick={handleToggleLock} icon={block.locked ? <Lock size={18} /> : <LockOpen size={18} />} alwaysOn={block.locked} />
|
<IconOnlyButton
|
||||||
|
onClick={() => switchLock(block)}
|
||||||
|
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
||||||
|
alwaysOn={block.isLocked}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,53 +1,69 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChangeEvent, useReducer } from "react";
|
import { ChangeEvent, useState } from "react";
|
||||||
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 { editorReducer, IBlock } from "@/lib/editorReducer";
|
import { createBlock } from "@/app/actions/blocks";
|
||||||
|
import { IBlock, INote } from "@/lib/db/schema";
|
||||||
import IconOnlyButton from "../ui/IconOnlyButton";
|
import IconOnlyButton from "../ui/IconOnlyButton";
|
||||||
import Block from "./Block";
|
import Block from "./Block";
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
defaultTitle?: string;
|
note? : INote;
|
||||||
defaultBlocks?: IBlock[];
|
blocks? : IBlock[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBlocks = [
|
const defaultNoteId = uuidv4();
|
||||||
|
|
||||||
|
const defaultNote: INote = {
|
||||||
|
id: defaultNoteId,
|
||||||
|
title: "Untitled",
|
||||||
|
creationTime: new Date(),
|
||||||
|
lastEdited: new Date(),
|
||||||
|
authorId: uuidv4(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultBlocks: IBlock[] = [
|
||||||
{
|
{
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
tag: "",
|
tag: "",
|
||||||
locked: false,
|
order: 1,
|
||||||
lines: Array.from({ length: 4 }, () => ({
|
isLocked: false,
|
||||||
id: uuidv4(),
|
noteId: defaultNoteId,
|
||||||
text: "",
|
lines: ["", "", "", ""],
|
||||||
})),
|
},
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Editor(props: EditorProps) {
|
export default function Editor({ note = defaultNote, blocks = defaultBlocks }: EditorProps) {
|
||||||
const [state, dispatch] = useReducer(editorReducer, {
|
const [title, setTitle] = useState(note.title);
|
||||||
title: props.defaultTitle || "Untitled",
|
|
||||||
blocks: props.defaultBlocks || defaultBlocks,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpdateTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleUpdateTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
dispatch({ type: "update_title", title: e.target.value });
|
setTitle(e.target.value);
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddBlock = () => {
|
|
||||||
dispatch({ type: "add_block" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
dispatch({ type: "copy" });
|
let copyString = "";
|
||||||
|
|
||||||
|
blocks.forEach((block) => {
|
||||||
|
if (block.tag !== "") {
|
||||||
|
copyString += `[${block.tag}]`;
|
||||||
|
}
|
||||||
|
copyString += block.lines + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(copyString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center px-4 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={state.title} onChange={handleUpdateTitle} />
|
<input
|
||||||
{state.blocks.map((block) => <Block key={block.id} block={block} dispatch={dispatch} /> )}
|
className="font-bold text-xl w-full text-center focus:outline-none"
|
||||||
|
value={title}
|
||||||
|
onChange={handleUpdateTitle}
|
||||||
|
/>
|
||||||
|
{blocks.map((block) => <Block key={block.id} block={block} /> )}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<IconOnlyButton onClick={handleAddBlock} icon={<Plus size={24} />} />
|
<IconOnlyButton onClick={() => createBlock(note)} icon={<Plus size={24} />} />
|
||||||
<IconOnlyButton onClick={handleCopy} icon={<Copy size={24} />} title="Copy note to clipboard" />
|
<IconOnlyButton onClick={handleCopy} icon={<Copy size={24} />} title="Copy note to clipboard" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { notesTable } from "@/lib/db/schema";
|
import { INote } from "@/lib/db/schema";
|
||||||
import { deleteNote } from "@/lib/notes";
|
import { deleteNote } from "@/app/actions/notes";
|
||||||
import IconOnlyButton from "./IconOnlyButton";
|
import IconOnlyButton from "./IconOnlyButton";
|
||||||
|
|
||||||
function makeTimestamp(date: Date) {
|
function makeTimestamp(date: Date) {
|
||||||
@ -14,21 +13,20 @@ function makeTimestamp(date: Date) {
|
|||||||
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteCard({ note }: { note: typeof notesTable.$inferSelect }) {
|
export default function NoteCard({ note }: { note: INote }) {
|
||||||
const handleDeleteNote = async () => {
|
const handleDelete = async () => {
|
||||||
"use server";
|
"use server";
|
||||||
await deleteNote(note.id);
|
await deleteNote(note.id);
|
||||||
revalidatePath("/notes");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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.name}</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.lastEdited)}</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.creationTime)}</i>
|
||||||
</Link>
|
</Link>
|
||||||
<IconOnlyButton icon={<X size={24} />} onClick={handleDeleteNote} />
|
<IconOnlyButton icon={<X size={24} />} onClick={handleDelete} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { pgTable, json, varchar, timestamp, uuid } from "drizzle-orm/pg-core";
|
import { pgTable, varchar, timestamp, uuid, boolean, integer, json } from "drizzle-orm/pg-core";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
|
|
||||||
export const usersTable = pgTable("users", {
|
export const usersTable = pgTable("users", {
|
||||||
@ -13,10 +13,10 @@ export const usersRelations = relations(usersTable, ({ many }) => ({
|
|||||||
|
|
||||||
export const notesTable = pgTable("notes", {
|
export const notesTable = pgTable("notes", {
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
name: 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(),
|
authorId: uuid().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const notesRelations = relations(notesTable, ({ one }) => ({
|
export const notesRelations = relations(notesTable, ({ one }) => ({
|
||||||
@ -29,8 +29,10 @@ export const notesRelations = relations(notesTable, ({ one }) => ({
|
|||||||
export const blocksTable = pgTable("blocks", {
|
export const blocksTable = pgTable("blocks", {
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
tag: varchar({ length: 100 }).notNull().default(""),
|
tag: varchar({ length: 100 }).notNull().default(""),
|
||||||
lines: json().notNull().default([]),
|
lines: json().notNull().default(["", "", "", ""]).$type<string[]>(),
|
||||||
noteId: uuid(),
|
isLocked: boolean().notNull().default(false),
|
||||||
|
order: integer().notNull().default(1),
|
||||||
|
noteId: uuid().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
||||||
@ -39,3 +41,7 @@ export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
|||||||
references: [notesTable.id],
|
references: [notesTable.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export type IUser = typeof usersTable.$inferSelect;
|
||||||
|
export type INote = typeof notesTable.$inferSelect;
|
||||||
|
export type IBlock = typeof blocksTable.$inferSelect;
|
||||||
|
@ -1,192 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
export type ILine = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IBlock = {
|
|
||||||
id: string;
|
|
||||||
tag: string;
|
|
||||||
locked: boolean;
|
|
||||||
lines: ILine[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type EditorState = {
|
|
||||||
title: string;
|
|
||||||
blocks: IBlock[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Action =
|
|
||||||
| { type: "add_block" }
|
|
||||||
| { type: "copy" }
|
|
||||||
| { type: "delete_block", blockId: string }
|
|
||||||
| { type: "add_line", blockId: string }
|
|
||||||
| { type: "delete_line", blockId: string }
|
|
||||||
| { type: "update_title", title: string }
|
|
||||||
| { type: "update_line_text"; blockId: string; lineId: string; text: string }
|
|
||||||
| { type: "update_tag"; blockId: string; tag: string }
|
|
||||||
| { type: "move_block_up", blockId: string }
|
|
||||||
| { type: "move_block_down", blockId: string }
|
|
||||||
| { type: "toggle_lock", blockId: string };
|
|
||||||
|
|
||||||
export function editorReducer(state: EditorState, action: Action): EditorState {
|
|
||||||
switch (action.type) {
|
|
||||||
case "update_title":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
title: action.title,
|
|
||||||
};
|
|
||||||
|
|
||||||
case "add_block":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: [
|
|
||||||
...state.blocks,
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
tag: "",
|
|
||||||
locked: false,
|
|
||||||
lines: Array.from({ length: 4 }, () => ({
|
|
||||||
id: uuidv4(),
|
|
||||||
text: "",
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
case "copy":
|
|
||||||
let copyText = "";
|
|
||||||
|
|
||||||
state.blocks.forEach((block) => {
|
|
||||||
if (block.tag !== "") {
|
|
||||||
copyText += `[${block.tag}]\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
block.lines.forEach((line) => {
|
|
||||||
if (line.text !== "") {
|
|
||||||
copyText += line.text + "\n";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
copyText += "\n";
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(copyText);
|
|
||||||
return state;
|
|
||||||
|
|
||||||
case "delete_block":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.filter((block) => block.id !== action.blockId),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "add_line":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.map((block) => {
|
|
||||||
if (block.id === action.blockId) {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
lines: [...block.lines, { id: uuidv4(), text: "" }],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "delete_line":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.map((block) => {
|
|
||||||
if (block.id === action.blockId && block.lines.length > 0) {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
lines: block.lines.slice(0, -1),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "update_line_text":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.map((block) => {
|
|
||||||
if (block.id === action.blockId) {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
lines: block.lines.map((line) => {
|
|
||||||
if (line.id === action.lineId) {
|
|
||||||
return {
|
|
||||||
...line,
|
|
||||||
text: action.text,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "update_tag":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.map((block) => {
|
|
||||||
if (block.id === action.blockId) {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
tag: action.tag,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "move_block_up": {
|
|
||||||
const index = state.blocks.findIndex((b) => b.id === action.blockId);
|
|
||||||
if (index <= 0) return state;
|
|
||||||
const newBlocks = [...state.blocks];
|
|
||||||
[newBlocks[index - 1], newBlocks[index]] = [newBlocks[index], newBlocks[index - 1]];
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: newBlocks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "move_block_down": {
|
|
||||||
const index = state.blocks.findIndex((b) => b.id === action.blockId);
|
|
||||||
if (index === state.blocks.length - 1) return state;
|
|
||||||
const newBlocks = [...state.blocks];
|
|
||||||
[newBlocks[index], newBlocks[index + 1]] = [newBlocks[index + 1], newBlocks[index]];
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: newBlocks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "toggle_lock":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
blocks: state.blocks.map((block) => {
|
|
||||||
if (block.id === action.blockId) {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
locked: !block.locked,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
9
src/lib/hooks/useDebounce.ts
Normal file
9
src/lib/hooks/useDebounce.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function useDebounce(callback: () => void, deps: any[]) {
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(callback, 1000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, deps);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user