JSFiddle - React, Tailwind, and code Playground
HTML
<!-- The API key below only works in JSFiddle. If you don't have an API key, go to [https://console.cloud.google.com/google/maps-apis/start] and then replace the key parameter below with your API key from your project. -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Commutes and Destinations Map</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<!-- Defined commutes SVGs -->
<svg class="hide">
<defs>
<symbol id="commutes-initial-icon">
<path d="M41 20H18.6c-9.5 0-10.8 13.5 0 13.5h14.5C41 33.5 41 45 33 45H17.7" stroke="#D2E3FC" stroke-width="5"></path>
<path d="M41 22c.2 0 .4 0 .6-.2l.4-.5c.3-1 .7-1.7 1.1-2.5l2-3c.8-1 1.5-2 2-3 .6-1 .9-2.3.9-3.8 0-2-.7-3.6-2-5-1.4-1.3-3-2-5-2s-3.6.7-5 2c-1.3 1.4-2 3-2 5 0 1.4.3 2.6.8 3.6s1.2 2 2 3.2c.9 1 1.6 2 2 2.8.5.9 1 1.7 1.2 2.7l.4.5.6.2Zm0-10.5c-.7 0-1.3-.2-1.8-.7-.5-.5-.7-1.1-.7-1.8s.2-1.3.7-1.8c.5-.5 1.1-.7 1.8-.7s1.3.2 1.8.7c.5.5.7 1.1.7 1.8s-.2 1.3-.7 1.8c-.5.5-1.1.7-1.8.7Z" fill="#185ABC"></path>
<path d="m12 32-8 6v12h5v-7h6v7h5V38l-8-6Z" fill="#4285F4"></path>
</symbol>
</defs>
<use href="#commutes-initial-icon"/>
</svg>
<svg class="hide">
<defs>
<symbol id="commutes-add-icon">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</symbol>
</defs>
<use href="#commutes-add-icon"/>
</svg>
<svg class="hide">
<defs>
<symbol id="commutes-driving-icon">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.85 7h10.29l1.08 3.11H5.77L6.85 7zM19 17H5v-5h14v5z"/>
<circle cx="7.5" cy="14.5" r="1.5"/>
<circle cx="16.5" cy="14.5" r="1.5"/>
</symbol>
...
CSS
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
.commutes {
align-content: stretch;
color: #202124;
display: flex;
flex-direction: column;
flex-wrap: wrap;
font-family: Arial, sans-serif;
height: 100%;
min-height: 256px;
min-width: 360px;
overflow: auto;
width: 100%;
}
.commutes-info {
flex: 0 0 110px;
max-width: 100%;
overflow: hidden;
}
.commutes-initial-state {
border-radius: 8px;
border: 1px solid #dadce0;
display: flex;
height: 98px;
margin-top: 8px;
padding: 0 16px;
}
.commutes-initial-state svg {
align-self: center;
}
.commutes-initial-state .description {
align-self: center;
flex-grow: 1;
padding: 0 16px;
}
.commutes-initial-state .description .heading {
font: 22px/28px Arial, sans-serif;
margin: 0;
}
.commutes-initial-state .description p {
color: #5f6368;
font: 13px/20px Arial, sans-serif;
margin: 0;
}
.commutes-initial-state .add-button {
align-self: center;
background-color: #4285f4;
border-color: #4285f4;
border-radius: 4px;
border-style: solid;
color: #fff;
cursor: pointer;
display: inline-flex;
fill: #fff;
padding: 8px 16px 8px 8px;
white-space: nowrap;
}
.commutes-initial-state .add-button .label {
font: normal 600 15px/24px Arial, sans-serif;
padding-left: 8px;
}
@media (max-width: 535px) {
.commutes-initial-state svg {
display: none;
}
.commutes-initial-state .description {
padding-left: 0;
}
.commutes-initial-state .description .heading {
font-weight: bold;
font-size: 15px;
line-height: 24px;
}
}
.commutes-destinations {
display: none;
position: relative;
width: 100%;
}
.commutes-destinations:hover .visible {
display: block;
}
.commutes-destinations .destinations-container {
display: flex;
overflow-x: auto;
padding: 8px 8px 4px 8px;
white-space: nowrap;
width: 100%;
}
.commutes-destinations .destinations-container::-webkit-scrollbar {
display: none;
}
.commutes-destinations...
JavaScript
'use strict';
/**
* Element selectors for commutes widget.
*/
const commutesEl = {
map: document.querySelector('.map-view'),
initialStatePanel: document.querySelector('.commutes-initial-state'),
destinationPanel: document.querySelector('.commutes-destinations'),
modal: document.querySelector('.commutes-modal-container'),
};
/**
* Element selectors for commutes destination panel.
*/
const destinationPanelEl = {
addButton: commutesEl.destinationPanel.querySelector('.add-button'),
container: commutesEl.destinationPanel.querySelector('.destinations-container'),
list: commutesEl.destinationPanel.querySelector('.destination-list'),
scrollLeftButton: commutesEl.destinationPanel.querySelector('.left-control'),
scrollRightButton: commutesEl.destinationPanel.querySelector('.right-control'),
getActiveDestination: () => commutesEl.destinationPanel.querySelector('.destination.active'),
};
/**
* Element selectors for commutes modal popup.
*/
const destinationModalEl = {
title: commutesEl.modal.querySelector('h2'),
form: commutesEl.modal.querySelector('form'),
destinationInput: commutesEl.modal.querySelector('input[name="destination-address"]'),
errorMessage: commutesEl.modal.querySelector('.error-message'),
addButton: commutesEl.modal.querySelector('.add-destination-button'),
deleteButton: commutesEl.modal.querySelector('.delete-destination-button'),
editButton: commutesEl.modal.querySelector('.edit-destination-button'),
cancelButton: commutesEl.modal.querySelector('.cancel-button'),
getTravelModeInput: () => commutesEl.modal.querySelector('input[name="travel-mode"]:checked'),
};
/**
* Max number of destination allowed to be added to commutes panel.
*/
const MAX_NUM_DESTINATIONS = 10;
/**
* Bounds to bias search within ~50km distance.
*/
const BIAS_BOUND_DISTANCE = 0.5;
/**
* Hour in seconds.
*/
const HOUR_IN_SECONDS = 3600;
/**
* Minutes in seconds.
*/
const MIN_IN_SECONDS = 60;
/**
* Stroke colors for destination...