script.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. 'use strict';
  2. const form = document.querySelector('.form');
  3. const containerWorkouts = document.querySelector('.workouts');
  4. const inputType = document.querySelector('.form__input--type');
  5. const inputDistance = document.querySelector('.form__input--distance');
  6. const inputDuration = document.querySelector('.form__input--duration');
  7. const inputCadence = document.querySelector('.form__input--cadence');
  8. const inputElevation = document.querySelector('.form__input--elevation');
  9. const btnEdit = document.querySelector('.btn__edit');
  10. const btnRemoveAll = document.querySelector('.remover');
  11. const btnSort = document.querySelector('.btn--sort');
  12. class App {
  13. #map;
  14. #mapZoomLevel = 13;
  15. #mapEvent;
  16. #workouts = [];
  17. #sorted = false;
  18. constructor() {
  19. // Get user's position
  20. this._getPosition();
  21. // Get data from local storage
  22. this._getLocalStorage();
  23. // Attach event handlers
  24. form.addEventListener('submit', this._newWorkout.bind(this));
  25. inputType.addEventListener('change', this._toggleElevationField);
  26. containerWorkouts.addEventListener('click', this._moveToPopup.bind(this));
  27. containerWorkouts.addEventListener('click', this._editWorkout.bind(this));
  28. containerWorkouts.addEventListener('click', this._removeWorkout.bind(this));
  29. btnRemoveAll.addEventListener('click', this._removeAllWorkouts.bind(this));
  30. if (this.#workouts.length > 0) btnRemoveAll.classList.remove('btn--hidden');
  31. btnSort.addEventListener('click', this._sort.bind(this));
  32. }
  33. _getPosition() {
  34. if (navigator.geolocation)
  35. navigator.geolocation.getCurrentPosition(
  36. this._loadMap.bind(this),
  37. function () {
  38. alert(`Could not get your position`);
  39. }
  40. );
  41. }
  42. _loadMap(position) {
  43. const { latitude } = position.coords;
  44. const { longitude } = position.coords;
  45. const coords = [latitude, longitude];
  46. this.#map = L.map('map').setView(coords, this.#mapZoomLevel);
  47. L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
  48. attribution: `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>
  49. contributors`,
  50. }).addTo(this.#map);
  51. this.#map.on('click', this._showForm.bind(this));
  52. this.#workouts.forEach(work => this._renderWorkoutMarker(work));
  53. }
  54. _showForm(mapE) {
  55. this.#mapEvent = mapE;
  56. form.classList.remove('hidden');
  57. inputDistance.focus();
  58. }
  59. _hideForm() {
  60. // Empty inputs
  61. inputCadence.value =
  62. inputDistance.value =
  63. inputDuration.value =
  64. inputElevation.value =
  65. '';
  66. form.style.display = 'none';
  67. form.classList.add('hidden');
  68. setTimeout(() => (form.style.display = 'grid'));
  69. }
  70. _toggleElevationField() {
  71. inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
  72. inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
  73. }
  74. _newWorkout(e) {
  75. const validInputs = (...inputs) =>
  76. inputs.every(inp => Number.isFinite(inp));
  77. const allPositive = (...inputs) => inputs.every(inp => inp > 0);
  78. e.preventDefault();
  79. // Get data from form
  80. const type = inputType.value;
  81. const distance = +inputDistance.value;
  82. const duration = +inputDuration.value;
  83. const { lat, lng } = this.#mapEvent.latlng;
  84. let workout;
  85. // If workout running, create running object
  86. if (type === 'running') {
  87. const cadence = +inputCadence.value;
  88. // Check if data is valid
  89. if (
  90. !validInputs(distance, duration, cadence) ||
  91. !allPositive(distance, duration, cadence)
  92. )
  93. return alert('Inputs have to be positive numbers!');
  94. workout = new Running([lat, lng], distance, duration, cadence);
  95. }
  96. // If workout cycling, create cycling object
  97. if (type === 'cycling') {
  98. const elevation = +inputElevation.value;
  99. if (
  100. !validInputs(distance, duration, elevation) ||
  101. !allPositive(distance, duration)
  102. )
  103. return alert('Inputs have to be positive numbers!');
  104. workout = new Cycling([lat, lng], distance, duration, elevation);
  105. }
  106. // Add new object to array
  107. this.#workouts.push(workout);
  108. // Rednder workout on map as marker
  109. this._renderWorkoutMarker(workout);
  110. // Render workout on list
  111. this._renderWorkout(workout);
  112. // Clear input fields and hide form
  113. this._hideForm();
  114. // Set local storage to all workouts
  115. this._setLocalStorage();
  116. btnRemoveAll.classList.remove('btn--hidden');
  117. }
  118. _renderWorkoutMarker(workout) {
  119. L.marker(workout.coords)
  120. .addTo(this.#map)
  121. .bindPopup(
  122. L.popup({
  123. maxWidth: 250,
  124. minWidth: 100,
  125. autoClose: false,
  126. closeOnClick: false,
  127. className: `${workout.type}-popup`,
  128. })
  129. )
  130. .setPopupContent(
  131. `${workout.type === 'running' ? '🏃‍♂️' : '🚴‍♂️'} ${workout.description}`
  132. )
  133. .openPopup();
  134. }
  135. _renderWorkout(workout) {
  136. let html = `
  137. <li class="workout workout--${workout.type}" data-id="${workout.id}">
  138. <h2 class="workout__title">${workout.description}</h2>
  139. <div class="workout__details">
  140. <span class="workout__icon">${
  141. workout.type === 'running' ? '🏃‍♂️' : '🚴‍♂️'
  142. }</span>
  143. <span class="workout__value">${workout.distance}</span>
  144. <span class="workout__unit">km</span>
  145. </div>
  146. <div class="workout__details">
  147. <span class="workout__icon">⏱</span>
  148. <span class="workout__value">${workout.duration}</span>
  149. <span class="workout__unit">min</span>
  150. </div>
  151. `;
  152. if (workout.type === 'running')
  153. html += `
  154. <div class="workout__details">
  155. <span class="workout__icon">⚡️</span>
  156. <span class="workout__value">${workout.pace.toFixed(1)}</span>
  157. <span class="workout__unit">min/km</span>
  158. </div>
  159. <div class="workout__details">
  160. <span class="workout__icon">🦶🏼</span>
  161. <span class="workout__value">${workout.cadence}</span>
  162. <span class="workout__unit">spm</span>
  163. </div>
  164. `;
  165. if (workout.type === 'cycling')
  166. html += `
  167. <div class="workout__details">
  168. <span class="workout__icon">⚡️</span>
  169. <span class="workout__value">${workout.speed.toFixed(1)}</span>
  170. <span class="workout__unit">km/h</span>
  171. </div>
  172. <div class="workout__details">
  173. <span class="workout__icon">⛰</span>
  174. <span class="workout__value">${workout.elevationGain}</span>
  175. <span class="workout__unit">m</span>
  176. </div>
  177. `;
  178. html += `
  179. <div class="buttons">
  180. <button class="btn__edit">🖊</button>
  181. <button class="btn__submit btn--hidden">✔</button>
  182. <button class="btn__remove">✖</button>
  183. </div>
  184. </li>
  185. `;
  186. form.insertAdjacentHTML('afterend', html);
  187. }
  188. _moveToPopup(e) {
  189. const workoutEl = e.target.closest('.workout');
  190. if (!workoutEl) return;
  191. const workout = this.#workouts.find(
  192. work => work.id === workoutEl.dataset.id
  193. );
  194. this.#map.setView(workout.coords, this.#mapZoomLevel, {
  195. animate: true,
  196. pan: {
  197. duration: 1,
  198. },
  199. });
  200. }
  201. _setLocalStorage() {
  202. localStorage.setItem('workouts', JSON.stringify(this.#workouts));
  203. }
  204. _getLocalStorage(sorted) {
  205. const data = JSON.parse(localStorage.getItem('workouts'));
  206. if (!data) return;
  207. this.#workouts = data;
  208. while (form.nextSibling) {
  209. const htmlEl = form.nextSibling;
  210. htmlEl.remove();
  211. }
  212. const works = sorted
  213. ? this.#workouts.slice().sort((a, b) => a.distance - b.distance)
  214. : this.#workouts;
  215. works.forEach(work => this._renderWorkout(work));
  216. }
  217. reset() {
  218. localStorage.removeItem('workouts');
  219. location.reload();
  220. }
  221. _removeAllWorkouts() {
  222. while (form.nextSibling) {
  223. const htmlEl = form.nextSibling;
  224. htmlEl.remove();
  225. }
  226. btnRemoveAll.classList.add('btn--hidden');
  227. this.reset();
  228. }
  229. _editWorkout(e) {
  230. const btn = e.target;
  231. if (!btn.classList.contains('btn__edit')) return;
  232. const workoutEl = btn.closest('.workout');
  233. const workout = this.#workouts.find(
  234. work => work.id === workoutEl.dataset.id
  235. );
  236. console.log(workout);
  237. inputDistance.value = workout.distance;
  238. inputDuration.value = workout.duration;
  239. workout.type == 'running'
  240. ? (inputCadence.value = workout.cadence)
  241. : (inputElevation.value = workout.elevation);
  242. this._showForm();
  243. }
  244. _removeWorkout(e) {
  245. if (!e.target.classList.contains('btn__remove')) return;
  246. const workoutEl = e.target.closest('.workout');
  247. console.log(workoutEl);
  248. const workout = this.#workouts.find(
  249. work => work.id === workoutEl.dataset.id
  250. );
  251. this.#workouts.splice(this.#workouts.indexOf(workout), 1);
  252. localStorage.removeItem('workouts');
  253. workoutEl.remove();
  254. localStorage.removeItem('workouts');
  255. this._setLocalStorage();
  256. location.reload();
  257. }
  258. _sort(e) {
  259. e.preventDefault();
  260. console.log('start sorting');
  261. this.#sorted = !this.#sorted;
  262. this._getLocalStorage(this.#sorted);
  263. }
  264. }
  265. class Workout {
  266. date = new Date();
  267. id = (Date.now() + '').slice(-10); // unique identiofier
  268. constructor(coords, distance, duration) {
  269. this.coords = coords;
  270. this.distance = distance; // in km
  271. this.duration = duration; // in min
  272. }
  273. _setDescription() {
  274. // prettier-ignore
  275. const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
  276. 'September', 'October', 'November', 'December'];
  277. this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
  278. months[this.date.getMonth()]
  279. } ${this.date.getDate()}`;
  280. }
  281. }
  282. class Running extends Workout {
  283. type = 'running';
  284. constructor(coords, distance, duration, cadence) {
  285. super(coords, distance, duration);
  286. this.cadence = cadence;
  287. this._calcPace();
  288. this._setDescription();
  289. }
  290. _calcPace() {
  291. // min/km
  292. return (this.pace = this.duration / this.distance);
  293. }
  294. }
  295. class Cycling extends Workout {
  296. type = 'cycling';
  297. constructor(coords, distance, duration, elevationGain) {
  298. super(coords, distance, duration);
  299. this.elevationGain = elevationGain;
  300. this._calcSpeed();
  301. this._setDescription();
  302. }
  303. _calcSpeed() {
  304. return (this.speed = this.distance / this.duration);
  305. }
  306. }
  307. const app = new App();