feat: start editor database integration
This commit is contained in:
parent
2ad220e525
commit
917e460a96
@ -1,23 +1,42 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { blocksTable } from "@/lib/db/schema";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { blocksTable, IBlock, INote } from "@/lib/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function updateBlock({
|
||||
id,
|
||||
tag,
|
||||
lines,
|
||||
}: {
|
||||
id: string;
|
||||
tag?: string;
|
||||
lines?: string[];
|
||||
}) {
|
||||
export async function createBlock(note: INote) {
|
||||
await db
|
||||
.insert(blocksTable)
|
||||
.values({ noteId: note.id });
|
||||
|
||||
revalidatePath("/blocks/[id]");
|
||||
}
|
||||
|
||||
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
|
||||
.update(blocksTable)
|
||||
.set({
|
||||
...(tag !== undefined ? { tag } : {}),
|
||||
...(lines !== undefined ? { lines } : {}),
|
||||
})
|
||||
.where(eq(blocksTable.id, id));
|
||||
.set({ tag: newTag })
|
||||
.where(eq(blocksTable.id, block.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 { notesTable } from "./db/schema";
|
||||
import { getAuth } from "./auth";
|
||||
import { db } from "./db";
|
||||
import { notesTable } from "@/lib/db/schema";
|
||||
import { getAuth } from "@/lib/auth";
|
||||
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) {
|
||||
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) {
|
||||
if (!uuidValidate(noteId)) {
|
||||
return null;
|
||||
@ -57,4 +61,12 @@ export async function deleteNote(noteId: string) {
|
||||
}
|
||||
|
||||
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 { 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";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return { title: note.name };
|
||||
return { title: note.title };
|
||||
}
|
||||
|
||||
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
|
||||
@ -19,10 +20,15 @@ export default async function Note({ params }: { params: Promise<{ id: string }>
|
||||
if (!note) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const blocks = await getBlocks(note);
|
||||
if (!blocks) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Editor defaultTitle={note.name} />
|
||||
<Editor note={note} blocks={blocks} />
|
||||
</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 { redirect } from "next/navigation";
|
||||
import { getNotes } from "@/lib/notes";
|
||||
import { getNotes } from "@/app/actions/notes";
|
||||
import { getAuth } from "@/lib/auth";
|
||||
import NoteCard from "@/components/ui/NoteCard";
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { CircleQuestionMark, List, Plus, UserRound, UserRoundMinus } from "lucide-react";
|
||||
import { createNote } from "@/app/actions/notes";
|
||||
import { logOut } from "@/app/actions/auth";
|
||||
import { getAuth } from "@/lib/auth";
|
||||
import HeaderButton from "./ui/HeaderButton";
|
||||
@ -15,9 +16,7 @@ export default async function Header() {
|
||||
|
||||
{auth && (
|
||||
<div className="flex gap-2">
|
||||
<Link href="/notes/new">
|
||||
<HeaderButton title="new" icon={<Plus size={20} />} />
|
||||
</Link>
|
||||
<HeaderButton onClick={createNote} title="new" icon={<Plus size={20} />} />
|
||||
<Link href="/notes">
|
||||
<HeaderButton title="list" icon={<List size={20} />} />
|
||||
</Link>
|
||||
|
@ -2,47 +2,34 @@
|
||||
|
||||
import { ChangeEvent } from "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 LineInput from "./LineInput";
|
||||
|
||||
export default function Block({
|
||||
block,
|
||||
dispatch,
|
||||
}: {
|
||||
block: IBlock;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}) {
|
||||
export default function Block({ block }: { block: IBlock }) {
|
||||
const handleAddLine = () => {
|
||||
dispatch({ type: "add_line", blockId: block.id });
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleDeleteLine = () => {
|
||||
dispatch({ type: "delete_line", blockId: block.id });
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleTagUpdate = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({ type: "update_tag", blockId: block.id, tag: e.target.value });
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleLineUpdate = (e: ChangeEvent<HTMLInputElement>, line: ILine) => {
|
||||
dispatch({ type: "update_line_text", blockId: block.id, lineId: line.id, text: e.target.value });
|
||||
}
|
||||
|
||||
const handleDeleteBlock = () => {
|
||||
dispatch({ type: "delete_block", blockId: block.id });
|
||||
const handleLineUpdate = (e: ChangeEvent<HTMLInputElement>, line: string) => {
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleBlockUp = () => {
|
||||
dispatch({ type: "move_block_up", blockId: block.id });
|
||||
// TODO
|
||||
}
|
||||
|
||||
const handleBlockDown = () => {
|
||||
dispatch({ type: "move_block_down", blockId: block.id });
|
||||
}
|
||||
|
||||
const handleToggleLock = () => {
|
||||
dispatch({ type: "toggle_lock", blockId: block.id });
|
||||
// TODO
|
||||
}
|
||||
|
||||
return (
|
||||
@ -52,15 +39,15 @@ export default function Block({
|
||||
type="text"
|
||||
placeholder="enter tag..."
|
||||
className="w-full focus:outline-none"
|
||||
value={block.tag}
|
||||
disabled={block.locked}
|
||||
defaultValue={block.tag}
|
||||
disabled={block.isLocked}
|
||||
onChange={handleTagUpdate}
|
||||
/>
|
||||
{block.lines.map((line) => (
|
||||
{block.lines.map((line, i) => (
|
||||
<LineInput
|
||||
key={line.id}
|
||||
value={line.text}
|
||||
disabled={block.locked}
|
||||
key={i}
|
||||
defaultValue={line}
|
||||
disabled={block.isLocked}
|
||||
onChange={(e) => handleLineUpdate(e, line)}
|
||||
/>
|
||||
))}
|
||||
@ -74,8 +61,12 @@ export default function Block({
|
||||
<IconOnlyButton onClick={handleBlockDown} icon={<ArrowDown size={18} />} />
|
||||
</div>
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<IconOnlyButton onClick={handleDeleteBlock} icon={<X size={18} />} />
|
||||
<IconOnlyButton onClick={handleToggleLock} icon={block.locked ? <Lock size={18} /> : <LockOpen size={18} />} alwaysOn={block.locked} />
|
||||
<IconOnlyButton onClick={() => deleteBlock(block)} icon={<X size={18} />} />
|
||||
<IconOnlyButton
|
||||
onClick={() => switchLock(block)}
|
||||
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
|
||||
alwaysOn={block.isLocked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,53 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent, useReducer } from "react";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { Copy, Plus } from "lucide-react";
|
||||
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 Block from "./Block";
|
||||
|
||||
interface EditorProps {
|
||||
defaultTitle?: string;
|
||||
defaultBlocks?: IBlock[];
|
||||
note? : INote;
|
||||
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(),
|
||||
tag: "",
|
||||
locked: false,
|
||||
lines: Array.from({ length: 4 }, () => ({
|
||||
id: uuidv4(),
|
||||
text: "",
|
||||
})),
|
||||
}
|
||||
order: 1,
|
||||
isLocked: false,
|
||||
noteId: defaultNoteId,
|
||||
lines: ["", "", "", ""],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Editor(props: EditorProps) {
|
||||
const [state, dispatch] = useReducer(editorReducer, {
|
||||
title: props.defaultTitle || "Untitled",
|
||||
blocks: props.defaultBlocks || defaultBlocks,
|
||||
});
|
||||
export default function Editor({ note = defaultNote, blocks = defaultBlocks }: EditorProps) {
|
||||
const [title, setTitle] = useState(note.title);
|
||||
|
||||
const handleUpdateTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({ type: "update_title", title: e.target.value });
|
||||
}
|
||||
|
||||
const handleAddBlock = () => {
|
||||
dispatch({ type: "add_block" });
|
||||
setTitle(e.target.value);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col items-center px-4 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} />
|
||||
{state.blocks.map((block) => <Block key={block.id} block={block} dispatch={dispatch} /> )}
|
||||
<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"
|
||||
value={title}
|
||||
onChange={handleUpdateTitle}
|
||||
/>
|
||||
{blocks.map((block) => <Block key={block.id} block={block} /> )}
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { X } from "lucide-react";
|
||||
import { notesTable } from "@/lib/db/schema";
|
||||
import { deleteNote } from "@/lib/notes";
|
||||
import { INote } from "@/lib/db/schema";
|
||||
import { deleteNote } from "@/app/actions/notes";
|
||||
import IconOnlyButton from "./IconOnlyButton";
|
||||
|
||||
function makeTimestamp(date: Date) {
|
||||
@ -14,21 +13,20 @@ function makeTimestamp(date: Date) {
|
||||
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export default function NoteCard({ note }: { note: typeof notesTable.$inferSelect }) {
|
||||
const handleDeleteNote = async () => {
|
||||
export default function NoteCard({ note }: { note: INote }) {
|
||||
const handleDelete = async () => {
|
||||
"use server";
|
||||
await deleteNote(note.id);
|
||||
revalidatePath("/notes");
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">Creation date: {makeTimestamp(note.creationTime)}</i>
|
||||
</Link>
|
||||
<IconOnlyButton icon={<X size={24} />} onClick={handleDeleteNote} />
|
||||
<IconOnlyButton icon={<X size={24} />} onClick={handleDelete} />
|
||||
</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";
|
||||
|
||||
export const usersTable = pgTable("users", {
|
||||
@ -13,10 +13,10 @@ export const usersRelations = relations(usersTable, ({ many }) => ({
|
||||
|
||||
export const notesTable = pgTable("notes", {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
name: varchar({ length: 50 }).notNull().default("Untitled"),
|
||||
title: varchar({ length: 50 }).notNull().default("Untitled"),
|
||||
creationTime: timestamp().notNull().defaultNow(),
|
||||
lastEdited: timestamp().notNull().defaultNow(),
|
||||
authorId: uuid(),
|
||||
authorId: uuid().notNull(),
|
||||
});
|
||||
|
||||
export const notesRelations = relations(notesTable, ({ one }) => ({
|
||||
@ -29,8 +29,10 @@ export const notesRelations = relations(notesTable, ({ one }) => ({
|
||||
export const blocksTable = pgTable("blocks", {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
tag: varchar({ length: 100 }).notNull().default(""),
|
||||
lines: json().notNull().default([]),
|
||||
noteId: uuid(),
|
||||
lines: json().notNull().default(["", "", "", ""]).$type<string[]>(),
|
||||
isLocked: boolean().notNull().default(false),
|
||||
order: integer().notNull().default(1),
|
||||
noteId: uuid().notNull(),
|
||||
});
|
||||
|
||||
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
||||
@ -39,3 +41,7 @@ export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
||||
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