├── icon.png ├── logo.png ├── Mapty-flowchart.png ├── Mapty-architecture-final.png ├── Mapty-architecture-part-1.png ├── README.md ├── LICENSE ├── media.css ├── index.html ├── style.css └── script.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsalinazarpour/tehran-mapty/HEAD/icon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsalinazarpour/tehran-mapty/HEAD/logo.png -------------------------------------------------------------------------------- /Mapty-flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsalinazarpour/tehran-mapty/HEAD/Mapty-flowchart.png -------------------------------------------------------------------------------- /Mapty-architecture-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsalinazarpour/tehran-mapty/HEAD/Mapty-architecture-final.png -------------------------------------------------------------------------------- /Mapty-architecture-part-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsalinazarpour/tehran-mapty/HEAD/Mapty-architecture-part-1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Live demo: 4 | ## https://itsalinazarpour.github.io/tehran-mapty/ 5 | 6 | ## #OOP #asynchronous #API 7 | 8 | ### A project from **"The Complete JavaScript Course 2022: From Zero to Expert!"** Created by Jonas Schmedtmann 9 | 10 | 11 | ## I have made the following changes: 12 | 13 | 14 | - Design as a **Responsive** web 15 | - **Markup** and **styling** for new created **submenus** & realistic **error message** 16 | - Abilities to **delete** workout and **delete all** workouts 17 | - Position the map to **show all workouts** 18 | - **Geocode location** from coordinates and display it on the workout list 19 | - **Display neighbourhood** for workout time and place 20 | - Click on popup, **move map** to corresponding popup 21 | - Map zoom and view control 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ali Nazarpour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /media.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | body { 3 | padding: 1rem; 4 | padding-right: 0; 5 | 6 | } 7 | 8 | .workout { 9 | cursor: auto; 10 | } 11 | 12 | .sidebar { 13 | flex-basis: 40rem; 14 | padding: 2rem 1rem 2rem 1rem; 15 | } 16 | } 17 | 18 | @media (max-width: 900px) { 19 | 20 | html { 21 | font-size: 54.5%; 22 | } 23 | 24 | body { 25 | padding: 0; 26 | } 27 | 28 | .sidebar { 29 | flex-basis: 49%; 30 | padding: 1rem 1rem; 31 | border-top-left-radius: 0; 32 | 33 | } 34 | 35 | .workout { 36 | padding: 0.5rem 1.25rem; 37 | margin-bottom: 0.75rem; 38 | } 39 | 40 | .workouts { 41 | height: 100%; 42 | } 43 | 44 | .logo { 45 | height: 3.2rem; 46 | margin-bottom: 1rem; 47 | } 48 | 49 | 50 | .leaflet-marker-icon { 51 | margin-left: -10px !important; 52 | margin-top: -34px !important; 53 | width: 18px !important; 54 | height: 28px !important; 55 | } 56 | 57 | .leaflet-popup { 58 | bottom: -17px !important; 59 | left: -85px !important; 60 | } 61 | 62 | .leaflet-popup-content { 63 | margin: 9px 24px 9px 20px !important; 64 | } 65 | 66 | .form { 67 | padding: 0.5rem 1rem; 68 | margin-bottom: 0.75rem; 69 | gap: 0rem 0.5rem; 70 | height: 63.0625px; 71 | 72 | } 73 | 74 | /* .workout__details { 75 | overflow-x: scroll; 76 | } */ 77 | 78 | } 79 | 80 | @media (max-width: 600px) { 81 | html { 82 | font-size: 45.5%; 83 | } 84 | 85 | .form { 86 | height: 8.25rem; 87 | } 88 | 89 | .leaflet-popup { 90 | left: -72px !important; 91 | } 92 | 93 | .workout { 94 | gap: 0rem 0.5rem; 95 | height: 8.25rem; 96 | } 97 | 98 | 99 | body { 100 | flex-direction: column-reverse; 101 | } 102 | 103 | .sidebar { 104 | flex-basis: 48%; 105 | height: 48%; 106 | 107 | } 108 | 109 | .leaflet-touch .leaflet-bar a { 110 | width: 25px !important; 111 | height: 25px !important; 112 | line-height: 25px !important; 113 | font-size: 17px !important; 114 | } 115 | 116 | .dropdown-content { 117 | right: 10px; 118 | margin-top: 25px; 119 | } 120 | 121 | .leaflet-control-attribution { 122 | display: none !important; 123 | } 124 | 125 | .btn-right { 126 | top: 0.5rem !important; 127 | right: 0.1rem !important; 128 | } 129 | 130 | .workout-caption { 131 | padding: 1.2rem 2.25rem; 132 | height: 8.25rem; 133 | } 134 | 135 | .logo { 136 | height: 4rem; 137 | margin-bottom: 1rem; 138 | } 139 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | Tehran-mapty 44 | 45 | 46 | 184 | 185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-brand--1: #ffd99f; 3 | --color-brand--2: #ffff00; 4 | 5 | --color-dark--1: #002238; 6 | --color-dark--2: #00345c; 7 | --color-dark--3: #0050a1; 8 | --color-light--1: #aaa; 9 | --color-light--2: #ececec; 10 | --color-light--3: rgb(214, 222, 224); 11 | } 12 | 13 | * { 14 | margin: 0; 15 | padding: 0; 16 | box-sizing: inherit; 17 | } 18 | 19 | html { 20 | font-size: 62.5%; 21 | box-sizing: border-box; 22 | } 23 | 24 | body { 25 | font-family: 'Manrope', sans-serif; 26 | color: var(--color-light--2); 27 | font-weight: 400; 28 | line-height: 1.6; 29 | height: 100vh; 30 | overscroll-behavior-y: none; 31 | background-color: #fff; 32 | padding: 1.5rem; 33 | padding-right: 0; 34 | 35 | display: flex; 36 | } 37 | 38 | /* GENERAL */ 39 | a:link, 40 | a:visited { 41 | color: var(--color-brand--1); 42 | } 43 | 44 | /* SIDEBAR */ 45 | .sidebar { 46 | flex-basis: 50rem; 47 | background-color: var(--color-dark--1); 48 | padding: 3rem 5rem 4rem 5rem; 49 | display: flex; 50 | flex-direction: column; 51 | border-top-left-radius: 15px; 52 | } 53 | 54 | .logo { 55 | height: 5.2rem; 56 | align-self: center; 57 | margin-bottom: 4rem; 58 | } 59 | 60 | .workouts { 61 | list-style: none; 62 | height: 77vh; 63 | overflow-y: scroll; 64 | overflow-x: hidden; 65 | position: relative; 66 | } 67 | 68 | .workouts::-webkit-scrollbar { 69 | width: 0; 70 | } 71 | 72 | .workout { 73 | background-color: var(--color-dark--2); 74 | border-radius: 5px; 75 | position: relative; 76 | padding: 1.5rem 2.25rem; 77 | margin-bottom: 1.75rem; 78 | cursor: pointer; 79 | display: grid; 80 | grid-template-columns: 1fr 1fr 1fr 1fr; 81 | gap: 0.75rem 1.5rem; 82 | 83 | } 84 | 85 | .workout:nth-of-type(1) { 86 | z-index: 2; 87 | } 88 | 89 | .workout-caption { 90 | background-image: linear-gradient(to bottom, #00345c6b, #00345c14); 91 | border-radius: 5px; 92 | padding: 2.5rem 2.25rem; 93 | margin-bottom: 1.75rem; 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | font-size: 1.3rem; 98 | position: absolute; 99 | text-align: center; 100 | top: 0; 101 | width: 100%; 102 | z-index: 1; 103 | } 104 | 105 | .workout--running { 106 | border-left: 5px solid var(--color-brand--2); 107 | } 108 | 109 | .workout--cycling { 110 | border-left: 5px solid var(--color-brand--1); 111 | } 112 | 113 | .workout__title { 114 | font-size: 1.7rem; 115 | font-weight: 600; 116 | grid-column: 1 / -1; 117 | } 118 | 119 | .workout__details { 120 | display: flex; 121 | align-items: baseline; 122 | justify-content: center; 123 | 124 | } 125 | 126 | .workout__icon { 127 | font-size: 1.8rem; 128 | margin-right: 0.2rem; 129 | height: 0.28rem; 130 | } 131 | 132 | .workout__value { 133 | font-size: 1.5rem; 134 | margin-right: 0.5rem; 135 | } 136 | 137 | .workout__unit { 138 | font-size: 1.1rem; 139 | color: var(--color-light--1); 140 | text-transform: uppercase; 141 | font-weight: 800; 142 | } 143 | 144 | .form { 145 | background-color: var(--color-dark--2); 146 | border-radius: 5px; 147 | padding: 1.5rem 2.75rem; 148 | margin-bottom: 1.75rem; 149 | display: grid; 150 | grid-template-columns: 1fr 1fr; 151 | gap: 0.5rem 1.5rem; 152 | position: relative; 153 | /* Match height and activity boxes */ 154 | height: 9.25rem; 155 | transition: all 0.5s, transform 1ms; 156 | z-index: 2; 157 | } 158 | 159 | .form.hidden { 160 | transform: translateY(-30rem); 161 | height: 0; 162 | padding: 0 2.25rem; 163 | margin-bottom: 0; 164 | } 165 | 166 | .form__row { 167 | display: flex; 168 | align-items: center; 169 | } 170 | 171 | .form__row--hidden { 172 | display: none; 173 | } 174 | 175 | .form__label { 176 | flex: 0 0 45%; 177 | font-size: 1.5rem; 178 | font-weight: 600; 179 | } 180 | 181 | .form__input { 182 | width: 100%; 183 | padding: 0.3rem .5rem; 184 | font-family: inherit; 185 | font-size: 1.4rem; 186 | border: none; 187 | border-radius: 3px; 188 | background-color: var(--color-light--3); 189 | transition: all 0.2s; 190 | } 191 | 192 | .form__input:focus { 193 | outline: none; 194 | background-color: #fff; 195 | } 196 | 197 | .form__btn { 198 | display: none; 199 | } 200 | 201 | .copyright { 202 | margin-top: 1rem; 203 | font-size: 1.3rem; 204 | text-align: center; 205 | color: var(--color-light--1); 206 | } 207 | 208 | .twitter-link:link, 209 | .twitter-link:visited { 210 | color: var(--color-light--1); 211 | transition: all 0.2s; 212 | } 213 | 214 | .twitter-link:hover, 215 | .twitter-link:active { 216 | color: var(--color-light--2); 217 | } 218 | 219 | /* MAP */ 220 | #map { 221 | flex: 1; 222 | height: 100%; 223 | background-color: var(--color-light--1); 224 | } 225 | 226 | /* Popup width is defined in JS using options */ 227 | .leaflet-popup .leaflet-popup-content-wrapper { 228 | background-color: var(--color-dark--1); 229 | color: var(--color-light--2); 230 | border-radius: 5px; 231 | padding-right: 0.6rem; 232 | } 233 | 234 | .leaflet-popup .leaflet-popup-content { 235 | font-size: 1.5rem; 236 | padding: 0; 237 | text-align: center; 238 | } 239 | 240 | .leaflet-popup .leaflet-popup-tip { 241 | background-color: var(--color-dark--1); 242 | } 243 | 244 | .running-popup .leaflet-popup-content-wrapper { 245 | border-left: 5px solid var(--color-brand--2); 246 | } 247 | 248 | .cycling-popup .leaflet-popup-content-wrapper { 249 | border-left: 5px solid var(--color-brand--1); 250 | } 251 | 252 | /* dropdown menu */ 253 | .icons li { 254 | background: none repeat scroll 0 0 var(--color-light--1); 255 | height: 4px; 256 | width: 4px; 257 | line-height: 0; 258 | list-style: none outside none; 259 | margin: 2px; 260 | vertical-align: top; 261 | border-radius: 50%; 262 | pointer-events: none; 263 | transition: all .3s; 264 | } 265 | 266 | .icons:hover li { 267 | background: none repeat scroll 0 0 var(--color-light--2); 268 | 269 | } 270 | 271 | .btn-left { 272 | left: 0.4em; 273 | } 274 | 275 | .btn-right { 276 | right: 1rem; 277 | } 278 | 279 | .btn-left, .btn-right { 280 | position: absolute; 281 | top: 1rem; 282 | } 283 | 284 | .dropbtn { 285 | display: flex; 286 | position: absolute; 287 | padding: .5rem; 288 | color: white; 289 | font-size: 16px; 290 | border: none; 291 | cursor: pointer; 292 | } 293 | 294 | .dropdown { 295 | position: absolute; 296 | display: inline-block; 297 | right: 0.4em; 298 | } 299 | 300 | .dropdown-content { 301 | right: 10px; 302 | position: absolute; 303 | margin-top: 33px; 304 | display: none; 305 | background-color: var(--color-dark--3); 306 | min-width: 145px; 307 | overflow: auto; 308 | box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); 309 | z-index: 4; 310 | transition: all .3s; 311 | border-radius: 5px; 312 | } 313 | 314 | .dropdown-content a { 315 | color: var(--color-light--2); 316 | font-size: 1.3rem; 317 | padding: 7px 9px; 318 | text-decoration: none; 319 | display: block; 320 | } 321 | 322 | .dropdown a:hover { 323 | background-color: var(--color-dark--2) 324 | } 325 | 326 | .show { 327 | display: block; 328 | } 329 | 330 | /* a.edit-comming-soon { 331 | background-color: orangered; 332 | } */ -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const form = document.querySelector(".form"); 4 | const containerWorkouts = document.querySelector(".workouts"); 5 | const inputType = document.querySelector(".form__input--type"); 6 | const inputDistance = document.querySelector(".form__input--distance"); 7 | const inputDuration = document.querySelector(".form__input--duration"); 8 | const inputCadence = document.querySelector(".form__input--cadence"); 9 | const inputElevation = document.querySelector(".form__input--elevation"); 10 | 11 | /////////////////////////////////////////////////////////////////////// 12 | 13 | ////////////// Workout class 14 | class Workout { 15 | date = new Date(); 16 | id = (Date.now() + "").slice(-10); 17 | constructor(distance, duration, coords) { 18 | this.distance = distance; 19 | this.duration = duration; 20 | this.coords = coords; 21 | } 22 | 23 | _setDiscription() { 24 | // prettier-ignore 25 | const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 26 | 27 | this.discription = `${this.type === "running" ? "Running" : "Cycling"}: ${ 28 | months[this.date.getMonth()] 29 | } ${this.date.getDate()}`; 30 | } 31 | } 32 | 33 | ////////////// Running class 34 | class Running extends Workout { 35 | type = "running"; 36 | constructor(distance, duration, coords, cadance) { 37 | super(distance, duration, coords); 38 | this.cadance = cadance; 39 | this._calcPace(); 40 | this._setDiscription(); 41 | } 42 | 43 | _calcPace() { 44 | this.pace = (this.duration / this.distance).toFixed(1); 45 | return this.pace; 46 | } 47 | } 48 | 49 | ////////////// Cycling class 50 | class Cycling extends Workout { 51 | type = "cycling"; 52 | constructor(distance, duration, coords, elevationGain) { 53 | super(distance, duration, coords); 54 | this.elevationGain = elevationGain; 55 | this._calcSpeed(); 56 | this._setDiscription(); 57 | } 58 | 59 | _calcSpeed() { 60 | this.speed = (this.distance / (this.duration / 60)).toFixed(1); 61 | return this.speed; 62 | } 63 | } 64 | 65 | ////////////// App class 66 | class App { 67 | #mapEvent; 68 | #map; 69 | #workouts = []; 70 | #mapZoomLevel = 11; 71 | #dropBtns; 72 | #btnsClear; 73 | #btnsDelete; 74 | #btnsEdit; 75 | constructor() { 76 | // Get data from local storage 77 | this._getLocalStorage(); 78 | this.#dropBtns = document.querySelectorAll(".dropbtn"); 79 | this.#btnsClear = document.querySelectorAll(".clear"); 80 | this.#btnsDelete = document.querySelectorAll(".delete"); 81 | this.#btnsEdit = document.querySelectorAll(".edit"); 82 | 83 | // Load map 84 | this._loadMap(); 85 | 86 | ///////////////////////////// 87 | // Atach event handlers 88 | 89 | form.addEventListener("submit", this._newWorkout.bind(this)); 90 | inputType.addEventListener("change", this._toggleElevationField); 91 | containerWorkouts.addEventListener("click", this._moveToPopup.bind(this)); 92 | 93 | // Open dropdowns by click on dropbtn 94 | this.#dropBtns.forEach((dropBtn) => { 95 | dropBtn.addEventListener("click", this._showDropdown); 96 | }); 97 | 98 | // Close dropdowns by click anywhere 99 | document.body.addEventListener("click", this._closeDropdowns, true); 100 | 101 | // Clear all workouts 102 | this.#btnsClear.forEach((clear) => 103 | clear.addEventListener("click", this._clearAllWorkouts.bind(this)) 104 | ); 105 | 106 | // Delete a workout 107 | this.#btnsDelete.forEach((d) => d.addEventListener("click", this._delete)); 108 | 109 | // Edit btn 110 | this.#btnsEdit.forEach((edit) => 111 | edit.addEventListener("mouseover", this._edit) 112 | ); 113 | this.#btnsEdit.forEach((edit) => 114 | edit.addEventListener("mouseout", this._editLeave) 115 | ); 116 | 117 | // check body width for caption text 118 | this._changeCaption(); 119 | } 120 | 121 | _loadMap() { 122 | const tehranCoords = [35.7114346, 51.3529667]; 123 | this.#map = L.map("map").setView(tehranCoords, this.#mapZoomLevel); 124 | 125 | L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 126 | attribution: 127 | '© OpenStreetMap contributors', 128 | }).addTo(this.#map); 129 | 130 | // handling click on map 131 | this.#map.on("click", this._showForm.bind(this)); 132 | 133 | this.#workouts.forEach((workout) => { 134 | this._renderWorkoutMarker(workout); 135 | }); 136 | 137 | if (this.#workouts.length >= 1) { 138 | const allCoords = this.#workouts.map((workout) => workout.coords); 139 | this.#map.fitBounds(allCoords); 140 | } 141 | } 142 | 143 | _showForm(e) { 144 | this.#mapEvent = e; 145 | form.classList.remove("hidden"); 146 | inputDistance.focus(); 147 | } 148 | 149 | _hideform() { 150 | inputDistance.value = ""; 151 | inputCadence.value = ""; 152 | inputDuration.value = ""; 153 | inputElevation.value = ""; 154 | inputDistance.blur(); 155 | inputCadence.blur(); 156 | inputDuration.blur(); 157 | inputElevation.blur(); 158 | 159 | form.style.display = "none"; 160 | form.classList.add("hidden"); 161 | setTimeout(() => { 162 | form.style.display = "grid"; 163 | }, 1000); 164 | } 165 | 166 | _toggleElevationField() { 167 | inputCadence.closest(".form__row").classList.toggle("form__row--hidden"); 168 | inputElevation.closest(".form__row").classList.toggle("form__row--hidden"); 169 | } 170 | 171 | _newWorkout(e) { 172 | e.preventDefault(); 173 | // get data from form 174 | const type = inputType.value; 175 | const duration = +inputDuration.value; 176 | const distance = +inputDistance.value; 177 | const { lat, lng } = this.#mapEvent.latlng; 178 | let workout; 179 | 180 | // if workout is running, create running object 181 | if (type === "running") { 182 | const cadance = +inputCadence.value; 183 | // check if data is valid 184 | if ( 185 | duration <= 0 || 186 | distance <= 0 || 187 | cadance <= 0 || 188 | !(duration + distance + cadance) 189 | ) 190 | return Swal.fire({ 191 | icon: "error", 192 | title: "Make sure your inputs are positive numbers!", 193 | background: "#2d3439", 194 | heightAuto: false, 195 | width: "auto", 196 | }); 197 | 198 | workout = new Running(distance, duration, [lat, lng], cadance); 199 | } 200 | 201 | // if workout is cycling, create cycling object 202 | if (type === "cycling") { 203 | const elevation = +inputElevation.value; 204 | // check if data is valid 205 | if (duration <= 0 || distance <= 0 || !(duration + distance + elevation)) 206 | return Swal.fire({ 207 | icon: "error", 208 | title: 209 | "Make sure your Duration and Distance inputs are positive numbers!", 210 | background: "#2d3439", 211 | heightAuto: false, 212 | width: "auto", 213 | }); 214 | 215 | workout = new Cycling(distance, duration, [lat, lng], elevation); 216 | } 217 | 218 | // render workout on map as marker 219 | this._renderWorkoutMarker(workout); 220 | 221 | // add neighbourhood name to the workout heading 222 | this._setNeighbourhood(workout); 223 | } 224 | 225 | async _setNeighbourhood(workout) { 226 | try { 227 | // timeout counter 228 | const timeout = function (sec) { 229 | return new Promise(function (_, reject) { 230 | setTimeout(function () { 231 | reject(new Error("Request took too long!")); 232 | }, sec * 1000); 233 | }); 234 | }; 235 | 236 | // check if api doesn't response in 3 seconds 237 | const res = await Promise.race([ 238 | fetch( 239 | `https://geocode.xyz/${workout.coords.at(0)},${workout.coords.at( 240 | 1 241 | )}?geoit=json&auth=20350830646274585843x105669 ` 242 | ), 243 | timeout(3), 244 | ]); 245 | 246 | const data = await res.json(); 247 | workout.neighbourhood = await data.osmtags.name; 248 | } catch (err) { 249 | console.error(err); 250 | } finally { 251 | // add new object to workouts array 252 | this.#workouts.push(workout); 253 | 254 | // render workout on the list 255 | this._renderWorkoutInList(workout); 256 | 257 | // hide form and clear inputs 258 | this._hideform(); 259 | 260 | // set locale storage 261 | this._setLocalStorage(); 262 | 263 | // update dropbtns 264 | this.#dropBtns = document.querySelectorAll(".dropbtn"); 265 | this.#dropBtns.forEach((dropBtn) => { 266 | dropBtn.addEventListener("click", this._showDropdown); 267 | }); 268 | 269 | // update Clear all workouts 270 | this.#btnsClear = document.querySelectorAll(".clear"); 271 | this.#btnsClear.forEach((clear) => 272 | clear.addEventListener("click", this._clearAllWorkouts.bind(this)) 273 | ); 274 | 275 | // update delete a workout 276 | this.#btnsDelete = document.querySelectorAll(".delete"); 277 | this.#btnsDelete.forEach((d) => 278 | d.addEventListener("click", this._delete) 279 | ); 280 | 281 | // update Edit btn 282 | this.#btnsEdit = document.querySelectorAll(".edit"); 283 | this.#btnsEdit.forEach((edit) => 284 | edit.addEventListener("mouseover", this._edit) 285 | ); 286 | this.#btnsEdit.forEach((edit) => 287 | edit.addEventListener("mouseout", this._editLeave) 288 | ); 289 | } 290 | } 291 | 292 | _renderWorkoutMarker(workout) { 293 | var myIcon = L.icon({ 294 | iconUrl: "https://unpkg.com/leaflet@1.8.0/dist/images/marker-icon-2x.png", 295 | iconSize: [25, 41], 296 | iconAnchor: [24, 44], 297 | popupAnchor: [-12, -41], 298 | className: `${workout.id}`, 299 | }); 300 | L.marker(workout.coords, { icon: myIcon }) 301 | .addTo(this.#map) 302 | .bindPopup( 303 | L.popup({ 304 | maxWidth: 350, 305 | minWidth: 100, 306 | autoClose: true, 307 | closeOnClick: false, 308 | className: `${workout.type}-popup ${workout.id}`, 309 | }) 310 | ) 311 | .setPopupContent( 312 | `${workout.type === "running" ? "🏃" : "🚴‍♀️"} ${workout.discription}` 313 | ) 314 | .openPopup(); 315 | } 316 | 317 | _renderWorkoutInList(workout) { 318 | let html = ` 319 |
  • 320 | 335 |

    336 | ${workout.discription}  ${ 337 | workout.neighbourhood ? workout.neighbourhood : "" 338 | } 339 |

    340 |
    341 | ${ 342 | workout.type === "running" ? "🏃" : "🚴‍♀️" 343 | } 344 | ${workout.distance} 345 | km 346 |
    347 |
    348 | 349 | ${workout.duration} 350 | min 351 |
    `; 352 | 353 | if (workout.type === "running") { 354 | html += ` 355 |
    356 | ⚡️ 357 | ${workout.pace} 358 | min/km 359 |
    360 |
    361 | 🦶🏼 362 | ${workout.cadance} 363 | spm 364 |
    365 |
  • 366 | `; 367 | } else { 368 | html += ` 369 |
    370 | ⚡️ 371 | ${workout.speed} 372 | km/h 373 |
    374 |
    375 | 📈 376 | ${workout.elevationGain} 377 | m 378 |
    379 | 380 | `; 381 | } 382 | 383 | form.insertAdjacentHTML("afterend", html); 384 | form.classList.add("hidden"); 385 | } 386 | 387 | _moveToPopup(e) { 388 | const workoutEl = e.target.closest(".workout"); 389 | 390 | if (!workoutEl) return; 391 | 392 | const workout = this.#workouts.find( 393 | (workout) => workout.id === workoutEl.dataset.id 394 | ); 395 | 396 | this.#map.setView(workout.coords, this.#mapZoomLevel, { 397 | animate: true, 398 | pan: { 399 | duration: 1, 400 | }, 401 | }); 402 | } 403 | 404 | _setLocalStorage() { 405 | localStorage.setItem("workouts", JSON.stringify(this.#workouts)); 406 | } 407 | 408 | _getLocalStorage() { 409 | const data = JSON.parse(localStorage.getItem("workouts")); 410 | 411 | if (!data) return; 412 | 413 | this.#workouts = data; 414 | this.#workouts.forEach((workout) => { 415 | this._renderWorkoutInList(workout); 416 | }); 417 | } 418 | 419 | _showDropdown() { 420 | this.nextElementSibling.classList.toggle("show"); 421 | } 422 | 423 | _closeDropdowns(e) { 424 | if (!e.target.matches(".edit")) 425 | document 426 | .querySelectorAll(".dropdown-content") 427 | .forEach((d) => d.classList.remove("show")); 428 | } 429 | 430 | _clearAllWorkouts() { 431 | localStorage.removeItem("workouts"); 432 | const workoutElements = [...document.querySelectorAll(".workout")]; 433 | const popups = [...document.querySelectorAll(".leaflet-popup")]; 434 | const markers = [...document.querySelectorAll(".leaflet-marker-icon")]; 435 | // const shadows = [...document.querySelectorAll(".leaflet-shadow-pane")]; 436 | popups.forEach((popup) => (popup.style.display = "none")); 437 | markers.forEach((marker) => (marker.style.display = "none")); 438 | workoutElements.forEach((workout) => (workout.style.display = "none")); 439 | // shadows.forEach((shadow) => (shadow.style.display = "none")); 440 | this.#workouts = []; 441 | localStorage.setItem("workouts", JSON.stringify(this.#workouts)); 442 | } 443 | 444 | _delete() { 445 | app.#workouts = app.#workouts.filter( 446 | (workout) => workout.id != this.closest(".workout").dataset.id 447 | ); 448 | localStorage.setItem("workouts", JSON.stringify(app.#workouts)); 449 | this.closest(".workout").remove(); 450 | 451 | const popups = [...document.querySelectorAll(".leaflet-popup")]; 452 | const markers = [...document.querySelectorAll(".leaflet-marker-icon")]; 453 | 454 | popups.forEach((popup) => { 455 | if (popup.classList.contains(`${this.closest(".workout").dataset.id}`)) { 456 | popup.style.display = "none"; 457 | } 458 | }); 459 | 460 | markers.forEach((marker) => { 461 | if (marker.classList.contains(`${this.closest(".workout").dataset.id}`)) { 462 | marker.style.display = "none"; 463 | } 464 | }); 465 | } 466 | 467 | _edit() { 468 | this.textContent = `Comming soon ...`; 469 | this.style.fontStyle = "italic"; 470 | } 471 | 472 | _editLeave() { 473 | this.textContent = `Edit`; 474 | this.style.fontStyle = "inherit"; 475 | } 476 | 477 | _changeCaption() { 478 | if (document.querySelector("body").clientWidth <= 1200) { 479 | document.querySelector( 480 | ".workout-caption" 481 | ).innerHTML = `Touch the map to save your workout details.
    By reloading the page, you won't lose them!`; 482 | } 483 | } 484 | } 485 | 486 | const app = new App(); 487 | 488 | /////////////////////////////////////// 489 | // fetch( 490 | // "https://geocode.xyz/35.671031299865824,51.05498711253165?geoit=json&auth=722216078602093542842x107409" 491 | // ) 492 | // .then((res) => res.json()) 493 | // .then((data) => console.log(data)); 494 | --------------------------------------------------------------------------------