Skip to content

Instantly share code, notes, and snippets.

@ginpei
Created July 27, 2023 02:19
Show Gist options
  • Save ginpei/4a276a9e00601d0c76f238d2bfc05079 to your computer and use it in GitHub Desktop.
Save ginpei/4a276a9e00601d0c76f238d2bfc05079 to your computer and use it in GitHub Desktop.
import type { NextPage } from "next";
import { useState } from "react";
import { HStack } from "../../src/lib/layout/HStack";
import { VStack } from "../../src/lib/layout/VStack";
import { Button } from "../../src/lib/style/Button";
import { H1 } from "../../src/lib/style/H1";
import { H2 } from "../../src/lib/style/H2";
interface Task {
done: boolean;
id: string;
title: string;
}
interface History<T extends TaskAddActionInput, S extends "undo" | "redo"> {
action: string;
id: string;
input: T[S];
}
interface TaskState {
tasks: Task[];
}
interface TaskAddActionInput {
initial: {
title: string;
};
undo: {
taskId: string;
};
redo: {
index: number;
task: Task;
};
}
type TaskAction = [
(
state: TaskState,
input: TaskAddActionInput["initial"],
) => {
state: TaskState;
output: TaskAddActionInput["undo"];
},
(
state: TaskState,
input: TaskAddActionInput["undo"],
) => {
state: TaskState;
output: TaskAddActionInput["redo"];
},
(
state: TaskState,
input: TaskAddActionInput["redo"],
) => {
state: TaskState;
output: TaskAddActionInput["undo"];
},
];
function toTaskAction(action: TaskAction): TaskAction {
return action;
}
const taskActions = {
add: toTaskAction([
(state, input) => {
const task = {
done: false,
id: crypto.randomUUID(),
title: input.title,
};
const newState = {
tasks: [task, ...state.tasks],
};
return {
state: newState,
output: {
taskId: task.id,
},
};
},
(state, input) => {
const index = state.tasks.findIndex((task) => task.id === input.taskId);
if (index === -1) {
throw new Error(`Task not found: ${input.taskId}`);
}
const newTasks = [...state.tasks];
const [removedTask] = newTasks.splice(index, 1);
const newState: TaskState = {
...state,
tasks: newTasks,
};
if (!removedTask) {
throw new Error(`Task not found: ${input.taskId}`);
}
return {
state: newState,
output: {
index,
task: removedTask,
},
};
},
(state, input) => {
const newTasks = [...state.tasks];
newTasks.splice(input.index, 0, input.task);
const newState: TaskState = {
...state,
tasks: newTasks,
};
return {
state: newState,
output: {
taskId: input.task.id,
} satisfies TaskAddActionInput["undo"],
};
},
]),
};
const Home: NextPage = () => {
return <HistoryPage />;
};
export default Home;
function HistoryPage(): JSX.Element {
const [tasks, setTasks] = useState<Task[]>([
{
done: false,
id: "1",
title: "Task 1",
},
{
done: false,
id: "2",
title: "Task 2",
},
]);
const [history, setHistory] = useState<History<TaskAddActionInput, "undo">[]>(
[],
);
const [redoHistory, setRedoHistory] = useState<
History<TaskAddActionInput, "redo">[]
>([]);
function onUndoClick() {
const [lastHistory, ...restHistory] = history;
if (!lastHistory) {
return;
}
const { state, output } = taskActions.add[1]({ tasks }, lastHistory.input);
setTasks(state.tasks);
setHistory(restHistory);
setRedoHistory([
{
action: "add",
id: crypto.randomUUID(),
input: output,
},
...redoHistory,
]);
}
function onRedoClick() {
const [lastRedoHistory, ...restRedoHistory] = redoHistory;
if (!lastRedoHistory) {
return;
}
const { state, output } = taskActions.add[2](
{ tasks },
lastRedoHistory.input,
);
setTasks(state.tasks);
setHistory([
{
action: "add",
id: crypto.randomUUID(),
input: output,
},
...history,
]);
setRedoHistory(restRedoHistory);
}
return (
<div className="m-4">
<VStack>
<H1>History system</H1>
<div className="flex gap-8 [&>*]:w-1/2">
<VStack>
<H2>Tasks</H2>
<HStack>
<Button
onClick={() => {
const title = window.prompt("Task title");
if (title === null) {
return;
}
const { state, output } = taskActions.add[0](
{ tasks },
{ title },
);
setTasks(state.tasks);
setHistory([
{
action: "add",
id: crypto.randomUUID(),
input: { taskId: output.taskId },
},
...history,
]);
setRedoHistory([]);
}}
>
Add task...
</Button>
<Button onClick={() => console.log(tasks)}>Log</Button>
</HStack>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label className="flex gap-1 items-baseline hover:underline">
<input
checked={task.done}
onChange={(event) => {
setTasks(
tasks.map((t) =>
t.id === task.id
? { ...t, done: event.target.checked }
: t,
),
);
}}
type="checkbox"
/>
{task.title}
<small>({task.id})</small>
</label>
</li>
))}
</ul>
</VStack>
<VStack>
<H2>History</H2>
<HStack>
<Button disabled={history.length < 1} onClick={onUndoClick}>
← Undo
</Button>
<Button disabled={redoHistory.length < 1} onClick={onRedoClick}>
Redo →
</Button>
</HStack>
<ul className="[&>*]:border-t">
{[...history].reverse().map((history) => (
<li key={history.id}>
{history.action} {JSON.stringify(history.input)}
</li>
))}
{redoHistory.map((history) => (
<li className="text-gray-500" key={history.id}>
{history.action} {JSON.stringify(history.input)}
</li>
))}
</ul>
</VStack>
</div>
</VStack>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment