adds i18n
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
.DS_Store
|
||||
.vscode/
|
||||
search/
|
||||
|
||||
search/meilisearch
|
||||
search/data.ms
|
||||
search/dumps
|
||||
43
search/init.py
Normal file
43
search/init.py
Normal file
@@ -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()
|
||||
278
web/package-lock.json
generated
278
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
export let size: string = "2xl";
|
||||
export let size: string = "max-w-2xl";
|
||||
export function openModal() {
|
||||
document.body.style.position = "fixed";
|
||||
document.body.style.width = "100%";
|
||||
@@ -25,7 +25,7 @@
|
||||
{id}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class="relative w-full max-w-{size} max-h-full rounded-xl text-content"
|
||||
class="relative w-full {size} max-h-full rounded-xl text-content"
|
||||
>
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-background shadow">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Modal from "$lib/components/base/modal.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
@@ -8,7 +9,7 @@
|
||||
export let title: string = "Confirm Deletion";
|
||||
export let text: string;
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function confirm() {
|
||||
dispatch("confirm");
|
||||
@@ -19,11 +20,11 @@
|
||||
<Modal id="confirm-modal" {title} bind:openModal bind:closeModal>
|
||||
<p slot="content">{text}</p>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||
<button
|
||||
class="btn-danger"
|
||||
type="button"
|
||||
on:click={confirm}>Delete</button
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button class="btn-danger" type="button" on:click={confirm}
|
||||
>{$_("delete")}</button
|
||||
>
|
||||
</div></Modal
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
export let width: number = 256;
|
||||
</script>
|
||||
|
||||
@@ -10,5 +11,5 @@
|
||||
alt="Empty State showing a wanderer going into the distance"
|
||||
/>
|
||||
|
||||
<h3 class="text-2xl font-semibold text-center">No results found</h3>
|
||||
<h3 class="text-2xl font-semibold text-center">{$_("no-results")}</h3>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script>
|
||||
import LogoTextLight from "./logo/logo_text_light.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
</script>
|
||||
|
||||
<footer class="bg-footer-background text-white w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-y-8 p-12 mt-12 rounded-t-3xl">
|
||||
@@ -11,18 +13,18 @@
|
||||
<div>
|
||||
<h5 class="font-semibold">wanderer</h5>
|
||||
<ul class="mt-4 text-sm">
|
||||
<li>About</li>
|
||||
<li>Features</li>
|
||||
<li>Changelog</li>
|
||||
<li>License</li>
|
||||
<li>{$_('about')}</li>
|
||||
<li>{$_('features')}</li>
|
||||
<li>{$_('changelog')}</li>
|
||||
<li><a href="https://github.com/Flomp/wanderer/blob/main/LICENSE">{$_('license')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold">Community</h5>
|
||||
<ul class="mt-4 text-sm">
|
||||
<li>GitHub</li>
|
||||
<li>Issues</li>
|
||||
<li>Contribute</li>
|
||||
<li><a href="https://github.com/Flomp/wanderer">GitHub</a></li>
|
||||
<li><a href="https://github.com/Flomp/wanderer/issues">Issues</a></li>
|
||||
<li><a href="https://github.com/Flomp/wanderer/pulls">{$_('contribute')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { List } from "$lib/models/list";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
export let list: List;
|
||||
export let active: boolean = false;
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
{ text: $_("edit"), value: "edit" },
|
||||
{ text: $_("delete"), value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -21,7 +22,9 @@
|
||||
alt="avatar"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center">
|
||||
<div
|
||||
class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center"
|
||||
>
|
||||
<i class="fa fa-table-list text-5xl"></i>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<Modal
|
||||
id="list-modal"
|
||||
title="New List"
|
||||
title={$form.id ? $_("edit-list") : $_("new-list")}
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
@@ -67,7 +66,7 @@
|
||||
class="modal-content space-y-4"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<label for="avatar" class="text-sm font-medium block"> Avatar </label>
|
||||
<label for="avatar" class="text-sm font-medium block"> {$_('avatar')} </label>
|
||||
<input
|
||||
name="avatar"
|
||||
type="file"
|
||||
@@ -87,13 +86,13 @@
|
||||
<button
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={openAvatarBrowser}>Change...</button
|
||||
on:click={openAvatarBrowser}>{$_('change')}...</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
label={$_("name")}
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
@@ -101,14 +100,18 @@
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Description"
|
||||
label={$_("description")}
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||
<button class="btn-primary" type="submit" form="list-form">Save</button>
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button class="btn-primary" type="submit" form="list-form"
|
||||
>{$_("save")}</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { lists, lists_index } from "$lib/stores/list_store";
|
||||
import Modal from "../base/modal.svelte";
|
||||
import type { List } from "$lib/models/list";
|
||||
import { lists } from "$lib/stores/list_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Modal from "../base/modal.svelte";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
@@ -23,8 +24,8 @@
|
||||
|
||||
<Modal
|
||||
id="list-modal"
|
||||
title="Select List"
|
||||
size="md"
|
||||
title={$_("select-list")}
|
||||
size="max-w-sm"
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
import LogoText from "$lib/components/logo/logo_text.svelte";
|
||||
import { theme, toggleTheme } from "$lib/stores/theme_store";
|
||||
import { currentUser, logout } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { backInOut, cubicOut } from "svelte/easing";
|
||||
import { tweened } from "svelte/motion";
|
||||
import Drawer from "./base/drawer.svelte";
|
||||
import Dropdown from "./base/dropdown.svelte";
|
||||
import LogoTextLight from "./logo/logo_text_light.svelte";
|
||||
import Drawer from "./base/drawer.svelte";
|
||||
import { _, format } from "svelte-i18n";
|
||||
|
||||
const navBarItems = [
|
||||
let navBarItems = [
|
||||
{ text: "Home", value: "/" },
|
||||
{ text: "Trails", value: "/trails" },
|
||||
{ text: "Map", value: "/map" },
|
||||
{ text: "Lists", value: "/lists" },
|
||||
{ text: $_("trail", { values: { n: 2 } }), value: "/trails" },
|
||||
{ text: $_("map"), value: "/map" },
|
||||
];
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: "Profile", value: "profile", icon: "user" },
|
||||
{ text: "Logout", value: "logout", icon: "right-from-bracket" },
|
||||
{ text: $_("profile"), value: "profile", icon: "user" },
|
||||
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
|
||||
];
|
||||
|
||||
const indicatorPosition = tweened(0, {
|
||||
@@ -104,11 +105,19 @@
|
||||
data-sveltekit-preload-data="off">{item.text}</a
|
||||
>
|
||||
{/each}
|
||||
{#if $currentUser}
|
||||
<a
|
||||
class="font-semibold text-xl"
|
||||
href="/lists"
|
||||
data-sveltekit-preload-data="off"
|
||||
>{$_("list", { values: { n: 2 } })}</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<hr class="my-6 border-input-border" />
|
||||
<div class="flex flex-col basis-full">
|
||||
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
|
||||
><i class="fa fa-plus mr-2"></i>New Trail</a
|
||||
><i class="fa fa-plus mr-2"></i>{$_('new-trail')}</a
|
||||
>
|
||||
{#if $currentUser}
|
||||
<div class="basis-full"></div>
|
||||
@@ -116,9 +125,9 @@
|
||||
<div class="flex gap-4 items-center m-4">
|
||||
<img
|
||||
class="rounded-full w-8 aspect-square"
|
||||
src={$currentUser.avatar ||
|
||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt=""
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm">{$currentUser.username}</p>
|
||||
@@ -153,6 +162,14 @@
|
||||
data-sveltekit-preload-data="off">{item.text}</a
|
||||
>
|
||||
{/each}
|
||||
{#if $currentUser}
|
||||
<a
|
||||
class="font-semibold z-10"
|
||||
href="/lists"
|
||||
data-sveltekit-preload-data="off"
|
||||
>{$_("list", { values: { n: 2 } })}</a
|
||||
>
|
||||
{/if}
|
||||
</menu>
|
||||
{#if $currentUser}
|
||||
<div class="hidden md:flex gap-6 items-center">
|
||||
@@ -163,7 +180,7 @@
|
||||
on:click={() => toggleTheme()}
|
||||
></button>
|
||||
<a class="btn-primary btn-large" href="/trail/edit/new"
|
||||
><i class="fa fa-plus mr-2"></i>New Trail</a
|
||||
><i class="fa fa-plus mr-2"></i>{$_('new-trail')}</a
|
||||
>
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
@@ -176,8 +193,9 @@
|
||||
>
|
||||
<img
|
||||
class="rounded-full"
|
||||
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
|
||||
alt=""
|
||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
</button>
|
||||
</Dropdown>
|
||||
@@ -190,7 +208,7 @@
|
||||
: 'moon'}"
|
||||
on:click={() => toggleTheme()}
|
||||
></button>
|
||||
<a class="btn-primary btn-large" href="/login">Login</a>
|
||||
<a class="btn-primary btn-large" href="/login">{$_('login')}</a>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let src: string;
|
||||
export let isThumbnail: boolean = false;
|
||||
@@ -30,13 +31,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="tooltip"
|
||||
data-title="Make thumbnail"
|
||||
data-title={$_("make-thumbnail")}
|
||||
on:click={handleThumbnailClick}
|
||||
><i class="fa fa-file-image text-primary"></i></button
|
||||
>
|
||||
<button
|
||||
class="tooltip"
|
||||
data-title="Delete"
|
||||
data-title={$_("delete")}
|
||||
on:click={handleDeleteClick}
|
||||
type="button"><i class="fa fa-trash text-red-500"></i></button
|
||||
>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { SummitLog } from "$lib/models/summit_log";
|
||||
import { format, parse } from "date-fns";
|
||||
import Dropdown from "../base/dropdown.svelte";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let log: SummitLog;
|
||||
export let mode: "show" | "edit" = "show";
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{ text: $_("edit"), value: "edit" },
|
||||
{ text: $_("delete"), value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-4 my-2 border border-input-border rounded-xl">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="font-medium mr-2">{format(log.date, 'dd.MM.yyyy')}</h5>
|
||||
<h5 class="font-medium mr-2">{format(log.date, "dd.MM.yyyy")}</h5>
|
||||
|
||||
{#if mode == "edit"}
|
||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<Modal
|
||||
id="summit-log-modal"
|
||||
title="Add Entry"
|
||||
title={$form.id ? $_("edit-entry") : $_("add-entry")}
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="flex gap-4">
|
||||
<Datepicker
|
||||
name="date"
|
||||
label="Date"
|
||||
label={$_("date")}
|
||||
bind:value={$form.date}
|
||||
error={$errors.date}
|
||||
on:change={handleChange}
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="basis-full">
|
||||
<TextField
|
||||
name="text"
|
||||
label="Text"
|
||||
label={$_("text")}
|
||||
bind:value={$form.text}
|
||||
error={$errors.text}
|
||||
on:change={handleChange}
|
||||
@@ -59,9 +59,11 @@
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button class="btn-primary" type="submit" form="summit-log-form"
|
||||
>Save</button
|
||||
>{$_("save")}</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
111
web/src/lib/components/trail/map_with_elevation.svelte
Normal file
111
web/src/lib/components/trail/map_with_elevation.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import type { Map, Marker } from "leaflet";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let trail: Trail;
|
||||
export let markers: Marker[] = [];
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
let controlElevation: any;
|
||||
|
||||
$: if (trail.expand.gpx_data && controlElevation) {
|
||||
controlElevation.load(trail.expand.gpx_data);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
await import("leaflet.awesome-markers");
|
||||
//@ts-ignore
|
||||
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
||||
|
||||
map = L.map("map").setView([trail.lat ?? 0, trail.lon ?? 0], 14);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
const elevation_options = {
|
||||
height: 200,
|
||||
|
||||
theme: "lightblue-theme",
|
||||
detached: true,
|
||||
elevationDiv: "#elevation",
|
||||
closeBtn: false,
|
||||
followMarker: true,
|
||||
autofitBounds: true,
|
||||
imperial: $currentUser?.unit == "imperial" ?? false,
|
||||
reverseCoords: false,
|
||||
acceleration: false,
|
||||
slope: true,
|
||||
speed: false,
|
||||
altitude: true,
|
||||
time: true,
|
||||
distance: true,
|
||||
// Summary track info style: "inline" || "multiline" || false
|
||||
summary: false,
|
||||
downloadLink: false,
|
||||
ruler: false,
|
||||
legend: true,
|
||||
// Toggle "leaflet-almostover" integration
|
||||
almostOver: true,
|
||||
// Toggle "leaflet-distance-markers" integration
|
||||
distanceMarkers: false,
|
||||
// Toggle "leaflet-edgescale" integration
|
||||
edgeScale: false,
|
||||
// Toggle "leaflet-hotline" integration
|
||||
hotline: true,
|
||||
// Display track datetimes: true || false
|
||||
timestamps: false,
|
||||
waypoints: false,
|
||||
wptIcons: false,
|
||||
wptLabels: false,
|
||||
preferCanvas: true,
|
||||
trkStart: {
|
||||
icon: L.AwesomeMarkers.icon({
|
||||
icon: "circle-half-stroke",
|
||||
prefix: "fa",
|
||||
markerColor: "cadetblue",
|
||||
iconColor: "white",
|
||||
}),
|
||||
},
|
||||
trkEnd: {
|
||||
icon: L.AwesomeMarkers.icon({
|
||||
icon: "flag-checkered",
|
||||
prefix: "fa",
|
||||
markerColor: "cadetblue",
|
||||
iconColor: "white",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
controlElevation = L.control.elevation(elevation_options).addTo(map);
|
||||
|
||||
for (const waypoint of trail.expand.waypoints) {
|
||||
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||
marker.addTo(map);
|
||||
markers.push(marker);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="map-container" class="flex flex-col">
|
||||
<div id="map" class="rounded-xl z-0 basis-full"></div>
|
||||
<div id="elevation"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#map-container {
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
#map-container {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import { formatDistance, formatElevation, formatTimeHHMM } from "$lib/util/format_util";
|
||||
|
||||
export let trail: Trail;
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
</div>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500 whitespace-nowrap">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatMeters(
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
trail.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatMeters(
|
||||
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span
|
||||
>
|
||||
|
||||
136
web/src/lib/components/trail/trail_dropdown.svelte
Normal file
136
web/src/lib/components/trail/trail_dropdown.svelte
Normal file
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import {
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
lists_remove_trail,
|
||||
} from "$lib/stores/list_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { trails_delete } from "$lib/stores/trail_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
import ConfirmModal from "../confirm_modal.svelte";
|
||||
import ListSelectModal from "../list/list_select_modal.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let trail: Trail;
|
||||
export let mode: "overview" | "map";
|
||||
|
||||
let openConfirmModal: () => void;
|
||||
let openListSelectModal: () => void;
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
mode == "overview"
|
||||
? { text: $_("show-on-map"), value: "show", icon: "map" }
|
||||
: {
|
||||
text: $_("show-in-overview"),
|
||||
value: "show",
|
||||
icon: "table-columns",
|
||||
},
|
||||
|
||||
{ text: $_("directions"), value: "direction", icon: "car" },
|
||||
...(trail.gpx
|
||||
? [
|
||||
{
|
||||
text: $_("download-gpx"),
|
||||
value: "download",
|
||||
icon: "download",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ text: $_("add-to-list"), value: "list", icon: "bookmark" },
|
||||
{ text: $_("edit"), value: "edit", icon: "pen" },
|
||||
{ text: $_("delete"), value: "delete", icon: "trash" },
|
||||
];
|
||||
|
||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "show") {
|
||||
goto(
|
||||
mode == "overview"
|
||||
? `/map/trail/${trail.id!}`
|
||||
: `/trail/view/${trail.id!}`,
|
||||
);
|
||||
} else if (item.value == "list") {
|
||||
openListSelectModal();
|
||||
} else if (item.value == "direction") {
|
||||
window
|
||||
.open(
|
||||
`https://www.google.com/maps/dir/Current+Location/${trail.lat},${trail.lon}`,
|
||||
"_blank",
|
||||
)
|
||||
?.focus();
|
||||
} else if (item.value == "download") {
|
||||
downloadURI(getFileURL(trail, trail.gpx), trail.gpx!);
|
||||
} else if (item.value == "edit") {
|
||||
goto(`/trail/edit/${trail.id}`);
|
||||
} else if (item.value == "delete") {
|
||||
openConfirmModal();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadURI(uri: string, name: string) {
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("download", name);
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function deleteTrail() {
|
||||
trails_delete(trail).then(() => history.back());
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
try {
|
||||
if (list.trails?.includes(trail.id!)) {
|
||||
await lists_remove_trail(list, trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Removed trail from "${list.name}"`,
|
||||
});
|
||||
} else {
|
||||
await lists_add_trail(list, trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Added trail to "${list.name}"`,
|
||||
});
|
||||
}
|
||||
await lists_index();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error adding trail to list.",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
on:change={(e) => handleDropdownClick(e.detail)}
|
||||
let:toggleMenu={openDropdown}
|
||||
><button
|
||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
|
||||
on:click={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button></Dropdown
|
||||
>
|
||||
|
||||
<ConfirmModal
|
||||
text={$_("delete-trail-confirm")}
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteTrail}
|
||||
></ConfirmModal>
|
||||
<ListSelectModal
|
||||
bind:openModal={openListSelectModal}
|
||||
on:change={(e) => handleListSelection(e.detail)}
|
||||
></ListSelectModal>
|
||||
@@ -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 @@
|
||||
<Search
|
||||
bind:value={filter.q}
|
||||
on:update={update}
|
||||
placeholder="Search trails..."
|
||||
placeholder="{$_('search-trails')}..."
|
||||
></Search>
|
||||
</div>
|
||||
<button
|
||||
@@ -128,7 +129,7 @@
|
||||
{#if showTrailSearch}
|
||||
<hr class="my-4 border-separator" />
|
||||
{/if}
|
||||
<p class="text-sm font-medium pb-4">Category</p>
|
||||
<p class="text-sm font-medium pb-4">{$_("category")}</p>
|
||||
{#each categories as category, i}
|
||||
<div class="flex items-center mb-4">
|
||||
<input
|
||||
@@ -146,11 +147,11 @@
|
||||
{/each}
|
||||
<hr class="my-4 border-separator" />
|
||||
{#if showCitySearch}
|
||||
<p class="text-sm font-medium pb-4">Near</p>
|
||||
<p class="text-sm font-medium pb-4">{$_("near")}</p>
|
||||
<div class="mb-8">
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="Search cities..."
|
||||
placeholder="{$_('search-cities')}..."
|
||||
bind:value={citySearchQuery}
|
||||
on:update={(e) => searchCities(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
@@ -162,12 +163,12 @@
|
||||
on:set={() => update()}
|
||||
></Slider>
|
||||
<p>
|
||||
<span class="text-gray-500 text-sm">Radius:</span>
|
||||
{formatMeters(filter.near.radius)}
|
||||
<span class="text-gray-500 text-sm">{$_("radius")}:</span>
|
||||
{formatDistance(filter.near.radius)}
|
||||
</p>
|
||||
<hr class="my-4 border-separator" />
|
||||
{/if}
|
||||
<p class="text-sm font-medium pb-4">Distance</p>
|
||||
<p class="text-sm font-medium pb-4">{$_("distance")}</p>
|
||||
<DoubleSlider
|
||||
minValue={filter.distanceMin}
|
||||
maxValue={filter.distanceMax}
|
||||
@@ -176,11 +177,11 @@
|
||||
on:set={() => update()}
|
||||
></DoubleSlider>
|
||||
<div class="flex justify-between">
|
||||
<span>{formatMeters(filter.distanceMin)}</span>
|
||||
<span>{formatMeters(filter.distanceMax)}</span>
|
||||
<span>{formatDistance(filter.distanceMin)}</span>
|
||||
<span>{formatDistance(filter.distanceMax)}</span>
|
||||
</div>
|
||||
<hr class="my-4 border-separator" />
|
||||
<p class="text-sm font-medium pb-4">Elevation Gain</p>
|
||||
<p class="text-sm font-medium pb-4">{$_("elevation-gain")}</p>
|
||||
<DoubleSlider
|
||||
minValue={filter.elevationGainMin}
|
||||
maxValue={filter.elevationGainMax}
|
||||
@@ -189,11 +190,11 @@
|
||||
on:set={() => update()}
|
||||
></DoubleSlider>
|
||||
<div class="flex justify-between">
|
||||
<span>{formatMeters(filter.elevationGainMin)}</span>
|
||||
<span>{formatMeters(filter.elevationGainMax)}</span>
|
||||
<span>{formatElevation(filter.elevationGainMin)}</span>
|
||||
<span>{formatElevation(filter.elevationGainMax)}</span>
|
||||
</div>
|
||||
<hr class="my-4 border-separator" />
|
||||
<p class="text-sm font-medium pb-4">Completed</p>
|
||||
<p class="text-sm font-medium pb-4">{$_("completed")}</p>
|
||||
<RadioGroup
|
||||
name="completed"
|
||||
items={radioGroupItems}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import EmptyStateSearch from "../empty_states/empty_state_search.svelte";
|
||||
import TrailCard from "./trail_card.svelte";
|
||||
import TrailListItem from "./trail_list_item.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let filter: TrailFilter;
|
||||
export let trails: Trail[];
|
||||
|
||||
const displayOptions: SelectItem[] = [
|
||||
{ text: "Cards", value: "cards" },
|
||||
{ text: "List", value: "list" },
|
||||
{ text: $_('card', {values: {n: 2}}), value: "cards" },
|
||||
{ text: $_('list', {values: {n: 1}}), value: "list" },
|
||||
];
|
||||
|
||||
let selectedDisplayOption = displayOptions[0].value;
|
||||
@@ -19,10 +20,10 @@
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
const sortOptions: SelectItem[] = [
|
||||
{ text: "Alphabetical", value: "name" },
|
||||
{ text: "Creation date", value: "created" },
|
||||
{ text: "Distance", value: "distance" },
|
||||
{ text: "Elevation gain", value: "elevation_gain" },
|
||||
{ text: $_('alphabetical'), value: "name" },
|
||||
{ text: $_('creation-date'), value: "created" },
|
||||
{ text: $_('distance'), value: "distance" },
|
||||
{ text: $_('elevation-gain'), value: "elevation_gain" },
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
@@ -53,7 +54,7 @@
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-start gap-8 justify-end mx-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Sort</p>
|
||||
<p class="text-sm text-gray-500 pb-2">{$_('sort')}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
bind:value={filter.sort}
|
||||
@@ -70,7 +71,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Display as</p>
|
||||
<p class="text-sm text-gray-500 pb-2">{$_('display-as')}</p>
|
||||
|
||||
<Select
|
||||
bind:value={selectedDisplayOption}
|
||||
@@ -91,6 +92,7 @@
|
||||
class="max-w-full"
|
||||
class:basis-full={selectedDisplayOption === "list"}
|
||||
href="/trail/view/{trail.id}"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
{#if selectedDisplayOption === "cards"}
|
||||
<TrailCard {trail}></TrailCard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import { formatDistance, formatElevation, formatTimeHHMM } from "$lib/util/format_util";
|
||||
|
||||
export let trail: Trail;
|
||||
</script>
|
||||
@@ -21,12 +21,12 @@
|
||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatMeters(
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
trail.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatMeters(
|
||||
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span
|
||||
>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Dropdown from "../base/dropdown.svelte";
|
||||
|
||||
export let waypoint: Waypoint;
|
||||
export let mode: "show" | "edit" = "show";
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
{ text: $_("edit"), value: "edit" },
|
||||
{ text: $_("delete"), value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-4 border border-input-border rounded-md my-2 hover:bg-menu-item-background-hover">
|
||||
<div
|
||||
class="p-4 border border-input-border rounded-md my-2 hover:bg-menu-item-background-hover"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5>
|
||||
<i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
import { waypoint } from "$lib/stores/waypoint_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;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<Modal
|
||||
id="waypoint-modal"
|
||||
title="Add Waypoint"
|
||||
title={$form.id ? $_("edit-waypoint") : $_("add-waypoint")}
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="basis-full">
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
label={$_("name")}
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<TextField
|
||||
name="icon"
|
||||
label="Icon"
|
||||
label={$_("icon")}
|
||||
bind:value={$form.icon}
|
||||
icon={$form.icon}
|
||||
error={$errors.icon}
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Description"
|
||||
label={$_("description")}
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
@@ -70,14 +70,14 @@
|
||||
<div class="flex gap-4">
|
||||
<TextField
|
||||
name="lat"
|
||||
label="Latitude"
|
||||
label={$_("latitude")}
|
||||
bind:value={$form.lat}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="lon"
|
||||
label="Longitude"
|
||||
label={$_("longitude")}
|
||||
bind:value={$form.lon}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
@@ -85,9 +85,11 @@
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||
<button class="btn-secondary" on:click={closeModal}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button class="btn-primary" type="submit" form="waypoint-form"
|
||||
>Save</button
|
||||
>{$_("save")}</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
14
web/src/lib/i18n/index.ts
Normal file
14
web/src/lib/i18n/index.ts
Normal file
@@ -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,
|
||||
})
|
||||
96
web/src/lib/i18n/locales/de.json
Normal file
96
web/src/lib/i18n/locales/de.json
Normal file
@@ -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"
|
||||
}
|
||||
96
web/src/lib/i18n/locales/en.json
Normal file
96
web/src/lib/i18n/locales/en.json
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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<AuthModel | null> = writable<AuthModel | null>()
|
||||
export const currentUser: Writable<User | null> = writable<User | null>()
|
||||
|
||||
export async function users_create(user: User) {
|
||||
const model = await pb.collection('users').create({ ...user, passwordConfirm: user.password });
|
||||
@@ -26,3 +30,17 @@ 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<User>(id, user);
|
||||
|
||||
currentUser.set(model);
|
||||
}
|
||||
|
||||
export async function users_delete(user: User) {
|
||||
let success = await pb
|
||||
.collection("users")
|
||||
.delete(user.id);
|
||||
}
|
||||
10
web/src/lib/util/authorization_util.ts
Normal file
10
web/src/lib/util/authorization_util.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const privateRoutes = [
|
||||
"/profile",
|
||||
"/lists"
|
||||
]
|
||||
|
||||
export function isRouteProtected(path: string) {
|
||||
return privateRoutes.some(allowedPath =>
|
||||
path === allowedPath || path.startsWith(allowedPath + '/')
|
||||
);
|
||||
}
|
||||
@@ -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 "-";
|
||||
}
|
||||
|
||||
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 {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -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();
|
||||
@@ -183,8 +183,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
||||
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._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 = {};
|
||||
@@ -217,7 +217,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
||||
if (Array.isArray(src)) {
|
||||
return Promise.all(src.map(m => this.import(m)));
|
||||
}
|
||||
switch(src) {
|
||||
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;
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -316,7 +316,7 @@ 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;
|
||||
@@ -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;
|
||||
},
|
||||
@@ -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),
|
||||
@@ -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);
|
||||
@@ -609,7 +609,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
||||
let map = this._map;
|
||||
|
||||
if (opts.detached) {
|
||||
let { offsetWidth, offsetHeight} = this.eleDiv;
|
||||
let { offsetWidth, offsetHeight } = this.eleDiv;
|
||||
if (offsetWidth > 0) opts.width = offsetWidth;
|
||||
if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
|
||||
} else {
|
||||
@@ -670,7 +670,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
|
||||
|
||||
_initLayer() {
|
||||
this._layers
|
||||
.on('layeradd layerremove', ({layer, type}) => {
|
||||
.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')
|
||||
@@ -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?
|
||||
_loadFile(data) {
|
||||
this._parseFromString(data)
|
||||
.then( geojson => geojson && this._loadLayer(geojson));
|
||||
}).catch((err) => console.warn(err));
|
||||
.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("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + 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) {
|
||||
@@ -864,7 +859,7 @@ 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) {
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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]
|
||||
@@ -1054,7 +1049,7 @@ 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.
|
||||
@@ -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: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
|
||||
wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' }));
|
||||
}
|
||||
let marker = L.marker(latlng, { icon: wptIcons[sym] });
|
||||
if (content) {
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<script>
|
||||
import { beforeNavigate, goto } from "$app/navigation";
|
||||
import Toast from "$lib/components/base/toast.svelte";
|
||||
import Footer from "$lib/components/footer.svelte";
|
||||
import NavBar from "$lib/components/nav_bar.svelte";
|
||||
import { isRouteProtected } from "$lib/util/authorization_util";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import "../css/app.css";
|
||||
import "../css/components.css";
|
||||
import "../css/theme.css";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
|
||||
beforeNavigate((n) => {
|
||||
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
|
||||
n.cancel();
|
||||
goto("/login");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>| wanderer</title>
|
||||
</svelte:head>
|
||||
<NavBar></NavBar>
|
||||
<Toast></Toast>
|
||||
<slot />
|
||||
|
||||
8
web/src/routes/+layout.ts
Normal file
8
web/src/routes/+layout.ts
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Home | wanderer</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="hero grid grid-cols-1 md:grid-cols-2 md:px-8 md:gap-8"
|
||||
style="height: calc(100vh - 112px)"
|
||||
@@ -67,17 +72,16 @@
|
||||
class="flex flex-col justify-center gap-8 max-w-md mx-8 md:mx-auto lg:-mt-24"
|
||||
>
|
||||
<h2 class="text-7xl font-bold">
|
||||
Welcome to <span class="-tracking-[0.075em]">wanderer</span>
|
||||
{$_("welcome_to")} <span class="-tracking-[0.075em]">wanderer</span>
|
||||
</h2>
|
||||
<h5>
|
||||
Explore exciting trails, save your favorites, and experience the
|
||||
beauty of nature. Find your next adventure!
|
||||
{$_("hero_section_0_text")}
|
||||
</h5>
|
||||
<Search
|
||||
on:update={(e) => search(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
large={true}
|
||||
placeholder="Search for trails, places..."
|
||||
placeholder="{$_("search-for-trails-places")}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
</div>
|
||||
@@ -105,13 +109,12 @@
|
||||
{$currentUser ? "Trails for you" : "Explore some trails"}
|
||||
</h2>
|
||||
<h5>
|
||||
Here are some trails you might like. Or you can just go to the full
|
||||
list right now.
|
||||
{$_("hero_section_1_text")}
|
||||
</h5>
|
||||
<a
|
||||
class="inline-block btn-primary btn-large"
|
||||
href="/trails"
|
||||
role="button">Explore</a
|
||||
role="button">{$_("explore")}</a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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"
|
||||
>
|
||||
<div class="max-w-md md:mx-auto space-y-8">
|
||||
<h2 class="text-4xl md:text-5xl font-bold">Categories</h2>
|
||||
<h2 class="text-4xl md:text-5xl font-bold">{$_("categories")}</h2>
|
||||
<h5>
|
||||
Did you know? You cannot only save you hiking trails. There are many
|
||||
categories for all your adventures.
|
||||
{$_("hero_section_2_text")}
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
@@ -130,7 +132,10 @@
|
||||
class="grid grid-cols-1 lg:grid-cols-2 justify-items-center gap-8 py-8"
|
||||
>
|
||||
{#each $categories as category}
|
||||
<a href="/trails?category={category.id}" data-sveltekit-preload-data="off">
|
||||
<a
|
||||
href="/trails?category={category.id}"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<CategoryCard {category}></CategoryCard>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import ListCard from "$lib/components/list/list_card.svelte";
|
||||
import ListModal from "$lib/components/list/list_modal.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
@@ -14,11 +15,15 @@
|
||||
lists_index,
|
||||
lists_update,
|
||||
} from "$lib/stores/list_store";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let openListModal: () => void;
|
||||
let openConfirmModal: () => void;
|
||||
|
||||
let filter: TrailFilter = $page.data.filter;
|
||||
|
||||
let listToBeDeleted: List | null = null;
|
||||
|
||||
function beforeListModalOpen() {
|
||||
list.set(new List("", []));
|
||||
openListModal();
|
||||
@@ -47,12 +52,24 @@
|
||||
list.set(currentList);
|
||||
openListModal();
|
||||
} else if (item.value == "delete") {
|
||||
await lists_delete(currentList);
|
||||
await lists_index();
|
||||
openConfirmModal();
|
||||
listToBeDeleted = currentList;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteList() {
|
||||
if (!listToBeDeleted) {
|
||||
return;
|
||||
}
|
||||
await lists_delete(listToBeDeleted);
|
||||
await lists_index();
|
||||
listToBeDeleted = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-8 max-w-7xl mx-4 md:mx-auto"
|
||||
style="min-height: calc(100vh - 124px)"
|
||||
@@ -65,7 +82,7 @@
|
||||
on:click={beforeListModalOpen}
|
||||
>
|
||||
<i class="fa fa-plus text-xl aspect-square"></i>
|
||||
<h5 class="text-xl font-semibold">Create new list</h5>
|
||||
<h5 class="text-xl font-semibold">{$_("create-new-list")}</h5>
|
||||
</button>
|
||||
<hr class="border-separator my-2" />
|
||||
<div class="max-h-[512px] md:max-h-none overflow-y-auto">
|
||||
@@ -89,4 +106,9 @@
|
||||
on:update={async () => await lists_index()}
|
||||
></TrailList>
|
||||
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
||||
<ConfirmModal
|
||||
text={$_("delete-list-confirm")}
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteList}
|
||||
></ConfirmModal>
|
||||
</main>
|
||||
|
||||
@@ -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<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
@@ -51,6 +52,9 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("login")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
@@ -61,18 +65,18 @@
|
||||
{:else}
|
||||
<LogoTextTwoLineLight></LogoTextTwoLineLight>
|
||||
{/if}
|
||||
<h4 class="text-xl font-semibold">Save your adventures!</h4>
|
||||
<h4 class="text-xl font-semibold">{$_("slogan")}</h4>
|
||||
<div class="space-y-6 w-80">
|
||||
<TextField
|
||||
name="username"
|
||||
label="Username/Email"
|
||||
label="{$_('username')}/{$_('email')}"
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label="Password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
@@ -86,9 +90,9 @@
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
>Don't have an account? <a
|
||||
>{$_('no-account')} <a
|
||||
class="text-blue-500 underline"
|
||||
href="/register">Make one!</a
|
||||
href="/register">{$_('make-one')}</a
|
||||
></span
|
||||
>
|
||||
</form>
|
||||
|
||||
@@ -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<string, GPX> = {};
|
||||
@@ -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 @@
|
||||
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||
<h5><i class="fa fa-location-dot mr-3"></i>${trail.location}</h5>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500"><span class="shrink-0"><i
|
||||
class="fa fa-left-right mr-2"></i>${formatMeters(
|
||||
class="fa fa-left-right mr-2"></i>${formatDistance(
|
||||
trail.distance,
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatMeters(
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
||||
trail.duration,
|
||||
@@ -239,6 +249,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("map")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
||||
<div
|
||||
id="trail-list"
|
||||
@@ -250,7 +263,7 @@
|
||||
extraClasses="w-full"
|
||||
on:update={(e) => search(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
placeholder="Search for trails, places..."
|
||||
placeholder="{$_("search-for-trails-places")}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
<button
|
||||
|
||||
@@ -1,95 +1,81 @@
|
||||
<script lang="ts">
|
||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||
import Tabs from "$lib/components/tabs.svelte";
|
||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type { Map, Marker } from "leaflet";
|
||||
import type { Marker } from "leaflet";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import type { DataSource } from "photoswipe/lightbox";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import "photoswipe/style.css";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
let markers: Marker[] = [];
|
||||
let markers: Marker[];
|
||||
|
||||
const tabs = ["Description", "Waypoints", "Photos", "Summit book"];
|
||||
let lightbox: PhotoSwipeLightbox;
|
||||
let lightboxDataSource: DataSource;
|
||||
|
||||
const tabs = [
|
||||
$_("description"),
|
||||
$_("waypoints"),
|
||||
$_("photos"),
|
||||
$_("summit-book"),
|
||||
];
|
||||
let activeTab = 0;
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
await import("leaflet.awesome-markers");
|
||||
//@ts-ignore
|
||||
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
||||
onMount(() => {
|
||||
lightboxDataSource = $trail.photos.map((p) => ({
|
||||
src: p,
|
||||
}));
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
dataSource: lightboxDataSource,
|
||||
pswpModule: async () => await import("photoswipe"),
|
||||
});
|
||||
lightbox.init();
|
||||
|
||||
map = L.map("map").setView([$trail.lat, $trail.lon], 14);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
const elevation_options = {
|
||||
height: 200,
|
||||
|
||||
theme: "lightblue-theme",
|
||||
detached: true,
|
||||
elevationDiv: "#elevation",
|
||||
closeBtn: false,
|
||||
followMarker: true,
|
||||
autofitBounds: true,
|
||||
imperial: false,
|
||||
reverseCoords: false,
|
||||
acceleration: false,
|
||||
slope: true,
|
||||
speed: false,
|
||||
altitude: true,
|
||||
time: true,
|
||||
distance: true,
|
||||
// Summary track info style: "inline" || "multiline" || false
|
||||
summary: false,
|
||||
downloadLink: false,
|
||||
ruler: false,
|
||||
legend: true,
|
||||
// Toggle "leaflet-almostover" integration
|
||||
almostOver: true,
|
||||
// Toggle "leaflet-distance-markers" integration
|
||||
distanceMarkers: false,
|
||||
// Toggle "leaflet-edgescale" integration
|
||||
edgeScale: false,
|
||||
// Toggle "leaflet-hotline" integration
|
||||
hotline: true,
|
||||
// Display track datetimes: true || false
|
||||
timestamps: false,
|
||||
waypoints: false,
|
||||
wptIcons: false,
|
||||
wptLabels: false,
|
||||
preferCanvas: true,
|
||||
lightbox.on("beforeOpen", () => {
|
||||
const pswp = lightbox.pswp;
|
||||
const ds = pswp?.options?.dataSource;
|
||||
if (Array.isArray(ds)) {
|
||||
for (let idx = 0, len = ds.length; idx < len; idx++) {
|
||||
const item = ds[idx];
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
item.width = img.naturalWidth;
|
||||
item.height = img.naturalHeight;
|
||||
pswp?.refreshSlideContent(idx);
|
||||
};
|
||||
|
||||
const controlElevation = L.control
|
||||
.elevation(elevation_options)
|
||||
.addTo(map);
|
||||
controlElevation.load(getFileURL($trail, $trail.gpx));
|
||||
|
||||
// addGPXLayer($trail);
|
||||
|
||||
for (const waypoint of $trail.expand.waypoints) {
|
||||
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||
marker.addTo(map);
|
||||
markers.push(marker);
|
||||
img.src = item.src as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openMarkerPopup(i: number) {
|
||||
markers[i].openPopup();
|
||||
}
|
||||
|
||||
function openGallery(idx: number) {
|
||||
lightbox?.loadAndOpen(idx, lightboxDataSource);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1">
|
||||
<svelte:head>
|
||||
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1 gap-y-4">
|
||||
<div
|
||||
id="trail-details"
|
||||
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 rounded-xl"
|
||||
@@ -104,7 +90,7 @@
|
||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||
></div>
|
||||
<div
|
||||
class="flex absolute flex-wrap justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
|
||||
class="flex absolute justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
|
||||
>
|
||||
<div class="text-white">
|
||||
<h4 class="text-4xl font-bold">
|
||||
@@ -115,30 +101,33 @@
|
||||
{$trail.location}
|
||||
</h3>
|
||||
</div>
|
||||
{#if $currentUser && $currentUser.id == $trail.author}
|
||||
<TrailDropdown trail={$trail} mode="map"></TrailDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
<section class="grid grid-cols-2 sm:grid-cols-4 gap-y-4 justify-around">
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Distance</span>
|
||||
<span class="text-gray-500">{$_('distance')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatMeters($trail.distance)}</span
|
||||
>{formatDistance($trail.distance)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Elevation gain</span>
|
||||
<span class="text-gray-500">{$_('elevation-gain')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatMeters($trail.elevation_gain)}</span
|
||||
>{formatElevation($trail.elevation_gain)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Est. duration</span>
|
||||
<span class="text-gray-500">{$_('est-duration')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatTimeHHMM($trail.duration)}</span
|
||||
>
|
||||
</div>
|
||||
{#if $trail.expand.category}
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Category</span>
|
||||
<span class="text-gray-500">{$_('category')}</span>
|
||||
<span class="font-semibold"
|
||||
>{$trail.expand.category.name}</span
|
||||
>
|
||||
@@ -163,10 +152,13 @@
|
||||
</ul>
|
||||
{/if}
|
||||
{#if activeTab == 2}
|
||||
<div id="photo-gallery" class="">
|
||||
<div id="photo-gallery" class="space-y-4">
|
||||
{#each $trail.photos ?? [] as photo, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<img
|
||||
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
||||
on:click={() => openGallery(i)}
|
||||
src={photo}
|
||||
alt=""
|
||||
/>
|
||||
@@ -182,18 +174,11 @@
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
<div id="map-container" class="flex flex-col">
|
||||
<div id="map" class="rounded-xl z-0 basis-full"></div>
|
||||
<div id="elevation"></div>
|
||||
</div>
|
||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#map-container {
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
#map-container,
|
||||
#trail-details {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import RadioGroup, {
|
||||
type RadioItem,
|
||||
} from "$lib/components/base/radio_group.svelte";
|
||||
import Search, {
|
||||
type SearchItem,
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import Select, {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import { ms } from "$lib/meilisearch";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import {
|
||||
currentUser,
|
||||
logout,
|
||||
users_delete,
|
||||
users_update,
|
||||
} from "$lib/stores/user_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import RadioGroup, {
|
||||
type RadioItem,
|
||||
} from "$lib/components/base/radio_group.svelte";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { onMount } from "svelte";
|
||||
import { _, locale } from "svelte-i18n";
|
||||
|
||||
const languages: SelectItem[] = [
|
||||
{ text: "English", value: "en" },
|
||||
{ text: "German", value: "de" },
|
||||
{ text: $_("english"), value: "en" },
|
||||
{ text: $_("german"), value: "de" },
|
||||
];
|
||||
|
||||
const units: RadioItem[] = [
|
||||
{ text: "Metric", value: "metric" },
|
||||
{ text: "Imperial", value: "imperial" },
|
||||
{ text: $_("metric"), value: "metric" },
|
||||
{ text: $_("imperial"), value: "imperial" },
|
||||
];
|
||||
|
||||
let selectedLanguage = "en";
|
||||
@@ -28,6 +37,13 @@
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
let citySearchQuery: string = "";
|
||||
|
||||
let openConfirmModal: () => void;
|
||||
|
||||
onMount(() => {
|
||||
citySearchQuery = $currentUser?.location?.name ?? "";
|
||||
selectedLanguage = $currentUser?.language || "en";
|
||||
});
|
||||
|
||||
async function searchCities(q: string) {
|
||||
const result = await ms.index("cities500").search(q, { limit: 5 });
|
||||
searchDropdownItems = result.hits.map((h) => ({
|
||||
@@ -39,64 +55,134 @@
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {}
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
await users_update($currentUser!.id, {
|
||||
location: {
|
||||
name: item.value.name,
|
||||
lat: item.value.lat,
|
||||
lon: item.value.lon,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAvatarSelection() {
|
||||
const files = (
|
||||
document.getElementById("avatarInput") as HTMLInputElement
|
||||
).files;
|
||||
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("avatar", files[0]);
|
||||
|
||||
await users_update($currentUser!.id, formData);
|
||||
}
|
||||
|
||||
async function handleUnitSelection(e: RadioItem) {
|
||||
await users_update($currentUser!.id, {
|
||||
unit: e.value as "imperial" | "metric",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLanguageSelection(value: "en" | "de") {
|
||||
locale.set(value);
|
||||
await users_update($currentUser!.id, {
|
||||
language: value,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
await users_delete($currentUser!);
|
||||
logout();
|
||||
goto("/");
|
||||
}
|
||||
|
||||
function openFileBrowser() {
|
||||
document.getElementById("avatarInput")!.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("profile")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[356px_1fr] max-w-4xl mx-4 md:mx-auto gap-x-8 items-start"
|
||||
>
|
||||
{#if $currentUser}
|
||||
<div
|
||||
class="flex flex-col items-center rounded-xl border border-input-border p-4"
|
||||
>
|
||||
<div
|
||||
class="rounded-full w-32 aspect-square overflow-hidden relative group"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-32 aspect-square"
|
||||
src={$currentUser.avatar ||
|
||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt=""
|
||||
alt="avatar"
|
||||
/>
|
||||
<button
|
||||
class="absolute top-0 w-32 aspect-square opacity-0 group-hover:opacity-100 flex justify-center items-center bg-black/50 focus:bg-black/60 text-white cursor-pointer transition-opacity"
|
||||
on:click={openFileBrowser}
|
||||
>
|
||||
<i class="fa fa-pen"></i>
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
name="avatar"
|
||||
id="avatarInput"
|
||||
accept="image/*"
|
||||
style="display: none;"
|
||||
on:change={handleAvatarSelection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl mt-4 font-semibold">{$currentUser.username}</h3>
|
||||
<h5 class="font-medium">{$currentUser.email}</h5>
|
||||
</div>
|
||||
<div class="space-y-6 rounded-xl p-4">
|
||||
<h3 class="text-2xl font-semibold">Settings</h3>
|
||||
<h3 class="text-2xl font-semibold">{$_("settings")}</h3>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Default Location</h5>
|
||||
<h5 class="font-medium mb-1">{$_("default-location")}</h5>
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="Search cities..."
|
||||
placeholder="{$_('search-cities')}..."
|
||||
bind:value={citySearchQuery}
|
||||
on:update={(e) => searchCities(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
></Search>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Language</h5>
|
||||
<Select items={languages} bind:value={selectedLanguage}
|
||||
<h5 class="font-medium mb-1">{$_("language")}</h5>
|
||||
<Select
|
||||
items={languages}
|
||||
bind:value={selectedLanguage}
|
||||
on:change={(e) => handleLanguageSelection(e.detail)}
|
||||
></Select>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Units</h5>
|
||||
<h5 class="font-medium mb-1">{$_("units")}</h5>
|
||||
<RadioGroup
|
||||
name="unit"
|
||||
items={units}
|
||||
selected={0}
|
||||
on:change={() => {}}
|
||||
selected={$currentUser.unit == "metric" ? 0 : 1}
|
||||
on:change={(e) => handleUnitSelection(e.detail)}
|
||||
></RadioGroup>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h5 class="font-medium mb-1">Set new password</h5>
|
||||
<TextField placeholder="New password" label="" type="password"
|
||||
></TextField>
|
||||
<TextField placeholder="Repeat password" type="password"
|
||||
></TextField>
|
||||
</div>
|
||||
<hr class="border-input-border" />
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xl text-red-400">Danger zone</h4>
|
||||
<button class="btn-danger">Delete Account</button>
|
||||
<h4 class="text-xl text-red-400">{$_("danger-zone")}</h4>
|
||||
<button class="btn-danger" on:click={openConfirmModal}
|
||||
>{$_("delete-account")}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
<ConfirmModal
|
||||
text={$_("account-delete-confirm")}
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteAccount}
|
||||
></ConfirmModal>
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
import { users_create, type User, login } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { object, string } from "yup";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let loading: boolean = false;
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -53,6 +55,9 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('register')} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
@@ -63,25 +68,25 @@
|
||||
{:else}
|
||||
<LogoTextTwoLineLight></LogoTextTwoLineLight>
|
||||
{/if}
|
||||
<h4 class="text-xl font-semibold">Save your adventures!</h4>
|
||||
<h4 class="text-xl font-semibold">{$_("slogan")}</h4>
|
||||
<div class="space-y-6 w-80">
|
||||
<TextField
|
||||
name="username"
|
||||
label="Username"
|
||||
label={$_("username")}
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="email"
|
||||
label="Email"
|
||||
label={$_("email")}
|
||||
bind:value={$form.email}
|
||||
on:change={handleChange}
|
||||
error={$errors.email}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label="Password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
@@ -91,13 +96,12 @@
|
||||
primary={true}
|
||||
extraClasses={"min-w-full"}
|
||||
type="submit"
|
||||
{loading}>Register</Button
|
||||
{loading}>{$_("register")}</Button
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
>Already have an account? <a
|
||||
class="text-blue-500 underline"
|
||||
href="/login">Login!</a
|
||||
>{$_("already-account")}
|
||||
<a class="text-blue-500 underline" href="/login">{$_("login")}!</a
|
||||
></span
|
||||
>
|
||||
</form>
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
trails_update,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
@@ -32,6 +36,7 @@
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let data: { trail: Trail };
|
||||
|
||||
@@ -350,15 +355,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title
|
||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-trail")} | wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
||||
<form
|
||||
id="trail-form"
|
||||
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3>
|
||||
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||
>Upload GPX</button
|
||||
>{$_("upload-gpx")}</button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
@@ -369,17 +380,18 @@
|
||||
on:change={handleFileSelection}
|
||||
/>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Basic Info</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("basic-info")}</h3>
|
||||
<div class="flex gap-4 justify-around">
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Distance</span>
|
||||
<span class="font-medium">{formatMeters($form.distance)}</span>
|
||||
<span>{$_('distance')}</span>
|
||||
<span class="font-medium">{formatDistance($form.distance)}</span
|
||||
>
|
||||
<input type="hidden" name="distance" value={$form.distance} />
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<span>{$_("elevation-gain")}</span>
|
||||
<span class="font-medium"
|
||||
>{formatMeters($form.elevation_gain)}</span
|
||||
>{formatElevation($form.elevation_gain)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -388,7 +400,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span>{$_("est-duration")}</span>
|
||||
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
|
||||
>
|
||||
<input type="hidden" name="duration" value={$form.duration} />
|
||||
@@ -398,33 +410,34 @@
|
||||
</div>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
label={$_("name")}
|
||||
on:change={handleChange}
|
||||
error={$errors.name}
|
||||
bind:value={$form.name}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="location"
|
||||
label="Location"
|
||||
label={$_("location")}
|
||||
error={$errors.location}
|
||||
bind:value={$form.location}
|
||||
></TextField>
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Describe your trail"
|
||||
label={$_("describe-your-trail")}
|
||||
bind:value={$form.description}
|
||||
></Textarea>
|
||||
{#if $form.expand.category}
|
||||
<Select
|
||||
name="category"
|
||||
label="Category"
|
||||
label={$_("category")}
|
||||
bind:value={$form.expand.category.id}
|
||||
items={$categories.map((c) => ({ text: c.name, value: c.id }))}
|
||||
></Select>
|
||||
{/if}
|
||||
<Toggle name="public" label="Public" bind:value={$form.public}></Toggle>
|
||||
<Toggle name="public" label={$_("public")} bind:value={$form.public}
|
||||
></Toggle>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("waypoints")}</h3>
|
||||
<ul>
|
||||
{#each $form.expand.waypoints ?? [] as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
@@ -441,10 +454,10 @@
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeWaypointModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-waypoint")}</button
|
||||
>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Photos</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("photos")}</h3>
|
||||
<div class="flex gap-4 max-w-full overflow-x-auto shrink-0">
|
||||
<button
|
||||
class="btn-secondary h-32 w-32 m-2 shrink-0 grow-0 basis-auto"
|
||||
@@ -471,7 +484,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Summit Book</h3>
|
||||
<h3 class="text-xl font-semibold">{$_('summit-book')}</h3>
|
||||
<ul>
|
||||
{#each $form.expand.summit_logs ?? [] as log, i}
|
||||
<li>
|
||||
@@ -487,7 +500,7 @@
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeSummitLogModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Entry</button
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
|
||||
>
|
||||
<hr class="border-separator" />
|
||||
<Button
|
||||
@@ -495,7 +508,7 @@
|
||||
large={true}
|
||||
type="submit"
|
||||
extraClasses="mb-2"
|
||||
{loading}>Save Trail</Button
|
||||
{loading}>{$_("save-trail")}</Button
|
||||
>
|
||||
</form>
|
||||
<div class="rounded-xl" id="map"></div>
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||
import Dropdown from "$lib/components/base/dropdown.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
|
||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||
import Tabs from "$lib/components/tabs.svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import type { List } from "$lib/models/list";
|
||||
import {
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
lists_remove_trail,
|
||||
} from "$lib/stores/list_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { trail, trails_delete } from "$lib/stores/trail_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
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 { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type { Icon, Map, Marker } from "leaflet";
|
||||
@@ -26,9 +20,15 @@
|
||||
import type { DataSource } from "photoswipe";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import "photoswipe/style.css";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
const tabs = ["Description", "Waypoints", "Photos", "Summit book"];
|
||||
const tabs = [
|
||||
$_("description"),
|
||||
$_("waypoints"),
|
||||
$_("photos"),
|
||||
$_("summit-book"),
|
||||
];
|
||||
|
||||
let map: Map;
|
||||
|
||||
@@ -39,20 +39,6 @@
|
||||
let lightbox: PhotoSwipeLightbox;
|
||||
let lightboxDataSource: DataSource;
|
||||
|
||||
let openConfirmModal: () => void;
|
||||
let openListSelectModal: () => void;
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{ text: "Show on map", value: "map", icon: "map" },
|
||||
{ text: "Directions", value: "direction", icon: "car" },
|
||||
...($trail.gpx
|
||||
? [{ text: "Download GPX", value: "download", icon: "download" }]
|
||||
: []),
|
||||
{ text: "Add to list", value: "list", icon: "bookmark" },
|
||||
{ text: "Edit", value: "edit", icon: "pen" },
|
||||
{ text: "Delete", value: "delete", icon: "trash" },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
@@ -132,74 +118,16 @@
|
||||
lightbox?.loadAndOpen(idx, lightboxDataSource);
|
||||
}
|
||||
|
||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "map") {
|
||||
goto(`/map/trail/${$trail.id!}`);
|
||||
} else if (item.value == "list") {
|
||||
openListSelectModal();
|
||||
} else if (item.value == "direction") {
|
||||
window
|
||||
.open(
|
||||
`https://www.google.com/maps/dir/Current+Location/${$trail.lat},${$trail.lon}`,
|
||||
"_blank",
|
||||
)
|
||||
?.focus();
|
||||
} else if (item.value == "download") {
|
||||
downloadURI(getFileURL($trail, $trail.gpx), $trail.gpx!);
|
||||
} else if (item.value == "edit") {
|
||||
goto(`/trail/edit/${$trail.id}`);
|
||||
} else if (item.value == "delete") {
|
||||
openConfirmModal();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadURI(uri: string, name: string) {
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("download", name);
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function deleteTrail() {
|
||||
trails_delete($trail).then(() => history.back());
|
||||
}
|
||||
|
||||
async function toggleMapFullScreen() {
|
||||
goto(`/map/trail/${$trail.id!}`);
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
try {
|
||||
if (list.trails?.includes($trail.id!)) {
|
||||
await lists_remove_trail(list, $trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Removed trail from "${list.name}"`,
|
||||
});
|
||||
} else {
|
||||
await lists_add_trail(list, $trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Added trail to "${list.name}"`,
|
||||
});
|
||||
}
|
||||
await lists_index();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error adding trail to list.",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="trail-panel max-w-5xl mx-auto border border-input-border rounded-3xl overflow-hidden"
|
||||
>
|
||||
@@ -225,17 +153,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
{#if $currentUser && $currentUser.id == $trail.author}
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
on:change={(e) => handleDropdownClick(e.detail)}
|
||||
let:toggleMenu={openDropdown}
|
||||
><button
|
||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
|
||||
on:click={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button></Dropdown
|
||||
>
|
||||
<TrailDropdown trail={$trail} mode="overview"></TrailDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -243,26 +161,26 @@
|
||||
class="grid grid-cols-2 sm:grid-cols-4 gap-y-4 justify-around py-8 border-b border-input-border"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Distance</span>
|
||||
<span>{$_("distance")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatMeters($trail.distance)}</span
|
||||
>{formatDistance($trail.distance)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<span>{$_("elevation-gain")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatMeters($trail.elevation_gain)}</span
|
||||
>{formatElevation($trail.elevation_gain)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span>{$_("est-duration")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatTimeHHMM($trail.duration)}</span
|
||||
>
|
||||
</div>
|
||||
{#if $trail.expand.category}
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Category</span>
|
||||
<span>{$_("category")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{$trail.expand.category.name}</span
|
||||
>
|
||||
@@ -334,15 +252,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ConfirmModal
|
||||
text="Do you really want to delete this trail? This action cannot be undone."
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteTrail}
|
||||
></ConfirmModal>
|
||||
<ListSelectModal
|
||||
bind:openModal={openListSelectModal}
|
||||
on:change={(e) => handleListSelection(e.detail)}
|
||||
></ListSelectModal>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import { trails, trails_search_filter } from "$lib/stores/trail_store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let filterExpanded: boolean = true;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("trail", { values: { n: 2 } })} | wanderer</title>
|
||||
</svelte:head>
|
||||
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user