Mapty App: OOP, Geolocation, External Libraries, and More

vancouverΒ·2023λ…„ 7μ›” 6일
0

javascriptμ΄ν•΄ν•˜κΈ°

λͺ©λ‘ 보기
20/22

How to Plan Web Project

1. User Stories


πŸ‘‰ User Story : μ‚¬μš©μžμ˜ κ΄€μ μ—μ„œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ κΈ°λŠ₯을 μ„€λͺ…ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€
πŸ‘‰ Common format : [μ‚¬μš©μž μœ ν˜•]μœΌλ‘œμ„œ, [μ–΄λ–€ λ™μž‘]을 ν•˜κ³  μ‹Άμ–΄μ„œ, [μ–΄λ–€ 이점]을 μ–»κ³ μž ν•©λ‹ˆλ‹€.

  1. μ‚¬μš©μžλ‘œμ„œ, μ €λŠ” λŸ¬λ‹ μš΄λ™μ˜ μœ„μΉ˜, 거리, μ‹œκ°„, 속도, 그리고 λΆ„λ‹Ή 보폭(steps/minute)을 κΈ°λ‘ν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ λͺ¨λ“  λŸ¬λ‹ μš΄λ™ 기둝을 남길 수 μžˆμŠ΅λ‹ˆλ‹€.

  2. μ‚¬μš©μžλ‘œμ„œ, μ €λŠ” 사이클링 μš΄λ™μ˜ μœ„μΉ˜, 거리, μ‹œκ°„, 속도, 그리고 증강 고도(elevation gain)λ₯Ό κΈ°λ‘ν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ λͺ¨λ“  사이클링 μš΄λ™ 기둝을 남길 수 μžˆμŠ΅λ‹ˆλ‹€.

  3. μ‚¬μš©μžλ‘œμ„œ, μ €λŠ” ν•œλˆˆμ— λͺ¨λ“  μš΄λ™ 기둝을 λ³Ό 수 μžˆλ„λ‘ ν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ μ‹œκ°„μ΄ 지남에 따라 λ‚΄ 진전을 μ‰½κ²Œ 좔적할 수 μžˆμŠ΅λ‹ˆλ‹€.

  4. μ‚¬μš©μžλ‘œμ„œ, μ €λŠ” μš΄λ™ 기둝을 지도상에도 ν™•μΈν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ μ–΄λ””μ„œ κ°€μž₯ 많이 μš΄λ™ν•˜λŠ”μ§€ μ‰½κ²Œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

  5. μ‚¬μš©μžλ‘œμ„œ, μ €λŠ” 앱을 λ‚˜κ°€κ³  λ‚˜μ€‘μ— λ‹€μ‹œ 듀어와도 λͺ¨λ“  μš΄λ™ 기둝을 λ³Ό 수 있기λ₯Ό μ›ν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ 앱을 μ‹œκ°„μ΄ μ§€λ‚˜λ„ 계속 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

2. Features

  1. μœ„μΉ˜, 거리, μ‹œκ°„, 페이슀, λΆ„λ‹Ή 보폭(steps/minute)κ³Ό ν•¨κ»˜ λŸ¬λ‹ μš΄λ™μ„ κΈ°λ‘ν•©λ‹ˆλ‹€.
    πŸ‘‰ μ‚¬μš©μžκ°€ ν΄λ¦­ν•œ μœ„μΉ˜λ₯Ό 맡에 μΆ”κ°€ν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. (μœ„μΉ˜ μ’Œν‘œλ₯Ό μ–»λŠ” κ°€μž₯ 쒋은 λ°©λ²•μž…λ‹ˆλ‹€.)
    πŸ‘‰ ν˜„μž¬ μœ„μΉ˜λ₯Ό 기반으둜 ν•œ 지리적 μœ„μΉ˜ 정보λ₯Ό μ‚¬μš©ν•˜μ—¬ 맡을 ν‘œμ‹œν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. (μ‚¬μš©μž μΉœν™”μ μΈ λ°©λ²•μž…λ‹ˆλ‹€.)
    πŸ‘‰ 거리, μ‹œκ°„, 페이슀, λΆ„λ‹Ή 보폭(steps/minute)을 μž…λ ₯ν•  수 μžˆλŠ” 폼을 μ œκ³΅ν•©λ‹ˆλ‹€.
  1. μœ„μΉ˜, 거리, μ‹œκ°„, 속도, 고도 μƒμŠΉλŸ‰κ³Ό ν•¨κ»˜ 사이클링 μš΄λ™μ„ κΈ°λ‘ν•©λ‹ˆλ‹€.
    πŸ‘‰ 거리(distance), μ‹œκ°„(time), 속도(speed), 고도 μƒμŠΉλŸ‰(elevation gain)을 μž…λ ₯ν•  수 μžˆλŠ” 폼을 μ œκ³΅ν•©λ‹ˆλ‹€.
  1. ν•œλˆˆμ— λͺ¨λ“  μš΄λ™ 기둝을 ν™•μΈν•©λ‹ˆλ‹€.
    πŸ‘‰ μš΄λ™ 기둝을 λͺ©λ‘μœΌλ‘œ ν‘œμ‹œν•©λ‹ˆλ‹€.
  1. 지도 μƒμ—μ„œ μš΄λ™ 기둝을 ν™•μΈν•©λ‹ˆλ‹€.
    πŸ‘‰ μš΄λ™ 기둝을 지도에 ν‘œμ‹œν•©λ‹ˆλ‹€.
  1. 앱을 λ‚˜κ°€κ³  λ‚˜μ€‘μ— λŒμ•„μ™€λ„ λͺ¨λ“  μš΄λ™ 기둝을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.
    πŸ‘‰ λΈŒλΌμš°μ €μ˜ 둜컬 μŠ€ν† λ¦¬μ§€ APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μš΄λ™ 데이터λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.
    πŸ‘‰ νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ, 둜컬 μŠ€ν† λ¦¬μ§€μ—μ„œ μ €μž₯된 데이터λ₯Ό 읽어와 ν‘œμ‹œν•©λ‹ˆλ‹€.

3. FlowChart

Features

  1. ν˜„μž¬ μœ„μΉ˜λ₯Ό 기반으둜 지도λ₯Ό ν‘œμ‹œν•˜λŠ” κΈ°λŠ₯을 μœ„ν•΄ 지리적 μœ„μΉ˜ 정보(Geolocation)λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  2. μ‚¬μš©μžκ°€ ν΄λ¦­ν•œ μœ„μΉ˜λ₯Ό 기반으둜 ν•œ 지도λ₯Ό ν‘œμ‹œν•˜μ—¬ μƒˆλ‘œμš΄ μš΄λ™ 기둝을 μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  3. 거리, μ‹œκ°„, 페이슀, λΆ„λ‹Ή 보폭(steps/minute)을 μž…λ ₯ν•  수 μžˆλŠ” 폼을 μ œκ³΅ν•©λ‹ˆλ‹€.
  4. 거리, μ‹œκ°„, 속도, 고도 μƒμŠΉλŸ‰(elevation gain)을 μž…λ ₯ν•  수 μžˆλŠ” 폼을 μ œκ³΅ν•©λ‹ˆλ‹€.
  5. μš΄λ™ 기둝을 λͺ©λ‘μœΌλ‘œ ν‘œμ‹œν•©λ‹ˆλ‹€.
  6. μš΄λ™ 기둝을 지도에 ν‘œμ‹œν•©λ‹ˆλ‹€.
  7. μš΄λ™ 데이터λ₯Ό λΈŒλΌμš°μ €μ— μ €μž₯ν•˜κΈ° μœ„ν•΄ 둜컬 μŠ€ν† λ¦¬μ§€λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  8. νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ, μ €μž₯된 데이터λ₯Ό 읽어와 ν‘œμ‹œν•©λ‹ˆλ‹€.
  9. μš΄λ™ μœ„μΉ˜λ₯Ό 클릭할 λ•Œλ§ˆλ‹€ 지도λ₯Ό ν•΄λ‹Ή μœ„μΉ˜λ‘œ μ΄λ™ν•©λ‹ˆλ‹€.

μ‹€μ œ 개발 κ³Όμ •μ—μ„œλŠ” κ³„νš λ‹¨κ³„μ—μ„œ μ΅œμ’… ν”Œλ‘œμš°μ°¨νŠΈλ₯Ό μ™„μ„±ν•΄μ•Ό ν•˜λŠ” 것은 μ•„λ‹™λ‹ˆλ‹€. κ΅¬ν˜„ κ³Όμ • λ™μ•ˆ ν”Œλ‘œμš°μ°¨νŠΈλ‚˜ μ „λ°˜μ μΈ ꡬ쑰가 변경될 수 μžˆλŠ” 것이 μ •μƒμ μž…λ‹ˆλ‹€.

μ†Œν”„νŠΈμ›¨μ–΄ κ°œλ°œνŒ€μ΄ 더 λ§Žμ€ 톡찰λ ₯을 μ–»κ±°λ‚˜ μƒˆλ‘œμš΄ 도전에 μ§λ©΄ν•˜κ±°λ‚˜ ν”Όλ“œλ°±μ„ λ°›μœΌλ©΄μ„œ 초기 κ³„νšμ„ μ‘°μ •ν•˜κ³  κ°œμ„ ν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, μ• μžμΌ 개발 방법둠은 반볡적이고 점진적인 κ°œλ°œμ„ κ°•μ‘°ν•˜μ—¬ μœ μ—°μ„±κ³Ό 적응λ ₯을 κ°–μΆœ 수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. 이 방법둠은 정기적인 ν”Όλ“œλ°±κ³Ό 이해 κ΄€κ³„μžμ™€μ˜ ν˜‘μ—…μ„ μž₯λ €ν•˜μ—¬ ν”„λ‘œμ νŠΈκ°€ λ³€ν™”ν•˜λŠ” μš”κ΅¬ 사항에 맞게 진행될 수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€.

λ”°λΌμ„œ, κ³„νš 단계λ₯Ό μ‹œμž‘μ μœΌλ‘œ μ ‘κ·Όν•˜κ³  ν”„λ‘œμ νŠΈκ°€ μ§„ν–‰λ˜λ©΄μ„œ μ‘°μ •ν•  μ€€λΉ„λ₯Ό ν•΄μ•Ό ν•©λ‹ˆλ‹€. 적응λ ₯κ³Ό 민첩성은 성곡적인 μ†Œν”„νŠΈμ›¨μ–΄ μ†”λ£¨μ…˜μ„ μ œκ³΅ν•˜λŠ” 데 μ€‘μš”ν•œ μš”μ†Œμž…λ‹ˆλ‹€.

Using the Geolocation API

if (navigator.geolocation)
  navigator.geolocation.getCurrentPosition(
    function (position) {
      const { latitude } = position.coords;
      const { longitude } = position.coords;
      console.log(
        `https://www.google.pt/maps/@${latitude},${longitude},?entry=ttu`
      ); // google Map을 톡해 μžμ‹ μ˜ ν˜„μž¬μœ„μΉ˜λ₯Ό κ΅¬ν˜„.
    },
    function () {
      alert(`Could not get your position`); // 처음 λ‘œλ“œν• λ•Œ μœ„μΉ˜λ₯Ό ν—ˆμš©ν•˜μ§€ μ•ŠμœΌλ©΄ μ•Œλ¦Ό κ΅¬ν˜„
    }
  ); 

Displaying a Map Using Leaflet Library

if (navigator.geolocation)
  navigator.geolocation.getCurrentPosition(
    function (position) {
      const { latitude } = position.coords;
      const { longitude } = position.coords;
      console.log(
        `https://www.google.pt/maps/@${latitude},${longitude},?entry=ttu`
      ); // google Map을 톡해 μžμ‹ μ˜ ν˜„μž¬μœ„μΉ˜λ₯Ό κ΅¬ν˜„.
      const coords = [latitude, longitude]; // ν˜„μž¬μœ„μΉ˜ μ’Œν‘œλ₯Ό λ§€κ°œλ³€μˆ˜ν•¨.

      const map = L.map("map").setView(coords, 13); 
      // L은 Leaflet의 라이브러리λ₯Ό λœ»ν•¨. 
      //map은 html파일의 idλ₯Ό κ°€λ₯΄ν‚΄.

      L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);

      L.marker(coords)
        .addTo(map)
        .bindPopup("A pretty CSS popup.<br> Easily customizable.")
        .openPopup();
    },
    function () {
      alert(`Could not get your position`);
    }
  );

Displaying a Map Marker

 map.on(`click`, function (mapEvent) {
        console.log(mapEvent);
        const { lat, lng } = mapEvent.latlng; // 클릭을 ν• λ•Œλ§ˆλ‹€ λ§ˆμ»€κ°€ μ§€μ •λ˜κ³  ν•΄λ‹Ή μ’Œν‘œλ₯Ό ν‘œμ‹œν•˜λŠ” 이벀트.

        L.marker([lat, lng])
          .addTo(map)
          .bindPopup(
            L.popup({
              maxwidth: 250,
              minwidth: 100,
              autoClose: false,
              closeOnClick: false,
              className: `running-popup`,
            })
          )
          .setPopupContent(`Workout`)

Reference (leaflet의 popup 자료)

https://leafletjs.com/reference.html#popup

Rendering Workout Input Form

 map.on(`click`, function (mapE) {
        mapEvent = mapE;
        form.classList.remove(`hidden`);
        inputDistance.focus();
      });
    },
    function () {
      alert(`Could not get your position`);
    }
  );

form.addEventListener(`submit`, function (e) {
  e.preventDefault();

  // Clear input fields
  inputDistance.value =
    inputDuration.value =
    inputDistance.value =
    inputElevation.value =
      ``;

  // Display marker
  console.log(mapEvent);
  const { lat, lng } = mapEvent.latlng;

  L.marker([lat, lng])
    .addTo(map)
    .bindPopup(
      L.popup({
        maxwidth: 250,
        minwidth: 100,
        autoClose: false,
        closeOnClick: false,
        className: `running-popup`,
      })
    )
    .setPopupContent(`Workout`)
    .openPopup();
});

inputType.addEventListener(`change`, function () {
  inputElevation.closest(`.form__row`).classList.toggle(`form__row--hidden`);
  inputCadence.closest(`.form__row`).classList.toggle(`form__row--hidden`);
});

Refactoring and Managing Workout Data: Creating Classes

class WorkOut {
  date = new Date();
  id = (Date.now() + ``).slice(-10);

  constructor(coords, distance, duration) {
    this.coords = coords;
    this.distance = distance;
    this.duration = duration;
  }
}

class Running extends WorkOut {
  constructor(coords, distance, duration, cadance) {
    super(coords, distance, duration);
    this.cadance = cadance;
    this.calcPace(0);
  }
  calcPace() {
    // min/km
    this.pace = this.duration / this.distance;
    return this.pace;
  }
}

class Cycling extends WorkOut {
  constructor(coords, distance, duration, elevationGain) {
    super(coords, distance, duration);
    this.elevationGain = elevationGain;
  }
  clacSpeed() {
    // km/h
    this.speed = this.distance / (this.duration / 60);
    return this.speed;
  }
}
// const run1 = new Running([39, -12], 5.2, 24, 178);
// const cycling1 = new Cycling([39, -12], 27, 95, 523);
// console.log(run1, cycling1);
/////////////////////////////////////////
// APPLICATION ARCHITECTURE
class App {
  #map;
  #mapEvent;

  constructor() {
    this._getPosition();
    form.addEventListener(`submit`, this._newWorkout.bind(this));
    inputType.addEventListener(`change`, this._toggleElevationField);
  }

  _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.pt/maps/@${latitude},${longitude},?entry=ttu`
    ); // google Map을 톡해 μžμ‹ μ˜ ν˜„μž¬μœ„μΉ˜λ₯Ό κ΅¬ν˜„.
    const coords = [latitude, longitude];

    this.#map = L.map("map").setView(coords, 13);

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

    // Handling clicks on map
    this.#map.on(`click`, this._showForm.bind(this));
  }
  _showForm(mapE) {
    this.#mapEvent = mapE;
    form.classList.remove(`hidden`);
    inputDistance.focus();
  }

  _toggleElevationField() {
    inputElevation.closest(`.form__row`).classList.toggle(`form__row--hidden`);
    inputCadence.closest(`.form__row`).classList.toggle(`form__row--hidden`);
  }

  _newWorkout(e) {
    e.preventDefault();
    // Clear input fields
    inputDistance.value =
      inputDuration.value =
      inputDistance.value =
      inputElevation.value =
        ``;

    // Display marker

    const { lat, lng } = this.#mapEvent.latlng;

    L.marker([lat, lng])
      .addTo(this.#map)
      .bindPopup(
        L.popup({
          maxwidth: 250,
          minwidth: 100,
          autoClose: false,
          closeOnClick: false,
          className: `running-popup`,
        })
      )
      .setPopupContent(`Workout`)
      .openPopup();
  }
}
const app = new App();

Final

"use strict";

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

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

  _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()}`;
  }

  click() {
    this.clicks++;
  }
}

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.type = 'cycling';
    this.calcSpeed();
    this._setDescription();
  }

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

// const run1 = new Running([39, -12], 5.2, 24, 178);
// const cycling1 = new Cycling([39, -12], 27, 95, 523);
// console.log(run1, cycling1);

///////////////////////////////////////
// APPLICATION ARCHITECTURE
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");

class App {
  #map;
  #mapZoomLevel = 13;
  #mapEvent;
  #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));
  }

  _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.pt/maps/@${latitude},${longitude}`);

    const coords = [latitude, longitude];

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

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

    // Handling clicks 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));
    const allPositive = (...inputs) => inputs.every((inp) => inp > 0);

    e.preventDefault();

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

    // If workout running, create running object
    if (type === "running") {
      const cadence = +inputCadence.value;

      // Check if data is valid
      if (
        // !Number.isFinite(distance) ||
        // !Number.isFinite(duration) ||
        // !Number.isFinite(cadence)
        !validInputs(distance, duration, cadence) ||
        !allPositive(distance, duration, cadence)
      )
        return alert("Inputs have to be positive numbers!");

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

    // If workout cycling, create cycling object
    if (type === "cycling") {
      const elevation = +inputElevation.value;

      if (
        !validInputs(distance, duration, elevation) ||
        !allPositive(distance, duration)
      )
        return alert("Inputs have to 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) {
    // BUGFIX: When we click on a workout before the map has loaded, we get an error. But there is an easy fix:
    if (!this.#map) return;

    const workoutEl = e.target.closest(".workout");

    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,
      },
    });

    // using the public interface
    // workout.click();
  }

  _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);
    });
  }

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

const app = new App();

0개의 λŒ“κΈ€