JSFiddle - React, Tailwind, and code Playground

by SpAm

HTML

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="container">
  <div class="row">
    <div class="col-sm-12" style="text-align:center;">
      <button class="btn btn-default btn-sm" type="button" data-bind="click:addExtraRow">Add extra row</button>
    </div>
  </div>
  <div class="row">
    <div class="col-sm-12">
      <table class="table table-bordered table-consensed" style="width:auto;margin:0px auto">
        <thead>
          <tr class="bg-primary">
            <th>Start time</th>
            <th>End time</th>
            <th>Overtime</th>
          </tr>
        </thead>
        <tbody data-bind="foreach:entries">
          <tr>
            <td style="width:6em;">
              <input type="text" class="form-control input-sm timeField" data-bind="textInput:start, css:{'alert-success':startMoment}" maxlength="5"/>
            </td>
            <td style="width:6em;">
              <input type="text" class="form-control input-sm timeField" data-bind="textInput:end, css:{'alert-success':endMoment}" maxlength="5"/>
            </td>
            <td style="width:14em;" data-bind="text:total"></td>
          </tr>
        </tbody>
        <tbody>
          <tr class="bg-success">
            <td colspan="2">Overall Total</td>
            <td data-bind="text:overallTotal"></td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

CSS

.row {
  margin-top:10px;
}

JavaScript

function OverTimeCalculator(){
	var self = this;
  self.entries = ko.observableArray([]);
  
  for(var i=0;i<3;i++) {
  	self.entries.push(new OverTimeEntry());
  }
  
  self.overallTotalMinutes = ko.computed(function(){
  	var entries = self.entries().reduce(function(total, entry){
    	total+=entry.totalMinutes();
      return total;
    }, 0);
    
    return entries;
  });
  
  self.overallTotal = ko.computed(function(){
  	var tot = self.overallTotalMinutes();
    if( tot<60 ) {
    	return tot + " mins";
    }
  	return Math.floor((tot/60)) + " hour(s) " + ( tot%60 ) + " mins";
  });
  
  self.addExtraRow = function(){
  	self.entries.push(new OverTimeEntry());
  }
  
  ko.applyBindings(self);
}

$(document).on('keyup', '.timeField', function (e) {
  var time = $.trim($(this).val());

  if (/^[0-9]:[0-5][0-9]$/.test(time))
    time = "0" + time;

  if (/^([01][0-9]|2[0-3]):?[0-5][0-9]$/.test(time))
    time = time.indexOf(":") === -1 ? (time.replace(/(\S{2})/, "$1:")) : time;

  $(this).val(time);
  $(this).change();
});


function OverTimeEntry(){
	var self = this;
  self.start = ko.observable("");
  self.end = ko.observable("");
  
  self.startMoment = ko.computed(function(){
  	var start = self.start();
    return validateTime(start);
  });
  self.endMoment = ko.computed(function(){
  	var end = self.end();
    return validateTime(end);
  });
  self.totalMinutes = ko.computed(function(){
  	if( self.startMoment()&&self.endMoment() ) {
    	return calcTotal(self.startMoment(), self.endMoment());
    }
    return 0;
  });
  
  self.total = ko.computed(function(){
  	var tot = self.totalMinutes();
    if( tot<60 ) {
    	return tot + " mins";
    }
  	return Math.floor((tot/60)) + " hour(s) " + ( tot%60 ) + " mins";
  });
  
  
  function validateTime(time){
    if (time != "") {
      return moment(time, 'HH:mm', true).isValid() ? moment(time, 'HH:mm', true) : null;
    }
    return null;
  }
  
  function calcTotal(start, end){
  	var totalMinutes = 0;
  ...