adds settings collection

This commit is contained in:
Christian Beutel
2024-04-13 20:51:05 +02:00
parent f9f8998617
commit feb2214b1b
33 changed files with 1321 additions and 81 deletions

View File

@@ -0,0 +1,130 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "uavt73rsqcn1n13",
"created": "2024-04-13 13:54:26.023Z",
"updated": "2024-04-13 13:54:26.023Z",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "0sepzvkh",
"name": "language",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"en",
"de",
"fr",
"hu",
"nl",
"pl",
"pt",
"zh"
]
}
},
{
"system": false,
"id": "zwg1jl0d",
"name": "unit",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"metric",
"imperial"
]
}
},
{
"system": false,
"id": "jo1zcsbu",
"name": "mapFocus",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"trails",
"location"
]
}
},
{
"system": false,
"id": "ufhepjxo",
"name": "location",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
},
{
"system": false,
"id": "5uip7a4p",
"name": "user",
"type": "relation",
"required": false,
"presentable": false,
"unique": false,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}`
collection := &models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return daos.New(db).SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
return dao.DeleteCollection(collection)
})
}

View File

@@ -0,0 +1,162 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_")
if err != nil {
return err
}
// remove
collection.Schema.RemoveField("wjofulpg")
// remove
collection.Schema.RemoveField("t1wlsqyp")
// remove
collection.Schema.RemoveField("fhxhln9g")
// remove
collection.Schema.RemoveField("wosrk4ue")
query := dao.RecordQuery("_pb_users_auth_")
users := []*models.Record{}
if err := query.All(&users); err != nil {
return err
}
settingsCollection, err := dao.FindCollectionByNameOrId("settings")
if err != nil {
return err
}
for _, user := range users {
settings := models.NewRecord(settingsCollection)
language := user.Get("language")
unit := user.Get("unit")
location := user.Get("location")
settings.Set("language", language)
settings.Set("unit", unit)
settings.Set("location", location)
settings.Set("user", user.Id)
settings.Set("mapFocus", "trails")
if err := dao.SaveRecord(settings); err != nil {
return err
}
}
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("_pb_users_auth_")
if err != nil {
return err
}
// add
del_unit := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "wjofulpg",
"name": "unit",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"metric",
"imperial"
]
}
}`), del_unit); err != nil {
return err
}
collection.Schema.AddField(del_unit)
// add
del_language := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "t1wlsqyp",
"name": "language",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"en",
"de",
"fr",
"hu",
"nl",
"pl",
"pt",
"zh"
]
}
}`), del_language); err != nil {
return err
}
collection.Schema.AddField(del_language)
// add
del_mapView := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "fhxhln9g",
"name": "mapView",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"location",
"trails"
]
}
}`), del_mapView); err != nil {
return err
}
collection.Schema.AddField(del_mapView)
// add
del_location := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "wosrk4ue",
"name": "location",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 2000000
}
}`), del_location); err != nil {
return err
}
collection.Schema.AddField(del_location)
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,46 @@
package migrations
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/tools/types"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
collection.ListRule = types.Pointer("user = @request.auth.id")
collection.ViewRule = types.Pointer("user = @request.auth.id")
collection.CreateRule = types.Pointer("")
collection.UpdateRule = types.Pointer("user = @request.auth.id")
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("uavt73rsqcn1n13")
if err != nil {
return err
}
collection.ListRule = nil
collection.ViewRule = nil
collection.CreateRule = nil
collection.UpdateRule = nil
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,98 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `{
"id": "urytyc428mwlbqq",
"created": "2024-04-13 17:00:41.541Z",
"updated": "2024-04-13 17:00:41.541Z",
"name": "trails_bounding_box",
"type": "view",
"system": false,
"schema": [
{
"system": false,
"id": "iyhsoisl",
"name": "max_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
},
{
"system": false,
"id": "kx2qfztr",
"name": "max_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
},
{
"system": false,
"id": "z4qsnjeb",
"name": "min_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
},
{
"system": false,
"id": "p66xomdb",
"name": "min_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"query": "SELECT users.id, COALESCE(MAX(trails.lat), 0) AS max_lat, COALESCE(MAX(trails.lon), 0) AS max_lon, COALESCE(MIN(trails.lat), 0) AS min_lat, COALESCE(MIN(trails.lon), 0) AS min_lon FROM users LEFT JOIN trails ON users.id = trails.author GROUP BY users.id;"
}
}`
collection := &models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
return err
}
return daos.New(db).SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
return dao.DeleteCollection(collection)
})
}

View File

@@ -0,0 +1,205 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/types"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
collection.ViewRule = types.Pointer("@request.auth.id = id")
// remove
collection.Schema.RemoveField("iyhsoisl")
// remove
collection.Schema.RemoveField("kx2qfztr")
// remove
collection.Schema.RemoveField("z4qsnjeb")
// remove
collection.Schema.RemoveField("p66xomdb")
// add
new_max_lat := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "osfey1yx",
"name": "max_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), new_max_lat); err != nil {
return err
}
collection.Schema.AddField(new_max_lat)
// add
new_max_lon := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "eohslzky",
"name": "max_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), new_max_lon); err != nil {
return err
}
collection.Schema.AddField(new_max_lon)
// add
new_min_lat := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "kigvankd",
"name": "min_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), new_min_lat); err != nil {
return err
}
collection.Schema.AddField(new_min_lat)
// add
new_min_lon := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "ifnks9mg",
"name": "min_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), new_min_lon); err != nil {
return err
}
collection.Schema.AddField(new_min_lon)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db);
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
if err != nil {
return err
}
collection.ViewRule = nil
// add
del_max_lat := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "iyhsoisl",
"name": "max_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), del_max_lat); err != nil {
return err
}
collection.Schema.AddField(del_max_lat)
// add
del_max_lon := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "kx2qfztr",
"name": "max_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), del_max_lon); err != nil {
return err
}
collection.Schema.AddField(del_max_lon)
// add
del_min_lat := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "z4qsnjeb",
"name": "min_lat",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), del_min_lat); err != nil {
return err
}
collection.Schema.AddField(del_min_lat)
// add
del_min_lon := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "p66xomdb",
"name": "min_lon",
"type": "json",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSize": 1
}
}`), del_min_lon); err != nil {
return err
}
collection.Schema.AddField(del_min_lon)
// remove
collection.Schema.RemoveField("osfey1yx")
// remove
collection.Schema.RemoveField("eohslzky")
// remove
collection.Schema.RemoveField("kigvankd")
// remove
collection.Schema.RemoveField("ifnks9mg")
return dao.SaveCollection(collection)
})
}

10
web/package-lock.json generated
View File

@@ -26,7 +26,7 @@
"pdfkit": "^0.15.0",
"photoswipe": "^5.4.3",
"pocketbase": "^0.21.0",
"qrcode-with-logos": "^1.0.5",
"qrcode": "^1.4.4",
"svelte-i18n": "^4.0.0",
"three": "^0.161.0",
"yup": "^1.3.3"
@@ -4356,14 +4356,6 @@
"node": ">=10.13.0"
}
},
"node_modules/qrcode-with-logos": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/qrcode-with-logos/-/qrcode-with-logos-1.0.5.tgz",
"integrity": "sha512-ZHynBWdidxv57T5Lw8KBGpM6c/bwrL9buITPOrtX0PaoV0AgKkqHifdO+rKV7jMSiR/EcQWpvHI9ZarZonDq6g==",
"dependencies": {
"qrcode": "^1.4.4"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View File

@@ -51,7 +51,7 @@
"pdfkit": "^0.15.0",
"photoswipe": "^5.4.3",
"pocketbase": "^0.21.0",
"qrcode-with-logos": "^1.0.5",
"qrcode": "^1.4.4",
"svelte-i18n": "^4.0.0",
"three": "^0.161.0",
"yup": "^1.3.3"

4
web/src/app.d.ts vendored
View File

@@ -14,7 +14,9 @@ declare global {
interface Locals {
pb: PocketBase
ms: MeiliSearch
user: AuthModel | null
user: AuthModel | null,
settings: Settings | null
}
}
}

View File

@@ -1,8 +1,11 @@
import type { User } from '$lib/models/user'
import { pb } from '$lib/pocketbase'
import { currentUser, type User } from '$lib/stores/user_store'
import { settings_show } from '$lib/stores/settings_store';
import { currentUser } from '$lib/stores/user_store'
import { get } from 'svelte/store';
pb.authStore.loadFromCookie(document.cookie)
pb.authStore.onChange(() => {
currentUser.set(pb.authStore.model as User)
document.cookie = pb.authStore.exportToCookie({ httpOnly: false })
}, true)
}, true)

View File

@@ -1,11 +1,13 @@
import { env } from '$env/dynamic/private'
import { env as envPub } from '$env/dynamic/public'
import type { Settings } from '$lib/models/settings'
import { pb } from '$lib/pocketbase'
import { isRouteProtected } from '$lib/util/authorization_util'
import { redirect, type Handle } from '@sveltejs/kit'
import { MeiliSearch } from 'meilisearch'
import { locale } from 'svelte-i18n'
import { get } from 'svelte/store'
export const handle: Handle = async ({ event, resolve }) => {
// load the store data from the request cookie string
@@ -13,7 +15,7 @@ export const handle: Handle = async ({ event, resolve }) => {
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?r=' + url.pathname);
@@ -34,8 +36,10 @@ export const handle: Handle = async ({ event, resolve }) => {
}
let meiliApiKey: string = "";
let settings: Settings | undefined;
if (pb.authStore.model) {
meiliApiKey = pb.authStore.model.token
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.model.id}"`)
} else {
const r = await event.fetch(pb.buildUrl("/public/search/token"));
const response = await r.json();
@@ -46,11 +50,15 @@ export const handle: Handle = async ({ event, resolve }) => {
event.locals.ms = ms
event.locals.pb = pb
event.locals.user = pb.authStore.model
event.locals.settings = settings
const lang = pb.authStore.model?.language ?? event.request.headers.get('accept-language')?.split(',')[0]
const lang = settings?.language ?? event.request.headers.get('accept-language')?.split(',')[0]
if (lang) {
locale.set(lang)
if (pb.authStore.model) {
pb.authStore.model!.language = lang;
}
}
const response = await resolve(event)

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Trail } from "$lib/models/trail";
import { currentUser } from "$lib/stores/user_store";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import type { Map, Marker } from "leaflet";
@@ -52,7 +52,7 @@
closeBtn: false,
followMarker: true,
autofitBounds: true,
imperial: $currentUser?.unit == "imperial" ?? false,
imperial: $page.data.settings?.unit == "imperial" ?? false,
reverseCoords: false,
acceleration: false,
slope: true,

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
import {
@@ -64,12 +65,12 @@
<div class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
trail.distance
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain,
trail.elevation_gain
)}</span
>
<span

View File

@@ -12,6 +12,7 @@
import Search, { type SearchItem } from "../base/search.svelte";
import Slider from "../base/slider.svelte";
import type { SelectItem } from "../base/select.svelte";
import { page } from "$app/stores";
export let categories: Category[];
export let filterExpanded: boolean = true;

View File

@@ -31,6 +31,7 @@
import Textarea from "../base/textarea.svelte";
import CommentCard from "../comment/comment_card.svelte";
import PhotoGallery from "../photo_gallery.svelte";
import { page } from "$app/stores";
export let trail: Trail;
export let mode: "overview" | "map" = "map";

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util";
import {
@@ -53,12 +54,12 @@
<div class="flex mt-1 gap-4 text-sm text-gray-500">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
trail.distance
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain,
trail.elevation_gain
)}</span
>
<span

View File

@@ -0,0 +1,28 @@
class Settings {
id?: string;
unit?: "metric" | "imperial";
language?: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh";
mapFocus?: "trails" | "location";
location?: { name: string, lat: number, lon: number };
user?: string;
constructor(
unit: "metric" | "imperial",
language: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh",
mapFocus: "trails" | "location",
user: string,
params?: {
location: { name: string, lat: number, lon: number }
}
) {
this.unit = unit;
this.language = language;
this.mapFocus = mapFocus;
this.user = user;
this.location = params?.location;
}
}
export { Settings };

View File

@@ -115,6 +115,13 @@ interface TrailFilterValues {
max_duration: number,
}
interface TrailBoundingBox {
max_lat: number,
min_lat: number,
max_lon: number,
min_lon: number,
}
export { Trail };
export type { TrailFilter, TrailFilterValues };
export type { TrailFilter, TrailFilterValues, TrailBoundingBox };

View File

@@ -0,0 +1,10 @@
import type { Settings } from "./settings";
export type User = {
id: string,
username?: string,
email?: string,
password: string,
avatar?: string;
language?: string;
}

View File

@@ -0,0 +1,55 @@
import { invalidateAll } from "$app/navigation";
import { Settings } from "$lib/models/settings";
import { ClientResponseError } from "pocketbase";
export async function settings_show(userId: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/settings/' + userId, {
method: 'GET',
})
const response = await r.json();
if (r.ok) {
return response;
} else {
throw new ClientResponseError(response)
}
}
export async function settings_create(settings: Settings) {
const r = await fetch('/api/v1/settings', {
method: 'PUT',
body: JSON.stringify(settings),
})
if (r.ok) {
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}
export async function settings_update(settings: Settings) {
const r = await fetch('/api/v1/settings/' + settings.id, {
method: 'POST',
body: JSON.stringify(settings),
})
if (r.ok) {
await invalidateAll()
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}
export async function settings_delete(settings: Settings) {
const r = await fetch('/api/v1/settings/' + settings.id, {
method: 'DELETE',
})
if (r.ok) {
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}

View File

@@ -370,6 +370,18 @@ export async function trails_get_filter_values(f: (url: RequestInfo | URL, confi
}
}
export async function trails_get_bounding_box(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<TrailFilterValues> {
const r = await f('/api/v1/trail/bounding-box', {
method: 'GET',
})
if (r.ok) {
return await r.json();
} else {
throw new ClientResponseError(await r.json())
}
}
export async function fetchGPX(trail: Trail, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
if (!trail.gpx) {
return "";

View File

@@ -1,33 +1,26 @@
import { Settings } from "$lib/models/settings";
import type { User } from "$lib/models/user";
import { pb } from "$lib/pocketbase";
import { ClientResponseError, type AuthMethodsList } 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" | "fr" | "hu" | "nl" | "pl" | "pt" | "zh" ;
location?: { name: string, lat: number, lon: number }
}
import { settings_create } from "./settings_store";
export const currentUser: Writable<User | null> = writable<User | null>()
export async function users_create(user: User) {
user.unit = "metric";
const r = await fetch('/api/v1/user', {
let r = await fetch('/api/v1/user', {
method: 'PUT',
body: JSON.stringify({ ...user, passwordConfirm: user.password })
})
if (r.ok) {
return await r.json();
} else {
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const createdUser: User = await r.json();
const settings = new Settings("metric", "en", "trails", createdUser.id!)
await settings_create(settings);
}
export async function users_auth_methods(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch): Promise<AuthMethodsList> {

View File

@@ -1,3 +1,4 @@
import { page } from "$app/stores";
import { currentUser } from "$lib/stores/user_store";
import { get } from "svelte/store";
@@ -17,7 +18,7 @@ export function formatDistance(meters?: number) {
return "-";
}
const unit = get(currentUser)?.unit ?? "metric";
const unit = get(page).data.settings?.unit ?? "metric";
if (unit == "metric") {
if (meters >= 1000) {
@@ -38,7 +39,7 @@ export function formatElevation(meters?: number) {
return "-";
}
const unit = get(currentUser)?.unit ?? "metric";
const unit = get(page).data.settings?.unit ?? "metric";
if (unit == "metric") {
return `${Math.round(meters)} m`

View File

@@ -0,0 +1,366 @@
import QRCode from "qrcode";
'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function () { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
/*
* @Author: super
* @Date: 2019-06-27 16:29:43
* @Last Modified by: super
* @Last Modified time: 2019-06-27 17:46:21
*/
/**
* promisify promise化使得promisify(func).then()更加方便,不用每次都構造 promise
* Making Promise more convenient, without having to construct a promise every time
* @param f {function} 異步函數
*/
var promisify = function (f) {
return function () {
var args = Array.prototype.slice.call(arguments);
return new Promise(function (resolve, reject) {
args.push(function (err, result) {
if (err)
reject(err);
else
resolve(result);
});
f.apply(null, args);
});
};
};
/**
* 判斷是否是函數
* Determine if it is a function
* @param o {function} 函數
*/
function isFunction(o) {
return typeof o === "function";
}
/**
* 判斷是不是字符串
* Determine if it is a string
* @param o {string} 字符串
*/
function isString(o) {
return typeof o === "string";
}
// @ts-ignore
// import QRCode from "qrcode"
var toCanvas$1 = promisify(QRCode.toCanvas);
var renderQrCode = function (_a) {
var canvas = _a.canvas, content = _a.content, _b = _a.width, width = _b === void 0 ? 0 : _b, _c = _a.nodeQrCodeOptions, nodeQrCodeOptions = _c === void 0 ? {} : _c;
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
// according to the content length to choose different errorCorrectionLevel
nodeQrCodeOptions.errorCorrectionLevel =
nodeQrCodeOptions.errorCorrectionLevel || getErrorCorrectionLevel(content);
return getOriginWidth(content, nodeQrCodeOptions).then(function (_width) {
// 得到原始比例后还原至设定值再放大4倍以获取高清图
// Restore to the set value according to the original ratio, and then zoom in 4 times to get the HD image.
nodeQrCodeOptions.scale = width === 0 ? undefined : (width / _width) * 4;
// @ts-ignore
return toCanvas$1(canvas, content, nodeQrCodeOptions);
});
};
// 得到原QrCode的大小以便缩放得到正确的QrCode大小
// Get the size of the original QrCode
var getOriginWidth = function (content, nodeQrCodeOption) {
var _canvas = document.createElement("canvas");
// @ts-ignore
return toCanvas$1(_canvas, content, nodeQrCodeOption).then(function () { return _canvas.width; });
};
// 对于内容少的QrCode增大容错率
// Increase the fault tolerance for QrCode with less content
var getErrorCorrectionLevel = function (content) {
if (content.length > 36) {
return "M";
}
else if (content.length > 16) {
return "Q";
}
else {
return "H";
}
};
var drawLogo = function (_a) {
var canvas = _a.canvas, logo = _a.logo;
if (!logo)
return Promise.resolve();
if (logo === '')
return Promise.resolve();
var canvasWidth = canvas.width;
if (isString(logo)) {
logo = { src: logo };
}
var _b = logo, _c = _b.logoSize, logoSize = _c === void 0 ? 0.15 : _c, _d = _b.borderColor, borderColor = _d === void 0 ? "#ffffff" : _d, _e = _b.bgColor, bgColor = _e === void 0 ? borderColor || "#ffffff" : _e, _f = _b.borderSize, borderSize = _f === void 0 ? 0.05 : _f, crossOrigin = _b.crossOrigin, _g = _b.borderRadius, borderRadius = _g === void 0 ? 8 : _g, _h = _b.logoRadius, logoRadius = _h === void 0 ? 0 : _h;
var logoSrc = typeof logo === "string" ? logo : logo.src;
var logoWidth = canvasWidth * logoSize;
var logoXY = (canvasWidth * (1 - logoSize)) / 2;
var logoBgWidth = canvasWidth * (logoSize + borderSize);
var logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
var ctx = canvas.getContext("2d");
// logo 底色, draw logo background color
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius);
ctx.fillStyle = bgColor;
ctx.fill();
// logo
var image = new Image();
image.setAttribute("crossOrigin", crossOrigin || "anonymous");
image.src = logoSrc;
// 使用image绘制可以避免某些跨域情况
// Use image drawing to avoid some cross-domain situations
var drawLogoWithImage = function (image) {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
};
// 使用canvas绘制以获得更多的功能
// Use canvas to draw more features, such as borderRadius
var drawLogoWithCanvas = function (image) {
var canvasImage = document.createElement("canvas");
canvasImage.width = logoXY + logoWidth;
canvasImage.height = logoXY + logoWidth;
canvasImage
.getContext("2d")
.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
// @ts-ignore
ctx.fillStyle = ctx.createPattern(canvasImage, "no-repeat");
ctx.fill();
};
// 将 logo绘制到 canvas上
// Draw the logo on the canvas
return new Promise(function (resolve, reject) {
image.onload = function () {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
resolve();
};
image.onerror = function () {
reject('logo load fail!');
};
});
};
// draw radius
var canvasRoundRect = function (ctx) {
return function (x, y, w, h, r) {
var minSize = Math.min(w, h);
if (r > minSize / 2) {
r = minSize / 2;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
return ctx;
};
};
/*
* @Author: super
* @Date: 2019-06-27 16:29:34
* @Last Modified by: super
* @Last Modified time: 2019-06-27 16:47:22
*/
var toCanvas = function (options) {
return renderQrCode(options).then(function () { return drawLogo(options); });
};
var toImage = function (options, instance) {
return __awaiter(this, void 0, void 0, function () {
var canvas, image, _a, downloadName, download, startDownload;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
canvas = options.canvas;
if (options.logo) {
if (isString(options.logo)) {
options.logo = { src: options.logo };
}
options.logo.crossOrigin = 'Anonymous';
}
if (!!instance.ifCanvasDrawed) return [3 /*break*/, 2];
return [4 /*yield*/, toCanvas(options)];
case 1:
_b.sent();
_b.label = 2;
case 2:
image = options.image, _a = options.downloadName, downloadName = _a === void 0 ? 'qr-code' : _a;
download = options.download;
if (canvas.toDataURL()) {
image.src = canvas.toDataURL();
}
else {
throw new Error('Can not get the canvas DataURL');
}
instance.ifImageCreated = true;
if (download !== true && !isFunction(download)) {
return [2 /*return*/];
}
download = download === true ? function (start) { return start(); } : download;
startDownload = function () {
return saveImage(image, downloadName);
};
if (download) {
return [2 /*return*/, download(startDownload)];
}
return [2 /*return*/, Promise.resolve()];
}
});
});
};
/**save image */
var saveImage = function (image, name) {
return new Promise(function (resolve, reject) {
try {
var dataURL = image.src;
var link = document.createElement('a');
link.download = name;
link.href = dataURL;
link.dispatchEvent(new MouseEvent('click'));
resolve(true);
}
catch (err) {
reject(err);
}
});
};
var version = "1.0.5";
/*
* @Author: super
* @Date: 2019-06-27 16:29:31
* @Last Modified by: suporka
* @Last Modified time: 2020-03-04 12:24:50
*/
var QrCodeWithLogo = /** @class */ (function () {
function QrCodeWithLogo(option) {
this.ifCanvasDrawed = false;
this.ifImageCreated = false;
this.defaultOption = {
canvas: undefined,
image: undefined,
content: ''
};
this.option = Object.assign(this.defaultOption, option);
if (!this.option.canvas)
this.option.canvas = document.createElement("canvas");
if (!this.option.image)
this.option.image = document.createElement("img");
this.toCanvas().then(this.toImage.bind(this));
}
QrCodeWithLogo.prototype.toCanvas = function () {
var _this = this;
return toCanvas.call(this, this.option).then(function () {
_this.ifCanvasDrawed = true;
return Promise.resolve();
});
};
QrCodeWithLogo.prototype.toImage = function () {
return toImage(this.option, this);
};
QrCodeWithLogo.prototype.downloadImage = function (name) {
if (name === void 0) { name = 'qrcode.png'; }
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!!this.ifImageCreated) return [3 /*break*/, 2];
return [4 /*yield*/, this.toImage()];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/, saveImage(this.option.image, name)];
}
});
});
};
QrCodeWithLogo.prototype.getImage = function () {
return __awaiter(this, void 0, Promise, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!!this.ifImageCreated) return [3 /*break*/, 2];
return [4 /*yield*/, this.toImage()];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/, this.option.image];
}
});
});
};
QrCodeWithLogo.prototype.getCanvas = function () {
return __awaiter(this, void 0, Promise, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!!this.ifCanvasDrawed) return [3 /*break*/, 2];
return [4 /*yield*/, this.toCanvas()];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/, this.option.canvas];
}
});
});
};
QrCodeWithLogo.version = version;
return QrCodeWithLogo;
}());
export default QrCodeWithLogo;

View File

@@ -0,0 +1,7 @@
// +layout.ts
import '$lib/i18n';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return { settings: locals.settings }
}

View File

@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { beforeNavigate, goto } from "$app/navigation";
import Toast from "$lib/components/base/toast.svelte";
import Footer from "$lib/components/footer.svelte";
@@ -13,7 +13,7 @@
beforeNavigate((n) => {
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
n.cancel();
goto("/login?r="+n.to?.url?.pathname);
goto("/login?r=" + n.to?.url?.pathname);
}
});
</script>

View File

@@ -0,0 +1,14 @@
import type { Settings } from '$lib/models/settings';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function PUT(event: RequestEvent) {
const data = await event.request.json();
try {
const r = await pb.collection('settings').create<Settings>(data)
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -0,0 +1,32 @@
import type { Settings } from "$lib/models/settings";
import { pb } from "$lib/pocketbase";
import { error, json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await pb.collection('settings').getFirstListItem<Settings>(`user="${event.params.id}"`)
return json(r)
} catch (e: any) {
throw error(e.status, e);
}
}
export async function POST(event: RequestEvent) {
const data = await event.request.json()
try {
const r = await await pb.collection("settings").update<Settings>(event.params.id as string, data);
return json(r);
} catch (e: any) {
throw error(e.status, e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await pb.collection('settings').delete(event.params.id as string)
return json({ 'acknowledged': r });
} catch (e: any) {
throw error(e.status, e)
}
}

View File

@@ -0,0 +1,20 @@
import { type TrailBoundingBox, type TrailFilterValues } from '$lib/models/trail';
import { pb } from '$lib/pocketbase';
import { error, json, type RequestEvent } from '@sveltejs/kit';
export async function GET(event: RequestEvent) {
if (!pb.authStore.model) {
return json({
max_lat: 0,
min_lat: 0,
max_lon: 0,
min_lon: 0
});
}
try {
const r = await pb.collection('trails_bounding_box').getOne<TrailBoundingBox>(pb.authStore.model!.id)
return json(r)
} catch (e: any) {
throw error(e.status || 500, e);
}
}

View File

@@ -8,7 +8,12 @@
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
import TrailCard from "$lib/components/trail/trail_card.svelte";
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import type { Trail, TrailFilter } from "$lib/models/trail";
import type { Settings } from "$lib/models/settings";
import type {
Trail,
TrailBoundingBox,
TrailFilter,
} from "$lib/models/trail";
import { categories } from "$lib/stores/category_store";
import {
trails,
@@ -48,6 +53,8 @@
let showMap: boolean = true;
const filter: TrailFilter = $page.data.filter;
const maxBoundingBox: TrailBoundingBox = $page.data.boundingBox;
const settings: Settings = $page.data.settings;
onMount(async () => {
L = (await import("leaflet")).default;
@@ -90,11 +97,18 @@
];
map.fitBounds(boundingBox);
} else if ($currentUser && $currentUser.location) {
map.setView(
[$currentUser.location.lat, $currentUser.location.lon],
12,
);
} else if (settings && settings.mapFocus == "trails") {
const boundingBox: LatLngBoundsExpression = [
[maxBoundingBox.max_lat, maxBoundingBox.min_lon],
[maxBoundingBox.min_lat, maxBoundingBox.max_lon],
];
map.fitBounds(boundingBox);
} else if (
settings &&
settings.mapFocus == "location" &&
settings.location
) {
map.setView([settings.location.lat, settings.location.lon], 12);
} else {
navigator.geolocation.getCurrentPosition(
(position) => {

View File

@@ -1,9 +1,10 @@
import type { TrailFilter } from "$lib/models/trail";
import { categories_index } from "$lib/stores/category_store";
import { trails, trails_get_filter_values } from "$lib/stores/trail_store";
import { trails, trails_get_bounding_box, trails_get_filter_values } from "$lib/stores/trail_store";
import type { ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ params, locals, fetch }) => {
const boundingBox = await trails_get_bounding_box(fetch);
const filterValues = await trails_get_filter_values(fetch);
const filter: TrailFilter = {
@@ -27,5 +28,5 @@ export const load: ServerLoad = async ({ params, locals, fetch }) => {
trails.set([])
return { filter: filter }
return { filter: filter, boundingBox: boundingBox }
};

View File

@@ -4,13 +4,12 @@
import "$lib/assets/fonts/IBMPlexSans-SemiBold-bold";
import "$lib/assets/fonts/fa-solid-900-normal";
import Button from "$lib/components/base/button.svelte";
import Select, {
type SelectItem,
} from "$lib/components/base/select.svelte";
import Select from "$lib/components/base/select.svelte";
import LogoText from "$lib/components/logo/logo_text.svelte";
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
import type { Settings } from "$lib/models/settings";
import { show_toast } from "$lib/stores/toast_store";
import { trail } from "$lib/stores/trail_store";
import { currentUser } from "$lib/stores/user_store";
import {
formatDistance,
formatElevation,
@@ -22,21 +21,22 @@
} from "$lib/util/leaflet_util";
import { createRect, createText } from "$lib/util/svg_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js";
import { Canvg } from "canvg";
import { jsPDF } from "jspdf";
import type { Map } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import QrCodeWithLogo from "qrcode-with-logos";
import QrCodeWithLogo from "$lib/vendor/qr-code-with-logos/index";
import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n";
import { Canvg } from "canvg";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import { show_toast } from "$lib/stores/toast_store";
let map: Map;
let graticule: AutoGraticule;
const settings: Settings = $page.data.settings;
const paperSizes: { text: string; value: keyof typeof paperDimensions }[] =
[
{ text: "A4", value: "a4" },
@@ -375,7 +375,7 @@
let oneUnitInPixels = calculatePixelPerMeter(
map,
$currentUser && $currentUser.unit == "imperial" ? 1609.34 : 1000,
settings && settings.unit == "imperial" ? 1609.34 : 1000,
);
let unitsInRuler = width / oneUnitInPixels;
@@ -430,7 +430,7 @@
svg.appendChild(
createText(
$currentUser && $currentUser.unit == "imperial" ? "MI" : "KM",
settings && settings.unit == "imperial" ? "MI" : "KM",
segmentCount * multiplier * oneUnitInPixels - 20,
height / 2 - 7,
),
@@ -535,7 +535,9 @@
>
<span
><i class="fa fa-left-right mr-2"
></i>{formatDistance($trail.distance)}</span
></i>{formatDistance(
$trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import RadioGroup, {
type RadioItem,
} from "$lib/components/base/radio_group.svelte";
@@ -10,6 +11,7 @@
type SelectItem,
} from "$lib/components/base/select.svelte";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
import { settings_update } from "$lib/stores/settings_store";
import {
currentUser,
logout,
@@ -21,6 +23,8 @@
import { onMount } from "svelte";
import { _, locale } from "svelte-i18n";
const settings = $page.data.settings;
const languages: SelectItem[] = [
{ text: $_("chinese"), value: "zh" },
{ text: $_("german"), value: "de" },
@@ -32,12 +36,18 @@
{ text: $_("portuguese"), value: "pt" },
];
const mapFocus: SelectItem[] = [
{ text: $_("trail", { values: { n: 2 } }), value: "trails" },
{ text: $_("location"), value: "location" },
];
const units: RadioItem[] = [
{ text: $_("metric"), value: "metric" },
{ text: $_("imperial"), value: "imperial" },
];
let selectedLanguage = "en";
let selectedMapFocus = "trails";
let searchDropdownItems: SearchItem[] = [];
let citySearchQuery: string = "";
@@ -45,8 +55,9 @@
let openConfirmModal: () => void;
onMount(() => {
citySearchQuery = $currentUser?.location?.name ?? "";
selectedLanguage = $currentUser?.language || "en";
citySearchQuery = settings?.location?.name ?? "";
selectedLanguage = settings?.language || "en";
selectedMapFocus = settings?.mapFocus ?? "trails";
});
async function searchCities(q: string) {
@@ -67,8 +78,8 @@
async function handleSearchClick(item: SearchItem) {
citySearchQuery = item.text;
await users_update({
id: $currentUser!.id,
await settings_update({
id: settings?.id!,
location: {
name: item.value.name,
lat: item.value.lat,
@@ -90,8 +101,8 @@
}
async function handleUnitSelection(e: RadioItem) {
await users_update({
id: $currentUser!.id,
await settings_update({
id: settings!.id,
unit: e.value as "imperial" | "metric",
});
}
@@ -100,12 +111,19 @@
value: "en" | "de" | "fr" | "hu" | "nl" | "pl" | "pt",
) {
locale.set(value);
await users_update({
id: $currentUser!.id,
await settings_update({
id: settings!.id,
language: value,
});
}
async function handleMapFocusSelection(value: "trails" | "location") {
await settings_update({
id: settings!.id,
mapFocus: value,
});
}
async function deleteAccount() {
await users_delete($currentUser!);
logout();
@@ -156,17 +174,6 @@
</div>
<div class="space-y-6 rounded-xl p-4">
<h3 class="text-2xl font-semibold">{$_("settings")}</h3>
<div>
<h5 class="font-medium mb-1">{$_("default-location")}</h5>
<Search
items={searchDropdownItems}
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
@@ -180,10 +187,30 @@
<RadioGroup
name="unit"
items={units}
selected={$currentUser.unit == "metric" ? 0 : 1}
selected={settings?.unit == "metric" ? 0 : 1}
on:change={(e) => handleUnitSelection(e.detail)}
></RadioGroup>
</div>
<div>
<h5 class="font-medium mb-1">Focus map on</h5>
<Select
items={mapFocus}
bind:value={selectedMapFocus}
on:change={(e) => handleMapFocusSelection(e.detail)}
></Select>
{#if selectedMapFocus == "location"}
<div class="mt-3">
<Search
items={searchDropdownItems}
placeholder="{$_('search-cities')}..."
bind:value={citySearchQuery}
on:update={(e) => searchCities(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
></Search>
</div>
{/if}
</div>
<hr class="border-input-border" />
<div class="space-y-4">
<h4 class="text-xl text-red-400">{$_("danger-zone")}</h4>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from "$app/stores";
import Button from "$lib/components/base/button.svelte";
import Datepicker from "$lib/components/base/datepicker.svelte";
import Select from "$lib/components/base/select.svelte";