From 5e19a4e8bb179bbcd099d5485927d85b3569fe69 Mon Sep 17 00:00:00 2001 From: Christian Beutel <> Date: Thu, 29 Feb 2024 23:26:18 +0100 Subject: [PATCH] adds i18n --- .gitignore | 5 +- search/init.py | 43 +++ web/package-lock.json | 278 ++++++++++++-- web/package.json | 1 + web/src/hooks.client.ts | 6 +- web/src/hooks.server.ts | 18 +- web/src/lib/components/base/modal.svelte | 4 +- web/src/lib/components/confirm_modal.svelte | 15 +- .../empty_states/empty_state_search.svelte | 3 +- web/src/lib/components/footer.svelte | 16 +- web/src/lib/components/list/list_card.svelte | 9 +- web/src/lib/components/list/list_modal.svelte | 25 +- .../components/list/list_select_modal.svelte | 11 +- web/src/lib/components/nav_bar.svelte | 46 ++- web/src/lib/components/photo_card.svelte | 5 +- .../summit_log/summit_log_card.svelte | 11 +- .../summit_log/summit_log_modal.svelte | 20 +- .../trail/map_with_elevation.svelte | 111 ++++++ .../lib/components/trail/trail_card.svelte | 6 +- .../components/trail/trail_dropdown.svelte | 136 +++++++ .../trail/trail_filter_panel.svelte | 35 +- .../lib/components/trail/trail_list.svelte | 18 +- .../components/trail/trail_list_item.svelte | 6 +- .../components/waypoint/waypoint_card.svelte | 11 +- .../components/waypoint/waypoint_modal.svelte | 20 +- web/src/lib/i18n/index.ts | 14 + web/src/lib/i18n/locales/de.json | 96 +++++ web/src/lib/i18n/locales/en.json | 96 +++++ web/src/lib/stores/user_store.ts | 22 +- web/src/lib/util/authorization_util.ts | 10 + web/src/lib/util/format_util.ts | 35 +- .../vendor/leaflet-elevation/src/control.js | 361 +++++++++--------- .../vendor/leaflet-elevation/src/index.css | 17 +- web/src/routes/+layout.svelte | 13 +- web/src/routes/+layout.ts | 8 + web/src/routes/+page.svelte | 27 +- web/src/routes/lists/+page.svelte | 28 +- web/src/routes/login/+page.svelte | 16 +- web/src/routes/map/+page.svelte | 23 +- web/src/routes/map/trail/[id]/+page.svelte | 157 ++++---- web/src/routes/profile/+page.svelte | 152 ++++++-- web/src/routes/register/+page.svelte | 20 +- web/src/routes/trail/edit/[id]/+page.svelte | 53 ++- web/src/routes/trail/view/[id]/+page.svelte | 145 ++----- web/src/routes/trails/+page.svelte | 6 +- 45 files changed, 1520 insertions(+), 638 deletions(-) create mode 100644 search/init.py create mode 100644 web/src/lib/components/trail/map_with_elevation.svelte create mode 100644 web/src/lib/components/trail/trail_dropdown.svelte create mode 100644 web/src/lib/i18n/index.ts create mode 100644 web/src/lib/i18n/locales/de.json create mode 100644 web/src/lib/i18n/locales/en.json create mode 100644 web/src/lib/util/authorization_util.ts create mode 100644 web/src/routes/+layout.ts diff --git a/.gitignore b/.gitignore index 3e8bbfc0..2a2442b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .DS_Store .vscode/ -search/ \ No newline at end of file + +search/meilisearch +search/data.ms +search/dumps \ No newline at end of file diff --git a/search/init.py b/search/init.py new file mode 100644 index 00000000..0d9c0f49 --- /dev/null +++ b/search/init.py @@ -0,0 +1,43 @@ +from meilisearch import Client +import json + +MEILISEARCH_URL='http://localhost:7700' +MEILISEARCH_API_KEY='p2gYZAWODOrwTPr4AYoahCZ9CI8y9bUd0yQLGk-E3m8' + +client = Client(MEILISEARCH_URL, MEILISEARCH_API_KEY) + +def init(): + client.create_index('cities500', {'primaryKey': 'id'}) + + client.index('cities500').update_settings({ + 'sortableAttributes': ['_geo',], + 'filterableAttributes': ['_geo'] + }) + + client.create_index('trails', {'primaryKey': 'id'}) + + client.index('trails').update_settings({ + 'sortableAttributes': ['name', 'distance', 'elevation_gain', 'created',], + 'filterableAttributes': ['category', 'distance', 'elevation_gain', 'completed', '_geo', 'public', 'author'] + }) + + + json_file = open('cities500.json', encoding='utf-8') + cities = json.load(json_file) + + client.index('cities500').add_documents(cities) + +def generate_public_token(): + search_key = client.get_keys().results[0] + + search_rules = { + 'trails': { + 'filter': 'public=true' + }, + 'cities500': {} + } + token = client.generate_tenant_token(api_key_uid=search_key.uid, search_rules=search_rules, api_key=search_key.key) + + print(token) + +generate_public_token() \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 4487a15b..7de68033 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,6 +22,7 @@ "nouislider": "^15.7.1", "photoswipe": "^5.4.3", "pocketbase": "^0.21.0", + "svelte-i18n": "^4.0.0", "three": "^0.161.0", "yup": "^1.3.3" }, @@ -74,7 +75,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -90,7 +90,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -106,7 +105,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -122,7 +120,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -138,7 +135,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -154,7 +150,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -170,7 +165,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -186,7 +180,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -202,7 +195,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -218,7 +210,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -234,7 +225,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -250,7 +240,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -266,7 +255,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -282,7 +270,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -298,7 +285,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -314,7 +300,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -330,7 +315,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -346,7 +330,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -362,7 +345,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -378,7 +360,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -394,7 +375,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -410,7 +390,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -426,7 +405,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -435,6 +413,50 @@ "node": ">=12" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz", + "integrity": "sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-skeleton-parser": "1.8.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz", + "integrity": "sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@fortawesome/fontawesome-free": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", @@ -1328,6 +1350,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1442,6 +1479,15 @@ "node": ">=4" } }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "node_modules/date-fns": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", @@ -1484,7 +1530,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1551,17 +1596,61 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "dev": true }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -1610,6 +1699,25 @@ "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", "dev": true }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1618,6 +1726,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -1641,6 +1758,19 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1797,14 +1927,12 @@ "node_modules/globalyzer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "node_modules/graceful-fs": { "version": "4.2.11", @@ -1875,6 +2003,17 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/intl-messageformat": { + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "tslib": "^2.4.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1938,6 +2077,11 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -2075,6 +2219,14 @@ "node": "14 || >=16.14" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -2099,6 +2251,21 @@ "cross-fetch": "^3.1.6" } }, + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2216,7 +2383,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "engines": { "node": ">=4" } @@ -2265,6 +2431,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2891,7 +3062,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "dependencies": { "mri": "^1.1.0" }, @@ -3275,6 +3445,34 @@ "svelte": "^3.19.0 || ^4.0.0" } }, + "node_modules/svelte-i18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.0.tgz", + "integrity": "sha512-4vivjKZADUMRIhTs38JuBNy3unbnh9AFRxWFLxq62P4NHic+/BaIZZlAsvqsCdnp7IdJf5EoSiH6TNdItcjA6g==", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^10.5.3", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "svelte": "^3 || ^4" + } + }, + "node_modules/svelte-i18n/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/svelte-preprocess": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", @@ -3421,6 +3619,15 @@ "three": ">= 0.151.0" } }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, "node_modules/tiny-case": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", @@ -3430,7 +3637,6 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" @@ -3527,8 +3733,12 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, "node_modules/type-detect": { "version": "4.0.8", diff --git a/web/package.json b/web/package.json index 4f3777d7..5758f012 100644 --- a/web/package.json +++ b/web/package.json @@ -45,6 +45,7 @@ "nouislider": "^15.7.1", "photoswipe": "^5.4.3", "pocketbase": "^0.21.0", + "svelte-i18n": "^4.0.0", "three": "^0.161.0", "yup": "^1.3.3" } diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts index 78ecc118..19ea1fe5 100644 --- a/web/src/hooks.client.ts +++ b/web/src/hooks.client.ts @@ -1,8 +1,8 @@ import { pb } from '$lib/pocketbase' -import { currentUser } from '$lib/stores/user_store' +import { currentUser, type User } from '$lib/stores/user_store' pb.authStore.loadFromCookie(document.cookie) pb.authStore.onChange(() => { - currentUser.set(pb.authStore.model) + currentUser.set(pb.authStore.model as User) document.cookie = pb.authStore.exportToCookie({ httpOnly: false }) -}, true) \ No newline at end of file +}, true) diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts index cd4d7e2e..ba4491a8 100644 --- a/web/src/hooks.server.ts +++ b/web/src/hooks.server.ts @@ -1,10 +1,20 @@ import { regenerateInstance } from '$lib/meilisearch' import { pb } from '$lib/pocketbase' -import type { Handle } from '@sveltejs/kit' +import { isRouteProtected } from '$lib/util/authorization_util' +import { redirect, type Handle } from '@sveltejs/kit' +import { locale } from 'svelte-i18n' export const handle: Handle = async ({ event, resolve }) => { // load the store data from the request cookie string pb.authStore.loadFromCookie(event.request.headers.get('cookie') || '') + + const url = new URL(event.request.url); + + // validate the user existence and if the path is acceesible + if (!pb.authStore.model && isRouteProtected(url.pathname)) { + throw redirect(302, '/login'); + } + regenerateInstance(); try { // get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any) @@ -19,6 +29,12 @@ export const handle: Handle = async ({ event, resolve }) => { event.locals.pb = pb event.locals.user = pb.authStore.model + const lang = pb.authStore.model?.language ?? event.request.headers.get('accept-language')?.split(',')[0] + + if (lang) { + locale.set(lang) + } + const response = await resolve(event) // send back the default 'pb_auth' cookie to the client with the latest store state diff --git a/web/src/lib/components/base/modal.svelte b/web/src/lib/components/base/modal.svelte index 63b48482..78678c12 100644 --- a/web/src/lib/components/base/modal.svelte +++ b/web/src/lib/components/base/modal.svelte @@ -1,7 +1,7 @@ @@ -10,5 +11,5 @@ alt="Empty State showing a wanderer going into the distance" /> -

No results found

+

{$_("no-results")}

diff --git a/web/src/lib/components/footer.svelte b/web/src/lib/components/footer.svelte index 15fcd764..09e178ba 100644 --- a/web/src/lib/components/footer.svelte +++ b/web/src/lib/components/footer.svelte @@ -1,5 +1,7 @@ diff --git a/web/src/lib/components/list/list_card.svelte b/web/src/lib/components/list/list_card.svelte index 69817d54..b775e80e 100644 --- a/web/src/lib/components/list/list_card.svelte +++ b/web/src/lib/components/list/list_card.svelte @@ -1,12 +1,13 @@ @@ -21,7 +22,9 @@ alt="avatar" /> {:else} -
+
{/if} diff --git a/web/src/lib/components/list/list_modal.svelte b/web/src/lib/components/list/list_modal.svelte index 7a993bcf..20e25515 100644 --- a/web/src/lib/components/list/list_modal.svelte +++ b/web/src/lib/components/list/list_modal.svelte @@ -6,10 +6,10 @@ import { list } from "$lib/stores/list_store"; import { createForm } from "$lib/vendor/svelte-form-lib/index"; import { util } from "$lib/vendor/svelte-form-lib/util"; + import { _ } from "svelte-i18n"; import Modal from "../base/modal.svelte"; import TextField from "../base/text_field.svelte"; import Textarea from "../base/textarea.svelte"; - export let openModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined; @@ -37,9 +37,8 @@ } function handleAvatarSelection() { - const files = ( - document.getElementById("avatar") as HTMLInputElement - ).files; + const files = (document.getElementById("avatar") as HTMLInputElement) + .files; if (!files) { return; @@ -55,7 +54,7 @@ - + Change...{$_('change')}...
- - + +
diff --git a/web/src/lib/components/list/list_select_modal.svelte b/web/src/lib/components/list/list_select_modal.svelte index 8efd7f22..4186da3b 100644 --- a/web/src/lib/components/list/list_select_modal.svelte +++ b/web/src/lib/components/list/list_select_modal.svelte @@ -1,10 +1,11 @@
-
{format(log.date, 'dd.MM.yyyy')}
+
{format(log.date, "dd.MM.yyyy")}
{#if mode == "edit"} diff --git a/web/src/lib/components/summit_log/summit_log_modal.svelte b/web/src/lib/components/summit_log/summit_log_modal.svelte index f73c4f5f..6f88a53d 100644 --- a/web/src/lib/components/summit_log/summit_log_modal.svelte +++ b/web/src/lib/components/summit_log/summit_log_modal.svelte @@ -4,11 +4,11 @@ import { summitLogSchema, type SummitLog } from "$lib/models/summit_log"; import { summitLog } from "$lib/stores/summit_log_store"; import { createForm } from "$lib/vendor/svelte-form-lib/index"; + import { util } from "$lib/vendor/svelte-form-lib/util"; + import { _ } from "svelte-i18n"; import Datepicker from "../base/datepicker.svelte"; import Modal from "../base/modal.svelte"; import TextField from "../base/text_field.svelte"; - import { util } from "$lib/vendor/svelte-form-lib/util"; - export let openModal: (() => void) | undefined = undefined; export let closeModal: (() => void) | undefined = undefined; @@ -17,17 +17,17 @@ const { form, errors, handleChange, handleSubmit } = createForm({ initialValues: $summitLog, validationSchema: summitLogSchema, - onSubmit: async (submittedValues) => { + onSubmit: async (submittedValues) => { dispatch("save", submittedValues); closeModal!(); }, }); - $: form.set(util.cloneDeep($summitLog)); + $: form.set(util.cloneDeep($summitLog));
- + {$_("save")}
diff --git a/web/src/lib/components/trail/map_with_elevation.svelte b/web/src/lib/components/trail/map_with_elevation.svelte new file mode 100644 index 00000000..20d967c3 --- /dev/null +++ b/web/src/lib/components/trail/map_with_elevation.svelte @@ -0,0 +1,111 @@ + + +
+
+
+
+ + diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index 331836a0..025242b5 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -1,7 +1,7 @@ + + handleDropdownClick(e.detail)} + let:toggleMenu={openDropdown} + > + + + handleListSelection(e.detail)} +> diff --git a/web/src/lib/components/trail/trail_filter_panel.svelte b/web/src/lib/components/trail/trail_filter_panel.svelte index 07b7d0ab..15ae22f0 100644 --- a/web/src/lib/components/trail/trail_filter_panel.svelte +++ b/web/src/lib/components/trail/trail_filter_panel.svelte @@ -3,7 +3,7 @@ import type { Category } from "$lib/models/category"; import type { TrailFilter } from "$lib/models/trail"; import { country_codes } from "$lib/util/country_code_util"; - import { formatMeters } from "$lib/util/format_util"; + import { formatDistance, formatElevation } from "$lib/util/format_util"; import { createEventDispatcher } from "svelte"; import DoubleSlider from "../base/double_slider.svelte"; import type { RadioItem } from "../base/radio_group.svelte"; @@ -11,6 +11,7 @@ import Search, { type SearchItem } from "../base/search.svelte"; import Slider from "../base/slider.svelte"; import { slide } from "svelte/transition"; + import { _ } from "svelte-i18n"; export let categories: Category[]; export let filterExpanded: boolean = true; @@ -33,9 +34,9 @@ const dispatch = createEventDispatcher(); const radioGroupItems: RadioItem[] = [ - { text: "Completed", value: "completed" }, - { text: "Not completed", value: "not_completed" }, - { text: "No preference", value: "no_preference" }, + { text: $_("completed"), value: "completed" }, + { text: $_("not-completed"), value: "not_completed" }, + { text: $_("no-preference"), value: "no_preference" }, ]; let searchDropdownItems: SearchItem[] = []; @@ -112,7 +113,7 @@
+ {$_("save")}
diff --git a/web/src/lib/i18n/index.ts b/web/src/lib/i18n/index.ts new file mode 100644 index 00000000..a90f0130 --- /dev/null +++ b/web/src/lib/i18n/index.ts @@ -0,0 +1,14 @@ +import { browser } from '$app/environment' +import { currentUser } from '$lib/stores/user_store' +import { init, register } from 'svelte-i18n' +import { get } from 'svelte/store' + +const defaultLocale = 'en' + +register('en', () => import('./locales/en.json')) +register('de', () => import('./locales/de.json')) + +init({ + fallbackLocale: defaultLocale, + initialLocale: browser ? get(currentUser)?.language ?? window.navigator.language : defaultLocale, +}) \ No newline at end of file diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json new file mode 100644 index 00000000..d89c3a3d --- /dev/null +++ b/web/src/lib/i18n/locales/de.json @@ -0,0 +1,96 @@ +{ + "about": "Über", + "account-delete-confirm": "Du bist dabei, dein Konto zu löschen. Alle deine Routen werden ebenfalls gelöscht. Möchtest du fortfahren?", + "add-entry": "Eintrag hinzufügen", + "add-to-list": "Zu Liste hinzufügen", + "add-waypoint": "Wegpunkt hinzufügen", + "alphabetical": "Alphabetisch", + "already-account": "Du hast bereits ein Konto?", + "avatar": "Avatar", + "basic-info": "Basisinformation", + "cancel": "Abbrechen", + "card": "{n, plural, =1 {Karte} other {Karten}}", + "categories": "Kategorien", + "category": "Kategorie", + "change": "Ändern", + "changelog": "Changelog", + "completed": "Abgeschlossen", + "contribute": "Mitwirken", + "create-new-list": "Neue Liste erstellen", + "creation-date": "Erstellungsdatum", + "danger-zone": "Gefahrenzone", + "date": "Datum", + "default-location": "Standort", + "delete": "Löschen", + "delete-account": "Konto löschen", + "delete-list-confirm": "Möchten Sie diese Liste wirklich löschen? Die Routen in der Liste sind danach weiterhin verfügbar.", + "delete-trail-confirm": "Möchtest du diese Route wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "describe-your-trail": "Beschreibe deine Route", + "description": "Beschreibung", + "directions": "Wegbeschreibung", + "display-as": "Anzeigen als", + "distance": "Distanz", + "download-gpx": "GPX herunterladen", + "edit": "Bearbeiten", + "edit-entry": "Eintrag bearbeiten", + "edit-list": "Liste bearbeiten", + "edit-waypoint": "Wegpunkt bearbeiten", + "elevation-gain": "Höhenunterschied", + "email": "Email", + "english": "Englisch", + "est-duration": "Gesch. Dauer", + "explore": "Erkunden", + "features": "Feature", + "german": "Deutsch", + "hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!", + "hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.", + "hero_section_2_text": "Wusstest due schon? Du kannst nicht nur deine Wanderwege speichern. Es gibt viele Kategorien für alle deine Abenteuer.", + "icon": "Icon", + "imperial": "Amerikanisch", + "language": "Sprache", + "latitude": "Breitengrad", + "license": "Lizenz", + "list": "{n, plural, =1 {Liste} other {Listen}}", + "location": "Standort", + "login": "Login", + "logout": "Logout", + "longitude": "Längengrad", + "make-one": "Neues erstellen!", + "make-thumbnail": "Thumbnail festlegen", + "map": "Karte", + "metric": "Metrisch", + "name": "Name", + "near": "Nahe", + "new-list": "Neue Liste", + "new-trail": "Neue Route", + "no-account": "Du hast noch kein Konto?", + "no-preference": "Keine Präferenz", + "no-results": "Keine Ergebnisse gefunden", + "not-completed": "Nicht abgeschlossen", + "password": "Passwort", + "photos": "Fotos", + "pick-a-trail": "Route auswählen", + "profile": "Profil", + "public": "Öffentlich", + "radius": "Radius", + "register": "Registrieren", + "save": "Speichern", + "save-trail": "Route speichern", + "search-cities": "Städte suchen", + "search-for-trails-places": "Suche nach Wegen, Orten", + "search-trails": "Route suchen", + "select-list": "Liste auswählen", + "settings": "Einstellungen", + "show-in-overview": "In der Übersicht anzeigen", + "show-on-map": "Auf der Karte anzeigen", + "slogan": "Speichere deine Abenteuer!", + "sort": "Sortieren", + "summit-book": "Gipfelbuch", + "text": "Text", + "trail": "{n, plural, =1 {Route} other {Routen}}", + "units": "Einheiten", + "upload-gpx": "GPX hochladen", + "username": "Nutzername", + "waypoints": "Wegpunkte", + "welcome_to": "Willkommen bei" +} diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json new file mode 100644 index 00000000..5f586c09 --- /dev/null +++ b/web/src/lib/i18n/locales/en.json @@ -0,0 +1,96 @@ +{ + "about": "About", + "account-delete-confirm": "You are about to delete your account. All your trails will also be deleted. Do you want to proceed?", + "add-entry": "Add Entry", + "add-to-list": "Add to list", + "add-waypoint": "Add Waypoint", + "alphabetical": "Alphabetical", + "already-account": "Already have an account?", + "avatar": "Avatar", + "basic-info": "Basic Info", + "cancel": "Cancel", + "card": "{n, plural, =1 {Card} other {Cards}}", + "categories": "Categories", + "category": "Category", + "change": "Change", + "changelog": "Changelog", + "completed": "Completed", + "contribute": "Contribute", + "create-new-list": "Create new list", + "creation-date": "Creation date", + "danger-zone": "Danger zone", + "date": "Date", + "default-location": "Default Location", + "delete": "Delete", + "delete-account": "Delete Account", + "delete-list-confirm": "Do you really want to delete this list? The trails in the list will still be available.", + "delete-trail-confirm": "Do you really want to delete this trail? This action cannot be undone.", + "describe-your-trail": "Describe your trail", + "description": "Description", + "directions": "Directions", + "display-as": "Display as", + "distance": "Distance", + "download-gpx": "Download GPX", + "edit": "Edit", + "edit-entry": "Edit Entry", + "edit-list": "Edit List", + "edit-waypoint": "Edit Waypoint", + "elevation-gain": "Elevation Gain", + "email": "Email", + "english": "English", + "est-duration": "Est. duration", + "explore": "Explore", + "features": "Features", + "german": "German", + "hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!", + "hero_section_1_text": "Here are some trails you might like. Or you can just go to the full list right now.", + "hero_section_2_text": "Did you know? You cannot only save you hiking trails. There are many categories for all your adventures.", + "icon": "Icon", + "imperial": "Imperial", + "language": "Language", + "latitude": "Latitude", + "license": "License", + "list": "{n, plural, =1 {List} other {Lists}}", + "location": "Location", + "login": "Login", + "logout": "Logout", + "longitude": "Longitude", + "make-one": "Make one!", + "make-thumbnail": "Make thumbnail", + "map": "Map", + "metric": "Metric", + "name": "Name", + "near": "Near", + "new-list": "New List", + "new-trail": "New Trail", + "no-account": "Don't have an account?", + "no-preference": "No preference", + "no-results": "No results found", + "not-completed": "Not completed", + "password": "Password", + "photos": "Photos", + "pick-a-trail": "Pick a trail", + "profile": "Profile", + "public": "Public", + "radius": "Radius", + "register": "Register", + "save": "Save", + "save-trail": "Save Trail", + "search-cities": "Search cities", + "search-for-trails-places": "Search for trails, places", + "search-trails": "Search trails", + "select-list": "Select List", + "settings": "Settings", + "show-in-overview": "Show in overview", + "show-on-map": "Show on map", + "slogan": "Save your adventures!", + "sort": "Sort", + "summit-book": "Summit Book", + "text": "Text", + "trail": "{n, plural, =1 {Trail} other {Trails}}", + "units": "Units", + "upload-gpx": "Upload GPX", + "username": "Username", + "waypoints": "Waypoints", + "welcome_to": "Welcome to" +} diff --git a/web/src/lib/stores/user_store.ts b/web/src/lib/stores/user_store.ts index 196423bc..3b640e7d 100644 --- a/web/src/lib/stores/user_store.ts +++ b/web/src/lib/stores/user_store.ts @@ -1,15 +1,19 @@ import { regenerateInstance } from "$lib/meilisearch"; import { pb } from "$lib/pocketbase"; -import { type AuthModel } from 'pocketbase'; import { writable, type Writable } from "svelte/store"; export type User = { + id: string, username?: string, email?: string, password: string, + avatar?: string; + unit?: "metric" | "imperial"; + language?: "en" | "de"; + location?: {name: string, lat: number, lon: number} } -export const currentUser: Writable = writable() +export const currentUser: Writable = writable() export async function users_create(user: User) { const model = await pb.collection('users').create({ ...user, passwordConfirm: user.password }); @@ -25,4 +29,18 @@ export async function login(user: User) { export async function logout() { pb.authStore.clear(); regenerateInstance() +} + +export async function users_update(id: string, user: User | { [K in keyof User]?: User[K] } | FormData) { + let model = await pb + .collection("users") + .update(id, user); + + currentUser.set(model); +} + +export async function users_delete(user: User) { + let success = await pb + .collection("users") + .delete(user.id); } \ No newline at end of file diff --git a/web/src/lib/util/authorization_util.ts b/web/src/lib/util/authorization_util.ts new file mode 100644 index 00000000..f167c1a9 --- /dev/null +++ b/web/src/lib/util/authorization_util.ts @@ -0,0 +1,10 @@ +const privateRoutes = [ + "/profile", + "/lists" + ] + + export function isRouteProtected(path: string) { + return privateRoutes.some(allowedPath => + path === allowedPath || path.startsWith(allowedPath + '/') + ); + } \ No newline at end of file diff --git a/web/src/lib/util/format_util.ts b/web/src/lib/util/format_util.ts index ab28723c..55dcc2bb 100644 --- a/web/src/lib/util/format_util.ts +++ b/web/src/lib/util/format_util.ts @@ -1,3 +1,5 @@ +import { currentUser } from "$lib/stores/user_store"; +import { get } from "svelte/store"; export function formatTimeHHMM(minutes?: number) { if (!minutes) { @@ -10,14 +12,39 @@ export function formatTimeHHMM(minutes?: number) { return (h < 10 ? "0" : "") + h.toString() + "h " + (m < 10 ? "0" : "") + Math.round(m).toString() + "m"; } -export function formatMeters(meters?: number) { +export function formatDistance(meters?: number) { if (meters === undefined) { return "-"; } - if (meters % 1 === 0) { - return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`; + + const unit = get(currentUser)?.unit ?? "metric"; + + if (unit == "metric") { + if (meters % 1 === 0) { + return meters >= 1000 ? `${(meters / 1000)} km` : `${meters} m`; + } else { + return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${Math.round(meters)} m` + } } else { - return meters >= 1000 ? `${(meters / 1000).toFixed(2)} km` : `${Math.round(meters)} m` + const miles = meters * 0.000621371; + const roundedMiles = miles.toFixed(2); + + return `${roundedMiles} mi`; + } +} + +export function formatElevation(meters?: number) { + if (meters === undefined) { + return "-"; } + const unit = get(currentUser)?.unit ?? "metric"; + + if (unit == "metric") { + return `${Math.round(meters)} m` + } else { + const feet = meters * 3.28084; + + return `${Math.round(feet)} ft`; + } } diff --git a/web/src/lib/vendor/leaflet-elevation/src/control.js b/web/src/lib/vendor/leaflet-elevation/src/control.js index 27ecf779..12c3847a 100644 --- a/web/src/lib/vendor/leaflet-elevation/src/control.js +++ b/web/src/lib/vendor/leaflet-elevation/src/control.js @@ -1,4 +1,4 @@ -import * as _ from './utils'; +import * as _ from './utils'; import { Options } from './options'; // "leaflet-i18n" fallback @@ -11,20 +11,20 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events, options: Options, - __mileFactor: 0.621371, // 1 km = (0.621371 mi) - __footFactor: 3.28084, // 1 m = (3.28084 ft) - __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js', - __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.2/dist/togeojson.umd.js', - __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js', - __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js', - __LHOTLINE: '../libs/leaflet-hotline.min.js', - __LDISTANCEM: '../libs/leaflet-distance-marker.min.js', - __LEDGESCALE: '../libs/leaflet-edgescale.min.js', - __LCHART: '../src/components/chart.js', - __LMARKER: '../src/components/marker.js', - __LSUMMARY: '../src/components/summary.js', + __mileFactor: 0.621371, // 1 km = (0.621371 mi) + __footFactor: 3.28084, // 1 m = (3.28084 ft) + __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js', + __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.2/dist/togeojson.umd.js', + __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js', + __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js', + __LHOTLINE: '../libs/leaflet-hotline.min.js', + __LDISTANCEM: '../libs/leaflet-distance-marker.min.js', + __LEDGESCALE: '../libs/leaflet-edgescale.min.js', + __LCHART: '../src/components/chart.js', + __LMARKER: '../src/components/marker.js', + __LSUMMARY: '../src/components/summary.js', __modulesFolder: '../src/handlers/', - __btnIcon: '../images/elevation.svg', + __btnIcon: '../images/elevation.svg', /* * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data @@ -38,7 +38,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ this._addLayer(layer); this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info }); } else { - this.once('modules_loaded', () => this.addData(d,layer)); + this.once('modules_loaded', () => this.addData(d, layer)); } }); }, @@ -61,15 +61,15 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ * Reset data and display */ clear() { - if (this._marker) this._marker.remove(); - if (this._chart) this._clearChart(); - if (this._layers) this._clearLayers(this._layers); - if (this._markers) this._clearLayers(this._markers); + if (this._marker) this._marker.remove(); + if (this._chart) this._clearChart(); + if (this._layers) this._clearLayers(this._layers); + if (this._markers) this._clearLayers(this._markers); if (this._circleMarkers) this._circleMarkers.remove(); - if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233 - if (this._hotline) this._clearLayers(this._hotline); + if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233 + if (this._hotline) this._clearLayers(this._hotline); - this._data = []; + this._data = []; this.track_info = {}; this._fireEvt("eledata_clear"); @@ -79,7 +79,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ _clearChart() { if (this._events && this._events.elechart_updated) { - this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx)); + this._events.elechart_updated.forEach(({ fn, ctx }) => this.off('elechart_updated', fn, ctx)); } if (this._chart && this._chart._container) { this._chart._container.selectAll('g.point .point').remove(); @@ -177,26 +177,26 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240 opts = L.setOptions(this, L.extend({}, _.cloneDeep(Options), opts)); // "deep copy" nested objects (multiple charts) - this._data = []; - this._layers = L.featureGroup(); - this._markers = L.featureGroup(); - this._hotline = L.featureGroup(); - this._circleMarkers = L.featureGroup(); + this._data = []; + this._layers = L.featureGroup(); + this._markers = L.featureGroup(); + this._hotline = L.featureGroup(); + this._circleMarkers = L.featureGroup(); this._markedSegments = L.polyline([]); - this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart)); - this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd)); - this._chartEnabled = true; - this._yCoordMax = -Infinity; - this.track_info = {}; + this._start = L.marker([0, 0], (opts.trkStart || Options.trkStart)); + this._end = L.marker([0, 0], (opts.trkEnd || Options.trkEnd)); + this._chartEnabled = true; + this._yCoordMax = -Infinity; + this.track_info = {}; // this.handlers = []; - if (opts.followMarker) this._setMapView = _.throttle(this._setMapView, 300, this); - if (opts.legend) opts.margins.bottom += 30; - if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme; - if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons; + if (opts.followMarker) this._setMapView = _.throttle(this._setMapView, 300, this); + if (opts.legend) opts.margins.bottom += 30; + if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme; + if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons; if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers; - if (opts.trkStart) this._start.addTo(this._circleMarkers); - if (opts.trkEnd) this._end.addTo(this._circleMarkers); + if (opts.trkStart) this._start.addTo(this._circleMarkers); + if (opts.trkEnd) this._end.addTo(this._circleMarkers); this._markedSegments.setStyle(opts.polylineSegments); @@ -217,14 +217,14 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (Array.isArray(src)) { return Promise.all(src.map(m => this.import(m))); } - switch(src) { - case this.__D3: condition = typeof d3 !== 'object'; break; - case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break; - case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break; - case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break; - case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break; - case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break; - case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break; + switch (src) { + case this.__D3: condition = typeof d3 !== 'object'; break; + case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break; + case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break; + case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break; + case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break; + case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break; + case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break; } return condition !== false ? import(_.resolveURL(src, this.options.srcFolder)) : Promise.resolve(); }, @@ -233,7 +233,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ * Load elevation data (GPX, GeoJSON, KML or TCX). */ load(data) { - this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data)); + this._parseFromString(data).then(geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data)); }, /** @@ -270,17 +270,17 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ this._container = null; map - .off('zoom viewreset zoomanim', this._hideMarker, this) - .off('resize', this._resetView, this) - .off('resize', this._resizeChart, this) - .off('mousedown', this._resetDrag, this); + .off('zoom viewreset zoomanim', this._hideMarker, this) + .off('resize', this._resetView, this) + .off('resize', this._resizeChart, this) + .off('mousedown', this._resetDrag, this); - _.off(map.getContainer(), 'mousewheel', this._resetDrag, this); - _.off(map.getContainer(), 'touchstart', this._resetDrag, this); - _.off(document, 'keydown', this._onKeyDown, this); + _.off(map.getContainer(), 'mousewheel', this._resetDrag, this); + _.off(map.getContainer(), 'touchstart', this._resetDrag, this); + _.off(document, 'keydown', this._onKeyDown, this); this - .off('eledata_added eledata_loaded', this._updateChart, this) + .off('eledata_added eledata_loaded', this._updateChart, this) .off('eledata_added eledata_loaded', this._updateSummary, this); this.fire('remove'); @@ -316,16 +316,16 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ } // Standard GeoJSON - if (d.type === "FeatureCollection" ) { + if (d.type === "FeatureCollection") { return _.each(d.features, feature => this._addData(feature)); } else if (d.type === "Feature") { let geom = d.geometry; if (geom) { switch (geom.type) { - case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties); - case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i)); + case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties); + case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i)); case 'Point': - default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type); + default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type); } } } @@ -343,19 +343,19 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ _addGeoJSONData(coords, properties, nestingLevel) { // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON" - let props = (properties && properties.coordinateProperties) || properties; + let props = (properties && properties.coordinateProperties) || properties; coords.forEach((point, i) => { // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"]; point.meta = point.meta ?? { time: null, ele: null }; - + point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]); this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel }); this._addPoint( - point.lat ?? point[1], + point.lat ?? point[1], point.lng ?? point[0], point.alt ?? point.meta.ele ?? point[2] ); @@ -390,7 +390,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (layer) this._layers.addLayer(layer) // Postpone adding the distance markers (lazy: true) if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) { - layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']()); + layer.on('add remove', ({ target, type }) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']()); } return layer; }, @@ -415,8 +415,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ this.once('eledata_clear', () => { map.almostOver.removeLayer(layer); map - .off('almost:move', this._onMouseMoveLayer, this) - .off('almost:out', this._onMouseOut, this); + .off('almost:move', this._onMouseMoveLayer, this) + .off('almost:out', this._onMouseOut, this); }) } }) : Promise.resolve(); @@ -445,7 +445,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ .then(() => { layer.eachLayer((trkseg) => { if (trkseg.feature.geometry.type != "Point") { - let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))}); + let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1)) }); let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, { renderer: L.Hotline.renderer(), min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0, @@ -460,8 +460,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ outlineWidth: 1 }).addTo(this._hotline); let alpha = trkseg.options.style && trkseg.options.style.opacity || 1; - trkseg.on('add remove', ({type}) => { - trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)}); + trkseg.on('add remove', ({ type }) => { + trkseg.setStyle({ opacity: (type == 'add' ? 0 : alpha) }); line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map); if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild); }); @@ -478,7 +478,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (map) { if (this._data.length) { this._start.setLatLng(this._data[0].latlng); - this._end.setLatLng(this._data[this._data.length -1].latlng); + this._end.setLatLng(this._data[this._data.length - 1].latlng); } Promise.all([ this._initHotLine(layer), @@ -521,27 +521,27 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ */ _fixCanvasPaths() { let oldProto = L.Canvas.prototype._fillStroke; - let control = this; + let control = this; - let theme = this.options.theme.split(' ')[0].replace('-theme', ''); - let color = _.Colors[theme] || {}; + let theme = this.options.theme.split(' ')[0].replace('-theme', ''); + let color = _.Colors[theme] || {}; L.Canvas.include({ _fillStroke(ctx, layer) { if (control._layers.hasLayer(layer)) { - let options = layer.options; + let options = layer.options; - options.color = color.line || color.area || theme; + options.color = color.line || color.area || theme; options.stroke = !!options.color; oldProto.call(this, ctx, layer); if (options.stroke && options.weight !== 0) { - let oldVal = ctx.globalCompositeOperation || 'source-over'; + let oldVal = ctx.globalCompositeOperation || 'source-over'; ctx.globalCompositeOperation = 'destination-over' - ctx.strokeStyle = color.outline || '#FFF'; - ctx.lineWidth = options.weight * 1.75; + ctx.strokeStyle = color.outline || '#FFF'; + ctx.lineWidth = options.weight * 1.75; ctx.stroke(); ctx.globalCompositeOperation = oldVal; } @@ -560,7 +560,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ */ _fixTooltipSize() { this.on('elechart_init', () => - this.once('elechart_change elechart_hover', ({data, xCoord}) => { + this.once('elechart_change elechart_hover', ({ data, xCoord }) => { if (this._chartEnabled) { this._chart._showDiagramIndicator(data, xCoord); this._chart._showDiagramIndicator(data, xCoord); @@ -606,16 +606,16 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ */ _initChart(container) { let opts = this.options; - let map = this._map; + let map = this._map; if (opts.detached) { - let { offsetWidth, offsetHeight} = this.eleDiv; - if (offsetWidth > 0) opts.width = offsetWidth; - if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size. + let { offsetWidth, offsetHeight } = this.eleDiv; + if (offsetWidth > 0) opts.width = offsetWidth; + if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size. } else { - let { clientWidth } = map.getContainer(); - opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width; - this._container.style.maxWidth = opts._maxWidth + 'px'; + let { clientWidth } = map.getContainer(); + opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width; + this._container.style.maxWidth = opts._maxWidth + 'px'; if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30; } @@ -623,75 +623,75 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ .import([this.__D3, this.__LCHART]) .then((m) => { - let chart = this._chart = new (m[1] || Elevation).Chart(opts, this); - - this._x = this._chart._x; - this._y = this._chart._y; - - d3 - .select(container) - .call(chart.render()) - - chart - .on('reset_drag', this._hideMarker, this) - .on('mouse_enter', this._onMouseEnter, this) - .on('dragged', this._onDragEnd, this) - .on('mouse_move', this._onMouseMove, this) - .on('mouse_out', this._onMouseOut, this) - .on('ruler_filter', this._onRulerFilter, this) - .on('zoom', this._updateChart, this) - .on('elepath_toggle', this._onToggleChart, this) - .on('margins_updated', this._resizeChart, this); - - - this.fire("elechart_init"); + let chart = this._chart = new (m[1] || Elevation).Chart(opts, this); - map - .on('zoom viewreset zoomanim', this._hideMarker, this) - .on('resize', this._resetView, this) - .on('resize', this._resizeChart, this) - .on('rotate', this._rotateMarker, this) - .on('mousedown', this._resetDrag, this); + this._x = this._chart._x; + this._y = this._chart._y; - _.on(map.getContainer(), 'mousewheel', this._resetDrag, this); - _.on(map.getContainer(), 'touchstart', this._resetDrag, this); - _.on(document, 'keydown', this._onKeyDown, this); + d3 + .select(container) + .call(chart.render()) - this - .on('eledata_added eledata_loaded', this._updateChart, this) - .on('eledata_added eledata_loaded', this._updateSummary, this); + chart + .on('reset_drag', this._hideMarker, this) + .on('mouse_enter', this._onMouseEnter, this) + .on('dragged', this._onDragEnd, this) + .on('mouse_move', this._onMouseMove, this) + .on('mouse_out', this._onMouseOut, this) + .on('ruler_filter', this._onRulerFilter, this) + .on('zoom', this._updateChart, this) + .on('elepath_toggle', this._onToggleChart, this) + .on('margins_updated', this._resizeChart, this); - this._updateChart(); - this._updateSummary(); - }); + + this.fire("elechart_init"); + + map + .on('zoom viewreset zoomanim', this._hideMarker, this) + .on('resize', this._resetView, this) + .on('resize', this._resizeChart, this) + .on('rotate', this._rotateMarker, this) + .on('mousedown', this._resetDrag, this); + + _.on(map.getContainer(), 'mousewheel', this._resetDrag, this); + _.on(map.getContainer(), 'touchstart', this._resetDrag, this); + _.on(document, 'keydown', this._onKeyDown, this); + + this + .on('eledata_added eledata_loaded', this._updateChart, this) + .on('eledata_added eledata_loaded', this._updateSummary, this); + + this._updateChart(); + this._updateSummary(); + }); }, _initLayer() { this._layers - .on('layeradd layerremove', ({layer, type}) => { - let node = layer.getElement && layer.getElement(); - _.toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd'); - _.toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd') - _.toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd'); + .on('layeradd layerremove', ({ layer, type }) => { + let node = layer.getElement && layer.getElement(); + _.toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd'); + _.toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd') + _.toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd'); }); }, _initMarker(map) { - let pane = map.getPane('elevationPane'); + let pane = map.getPane('elevationPane'); if (!pane) { - pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane')); - pane.style.zIndex = 625; // This pane is above markers but below popups. + pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane')); + pane.style.zIndex = 625; // This pane is above markers but below popups. pane.style.pointerEvents = 'none'; } if (this._renderer) this._renderer.remove() - this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer + this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer this.import([this.__D3, this.__LMARKER]) .then((m) => { - this._marker = new (m[1] || Elevation).Marker(this.options, this); + this._marker = new (m[1] || Elevation).Marker(this.options, this); this.fire("elechart_marker"); }); }, @@ -707,8 +707,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ this.options.collapsed ? this._collapse() : this._expand(); if (this.options.autohide) { - _.on(container, 'mouseover', this._expand, this); - _.on(container, 'mouseout', this._collapse, this); + _.on(container, 'mouseover', this._expand, this); + _.on(container, 'mouseout', this._collapse, this); this._map.on('click', this._collapse, this); } @@ -722,7 +722,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ }, _initSummary(container) { - this.import(this.__LSUMMARY).then((m)=>{ + this.import(this.__LSUMMARY).then((m) => { this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this); this.on('elechart_init', () => { @@ -734,14 +734,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /** * Retrieve data from a remote url (HTTP). */ - _loadFile(url) { - fetch(url) - .then((response) => response.text()) - .then((data) => { - this._downloadURL = url; // TODO: handle multiple urls? - this._parseFromString(data) - .then( geojson => geojson && this._loadLayer(geojson)); - }).catch((err) => console.warn(err)); + _loadFile(data) { + this._parseFromString(data) + .then(geojson => geojson && this._loadLayer(geojson)); }, /** @@ -790,14 +785,14 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if ([true, 'dots'].includes(waypoints)) { this._registerCheckPoint({ latlng: latlng, - label : ([true, 'dots'].includes(wptLabels) ? name : '') + label: ([true, 'dots'].includes(wptLabels) ? name : '') }); } // Handle map waypoints (markers) if ([true, 'markers'].includes(waypoints) && wptIcons != false) { return this._registerMarker({ - latlng : latlng, - sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(), + latlng: latlng, + sym: (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(), content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("" + name + "" + (desc.length > 0 ? '
' + desc : '')) }); } @@ -819,15 +814,15 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ return layer; }, - _onDragEnd({ dragstart, dragend}) { + _onDragEnd({ dragstart, dragend }) { this._hideMarker(); this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng])); this.fire("elechart_dragged"); }, - _onKeyDown({key}) { - if (!this.options.detached && key === "Escape"){ + _onKeyDown({ key }) { + if (!this.options.detached && key === "Escape") { this._collapse() }; }, @@ -842,7 +837,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /* * Handles the moueseover the chart and displays distance and altitude level. */ - _onMouseMove({xCoord}) { + _onMouseMove({ xCoord }) { if (this._chartEnabled && this._data.length) { let item = this._findItemForX(xCoord); if (item) { @@ -856,7 +851,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ } this.fire("elechart_change", { data: item, xCoord: xCoord }); - this.fire("elechart_hover", { data: item, xCoord: xCoord }); + this.fire("elechart_hover", { data: item, xCoord: xCoord }); } } }, @@ -864,16 +859,16 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /* * Handles mouseover events of the data layers on the map. */ - _onMouseMoveLayer({latlng}) { + _onMouseMoveLayer({ latlng }) { if (this._data.length) { let item = this._findItemForLatLng(latlng); if (item) { let xCoord = item.xDiagCoord; - + if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord); - + this._updateMarker(item); - + this.fire("elechart_change", { data: item, xCoord: xCoord }); } } @@ -898,7 +893,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /** * Handles the drag event over the ruler filter. */ - _onRulerFilter({coords}) { + _onRulerFilter({ coords }) { this._updateMapSegments(coords); }, @@ -958,7 +953,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (data.indexOf("<") != 0) { throw 'Invalid XML'; } - let xml = (new DOMParser()).parseFromString(data, "text/xml"); + let xml = (new DOMParser()).parseFromString(data, "text/xml"); let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx" let name = xml.getElementsByTagName('name'); if (xml.getElementsByTagName('parsererror').length) { @@ -967,7 +962,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (!(type in toGeoJSON)) { type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx'; } - let geojson = toGeoJSON[type](xml); + let geojson = toGeoJSON[type](xml); geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : ''; return geojson; }, @@ -1006,7 +1001,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ /** * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...) */ - _registerDataAttribute(props) { + _registerDataAttribute(props) { // parse of "coordinateProperties" for later usage if (props.coordPropsToMeta) { @@ -1020,7 +1015,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ let lastValid = {}; // iteration - this.on("elepoint_added", ({index, point}) => { + this.on("elepoint_added", ({ index, point }) => { i = index; prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i] @@ -1032,11 +1027,11 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ // check and fix missing data on last added point if (i > 0 && isNaN(prev[attr])) { if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) { - prev[attr] = (lastValid[attr] + curr[attr]) / 2; + prev[attr] = (lastValid[attr] + curr[attr]) / 2; } else if (!isNaN(lastValid[attr])) { - prev[attr] = lastValid[attr]; + prev[attr] = lastValid[attr]; } else if (!isNaN(curr[attr])) { - prev[attr] = curr[attr]; + prev[attr] = curr[attr]; } // update "yAttr" and "xAttr" if (props.meta) { @@ -1054,9 +1049,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ // Limit "crazy" delta values. if (props.deltaMax) { - curr[attr] =_.wrapDelta(curr[attr], prev[attr], props.deltaMax); + curr[attr] = _.wrapDelta(curr[attr], prev[attr], props.deltaMax); } - + // Range of acceptable values. if (props.clampRange) { curr[attr] = _.clamp(curr[attr], props.clampRange); @@ -1135,7 +1130,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ if (this.options[name] !== "summary") { if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale)); - if (path) this._registerAreaPath(L.extend({ name }, path)); + if (path) this._registerAreaPath(L.extend({ name }, path)); } if (tooltip || props.tooltips) { @@ -1149,11 +1144,11 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ } }, - _registerMarker({latlng, sym, content}) { + _registerMarker({ latlng, sym, content }) { let { wptIcons } = this.options; // generate and cache appropriate icon symbol if (!wptIcons.hasOwnProperty(sym)) { - wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '' } )); + wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '' })); } let marker = L.marker(latlng, { icon: wptIcons[sym] }); if (content) { @@ -1162,12 +1157,12 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ } return this._addMarker(marker) }, - + /** * Add chart or marker tooltip info */ _registerTooltip(props) { - props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart }))); + props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart }))); props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker }))); }, @@ -1175,7 +1170,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ * Add summary info to diagram */ _registerSummary(props) { - this.on('elechart_summary', () => this._summary._registerSummary(props)); + this.on('elechart_summary', () => this._summary._registerSummary(props)); }, /* @@ -1229,7 +1224,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ _setMapView(item) { if (this._map && this.options.followMarker) { let zoom = this._map.getZoom(); - let z = this.options.zFollow; + let z = this.options.zFollow; if (typeof z === "number") { this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 }); } else if (!this._map.getBounds().contains(item.latlng)) { @@ -1244,13 +1239,13 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ _updateChart() { if (this._chart && this._container) { this.fire("elechart_axis"); - + this._chart.update({ data: this._data, options: this.options }); - - this._x = this._chart._x; - this._y = this._chart._y; - - this.fire('elechart_updated'); + + this._x = this._chart._x; + this._y = this._chart._y; + + this.fire('elechart_updated'); } }, @@ -1260,10 +1255,10 @@ export const Elevation = L.Control.Elevation = L.Control.extend({ _updateMarker(item) { if (this._marker) { this._marker.update({ - map : this._map, - item : item, - yCoordMax : this._yCoordMax || 0, - options : this.options + map: this._map, + item: item, + yCoordMax: this._yCoordMax || 0, + options: this.options }); } }, diff --git a/web/src/lib/vendor/leaflet-elevation/src/index.css b/web/src/lib/vendor/leaflet-elevation/src/index.css index 9b291740..ae1048af 100644 --- a/web/src/lib/vendor/leaflet-elevation/src/index.css +++ b/web/src/lib/vendor/leaflet-elevation/src/index.css @@ -22,7 +22,7 @@ } .elevation-control .grid, -.elevation-control .area > foreignObject, +.elevation-control .area>foreignObject, .elevation-control .axis, .elevation-control .tooltip, .height-focus.line { @@ -49,13 +49,17 @@ .elevation-control .axis text, .elevation-control .legend text, .elevation-control .point text { - fill: #000; + fill: #fff; font-weight: 700; paint-order: stroke fill; - stroke: #fff; + stroke: #000; stroke-width: 2px } +.elevation-control .legend .legend-switcher-symbol { + fill: rgb(135, 135, 135); +} + .elevation-control .y.axis text { text-anchor: end; } @@ -229,11 +233,11 @@ margin: auto; } -.elevation-control.elevation-collapsed > * { +.elevation-control.elevation-collapsed>* { display: none; } -.elevation-control.elevation-collapsed > .elevation-toggle-icon { +.elevation-control.elevation-collapsed>.elevation-toggle-icon { display: inline-block; } @@ -312,6 +316,7 @@ --ele-stroke: #4682B4; --ele-circle: #fff; --ele-line: #000; + --ele-grid: rgba(var(--input-border)); } .elevation-detached.lightblue-theme .area { @@ -326,4 +331,4 @@ text-align: center; color: #000; background: #fff; -} +} \ No newline at end of file diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index d24a1004..c3ebd940 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,16 +1,23 @@ - - | wanderer - diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 00000000..dcfc6230 --- /dev/null +++ b/web/src/routes/+layout.ts @@ -0,0 +1,8 @@ +// +layout.ts +import '$lib/i18n' +import { waitLocale } from 'svelte-i18n' +import type { LayoutLoad } from './$types' + +export const load: LayoutLoad = async () => { + await waitLocale() +} \ No newline at end of file diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 74464f93..d0946d6a 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -12,6 +12,7 @@ import { currentUser } from "$lib/stores/user_store"; import { country_codes } from "$lib/util/country_code_util"; import { Canvas } from "@threlte/core"; + import { _ } from "svelte-i18n"; let searchDropdownItems: SearchItem[] = []; @@ -59,6 +60,10 @@ } + + Home | wanderer + +

- Welcome to wanderer + {$_("welcome_to")} wanderer

- Explore exciting trails, save your favorites, and experience the - beauty of nature. Find your next adventure! + {$_("hero_section_0_text")}
search(e.detail)} on:click={(e) => handleSearchClick(e.detail)} large={true} - placeholder="Search for trails, places..." + placeholder="{$_("search-for-trails-places")}..." items={searchDropdownItems} > @@ -105,13 +109,12 @@ {$currentUser ? "Trails for you" : "Explore some trails"}
- Here are some trails you might like. Or you can just go to the full - list right now. + {$_("hero_section_1_text")}
Explore{$_("explore")}
@@ -119,10 +122,9 @@ class="max-w-7xl mx-auto mt-8 px-8 xl:px-0 grid grid-cols-1 md:grid-cols-2 items-center" >
-

Categories

+

{$_("categories")}

- Did you know? You cannot only save you hiking trails. There are many - categories for all your adventures. + {$_("hero_section_2_text")}
{#each $categories as category} - + {/each} diff --git a/web/src/routes/lists/+page.svelte b/web/src/routes/lists/+page.svelte index 3dd58d02..a326ef6a 100644 --- a/web/src/routes/lists/+page.svelte +++ b/web/src/routes/lists/+page.svelte @@ -1,6 +1,7 @@ + + {$_("list", { values: { n: 2 } })} | wanderer +
-
Create new list
+
{$_("create-new-list")}

@@ -89,4 +106,9 @@ on:update={async () => await lists_index()} > +
diff --git a/web/src/routes/login/+page.svelte b/web/src/routes/login/+page.svelte index a4e35aa5..1fcd0b3e 100644 --- a/web/src/routes/login/+page.svelte +++ b/web/src/routes/login/+page.svelte @@ -10,11 +10,12 @@ import { ClientResponseError } from "pocketbase"; import { object, string } from "yup"; import { theme } from "$lib/stores/theme_store"; - + import { _ } from "svelte-i18n"; let loading: boolean = false; const { form, errors, handleChange, handleSubmit } = createForm({ initialValues: { + id: "", username: "", password: "", }, @@ -51,6 +52,9 @@ }); + + {$_("login")} | wanderer +
diff --git a/web/src/routes/map/+page.svelte b/web/src/routes/map/+page.svelte index dbcfef45..d3957d54 100644 --- a/web/src/routes/map/+page.svelte +++ b/web/src/routes/map/+page.svelte @@ -15,9 +15,14 @@ trails, trails_search_bounding_box, } from "$lib/stores/trail_store"; + import { currentUser } from "$lib/stores/user_store"; import { country_codes } from "$lib/util/country_code_util"; import { getFileURL } from "$lib/util/file_util"; - import { formatMeters, formatTimeHHMM } from "$lib/util/format_util"; + import { + formatDistance, + formatElevation, + formatTimeHHMM, + } from "$lib/util/format_util"; import "$lib/vendor/leaflet-elevation/src/index.css"; import type { GPX, @@ -31,8 +36,8 @@ import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css"; import "leaflet/dist/leaflet.css"; import { onMount } from "svelte"; + import { _ } from "svelte-i18n"; import { slide } from "svelte/transition"; - let L: any; let map: Map; let gpxLayers: Record = {}; @@ -85,6 +90,11 @@ ]; map.fitBounds(boundingBox); + } else if ($currentUser && $currentUser.location) { + map.setView( + [$currentUser.location.lat, $currentUser.location.lon], + 12, + ); } else { navigator.geolocation.getCurrentPosition( (position) => { @@ -202,9 +212,9 @@

${trail.name}

${trail.location}
${formatMeters( + class="fa fa-left-right mr-2">${formatDistance( trail.distance, - )} ${formatMeters( + )} ${formatElevation( trail.elevation_gain, )} ${formatTimeHHMM( trail.duration, @@ -239,6 +249,9 @@ } + + {$_("map")} | wanderer +
search(e.detail)} on:click={(e) => handleSearchClick(e.detail)} - placeholder="Search for trails, places..." + placeholder="{$_("search-for-trails-places")}..." items={searchDropdownItems} >
-
-
-
-
+