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>