<div id="app"></div>
.flex-row {
display: flex;
flex-direction: row;
margin-bottom: 0.5rem;
justify-content: center;
}
.flex-row > :not(:last-child) {
margin-right: 1rem;
}
.counter {
text-align: center;
max-width: 30rem;
margin: 0 auto;
}
.counter__value {
min-width: 2rem;
text-align: right;
}
.history__list {
border: solid 1px black;
padding: 0.5rem;
}
/*
* https://frontendeval.com/questions/undoable-counter
*
* Create a simple counter with undo/redo functionality
*/
import React, { useState, useEffect } from "https://esm.sh/react@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
import * as TypesReact from "https://cdn.skypack.dev/@types/react";
import * as TypesReactDom from "https://cdn.skypack.dev/@types/react-dom";
const plusOrMinus = (number: number): string =>
number >= 0 ? "+" + number.toString() : number.toString();
interface HistoryProps {
history: number[];
currentCount: number;
}
const History = ({ history, currentCount }: HistoryProps) => {
let count = currentCount;
return (
<div class="history">
<h2>History</h2>
<ul class="history__list">
{[...history].reverse().map((h) => {
const output = count;
count -= h;
const change = `${count} -> ${output}`;
return (
<li className="flex-row">
<span>{plusOrMinus(h)}</span>
<span>{change}</span>
</li>
);
})}
</ul>
</div>
);
};
interface CounterAdjustProps {
number: number;
handler: (n: number) => void;
}
const CounterAdjust = ({ number, handler }: CounterAdjustProps) => {
return <button onClick={() => handler(number)}>{plusOrMinus(number)}</button>;
};
const Counter = () => {
const [count, setCount] = useState(0);
const [history, setHistory] = useState<number[]>([]);
const [undone, setUndone] = useState<number[]>([]);
const adjust = (n: number): void => {
setHistory((h) => [...h, n].slice(-50));
setCount((last) => last + n);
};
const handleAdjust = (n: number): void => {
adjust(n);
setUndone([]);
};
const undo = (): void => {
const n = history[history.length - 1];
if (n === undefined) return;
setCount((last) => last - n);
setHistory((h) => h.slice(0, -1));
setUndone((u) => [...u, n]);
};
const redo = (): void => {
const n = undone[undone.length - 1];
adjust(n);
setUndone((u) => u.slice(0, -1));
};
// load saved state
useEffect(() => {
const state = window.localStorage.getItem("state");
if (!state) return;
const { count, history, undone } = JSON.parse(state);
setCount(count);
setHistory(history);
setUndone(undone);
}, []);
// save state on change
useEffect(() => {
const state = JSON.stringify({ count, history, undone });
window.localStorage.setItem("state", state);
}, [count, history, undone]);
return (
<div className="counter">
<h1>Undoable counter</h1>
<div className="flex-row">
<button onClick={undo}>Undo</button>
<button onClick={redo} disabled={undone.length === 0}>
Redo
</button>
</div>
<div className="flex-row">
<CounterAdjust number={-100} handler={handleAdjust} />
<CounterAdjust number={-10} handler={handleAdjust} />
<CounterAdjust number={-1} handler={handleAdjust} />
<span className="counter__value">{count}</span>
<CounterAdjust number={1} handler={handleAdjust} />
<CounterAdjust number={10} handler={handleAdjust} />
<CounterAdjust number={100} handler={handleAdjust} />
</div>
{history.length ? (
<History history={history} currentCount={count} />
) : null}
</div>
);
};
const App = () => {
return <Counter />;
};
ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.