├── 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 |
--------------------------------------------------------------------------------