Project shows: A design flowchart and an architecture diagram are developed before coding begins. The ‘use strict‘ line is introduced. With strict mode, you can not, for example, use undeclared variables. Class objects are used with the constructor(), extends and super() methods.

– class Workout { … constructor(coords, distance, duration) { … } }
– class Running extends Workout { … constructor(coords, distance, duration, cadence) { super(coords, distance, duration) … }

The App() class is introduced as the parent function for the entire DOM. The App specifies private variables using the # prefix. The App also takes ownership of certain event listeners that are attached to different types of events. The browsers local storage is accessed for the first time.

– class App { #map …
   constructor() {
    // Get user’s position
    this._getPosition();
    // Get data from local storage
    this._getLocalStorage();
    // Attach event handlers
    form.addEventListener(‘submit’, this._newWorkout.bind(this))
    inputType.addEventListener(‘change, this._toggleElevationField)
    containerWorkouts.addEventListener(‘click’, this._moveToPopup.bind(this)) … }

A html form element is used to enter data.

An external API is used to render a map element and the web navigator interface is used to map positions. The Navigator.geolocation read-only property returns a Geolocation object that gives Web content access to the location of the device.

– _getPosition() { … navigator.geolocation.getCurrentPosition(this._loadMap.bind(this), … } ) }
loadMap(position) { 
    const { latitude } = position.coords;
    const { longitude } = position.coords;
    const coords = [latitude, longitude];
    this.#map = L.map(‘map’).setView(coords, this.#mapZoomLevel);
     L.tileLayer(‘https://tile.openstreetmap.org/{z}/{x}/{y}.png’, { attribution:
        ‘&copy; <a href=”https://www.openstreetmap.org/copyright”>OpenStreetMap</a> contributors’, }).addTo(this.#map);
    // Handling click on map
    this.#map.on(‘click’, this._showForm.bind(this)); …}
_showForm(mapE) {
    this.#mapEvent = mapE;
    form.classList.remove(‘hidden’);
    inputDistance.focus() } – this sets the mouse cursor focus to the inputDistance DOM element.

Clicking on the map API renders the workout form which allows the user to enter workout data (which is validated). When the user hits the enter key a new workout is created and added to the list of App #workouts.

_newWorkout(e) {
    const validInputs = (inputs=> inputs.every(inp => Number.isFinite(inp));
     …
    const allPositive = (inputs=> inputs.every(inp => inp > 0);
     …
      // Check if data is valid
      if (!validInputs(distance, duration, cadence) ||!allPositive(distance, duration, cadence))
        return alert(‘Inputs must be positive numbers!’);
      workout = new([lat, lng], distance, duration, cadence);
     …
    this.#workouts.push(workout)
     …
    // Set local storage to all workouts
    this._setLocalStorage() }

The final part is setting/getting the #workouts from the browsers local storage:
_setLocalStorage() {
    localStorage.setItem(workouts, JSON.stringify(this.#workouts));
  }

  _getLocalStorage() {
    const data = JSON.parse(localStorage.getItem(workouts));
    if (!data) return;
    this.#workouts = data;
    this.#workouts.forEach(work => { this._renderWorkout(work) })
  }

<!DOCTYPE html>
<html lang=“en”>
  <head>
    <meta charset=“UTF-8” />
    <meta name=“viewport” content=“width=device-width, initial-scale=1.0” />
    <meta http-equiv=“X-UA-Compatible” content=“ie=edge” />
    <link rel=“shortcut icon” type=“image/png” href=“/icon.png” />

    <link
      href=“https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap”
      rel=“stylesheet”
    />

    <link
      rel=“stylesheet”
      href=“https://unpkg.com/[email protected]/dist/leaflet.css”
      integrity=“sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=”
      crossorigin=“”
    />
    <script
      defer
      src=“https://unpkg.com/[email protected]/dist/leaflet.js”
      integrity=“sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=”
      crossorigin=“”
    ></script>

    <link rel=“stylesheet” href=“style.css” />

    <script defer src=“script.js”></script>
    <title>mapty // Map your workouts</title>
  </head>
  <body>
    <div class=“sidebar”>
      <div class=“logo-clearWO-Style”>
        <img src=“logo.png” alt=“Logo” class=“logo” />
        <button class=“clear__workouts__btn”>Clear Workouts</button>
      </div>
      <ul class=“workouts”>
        <form class=“form hidden”>
          <div class=“form__row”>
            <label class=“form__label”>Type</label>
            <select class=“form__input form__input–type”>
              <option value=“running”>Running</option>
              <option value=“cycling”>Cycling</option>
            </select>
          </div>
          <div class=“form__row”>
            <label class=“form__label”>Distance</label>
            <input class=“form__input form__input–distance” placeholder=“km” />
          </div>
          <div class=“form__row”>
            <label class=“form__label”>Duration</label>
            <input
              class=“form__input form__input–duration”
              placeholder=“min”
            />
          </div>
          <div class=“form__row”>
            <label class=“form__label”>Cadence</label>
            <input
              class=“form__input form__input–cadence”
              placeholder=“step/min”
            />
          </div>
          <div class=“form__row form__row–hidden”>
            <label class=“form__label”>Elev Gain</label>
            <input
              class=“form__input form__input–elevation”
              placeholder=“meters”
            />
          </div>
          <button class=“form__btn”>OK</button>
        </form>

        <!– <li class=”workout workout–running” data-id=”1234567890″>
          <h2 class=”workout__title”>Running on April 14</h2>
          <div class=”workout__details”>
            <span class=”workout__icon”>🏃‍♂️</span>
            <span class=”workout__value”>5.2</span>
            <span class=”workout__unit”>km</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⏱</span>
            <span class=”workout__value”>24</span>
            <span class=”workout__unit”>min</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⚡️</span>
            <span class=”workout__value”>4.6</span>
            <span class=”workout__unit”>min/km</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>🦶🏼</span>
            <span class=”workout__value”>178</span>
            <span class=”workout__unit”>spm</span>
          </div>
        </li>

        <li class=”workout workout–cycling” data-id=”1234567891″>
          <h2 class=”workout__title”>Cycling on April 5</h2>
          <div class=”workout__details”>
            <span class=”workout__icon”>🚴‍♀️</span>
            <span class=”workout__value”>27</span>
            <span class=”workout__unit”>km</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⏱</span>
            <span class=”workout__value”>95</span>
            <span class=”workout__unit”>min</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⚡️</span>
            <span class=”workout__value”>16</span>
            <span class=”workout__unit”>km/h</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⛰</span>
            <span class=”workout__value”>223</span>
            <span class=”workout__unit”>m</span>
          </div>
        </li> –>
      </ul>

      <p class=“copyright”>
        &copy; Copyright by
        <a
          class=“twitter-link”
          target=“_blank”
          href=“https://twitter.com/jonasschmedtman”
          >Jonas Schmedtmann</a
        >. Use for learning or your portfolio. Don’t use to teach. Don’t claim
        as your own.
      </p>
    </div>

    <div id=“map”></div>
  </body>
</html>
‘use strict’;

 

class Workout {
  date = new Date();
  id = (Date.now() + ).slice(10);

 

  constructor(coords, distance, duration) {
    //this.date = …
    //this.id = …
    this.coords = coords; // [lat, lng]
    this.distance = distance; // in km’s
    this.duration = duration; // in min’s
  }

 

  _setDescription() {
    // prettier-ignore
    const months = [‘January’, ‘February’, ‘March’, ‘April’, ‘May’, ‘June’, ‘July’, ‘August’, ‘September’, ‘October’, ‘November’, ‘December’];

 

    this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
      months[this.date.getMonth()]
    } ${this.date.getDate()}`;
  }
}

 

class Running extends Workout {
  type = ‘running’;

 

  constructor(coords, distance, duration, cadence) {
    super(coords, distance, duration);
    this.cadence = cadence;
    this.calcPace();
    this._setDescription();
  }

 

  calcPace() {
    // min/km
    this.pace = this.duration / this.distance;
    return this.pace;
  }
}

 

class Cycling extends Workout {
  type = ‘cycling’;

 

  constructor(coords, distance, duration, elevationGain) {
    super(coords, distance, duration);
    this.elevationGain = elevationGain;
    this.calcSpeed();
    this._setDescription();
  }

 

  calcSpeed() {
    // km/h
    this.speed = this.distance / (this.duration / 60);
    return this.speed;
  }
}

 

// see APPLICATION ARCHITECTURE png
//
const form = document.querySelector(‘.form’);
const containerWorkouts = document.querySelector(‘.workouts’);
const inputType = document.querySelector(‘.form__input–type’);
const inputDistance = document.querySelector(‘.form__input–distance’);
const inputDuration = document.querySelector(‘.form__input–duration’);
const inputCadence = document.querySelector(‘.form__input–cadence’);
const inputElevation = document.querySelector(‘.form__input–elevation’);
const clearWorkouts = document.querySelector(‘.clear__workouts__btn’);

 

class App {
  #map;
  #mapEvent;
  #mapZoomLevel = 13;
  #workouts = [];

 

  constructor() {
    // Get user’s position
    this._getPosition();

 

    // Get data from local storage
    this._getLocalStorage();

 

    // Attach event handlers
    form.addEventListener(‘submit’, this._newWorkout.bind(this));
    inputType.addEventListener(‘change’, this._toggleElevationField);
    containerWorkouts.addEventListener(‘click’, this._moveToPopup.bind(this));
    clearWorkouts.addEventListener(‘click’, this.reset);
  }

 

  _getPosition() {
    if (navigator.geolocation)
      navigator.geolocation.getCurrentPosition(
        this._loadMap.bind(this),
        function () {
          alert(‘Could not get your position’);
        }
      );
  }

 

  _loadMap(position) {
    const { latitude } = position.coords;
    const { longitude } = position.coords;
    console.log(
      ‘https://www.google.com/maps/@${latitude},${logitude},14z?entry=ttu&g_ep=EgoyMDI0MTAwOS4wIKXMDSoASAFQAw%3D%3D’
    );

 

    const coords = [latitude, longitude];

 

    this.#map = L.map(‘map’).setView(coords, this.#mapZoomLevel);

 

    //console.log(map);

 

    L.tileLayer(‘https://tile.openstreetmap.org/{z}/{x}/{y}.png’, {
      attribution:
        ‘&copy; <a href=”https://www.openstreetmap.org/copyright”>OpenStreetMap</a> contributors’,
    }).addTo(this.#map);

 

    // Handling click on map
    this.#map.on(‘click’, this._showForm.bind(this));

 

    this.#workouts.forEach(work => {
      this._renderWorkoutMarker(work);
    });
  }

 

  _showForm(mapE) {
    this.#mapEvent = mapE;
    form.classList.remove(‘hidden’);
    inputDistance.focus();
  }

 

  _hideForm() {
    // Empty inputs
    inputDistance.value =
      inputDuration.value =
      inputCadence.value =
      inputElevation.value =
        ;

 

    form.style.display = ‘none’;
    form.classList.add(‘hidden’);
    setTimeout(() => (form.style.display = ‘grid’), 1000);
  }

 

  _toggleElevationField() {
    inputElevation.closest(‘.form__row’).classList.toggle(‘form__row–hidden’);
    inputCadence.closest(‘.form__row’).classList.toggle(‘form__row–hidden’);
  }

 

  _newWorkout(e) {
    const validInputs = (inputs) =>
      inputs.every(inp => Number.isFinite(inp));
    e.preventDefault();
    const allPositive = (inputs) => inputs.every(inp => inp > 0);

 

    // Get data from the form
    const type = inputType.value;
    const distance = +inputDistance.value;
    const duration = +inputDuration.value;
    const { lat, lng } = this.#mapEvent.latlng;
    let workout;

 

    // If activity running, create Running object
    if (type === ‘running’) {
      const cadence = +inputCadence.value;

 

      // Check if data is valid
      if (
        !validInputs(distance, duration, cadence) ||
        !allPositive(distance, duration, cadence)
      )
        return alert(‘Inputs must be positive numbers!’);

 

      workout = new Running([lat, lng], distance, duration, cadence);
    }

 

    // If activity cycling, create Cycling object
    if (type === ‘cycling’) {
      const elevation = +inputElevation.value;

 

      if (
        !validInputs(distance, duration, elevation) ||
        !allPositive(distance, duration)
      )
        return alert(‘Inputs must be positive numbers!’);

 

      workout = new Cycling([lat, lng], distance, duration, elevation);
    }

 

    // Add new object to workout array
    this.#workouts.push(workout);

 

    // Render workout on map as marker
    this._renderWorkoutMarker(workout);

 

    // Render workout on list
    this._renderWorkout(workout);

 

    // Hide form + clear input fields
    this._hideForm();

 

    // SEt local storage to all workouts
    this._setLocalStorage();
  }

 

  _renderWorkoutMarker(workout) {
    L.marker(workout.coords)
      .addTo(this.#map)
      .bindPopup(
        L.popup({
          maxWidth: 250,
          minWidth: 100,
          autoClose: false,
          closeOnClick: false,
          className: `${workout.type}-popup`,
        })
      )
      .setPopupContent(
        `${workout.type === ‘running’ ? ‘🏃‍♂️’ : ‘🚴‍♀️’} ${workout.description}`
      )
      .openPopup();
  }

 

  _renderWorkout(workout) {
    let html = `
    <li class=”workout workout–${workout.type}” data-id=”${workout.id}“>
          <h2 class=”workout__title”>${workout.description}</h2>
          <div class=”workout__details”>
            <span class=”workout__icon”>${
              workout.type === ‘running’ ? ‘🏃‍♂️’ : ‘🚴‍♀️’
            }</span>
            <span class=”workout__value”>${workout.distance}</span>
            <span class=”workout__unit”>km</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⏱</span>
            <span class=”workout__value”>${workout.duration}</span>
            <span class=”workout__unit”>min</span>
          </div>
          `;

 

    if (workout.type === ‘running’) {
      html += `
            <div class=”workout__details”>
            <span class=”workout__icon”>⚡️</span>
            <span class=”workout__value”>${workout.pace.toFixed(1)}</span>
            <span class=”workout__unit”>min/km</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>🦶🏼</span>
            <span class=”workout__value”>${workout.cadence}</span>
            <span class=”workout__unit”>spm</span>
          </div>
        </li>
        `;
    }

 

    if (workout.type === ‘cycling’) {
      html += `
            <div class=”workout__details”>
            <span class=”workout__icon”>⚡️</span>
            <span class=”workout__value”>${workout.speed.toFixed(1)}</span>
            <span class=”workout__unit”>km/h</span>
          </div>
          <div class=”workout__details”>
            <span class=”workout__icon”>⛰</span>
            <span class=”workout__value”>${workout.elevationGain}</span>
            <span class=”workout__unit”>m</span>
          </div>
        </li>
        `;
    }

 

    form.insertAdjacentHTML(‘afterend’, html);
  }

 

  _moveToPopup(e) {
    const workoutEl = e.target.closest(‘.workout’);
    console.log(workoutEl);

 

    if (!workoutEl) return;

 

    const workout = this.#workouts.find(
      work => work.id === workoutEl.dataset.id
    );

 

    this.#map.setView(workout.coords, this.#mapZoomLevel, {
      animate: true,
      pan: {
        duration: 1,
      },
    });
  }

 

  // Only used for small amounts of data
  _setLocalStorage() {
    localStorage.setItem(‘workouts’, JSON.stringify(this.#workouts));
  }

 

  _getLocalStorage() {
    const data = JSON.parse(localStorage.getItem(‘workouts’));
    console.log(data);

 

    if (!data) return;

 

    this.#workouts = data;

 

    this.#workouts.forEach(work => {
      // creates workout without Running/Cycling base classes
      this._renderWorkout(work);
    });
  }

 

  reset() {
    localStorage.removeItem(‘workouts’);
    location.reload();
  }
}

 

const app = new App();
:root {
  –color-brand–1: #ffb545;
  –color-brand–2: #00c46a;

  –color-dark–1: #2d3439;
  –color-dark–2: #42484d;
  –color-light–1: #aaa;
  –color-light–2: #ececec;
  –color-light–3: rgb(214, 222, 224);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
}

html {
  font-size: 62.5%;
  box-sizing: border-box;
}

body {
  font-family: ‘Manrope’, sans-serif;
  color: var(–color-light–2);
  font-weight: 400;
  line-height: 1.6;
  height: 100vh;
  overscroll-behavior-y: none;

  background-color: #fff;
  padding: 2.5rem;

  display: flex;
}

/* GENERAL */
a:link,
a:visited {
  color: var(–color-brand–1);
}

.logo-clearWO-Style {
  display: flex;
  align-items: center;
  align-self: center;
  gap: 16px;
}

/* SIDEBAR */
.sidebar {
  flex-basis: 50rem;
  background-color: var(–color-dark–1);
  padding: 3rem 5rem 4rem 5rem;
  display: flex;
  flex-direction: column;
}

.logo {
  height: 5.2rem;
  align-self: center;
  margin-bottom: 4rem;
}
.clear__workouts__btn {
  align-self: center;
  margin-bottom: 4rem;
  padding: 4px;
}

.workouts {
  list-style: none;
  height: 77vh;
  overflow-y: scroll;
  overflow-x: hidden;
}

.workouts::-webkit-scrollbar {
  width: 0;
}

.workout {
  background-color: var(–color-dark–2);
  border-radius: 5px;
  padding: 1.5rem 2.25rem;
  margin-bottom: 1.75rem;
  cursor: pointer;

  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  gap: 0.75rem 1.5rem;
}
.workout–running {
  border-left: 5px solid var(–color-brand–2);
}
.workout–cycling {
  border-left: 5px solid var(–color-brand–1);
}

.workout__title {
  font-size: 1.7rem;
  font-weight: 600;
  grid-column: 1 / -1;
  align-items: center;
}
.workout__delete {
  font-size: 1.7rem;
  font-weight: 600;
  grid-column: 2 / -1;
}
.workout__details {
  display: flex;
  align-items: baseline;
}
.workout__icon {
  font-size: 1.8rem;
  margin-right: 0.2rem;
  height: 0.28rem;
}
.workout__value {
  font-size: 1.5rem;
  margin-right: 0.5rem;
}
.workout__unit {
  font-size: 1.1rem;
  color: var(–color-light–1);
  text-transform: uppercase;
  font-weight: 800;
}

.form {
  background-color: var(–color-dark–2);
  border-radius: 5px;
  padding: 1.5rem 2.75rem;
  margin-bottom: 1.75rem;

  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem 2.5rem;

  /* Match height and activity boxes */
  height: 9.25rem;
  transition: all 0.5s, transform 1ms;
}

.form.hidden {
  transform: translateY(-30rem);
  height: 0;
  padding: 0 2.25rem;
  margin-bottom: 0;
  opacity: 0;
}

.form__row {
  display: flex;
  align-items: center;
}

.form__row–hidden {
  display: none;
}

.form__label {
  flex: 0 0 50%;
  font-size: 1.5rem;
  font-weight: 600;
}

.form__input {
  width: 100%;
  padding: 0.3rem 1.1rem;
  font-family: inherit;
  font-size: 1.4rem;
  border: none;
  border-radius: 3px;
  background-color: var(–color-light–3);
  transition: all 0.2s;
}

.form__input:focus {
  outline: none;
  background-color: #fff;
}

.form__btn {
  display: none;
}

.copyright {
  margin-top: auto;
  font-size: 1.3rem;
  text-align: center;
  color: var(–color-light–1);
}

.twitter-link:link,
.twitter-link:visited {
  color: var(–color-light–1);
  transition: all 0.2s;
}

.twitter-link:hover,
.twitter-link:active {
  color: var(–color-light–2);
}

/* MAP */
#map {
  flex: 1;
  height: 100%;
  background-color: var(–color-light–1);
}

/* Popup width is defined in JS using options */
.leaflet-popup .leaflet-popup-content-wrapper {
  background-color: var(–color-dark–1);
  color: var(–color-light–2);
  border-radius: 5px;
  padding-right: 0.6rem;
}
.leaflet-popup .leaflet-popup-content {
  font-size: 1.5rem;
}
.leaflet-popup .leaflet-popup-tip {
  background-color: var(–color-dark–1);
}

.running-popup .leaflet-popup-content-wrapper {
  border-left: 5px solid var(–color-brand–2);
}

.cycling-popup .leaflet-popup-content-wrapper {
  border-left: 5px solid var(–color-brand–1);
}