feat: add block movement

This commit is contained in:
Kirill Siukhin 2025-07-11 00:01:25 +05:00
parent 3b51161203
commit 8b1de14f79
4 changed files with 61 additions and 20 deletions

View File

@ -1,6 +1,7 @@
import { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getNote } from "@/lib/notes"; import { getNote } from "@/lib/notes";
import { Metadata } from "next"; 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;
@ -20,8 +21,8 @@ export default async function Note({ params }: { params: Promise<{ id: string }>
} }
return ( return (
<div> <div className="flex justify-center">
{note.name} <Editor defaultTitle={note.name} />
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { ChangeEvent } from "react"; import { ChangeEvent } from "react";
import { Lock, LockOpen, Menu, 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 { Action, IBlock, ILine } from "@/lib/editorReducer";
import IconOnlyButton from "../ui/IconOnlyButton"; import IconOnlyButton from "../ui/IconOnlyButton";
import LineInput from "./LineInput"; import LineInput from "./LineInput";
@ -33,6 +33,14 @@ export default function Block({
dispatch({ type: "delete_block", blockId: block.id }); dispatch({ type: "delete_block", blockId: block.id });
} }
const handleBlockUp = () => {
dispatch({ type: "move_block_up", blockId: block.id });
}
const handleBlockDown = () => {
dispatch({ type: "move_block_down", blockId: block.id });
}
const handleToggleLock = () => { const handleToggleLock = () => {
dispatch({ type: "toggle_lock", blockId: block.id }); dispatch({ type: "toggle_lock", blockId: block.id });
} }
@ -57,14 +65,17 @@ export default function Block({
/> />
))} ))}
<div className="flex items-center mx-2 mt-2"> <div className="flex items-center mx-2 mt-2">
<div className="flex gap-2"> <div className="flex gap-1 mr-4">
<IconOnlyButton onClick={handleAddLine} icon={<Plus size={18} />} /> <IconOnlyButton onClick={handleAddLine} icon={<Plus size={18} />} />
<IconOnlyButton onClick={handleDeleteLine} icon={<Minus size={18} />} /> <IconOnlyButton onClick={handleDeleteLine} icon={<Minus size={18} />} />
</div> </div>
<div className="flex gap-1">
<IconOnlyButton onClick={handleBlockUp} icon={<ArrowUp size={18} />} />
<IconOnlyButton onClick={handleBlockDown} icon={<ArrowDown size={18} />} />
</div>
<div className="flex gap-2 ml-auto"> <div className="flex gap-2 ml-auto">
<IconOnlyButton onClick={handleDeleteBlock} icon={<X size={18} />} /> <IconOnlyButton onClick={handleDeleteBlock} icon={<X size={18} />} />
<IconOnlyButton onClick={handleToggleLock} icon={block.locked ? <Lock size={18} /> : <LockOpen size={18} />} alwaysOn={block.locked} /> <IconOnlyButton onClick={handleToggleLock} icon={block.locked ? <Lock size={18} /> : <LockOpen size={18} />} alwaysOn={block.locked} />
<IconOnlyButton icon={<Menu size={18} />} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,22 +3,29 @@
import { useReducer } from "react"; import { useReducer } 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 } from "@/lib/editorReducer"; import { editorReducer, IBlock } from "@/lib/editorReducer";
import IconOnlyButton from "../ui/IconOnlyButton"; import IconOnlyButton from "../ui/IconOnlyButton";
import Block from "./Block"; import Block from "./Block";
export default function Editor() { interface EditorProps {
const [state, dispatch] = useReducer(editorReducer, [ defaultTitle?: string;
{ defaultBlocks?: IBlock[];
}
const defaultBlocks = [
{
id: uuidv4(),
tag: "",
locked: false,
lines: Array.from({ length: 4 }, () => ({
id: uuidv4(), id: uuidv4(),
tag: "", text: "",
locked: false, })),
lines: Array.from({ length: 4 }, () => ({ }
id: uuidv4(), ];
text: "",
})), export default function Editor(props: EditorProps) {
} const [state, dispatch] = useReducer(editorReducer, props.defaultBlocks || defaultBlocks);
]);
const handleAddBlock = () => { const handleAddBlock = () => {
dispatch({ type: "add_block" }); dispatch({ type: "add_block" });
@ -30,7 +37,7 @@ export default function Editor() {
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 px-4 max-w-2xl w-full gap-4">
<input className="font-bold text-xl w-full text-center focus:outline-none" defaultValue="Untitled" /> <input className="font-bold text-xl w-full text-center focus:outline-none" defaultValue={props.defaultTitle || "Untitled"} />
{state.map((block) => <Block key={block.id} block={block} dispatch={dispatch} /> )} {state.map((block) => <Block key={block.id} block={block} dispatch={dispatch} /> )}
<div className="flex gap-2"> <div className="flex gap-2">
<IconOnlyButton onClick={handleAddBlock} icon={<Plus size={24} />} /> <IconOnlyButton onClick={handleAddBlock} icon={<Plus size={24} />} />

View File

@ -22,6 +22,8 @@ export type Action =
| { type: "delete_line", blockId: string } | { type: "delete_line", blockId: string }
| { type: "update_line_text"; blockId: string; lineId: string; text: string } | { type: "update_line_text"; blockId: string; lineId: string; text: string }
| { type: "update_tag"; blockId: string; tag: 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 }; | { type: "toggle_lock", blockId: string };
export function editorReducer(state: EditorState, action: Action): EditorState { export function editorReducer(state: EditorState, action: Action): EditorState {
@ -48,7 +50,11 @@ export function editorReducer(state: EditorState, action: Action): EditorState {
copyText += `[${block.tag}]\n`; copyText += `[${block.tag}]\n`;
} }
block.lines.forEach((line) => copyText += line.text + "\n") block.lines.forEach((line) => {
if (line.text !== "") {
copyText += line.text + "\n";
}
})
copyText += "\n"; copyText += "\n";
}); });
@ -115,6 +121,22 @@ export function editorReducer(state: EditorState, action: Action): EditorState {
} }
}); });
case "move_block_up": {
const index = state.findIndex((b) => b.id === action.blockId);
if (index <= 0) return state;
const newState = [...state];
[newState[index - 1], newState[index]] = [newState[index], newState[index - 1]];
return newState;
}
case "move_block_down": {
const index = state.findIndex((b) => b.id === action.blockId);
if (index === state.length - 1) return state;
const newState = [...state];
[newState[index], newState[index + 1]] = [newState[index + 1], newState[index]];
return newState;
}
case "toggle_lock": case "toggle_lock":
return state.map((block) => { return state.map((block) => {
if (block.id === action.blockId) { if (block.id === action.blockId) {