feat: implement editor state management with useReducer
This commit is contained in:
parent
c0499a5c7a
commit
c353f48259
@ -1,22 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Plus } from "lucide-react";
|
||||
import { ChangeEvent } from "react";
|
||||
import LineInputGroup from "@/components/LineInputGroup";
|
||||
import IconOnlyButton from "@/components/IconOnlyButton";
|
||||
import Editor from "@/components/Editor";
|
||||
|
||||
export default function Home() {
|
||||
const saveChanges = (e: ChangeEvent) => {
|
||||
// TODO
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<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" onChange={saveChanges} defaultValue="Song" />
|
||||
<LineInputGroup size={4} onChange={saveChanges} />
|
||||
<IconOnlyButton icon={<Plus size={24} />} />
|
||||
</div>
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
73
src/components/Block.tsx
Normal file
73
src/components/Block.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { Lock, LockOpen, Menu, Minus, Plus, X } from "lucide-react";
|
||||
import { Action, IBlock, ILine } from "@/lib/editorReducer";
|
||||
import LineInput from "./LineInput";
|
||||
import IconOnlyButton from "./IconOnlyButton";
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
export default function Block({
|
||||
block,
|
||||
dispatch,
|
||||
}: {
|
||||
block: IBlock;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}) {
|
||||
const handleAddLine = () => {
|
||||
dispatch({ type: "add_line", blockId: block.id });
|
||||
}
|
||||
|
||||
const handleDeleteLine = () => {
|
||||
dispatch({ type: "delete_line", blockId: block.id });
|
||||
}
|
||||
|
||||
const handleTagUpdate = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch({ type: "update_tag", blockId: block.id, tag: e.target.value });
|
||||
}
|
||||
|
||||
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 handleToggleLock = () => {
|
||||
dispatch({ type: "toggle_lock", blockId: block.id });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border-2 border-neutral-800 rounded-lg p-3 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter tag..."
|
||||
className="w-full focus:outline-none"
|
||||
value={block.tag}
|
||||
disabled={block.locked}
|
||||
onChange={handleTagUpdate}
|
||||
/>
|
||||
{block.lines.map((line) => (
|
||||
<LineInput
|
||||
key={line.id}
|
||||
value={line.text}
|
||||
disabled={block.locked}
|
||||
onChange={(e) => handleLineUpdate(e, line)}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center mx-2 mt-2">
|
||||
<div className="flex gap-2">
|
||||
<IconOnlyButton onClick={handleAddLine} icon={<Plus size={18} />} />
|
||||
<IconOnlyButton onClick={handleDeleteLine} icon={<Minus 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 icon={<Menu size={18} />} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
33
src/components/Editor.tsx
Normal file
33
src/components/Editor.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useReducer } from "react";
|
||||
import { editorReducer } from "@/lib/editorReducer";
|
||||
import Block from "./Block";
|
||||
import IconOnlyButton from "./IconOnlyButton";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
export default function Editor() {
|
||||
const [state, dispatch] = useReducer(editorReducer, [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
tag: "",
|
||||
locked: false,
|
||||
lines: Array.from({ length: 4 }, () => ({
|
||||
id: crypto.randomUUID(),
|
||||
text: "",
|
||||
})),
|
||||
}
|
||||
]);
|
||||
|
||||
const handleAddBlock = () => {
|
||||
dispatch({ type: "add_block" });
|
||||
}
|
||||
|
||||
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="Song" />
|
||||
{state.map((block) => <Block key={block.id} block={block} dispatch={dispatch} /> )}
|
||||
<IconOnlyButton onClick={handleAddBlock} icon={<Plus size={24} />} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Lock, LockOpen, Menu, X } from "lucide-react";
|
||||
import LineInput from "./LineInput";
|
||||
import IconOnlyButton from "./IconOnlyButton";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
export default function LineInputGroup({
|
||||
size = 4,
|
||||
onChange,
|
||||
}: {
|
||||
size: number;
|
||||
onChange: (e: ChangeEvent) => void;
|
||||
}) {
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
const switchLock = () => setLocked((locked) => !locked);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
<div className="border-2 border-neutral-800 rounded-lg p-3 w-full">
|
||||
<input type="text" placeholder="enter tag..." className="w-full focus:outline-none" disabled={locked} />
|
||||
{[...Array(size)].map((_, i) => <LineInput key={i} onChange={onChange} disabled={locked} />)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<IconOnlyButton icon={<X size={18} />} />
|
||||
<IconOnlyButton alwaysOn={locked} icon={locked ? <Lock size={18} /> : <LockOpen size={18} />} onClick={switchLock} />
|
||||
<IconOnlyButton icon={<Menu size={18} />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
115
src/lib/editorReducer.ts
Normal file
115
src/lib/editorReducer.ts
Normal file
@ -0,0 +1,115 @@
|
||||
export type ILine = {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type IBlock = {
|
||||
id: string;
|
||||
tag: string;
|
||||
locked: boolean;
|
||||
lines: ILine[];
|
||||
};
|
||||
|
||||
type EditorState = IBlock[];
|
||||
|
||||
export type Action =
|
||||
| { type: "add_block" }
|
||||
| { type: "delete_block", blockId: string }
|
||||
| { type: "add_line", blockId: string }
|
||||
| { type: "delete_line", blockId: string }
|
||||
| { type: "update_line_text"; blockId: string; lineId: string; text: string }
|
||||
| { type: "update_tag"; blockId: string; tag: string }
|
||||
| { type: "toggle_lock", blockId: string };
|
||||
|
||||
export function editorReducer(state: EditorState, action: Action): EditorState {
|
||||
switch (action.type) {
|
||||
case "add_block":
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
tag: "",
|
||||
locked: false,
|
||||
lines: Array.from({ length: 4 }, () => ({
|
||||
id: crypto.randomUUID(),
|
||||
text: "",
|
||||
})),
|
||||
}
|
||||
];
|
||||
|
||||
case "delete_block":
|
||||
return state.filter((block) => block.id !== action.blockId);
|
||||
|
||||
case "add_line":
|
||||
return state.map((block) => {
|
||||
if (block.id === action.blockId) {
|
||||
return {
|
||||
...block,
|
||||
lines: [...block.lines, { id: crypto.randomUUID(), text: "" }],
|
||||
};
|
||||
} else {
|
||||
return block;
|
||||
}
|
||||
});
|
||||
|
||||
case "delete_line":
|
||||
return state.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.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.map((block) => {
|
||||
if (block.id === action.blockId) {
|
||||
return {
|
||||
...block,
|
||||
tag: action.tag,
|
||||
};
|
||||
} else {
|
||||
return block;
|
||||
}
|
||||
});
|
||||
|
||||
case "toggle_lock":
|
||||
return state.map((block) => {
|
||||
if (block.id === action.blockId) {
|
||||
return {
|
||||
...block,
|
||||
locked: !block.locked,
|
||||
};
|
||||
} else {
|
||||
return block;
|
||||
}
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user