adds docs

This commit is contained in:
Christian Beutel
2024-09-14 18:06:07 +02:00
parent ca34e6db95
commit 414fa9b752
29 changed files with 902 additions and 314 deletions

View File

@@ -6,7 +6,7 @@
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
export let title: string = "Confirm Deletion";
export let title: string = $_('confirm-deletion');
export let text: string;
export let action: string = "delete";

View File

@@ -6,43 +6,28 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import { _ } from "svelte-i18n";
import ShareInfo from "../share_info.svelte";
import { currentUser } from "$lib/stores/user_store";
export let list: List;
export let active: boolean = false;
$: cumulativeDistance = list.expand?.trails.reduce(
$: cumulativeDistance = list.expand?.trails?.reduce(
(s, b) => s + b.distance!,
0,
);
$: cumulativeElevationGain = list.expand?.trails.reduce(
$: cumulativeElevationGain = list.expand?.trails?.reduce(
(s, b) => s + b.elevation_gain!,
0,
);
$: cumulativeDuration = list.expand?.trails.reduce(
$: cumulativeDuration = list.expand?.trails?.reduce(
(s, b) => s + b.duration!,
0,
);
$: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 0;
$: allowEdit =
list.author == $currentUser?.id ||
list.expand?.list_share_via_list?.some((s) => s.permission == "edit");
$: dropdownItems = [
...(list.author == $currentUser?.id
? [{ text: $_("share"), value: "share" }]
: []),
...(allowEdit ? [{ text: $_("edit"), value: "edit" }] : []),
...(list.author == $currentUser?.id
? [{ text: $_("delete"), value: "delete" }]
: []),
];
</script>
<div
@@ -72,9 +57,6 @@
{#if listIsShared}
<ShareInfo type="list" subject={list}></ShareInfo>
{/if}
{#if dropdownItems.length}
<Dropdown items={dropdownItems} on:change></Dropdown>
{/if}
</div>
<div class="flex mt-1 gap-x-4 text-sm text-gray-500 whitespace-nowrap flex-wrap">
<span
@@ -94,9 +76,9 @@
>
</div>
<p class="text-sm text-gray-500 mb-2">
{list.expand?.trails.length ?? 0}
{list.expand?.trails?.length ?? 0}
{$_("trail", {
values: { n: list.expand?.trails.length ?? 0 },
values: { n: list.expand?.trails?.length ?? 0 },
})}
</p>
<p

View File

@@ -1,113 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { browser } from "$app/environment";
import { listSchema, type List } from "$lib/models/list";
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";
import { getFileURL } from "$lib/util/file_util";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
const dispatch = createEventDispatcher();
let previewURL = "";
const { form, errors, handleChange, handleSubmit } = createForm<List>({
initialValues: $list,
validationSchema: listSchema,
onSubmit: async (submittedList) => {
dispatch("save", { list: submittedList, avatar: (document.getElementById("avatar") as HTMLInputElement).files![0] });
(document.getElementById("avatar") as HTMLInputElement).value = "";
closeModal!();
},
});
function openAvatarBrowser() {
document.getElementById("avatar")!.click();
}
function handleAvatarSelection() {
const files = (document.getElementById("avatar") as HTMLInputElement)
.files;
if (!files) {
return;
}
previewURL = URL.createObjectURL(files[0]);
}
$: if (browser) {
form.set(util.cloneDeep($list));
previewURL = getFileURL($list, $list.avatar) ?? "";
}
</script>
<Modal
id="list-modal"
title={$form.id ? $_("edit-list") : $_("new-list")}
let:openModal
bind:openModal
bind:closeModal
>
<slot {openModal} />
<form
id="list-form"
slot="content"
class="modal-content space-y-4"
on:submit={handleSubmit}
>
<label for="avatar" class="text-sm font-medium block"> {$_('avatar')} </label>
<input
name="avatar"
type="file"
id="avatar"
accept="image/*"
style="display: none;"
on:change={handleAvatarSelection}
/>
<div class="flex items-center gap-4">
{#if previewURL.length > 0}
<img
class="w-32 aspect-square rounded-full object-cover"
alt="avatar"
src={previewURL}
/>
{/if}
<button
class="btn-secondary"
type="button"
on:click={openAvatarBrowser}>{$_('change')}...</button
>
</div>
<TextField
name="name"
label={$_("name")}
bind:value={$form.name}
error={$errors.name}
on:change={handleChange}
></TextField>
<Textarea
name="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" name="save"
>{$_("save")}</button
>
</div>
</Modal>

View File

@@ -0,0 +1,156 @@
<script lang="ts">
import type { List } from "$lib/models/list";
import { getFileURL } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { _ } from "svelte-i18n";
import TrailListItem from "../trail/trail_list_item.svelte";
import type { Trail } from "$lib/models/trail";
import { createEventDispatcher } from "svelte";
import { currentUser } from "$lib/stores/user_store";
import Dropdown from "../base/dropdown.svelte";
import ShareInfo from "../share_info.svelte";
export let list: List;
const dispatch = createEventDispatcher();
$: cumulativeDistance = list.expand?.trails?.reduce(
(s, b) => s + b.distance!,
0,
);
$: cumulativeElevationGain = list.expand?.trails?.reduce(
(s, b) => s + b.elevation_gain!,
0,
);
$: cumulativeDuration = list.expand?.trails?.reduce(
(s, b) => s + b.duration!,
0,
);
$: allowEdit =
list.author == $currentUser?.id ||
list.expand?.list_share_via_list?.some((s) => s.permission == "edit");
$: dropdownItems = [
...(list.author == $currentUser?.id
? [{ text: $_("share"), value: "share", icon: "share" }]
: []),
...(allowEdit
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
: []),
...(list.author == $currentUser?.id
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
: []),
];
$: listIsShared = (list.expand?.list_share_via_list?.length ?? 0) > 0;
let fullDescription: boolean = false;
function handleTrailSelect(trail: Trail, index: number) {
dispatch("click", { trail, index });
}
function handleTrailMouseEnter(trail: Trail, index: number) {
dispatch("mouseenter", { trail, index });
}
function handleTrailMouseLeave(trail: Trail, index: number) {
dispatch("mouseleave", { trail, index });
}
</script>
<div class="relative">
{#if listIsShared}
<div class="absolute top-8 right-8 bg-white rounded-full w-8 py-1 text-center">
<ShareInfo type="list" subject={list}></ShareInfo>
</div>
{/if}
{#if dropdownItems.length}
<div class="absolute bottom-8 right-8">
<Dropdown
items={dropdownItems}
on:change
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
>
</div>
{/if}
{#if list.avatar}
<img
class="w-full object-cover"
src={getFileURL(list, list.avatar)}
alt="avatar"
/>
{:else}
<div class="flex w-full shrink-0 items-center justify-center min-h-72">
<i class="fa fa-table-list text-5xl"></i>
</div>
{/if}
</div>
<div class="p-4 md:p-6">
<h4 class="text-2xl font-semibold mb-4">{list.name}</h4>
<hr />
<div class="flex my-4 gap-x-4 font-semibold whitespace-nowrap flex-wrap justify-around">
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
cumulativeDistance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
cumulativeElevationGain,
)}</span
>
<span
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
cumulativeDuration,
)}</span
>
</div>
<hr class="mb-4" />
<p
class="text-gray-500 whitespace-pre-wrap {fullDescription
? ''
: 'max-h-24 overflow-hidden text-ellipsis'}"
>
{!fullDescription
? list.description?.substring(0, 100)
: list.description}
{#if (list.description?.length ?? 0) > 100 && !fullDescription}
<button on:click={() => (fullDescription = true)}>
... <span class="underline">{$_("read-more")}</span></button
>
{/if}
</p>
<h5 class="text-xl font-semibold my-4">
{list.trails?.length ?? 0}
{$_("trail", { values: { n: list.trails?.length ?? 0 } })}
</h5>
<div class="space-y-2">
{#each list.expand?.trails ?? [] as trail, i}
<div
role="presentation"
on:click={() => handleTrailSelect(trail, i)}
on:mouseenter={() => handleTrailMouseEnter(trail, i)}
on:mouseleave={() => handleTrailMouseLeave(trail, i)}
>
<TrailListItem {trail} showDescription={false}></TrailListItem>
</div>
{/each}
</div>
</div>

View File

@@ -20,6 +20,7 @@
export let map: any | null = null;
export let options: any = {};
export let activeTrailIndex: number | null = 0;
export let markers: any[] = [];
const dispatch = createEventDispatcher();
@@ -28,7 +29,9 @@
let selectedMetric: "altitude" | "slope" | "speed" | false = "altitude";
$: gpxData = trails.map((t) => t.expand.gpx_data);
$: gpxData = trails.map((t) => {
return { id: t.id, gpx: t.expand.gpx_data };
});
$: if (
gpxData &&
gpxGroup &&
@@ -216,15 +219,20 @@
gpxGroup.on("selection_changed", ({ polyline }: { polyline: any }) => {
markerLayerGroup.clearLayers();
activeTrailIndex = null;
markers = [];
if (polyline._selected) {
activeTrailIndex = polyline.options.index;
activeTrailIndex = trails.findIndex(
(t) => t.id ==
polyline.options.id,
);
for (const waypoint of trails.at(activeTrailIndex!)?.expand
.waypoints ?? []) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(markerLayerGroup!);
markers.push(marker);
}
}
});
@@ -237,8 +245,8 @@
gpxGroup.on("route_loaded", ({ route }: { route: any }) => {
const trail = trails.find(
(t) =>
gpxGroup?._hashCode(t.expand.gpx_data) ==
route.options.hash,
t.id ==
route.options.id,
);
if (!trail) {
@@ -275,16 +283,28 @@
gpxGroup.addTo(map);
});
export function selectTrail(index: number) {
gpxGroup.select(index);
export function highlightTrail(id: string) {
gpxGroup?.highlightTrack(id);
}
export function openPopup(index: number) {
gpxGroup.openPopup(index);
export function unHighlightTrail(id: string) {
gpxGroup?.unHighlightTrack(id);
}
export function closePopup(index: number) {
gpxGroup.closePopup(index);
export function selectTrail(id: string) {
gpxGroup?.select(id);
}
export function resetSelection() {
gpxGroup?.resetSelection();
}
export function openPopup(id: string) {
gpxGroup?.openPopup(id);
}
export function closePopup(id: string) {
gpxGroup?.closePopup(id);
}
function switchHotline(metric: "altitude" | "slope" | "speed" | false) {

View File

@@ -23,7 +23,7 @@
import { pb } from "$lib/pocketbase";
export let trail: Trail;
export let mode: "overview" | "map";
export let mode: "overview" | "map" | "list";
let openConfirmModal: () => void;
let openListSelectModal: () => void;

View File

@@ -21,7 +21,11 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { createMarkerFromWaypoint, endIcon, startIcon } from "$lib/util/leaflet_util";
import {
createMarkerFromWaypoint,
endIcon,
startIcon,
} from "$lib/util/leaflet_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import type { Icon, Map, Marker } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
@@ -36,7 +40,7 @@
import ShareInfo from "../share_info.svelte";
export let trail: Trail;
export let mode: "overview" | "map" = "map";
export let mode: "overview" | "map" | "list" = "map";
export let markers: Marker[] = [];
const tabs = [
@@ -116,6 +120,10 @@
markers[i].openPopup();
}
function closeMarkerPopup(i: number) {
markers[i].closePopup();
}
async function toggleMapFullScreen() {
goto(`/map/trail/${trail.id!}`);
}
@@ -160,12 +168,19 @@
</script>
<div
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full"
class="trail-info-panel mx-auto {mode == 'list'
? ''
: 'border border-input-border rounded-3xl'} h-full"
style="max-width: min(100%, 64rem);"
>
<div class="trail-info-panel-header">
<section class="relative h-80">
<img class="w-full h-80 rounded-t-3xl" src={thumbnail} alt="" />
<img
class="w-full h-80 "
class:rounded-t-3xl={mode !== "list"}
src={thumbnail}
alt=""
/>
<div
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
></div>
@@ -186,7 +201,8 @@
</span>
{/if}
{#if trailIsShared}
<ShareInfo type="trail" subject={trail} large={true}></ShareInfo>
<ShareInfo type="trail" subject={trail} large={true}
></ShareInfo>
{/if}
</div>
{/if}
@@ -286,7 +302,7 @@
{#if activeTab == 1}
<ul>
{#each trail.expand.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(i)}>
<li on:mouseenter={() => openMarkerPopup(i)} on:mouseleave={() => closeMarkerPopup(i)}>
<WaypointCard {waypoint}></WaypointCard>
</li>
{/each}

View File

@@ -11,6 +11,8 @@
export let trail: Trail;
export let showDescription: boolean = true;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp";
@@ -51,9 +53,7 @@
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
{/if}
<h5>
<i class="fa fa-gauge mr-3"></i>{$_(
trail.difficulty ?? "?",
)}
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
</h5>
</div>
@@ -74,10 +74,12 @@
)}</span
>
</div>
<p
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
>
{trail.description}
</p>
{#if showDescription}
<p
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
>
{trail.description}
</p>
{/if}
</div>
</li>

View File

@@ -32,6 +32,7 @@
"completed": "Abgeschlossen",
"completion-status": "Abschlussstatus",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Mitwirken",
"copy-link": "Link kopieren",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Öffentlich",
"radius": "Radius",
"read-more": "",
"register": "Registrieren",
"removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",

View File

@@ -32,6 +32,7 @@
"completed": "Completed",
"completion-status": "Completion Status",
"confirm": "Confirm",
"confirm-deletion": "Confirm Deletion",
"confirm-share": "Confirm share",
"contribute": "Contribute",
"copy-link": "Copy Link",
@@ -155,6 +156,7 @@
"profile": "Profile",
"public": "Public",
"radius": "Radius",
"read-more": "Read more",
"register": "Register",
"removed-trail-from": "Removed trail from",
"required": "Required",

View File

@@ -32,6 +32,7 @@
"completed": "Compléter",
"completion-status": "L'état d'achèvement",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuer",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profile",
"public": "Publique",
"radius": "Rayon",
"read-more": "",
"register": "S'enregistrer",
"removed-trail-from": "Enlever l'itinéraire de",
"required": "Requis",

View File

@@ -32,6 +32,7 @@
"completed": "Teljesítve",
"completion-status": "Befejezés állapota",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Hozzájárulás",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Publikus",
"radius": "Átmérő",
"read-more": "",
"register": "Regisztráció",
"removed-trail-from": "Eltávolított nyomvonal a",
"required": "Kötelező",

View File

@@ -32,6 +32,7 @@
"completed": "Completato",
"completion-status": "Stato di completamento",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuisci",
"copy-link": "Copia link",
@@ -155,6 +156,7 @@
"profile": "Profilo",
"public": "Pubblico",
"radius": "Raggio",
"read-more": "",
"register": "Registrati",
"removed-trail-from": "Percorso rimosso da",
"required": "Obbligatorio",

View File

@@ -32,6 +32,7 @@
"completed": "Voltooid",
"completion-status": "Voltooiingsstatus",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Bijdragen",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profiel",
"public": "Openbaar",
"radius": "Straal",
"read-more": "",
"register": "Registreren",
"removed-trail-from": "De wandelroute is verwijderd van",
"required": "Verplicht",

View File

@@ -32,6 +32,7 @@
"completed": "Zakończono",
"completion-status": "Stan ukończenia",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Kontrybuuj",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Profil",
"public": "Publiczny",
"radius": "Promień",
"read-more": "",
"register": "Zarejestruj",
"removed-trail-from": "Usunięto ścieżkę z",
"required": "Wymagane",

View File

@@ -32,6 +32,7 @@
"completed": "Completada",
"completion-status": "Status de conclusão",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "Contribuir",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "Perfil",
"public": "Público",
"radius": "Raio",
"read-more": "",
"register": "Registo",
"removed-trail-from": "Trilha removida de",
"required": "Obrigatório",

View File

@@ -32,6 +32,7 @@
"completed": "已完成",
"completion-status": "完成状态",
"confirm": "",
"confirm-deletion": "",
"confirm-share": "",
"contribute": "贡献",
"copy-link": "",
@@ -155,6 +156,7 @@
"profile": "个人资料",
"public": "公开",
"radius": "半径",
"read-more": "",
"register": "注册",
"removed-trail-from": "路线已删除自",
"required": "必填",

View File

@@ -9,7 +9,7 @@ export class List {
avatar?: string;
trails?: string[];
expand?: {
trails: Trail[]
trails?: Trail[]
list_share_via_list?: ListShare[]
}

View File

@@ -7,8 +7,8 @@ import { writable, type Writable } from "svelte/store";
import { fetchGPX } from "./trail_store";
export const lists: Writable<List[]> = writable([])
export const list: Writable<List> = writable(new List("", []))
export const list: Writable<List | null> = writable(null)
export const listTrail: Writable<Trail | null> = writable(null);
export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/list?' + new URLSearchParams({
@@ -26,7 +26,7 @@ export async function lists_index(filter?: ListFilter, f: (url: RequestInfo | UR
lists.set(fetchedLists);
if (fetchedLists.length > 0) {
list.set(fetchedLists[0])
// list.set(fetchedLists[0])
}
return fetchedLists;
@@ -39,7 +39,7 @@ export async function lists_show(id: string, f: (url: RequestInfo | URL, config?
})
const response = await r.json()
for (const trail of response.expand.trails) {
for (const trail of response.expand?.trails ?? []) {
const gpxData: string = await fetchGPX(trail);
trail.expand.gpx_data = gpxData;
}

View File

@@ -140,40 +140,62 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
},
select(index) {
if (index > this._routes.length - 1) {
return
highlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this.setSelection(this._routes[index])
route.eachLayer((l) => {
this.highlight(route, l)
})
},
openPopup(index) {
if (index > this._routes.length - 1) {
return
unHighlightTrack(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this._routes[index].openPopup();
route.eachLayer((l) => {
this.unhighlight(route, l)
})
},
closePopup(index) {
if (index > this._routes.length - 1) {
return
select(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
this.setSelection(route)
},
this._routes[index].closePopup();
resetSelection() {
this.setSelection(this._selected)
if (this.options.flyToBounds) {
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
}
},
openPopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.openPopup();
},
closePopup(id) {
const route = this._routes.find(r => r.options.id == id)
if (!route) {
return;
}
route.closePopup();
},
_addTrack: function (track) {
if (track instanceof Object) {
this._loadGeoJSON(track);
} else if (track !== undefined) {
this._elevation._parseFromString(track)
.then(geojson => this._loadGeoJSON(geojson, this._hashCode(track), track.split('/').pop().split('#')[0].split('?')[0]))
}
},
this._elevation._parseFromString(track.gpx)
.then(geojson => this._loadGeoJSON(geojson, track.id, track.gpx.split('/').pop().split('#')[0].split('?')[0]))
_hashCode: (s) => s.split('').reduce((a, b) => (((a << 5) - a) + b.charCodeAt(0)) | 0, 0),
},
clear: function () {
this._elevation.clear()
@@ -197,9 +219,9 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
}
},
_loadGeoJSON: function (geojson, hash, fallbackName) {
_loadGeoJSON: function (geojson, id, fallbackName) {
if (geojson) {
geojson.hash = hash
geojson.id = id
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
this._loadRoute(geojson);
}
@@ -208,7 +230,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
_loadRoute: function (data) {
if (!data) return;
var line_style = {
color: this._hashToColor(data.hash),
color: this._stringToColor(data.id),
opacity: 0.75,
weight: 5,
distanceMarkers: this.options.distanceMarkers_options,
@@ -220,7 +242,7 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
distanceMarkers: line_style.distanceMarkers,
originalStyle: line_style,
isGroupLayer: true,
hash: data.hash,
id: data.id,
filter: feature => feature.geometry.type != "Point",
});
@@ -351,23 +373,20 @@ export const GpxGroup = L.GpxGroup = L.Class.extend({
}
},
_hashToColor(hash) {
// Ensure hash is a positive integer
hash = Math.abs(hash);
_stringToColor(input) {
// Define the maximum brightness to ensure the color is not too light
const maxBrightness = 250;
const maxBrightness = 200;
// Convert the hash to a color in a reduced RGB range to prevent light and green colors
const r = (hash >> 16) & 0xFF; // Extract the red component
const g = (hash >> 8) & 0xFF; // Extract the green component
const b = hash & 0xFF; // Extract the blue component
// Divide the string into 3 parts (5 characters each)
const redPart = input.slice(0, 5);
const greenPart = input.slice(5, 10);
const bluePart = input.slice(10, 15);
// Adjust the green value to avoid greenish colors
// Reducing the green component significantly to avoid strong green shades
const green = Math.floor((g / 255) * (maxBrightness * 0.5)); // Reduce green component range
const red = Math.floor((r / 255) * maxBrightness);
const blue = Math.floor((b / 255) * maxBrightness);
// Calculate the red, green, and blue values by summing the char codes of the parts
const red = Math.floor((redPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
const greenRaw = greenPart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256;
const green = Math.floor((greenRaw * 0.5) * (maxBrightness / 255)); // Reduce green to avoid greenish colors
const blue = Math.floor((bluePart.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 256) * (maxBrightness / 255));
// Format as HEX color
return `#${(1 << 24 | red << 16 | green << 8 | blue).toString(16).slice(1).toUpperCase()}`;

View File

@@ -4,19 +4,18 @@
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 ListPanel from "$lib/components/list/list_panel.svelte";
import ListShareModal from "$lib/components/list/list_share_modal.svelte";
import MapWithElevationMultiple from "$lib/components/trail/map_with_elevation_multiple.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import { List } from "$lib/models/list";
import type { TrailFilter } from "$lib/models/trail";
import type { Trail } from "$lib/models/trail";
import {
list,
lists,
lists_create,
lists_delete,
lists_index,
lists_update,
} from "$lib/stores/list_store";
import { fetchGPX } from "$lib/stores/trail_store";
import "$lib/vendor/leaflet-elevation/src/index.css";
@@ -27,136 +26,171 @@
import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n";
let openListModal: () => void;
let openConfirmModal: () => void;
let openShareModal: () => void;
let listToBeDeleted: List | null = null;
let map: Map;
let mapWithElevationMultiple: MapWithElevationMultiple;
let markers: any[];
let showMap: boolean = true;
let selectedList: List | null = null;
let selectedTrail: Trail | null = null;
onMount(() => {
if ($page.url.searchParams.get("list")) {
const listToFocus = $lists.find((l) => l.id == $page.url.searchParams.get("list"));
if(listToFocus) {
const listToFocus = $lists.find(
(l) => l.id == $page.url.searchParams.get("list"),
);
if (listToFocus) {
setCurrentList(listToFocus);
}
}
});
async function toggleMap() {
showMap = !showMap;
if (showMap) {
setCurrentList($list);
await tick();
map.invalidateSize();
}
}
async function saveList(e: CustomEvent<{ list: List; avatar?: File }>) {
const result = e.detail;
if (result.list.id) {
await lists_update(result.list, result.avatar);
} else {
await lists_create(result.list, result.avatar);
}
await lists_index();
}
async function handleDropdownClick(
e: CustomEvent<DropdownItem>,
currentList: List,
) {
async function handleDropdownClick(e: CustomEvent<DropdownItem>) {
const item = e.detail;
if (!selectedList) {
return;
}
if (item.value == "share") {
list.set(currentList);
await tick();
openShareModal();
} else if (item.value == "edit") {
goto("/lists/edit/" + currentList.id);
goto("/lists/edit/" + selectedList.id);
} else if (item.value == "delete") {
openConfirmModal();
listToBeDeleted = currentList;
}
}
async function deleteList() {
if (!listToBeDeleted) {
if (!selectedList) {
return;
}
await lists_delete(listToBeDeleted);
await lists_delete(selectedList);
await lists_index();
listToBeDeleted = null;
selectedList = null;
}
async function setCurrentList(item: List) {
if (item.expand && item.expand.trails.length > 0) {
if (
item.expand &&
item.expand.trails &&
item.expand.trails.length > 0
) {
for (const trail of item.expand.trails) {
const gpxData: string = await fetchGPX(trail);
trail.expand.gpx_data = gpxData;
}
}
list.set(item);
selectedList = item;
}
function back() {
if (selectedTrail) {
selectedTrail = null;
mapWithElevationMultiple.resetSelection();
} else if (selectedList) {
selectedList = null;
map.flyTo([0, 0], 4, {
duration: 0.25,
easeLinearity: 0.25,
noMoveStart: true,
});
}
}
function selectTrail(trail: Trail) {
selectedTrail = trail;
mapWithElevationMultiple.selectTrail(trail.id!);
window.scrollTo({ top: 0 });
}
function highlightTrail(trail: Trail) {
mapWithElevationMultiple.highlightTrail(trail.id!);
}
function unHighlightTrail(trail: Trail) {
mapWithElevationMultiple.unHighlightTrail(trail.id!);
}
</script>
<svelte:head>
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 mx-4">
<ul
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full"
<main class="grid grid-cols-1 md:grid-cols-[430px_1fr] gap-4 lg:gap-4 md:mx-4">
<div
class="list-list md:mx-auto rounded-xl border border-input-border max-h-full w-full order-1 md:order-none"
>
<div
class="flex gap-x-4 items-center justify-between p-4 bg-background z-50 rounded-xl"
>
<button
class="btn-icon"
class:btn-disabled={!selectedList}
disabled={!selectedList}
on:click={back}><i class="fa fa-arrow-left"></i></button
>
<a class="btn-primary btn-large text-center" href="/lists/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-list")}</a
>
<button type="button" class="btn-icon" on:click={toggleMap}
<!-- <button type="button" class="btn-icon" on:click={toggleMap}
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"
></i></button
>
> -->
</div>
<hr class="border-separator mb-2" />
<hr class="border-separator" />
<div class="px-4">
{#each $lists as item, i}
<li
class="list-list-item my-1"
on:click={() => setCurrentList(item)}
role="presentation"
>
<ListCard
list={item}
on:change={(e) => handleDropdownClick(e, item)}
active={item.id === $list.id}
></ListCard>
</li>
{/each}
<div>
{#if !selectedList}
<div class="px-4 mt-2">
{#each $lists as item, i}
<div
class="list-list-item my-1"
on:click={() => setCurrentList(item)}
role="presentation"
>
<ListCard list={item}></ListCard>
</div>
{/each}
</div>
{:else if selectedList && !selectedTrail}
<ListPanel
list={selectedList}
on:mouseenter={(e) => highlightTrail(e.detail.trail)}
on:mouseleave={(e) => unHighlightTrail(e.detail.trail)}
on:change={(e) => handleDropdownClick(e)}
on:click={(e) => selectTrail(e.detail.trail)}
></ListPanel>
{:else if selectedList && selectedTrail}
<TrailInfoPanel trail={selectedTrail} mode="list" {markers}
></TrailInfoPanel>
{/if}
</div>
</ul>
<div id="trail-map" class="sticky top-[62px]" class:hidden={!showMap}>
</div>
<div id="trail-map" class="md:sticky md:top-[62px]" class:hidden={!showMap}>
<MapWithElevationMultiple
trails={$list.expand?.trails ?? []}
trails={selectedList?.expand?.trails ?? []}
bind:map
bind:this={mapWithElevationMultiple}
bind:markers
options={{ itinerary: true, flyToBounds: true }}
></MapWithElevationMultiple>
</div>
<div class="min-w-0" class:hidden={showMap}>
<TrailList trails={$list.expand?.trails ?? []}></TrailList>
<TrailList trails={selectedList?.expand?.trails ?? []}></TrailList>
</div>
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
<ConfirmModal
text={$_("delete-list-confirm")}
bind:openModal={openConfirmModal}
on:confirm={deleteList}
></ConfirmModal>
<ListShareModal list={$list} bind:openShareModal></ListShareModal>
{#if selectedList}
<ListShareModal list={selectedList} bind:openShareModal
></ListShareModal>
{/if}
</main>
<style>

View File

@@ -126,14 +126,17 @@
async function handleSearchClick(item: SearchItem) {
const trail = await trails_show(item.value, true);
$form.trails?.push(trail.id!);
$form.expand!.trails = [...$form.expand!.trails, trail];
$form.expand = {
trails: [...($form.expand?.trails ?? []), trail],
list_share_via_list: $form.expand?.list_share_via_list,
};
}
function deleteTrail(trail: Trail) {
$form.trails = $form.trails?.filter((id) => id !== trail.id);
$form.expand!.trails = $form.expand!.trails.filter(
$form.expand!.trails = $form.expand!.trails!.filter(
(t) => t.id !== trail.id,
);
);
}
async function findNewTrailShares() {
@@ -174,6 +177,26 @@
text: $_("list-saved-successfully"),
});
}
function moveTrail(trail: Trail, index: number, direction: 1 | -1) {
(document?.activeElement as HTMLElement).blur()
if (!$form.trails || !$form.expand || !$form.expand.trails) {
return;
}
const previousTrail = $form.expand?.trails?.at(index + direction);
const previousTrailId = $form.trails?.at(index + direction);
if (!previousTrail || !previousTrailId) {
return;
}
$form.expand!.trails![index] = previousTrail;
$form.expand!.trails![index + direction] = trail;
$form.trails[index] = previousTrailId;
$form.trails[index + direction] = trail.id!;
}
</script>
<svelte:head>
@@ -181,6 +204,7 @@
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-list")} | wanderer</title
>
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[440px_1fr]">
<form
id="list-form"
@@ -246,14 +270,15 @@
placeholder="{$_('search-trails')}..."
items={searchDropdownItems}
></Search>
{#if $form.expand?.trails.length}
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $form.expand?.trails?.length}
{#each $form.expand?.trails ?? [] as trail, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex gap-4 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors items-center"
class:border-primary={i == activeTrailIndex}
on:click={() => map.selectTrail(i)}
class:bg-secondary-hover={i == activeTrailIndex}
role="presentation"
on:mouseenter={() => map.highlightTrail(trail.id ?? "")}
on:mouseleave={() => map.unHighlightTrail(trail.id ?? "")}
on:click={() => map.selectTrail(trail.id ?? "")}
>
<div class="shrink-0">
<img
@@ -268,17 +293,14 @@
/>
</div>
<div class="basis-full">
<div class="flex items-center justify-between">
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
<span class="text-sm"
><i class="fa fa-gauge mr-2"></i>{$_(
trail.difficulty ?? "?",
)}</span
>
</div>
<h4 class="font-semibold text-lg">
{trail.name}
</h4>
<span class="text-sm"
><i class="fa fa-gauge mr-2"></i>{$_(
trail.difficulty ?? "?",
)}</span
>
<div class="flex mt-1 gap-4 text-sm text-gray-500">
<span
><i class="fa fa-left-right mr-2"
@@ -296,12 +318,34 @@
>
</div>
</div>
<button
type="button"
class="btn-icon text-red-500"
on:click={() => deleteTrail(trail)}
><i class="fa fa-trash"></i></button
>
<div class="basis-0">
{#if i > 0}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, -1)}
type="button"
class="btn-icon"
>
<i class="fa fa-chevron-up"></i>
</button>
{/if}
{#if i < $form.expand?.trails?.length - 1}
<button
on:click|stopPropagation={() =>
moveTrail(trail, i, 1)}
type="button"
class="btn-icon"
>
<i class="fa fa-chevron-down"></i>
</button>
{/if}
<button
type="button"
class="btn-icon text-red-500"
on:click={() => deleteTrail(trail)}
><i class="fa fa-trash"></i></button
>
</div>
</div>
{/each}
{:else}
@@ -328,7 +372,7 @@
</main>
<ConfirmModal
text={$_('list-share-warning-update')}
text={$_("list-share-warning-update")}
title={$_("confirm-share")}
action="confirm"
bind:openModal={openConfirmModal}
@@ -337,7 +381,8 @@
<style>
@media only screen and (min-width: 768px) {
#trail-map, #list-form {
#trail-map,
#list-form {
height: calc(100vh - 124px);
}
}

View File

@@ -20,7 +20,7 @@ export const load: Load = async ({ params, fetch, data }) => {
return { list: list, previewUrl: previewURL }
} catch (e) {
} catch (e) {
if (e instanceof ClientResponseError) {
return error(e.status as any, e.message)
}

View File

@@ -104,12 +104,12 @@
);
}
function handleTrailCardMouseEnter(index: number) {
mapWithElevation.openPopup(index)
function handleTrailCardMouseEnter(trail: Trail) {
mapWithElevation.openPopup(trail.id!)
}
function handleTrailCardMouseLeave(index: number) {
mapWithElevation.closePopup(index)
function handleTrailCardMouseLeave(trail: Trail) {
mapWithElevation.closePopup(trail.id!)
}
async function handleFilterUpdate(filter: TrailFilter) {
@@ -210,8 +210,8 @@
<a href="map/trail/{trail.id}">
<TrailCard
{trail}
on:mouseenter={() => handleTrailCardMouseEnter(i)}
on:mouseleave={() => handleTrailCardMouseLeave(i)}
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
on:mouseleave={() => handleTrailCardMouseLeave(trail)}
></TrailCard>
</a>
{/each}