<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
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js