<div id="app"></div>
#app {
background-color: #FAF9F6;
}
h4 {
margin: 0;
text-align: center;
}
h1 {
margin: 1rem auto;
text-align: center;
}
span {
font-weight: 600;
font-size: 24px;
}
.container {
display: flex;
flex-flow: column;
height: 100vh;
.wrapper {
width: 90%;
margin: 0 auto 1rem;
display: flex;
flex-flow: column;
justify-content: center;
}
.counter {
margin-bottom: 4rem;
&__buttons {
display: flex;
width: 80%;
justify-content: space-around;
align-items: center;
margin: 0 auto 1rem auto;
max-width: 400px;
& > button {
border-radius: 8px;
padding: 1rem 4rem;
}
& > button:enabled {
color: white;
background-color: black;
}
}
&__operators {
margin: 0 auto 1rem;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: center;
max-width: 400px;
& > button {
width: 50px;
}
}
}
}
.empty, .history {
border: 1px solid transparent;
border-radius: 4px;
background-color: #F1F3FC;
padding: 1rem;
max-width: 400px;
margin: 0 auto 0;
}
.history {
&__header {
margin-bottom: 1rem;
}
&__item {
letter-spacing: 1px;
display: flex;
&--left {
width: 100px;
margin-left: 1rem;
font-weight: 600;
}
}
}
button, button:focus {
border: 1px solid transparent;
background-color: lightgrey;
border-radius: 5px;
font-weight: 600;
color: black;
padding: 5px 0;
}
button:hover {
cursor: pointer;
opacity: 0.9;
}
button:disabled {
cursor: not-allowed;
opacity: 1;
background-color: #ECECEC;
}
View Compiled
/*
* https://frontendeval.com/questions/undoable-counter
*
* Create a simple counter with undo/redo functionality
*/
// Notes:
// • Button - { text, handler(val) }
// • History - { listOfItems }
// • HistoryItem - { action, before, after }
// • How to store data => stack of object or array tuple
// • Render undo button based on `redoHistory` being empty or not
// • For every undo, push to `redoHistory`
// • If new action, clear redoHistory
// Optimization:
// Use queue vs. stack to avoid the linear operation of `reverse()` on each render
const Button = ({ text, onClick, disabled = false }) => {
return (
<button onClick={ onClick } disabled={ disabled }>{ text }</button>
)
}
const EmptyHistory = () => {
return (
<div className='empty'>
<h4>No history to show</h4>
</div>
)
}
const HistoryItem = ({ action, val, before, after }) => {
return (
<div className='history__item'>
<div className='history__item--left'>{action === 'sub' ? '-' : '+'}{val}</div>
<div className='history__item--right'>({before} → {after})</div>
</div>
)
}
const History = ({ historyItems }) => {
return (
<div className='history'>
<h4 className='history__header'>History</h4>
<div className='history__content'>
<div className='history__content__items'>
{
historyItems.map(({ action, val, before, after }) => (
<HistoryItem
action={ action }
val={ val }
before={ before }
after={ after }
/>
))
}
</div>
</div>
</div>
)
}
const UndoableCounter = () => {
const [total, setTotal] = React.useState(0);
const [history, setHistory] = React.useState([]);
const [redoHistory, setRedoHistory] = React.useState([]);
handleOperator = (action, val) => {
if (redoHistory.length) setRedoHistory([]);
const newHistory = history.slice();
const newTotal = action === 'sub' ? total - val : total + val;
if (newHistory.length === 50) newHistory.shift();
newHistory.push({
action,
val,
before: total,
after: newTotal
});
setTotal(newTotal);
setHistory(newHistory);
}
handleUndo = () => {
if (!history.length) return;
const newHistory = history.slice();
const { action, val, before, after } = newHistory.pop();
const newTotal = action === 'sub' ? total + val : total - val;
const newRedoHistory = redoHistory.slice();
newRedoHistory.push({
action,
val,
before,
after
});
setTotal(newTotal);
setHistory(newHistory);
setRedoHistory(newRedoHistory);
}
handleRedo = () => {
if (!redoHistory.length) return;
const newRedoHistory = redoHistory.slice();
const { action, val, before, after } = newRedoHistory.pop();
const newTotal = action === 'sub' ? total - val : total + val;
const newHistory = history.slice();
newHistory.push({
action,
val,
before,
after
});
setTotal(newTotal);
setHistory(newHistory);
setRedoHistory(newRedoHistory);
}
reverse = (arr) => {
let left = 0;
let right = arr.length - 1;
while (left < right) {
const temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
return arr;
}
return (
<div className='container'>
<div className='wrapper'>
<h1>Undoable Counter 📝</h1>
<div className='counter'>
{/* Undo/Redo buttons */}
<div className='counter__buttons'>
<Button className='button--undo' text={'Undo'} onClick={ handleUndo } />
<Button
className='button--redo'
text={'Redo'}
onClick={ handleRedo }
disabled={ redoHistory.length === 0 }
/>
</div>
{/* Operator Buttons + Value */}
<div className='counter__operators'>
<Button text={'-100'} onClick={ () => handleOperator('sub', 100) } />
<Button text={'-10'} onClick={ () => handleOperator('sub', 10) } />
<Button text={'-1'} onClick={ () => handleOperator('sub', 1) } />
<span>{ total }</span>
<Button text={'+1'} onClick={ () => handleOperator('add', 1) } />
<Button text={'+10'} onClick={ () => handleOperator('add', 10) } />
<Button text={'+100'} onClick={ () => handleOperator('add', 100) } />
</div>
{/* History */}
{
history.length
? <History historyItems={ reverse(history) }/>
: <EmptyHistory />
}
</div>
</div>
</div>
)
}
ReactDOM.render(<UndoableCounter />, document.getElementById('app'));
View Compiled
This Pen doesn't use any external CSS resources.