Simple Address Client

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Simple Address App</title>
    <meta charset=utf-8 />
    <meta name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
      crossorigin="anonymous">
    <style>
      #gmap {
        width:100%;
        height:400px;
        border:thin solid black;
      }
      #error {
        display:none;
      }
    </style>
  </head>
  <body>
    <div class='container-fluid'>
      <div class="alert alert-danger" id="error"></div>
      <h1>Simple Address App</h1>
      <div class="row">
        <div class="col-4">
          <button type="button" class="btn btn-primary" style="float:right"
            data-toggle="modal" data-target="#form_modal" data-whatever="New Address"
            id="new_address">
            New Address
          </button>
          <h3>Current addresses</h3>
          <ul class="list-group" id="list_addresses">
          </ul>
        </div>
        <div class="col-8">
          <div id="gmap">
          </div>
        </div>
      </div>
    </div>
    <div class="modal fade" id="form_modal" tabindex="-1" role="dialog" 
      aria-labeledby="address_title" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="address_title">Address</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <form>
              <div class="form-group">
                <label for="street">Street</label>
                <input type="text" class="form-control" id="street" name="street"
                      placeholder="Street">
              </div>
              <div class="form-group">
                <label for="city">City</label>
                <input type="text" class="form-control" id="city" name="city"
                      placeholder="City">
              </div>
              <div class="form-group">
                <label for="state">State</label>
                <input type="text" class="form-control" id="state" name="state"
                      placeholder="CA">
              </div>
              <div class="form-group">
                <label for="zip">Zip</label>
                <input type="text" class="form-control" id="zip" name="zip"
                      placeholder="Zip">
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">
              Cancel
            </button>
            <button type="button" class="btn btn-primary" data-dismiss="modal"
              id="save_address">
              Save
            </button>
          </div>
        </div>
      </div>
    </div>
    <script 
      src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
      integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
      crossorigin="anonymous"></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
      integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
      crossorigin="anonymous"></script>
    <script
      src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
      integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
      crossorigin="anonymous"></script>
    <script async defer
      src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBhJVCtTHcyNwA_d28370UMKIpogQTQasE&callback=initMap"
      type="text/javascript">
    </script>

    <!-- webpack bundling of multiple javascript files -->
    <script src="app.bundle.js"></script>

  </body>
</html>
<!--
25800 Carlos Bee Blvd, Hayward, CA 94542
25555 Hesperian Blvd, Hayward, CA 94545
43600 Mission Blvd, Fremont, CA 94539
21250 Stevens Creek Blvd, Cupertino, CA 95014
3095 Yerba Buena Rd, San Jose, CA 95135
3000 Mission College Blvd, Santa Clara, CA 95054
450 Serra Mall, Stanford, CA 94305
2100 Moorpark Ave, San Jose, CA 95128
1 Washington Sq, San Jose, CA 95192
-->
import Conf from '../address_app.conf.json';
  // webpack import

export default class AjaxAddresses {
  constructor( output_error = console.log) {
    this.output_error = output_error;   // output_error("error text")
  }

  done_get_all ( done_handler ) {
    $.ajax({
        type: "GET",
        url: `http://${Conf.host}:${Conf.port}/api/addresses`,
        error: this.reject.bind(this),
    })
      .done ( done_handler );
  }

  done_get_one(id, done_handler) {
    $.ajax({
        type: "GET",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        error: this.reject.bind(this),
    })
      .done ( done_handler );
  }

  done_delete (id, done_handler) {
    $.ajax({
        type: "DELETE",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        xhrFields: {
          withCredentials: true
        },
        error: this.reject.bind(this),
    })
      .done( done_handler );
  }

  done_post (addr_data, done_handler) {
    $.ajax({
        type: "POST",
        url: `http://${Conf.host}:${Conf.port}/api/addresses`,
        xhrFields: {
          withCredentials: true
        },
        data : JSON.stringify(addr_data),
        headers: {
          "Content-Type" : "application/json",
        },
        dataType: "json",
        error: this.reject.bind(this),
    })
      .done( done_handler );
  }

  done_put (address, done_handler) {
    let id = address["id"];
    delete address["id"];
    $.ajax({
        type: "PUT",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        xhrFields: {
          withCredentials: true
        },
        data: JSON.stringify(address),
        headers: {
          "Content-Type" : "application/json",
        },
        dataType: "json",
        error: this.reject.bind(this),
    })
      .done( done_handler );
  }

  promise_get_all () {
    return new Promise( (resolve) => {
      $.ajax({
        type: "GET",
        url: `http://${Conf.host}:${Conf.port}/api/addresses`,
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }
  promise_get_one (id) {
    return new Promise( (resolve) => {
      $.ajax({
        type: "GET",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }
  promise_delete (id) {
    return new Promise ( (resolve) => {
      $.ajax({
        type: "DELETE",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        xhrFields: {
          withCredentials: true
        },
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }
  promise_post (addr_data) {
    return new Promise ( (resolve) => {
      $.ajax({
        type: "POST",
        url: `http://${Conf.host}:${Conf.port}/api/addresses`,
        xhrFields: {
          withCredentials: true
        },
        data : JSON.stringify(addr_data),
        headers: {
          "Content-Type" : "application/json",
        },
        dataType: "json",
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }
  promise_put (address) {
    var id = address["id"];
    delete address["id"];
    return new Promise ( (resolve) => {
      $.ajax({
        type: "PUT",
        url: `http://${Conf.host}:${Conf.port}/api/addresses/${id}`,
        xhrFields: {
          withCredentials: true
        },
        data: JSON.stringify(address),
        headers: {
          "Content-Type" : "application/json",
        },
        dataType: "json",
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }

  promise_geocode (address) {
    var street = encodeURI(address["street"]);
    var city   = encodeURI(address["city"]);
    var state  = encodeURI(address["state"]);
    var zip    = encodeURI(address["zip"]);
    return new Promise ( (resolve) => {
      $.ajax({
        type: "GET",
        url: `http://${Conf.host}:${Conf.port}/api/geocode?street=${street}&city=${city}&state=${state}&zip=${zip}`,
        success: resolve,
        error: this.reject.bind(this),
      });
    });
  }

  // error function for failed AJAX call
  reject (xhr, textStatus, errorThrown) {
    let error_out = "";

    if (xhr.readyState == 0) {
      error_out = `Could not connect to ${Conf.host}:${Conf.port}`;
    }
    else {  // generic error
      error_out = `ajax failed: ${textStatus}, ${errorThrown}, ${xhr.responseText}`;
    }
    this.output_error (error_out);
  }
}

import AjaxAddress from "./AjaxAddress";
import { make_address_content } from "./util";

export default class ListAddresses  {
  constructor (address_map) {
    this.id_to_address = {};
    this.ajax_address = new AjaxAddress(
      (error_text) => { $("#error").show(300).text(error_text); }
    );
    this.address_map = address_map;
  }
  refresh() {
    var this_obj = this;
    var ajax_address = this.ajax_address;

    ajax_address.promise_get_all().then(
      function (addresses) {
        $("#list_addresses").html("");  // reset

        if (!Array.isArray(addresses)) {
          addresses = new Array();
        }

        var id_to_address = {};
        addresses.forEach ( (address) => {
          var id = address["id"];
          this_obj.add_marker(id, address);
          this_obj.append(address);
        });
      }
    );
  }

  append (address) {
    var id = address["id"];

    // no need to append. it's already there.
    if (this.id_to_address[id]) {
      return;
    }

    this.id_to_address[id] = address;
    var address_content = make_address_content(address);       
    $("#list_addresses").append(
      `
      <li class='list-group-item' id='address_${id}'>
        <div class='btn-group' role='group'
          aria-label='edit_delete' style='float:right'>
          <button type="button" class='btn btn-outline-secondary'
            data-target="form_modal" data-whatever="Edit Address"
            id='edit_${id}'>
            Edit
          </button>
          <button type="button" class='btn btn-outline-secondary'
            id='delete_${id}' >
            Delete
          </button>
        </div>
        <div id='address_content_${id}'>
          ${address_content}
        </div>
      </li>
      `
    );

    var this_obj = this;
    var address_map = this.address_map;
    $(`#edit_${id}`).click(function() {
      this_obj.edit_address(id);
    });
    $(`#delete_${id}`).click(function() {
      this_obj.delete_address(id);
    });
    $(`#address_${id}`).hover(
      function(evt) {
        address_map.pop_marker(id);
      },
      function(evt) {
        address_map.unpop_marker(id);
      }
    );
  }

  delete_address (id) {
    var this_obj = this;
    var address_map = this.address_map;
    this.ajax_address.promise_delete(id).then(function () {
      $(`#address_${id}`).remove();
      delete this_obj.id_to_address[id];
      address_map.delete_marker(id);
    });
  }

  edit_address (id) {
    var this_obj = this;

    var address = this.id_to_address[id];
    $("#street").val(address["street"]);
    $("#city"  ).val(address["city"  ]);
    $("#state" ).val(address["state" ]);
    $("#zip"   ).val(address["zip"   ]);

    $("#address_title").text("Edit Address");
    $("#form_modal").modal("show");
    $("#save_address").off().click(function () {
      this_obj.put_address(id);
    });
  }

  post_address() {
    var this_obj = this;
    var addr_data = new Object;
    addr_data.street = $("#street").val();
    addr_data.city   = $("#city"  ).val();
    addr_data.state  = $("#state" ).val();
    addr_data.zip    = $("#zip"   ).val();

    this.ajax_address.promise_post(addr_data).then( (address) => {
      var id = address["id"];
      this_obj.append(address);
      this_obj.add_marker(id, address);
    });
  }

  put_address(id) {
    var address = {
      street: $("#street").val(),
      city  : $("#city"  ).val(),
      state : $("#state" ).val(),
      zip   : $("#zip"   ).val(),
      id    : id
    };
    this.delete_marker(id);

    var this_obj = this;
    this.ajax_address.promise_put(address).then( () => {
      var content = make_address_content(address);
      $(`#address_content_${id}`).html(content);
      this_obj.id_to_address[id] = address;
      this_obj.add_marker(id, address);
    });
  }

  add_marker (id, address) {
    var address_map = this.address_map;
    if (address["lat"] || address["lng"]) {
      setTimeout(
        function() {
          address_map.add_marker(id, address);
        },
        Math.floor(Math.random() * 1000)
      );
      return;
    }

    this.ajax_address.promise_geocode(address).then(
      function(lat_lng) {
        address["lat"] = lat_lng["lat"];
        address["lng"] = lat_lng["lng"];
        address_map.add_marker(id, address);
      },
    );
  }

  delete_marker (id) {
    this.address_map.delete_marker(id);
  }
};
export default class AddressMap{
  constructor() {
    this.id_to_marker = {};
    this.id_to_popup = {};
    this.map = null;
  }
  init() {
    this.map = new google.maps.Map(
      document.getElementById("gmap"),
      {
        zoom: 10,
        center: {lat: 37.4230750, lng: -121.8818120},
      }
    );
  }
  add_marker(id, address) {
    var street = address["street"];
    var lat    = address["lat"   ];
    var lng    = address["lng"   ];

    var marker = new google.maps.Marker({
      title     : street,
      position  : {'lat': lat, 'lng': lng},
      animation : google.maps.Animation.DROP,
      map       : this.map,
    });
    this.id_to_marker[id] = marker;
  }
  delete_marker(id) {
    var marker = this.id_to_marker[id];
    marker.setMap(null);
    delete this.id_to_marker[id];
  }
  pop_marker(id) {
    var marker = this.id_to_marker[id];
    if (!marker) {   // marker not yet initialized
      return;
    }

    marker.setLabel("A"+id);
  }
  unpop_marker(id) {
    var marker = this.id_to_marker[id];
    if (!marker) {   // marker not yet initialized
      return;
    }

    marker.setLabel(null);
  }
}

export function escape_html (str) {
  str += "";  // force to string
  return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

export function make_address_content(address) {
  var street = escape_html(address["street"]);
  var city   = escape_html(address["city"  ]);
  var state  = escape_html(address["state" ]);
  var zip    = escape_html(address["zip"   ]);

  return `
    ${street}<br>
    ${city}, ${state} ${zip}
  `;
}