Migrates to svelte 5

This commit is contained in:
Christian Beutel
2025-01-24 17:46:13 +01:00
parent 7571f6e9b9
commit 3721b7f537
106 changed files with 5524 additions and 4232 deletions

View File

@@ -1,43 +1,55 @@
<script lang="ts">
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import type { SummitLog } from "$lib/models/summit_log";
import { _ } from "svelte-i18n";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import { theme } from "$lib/stores/theme_store";
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
import { theme } from "$lib/stores/theme_store";
let thumbnail: string =
$theme === "light" ? emptyStateTrailLight : emptyStateTrailDark;
let thumbnail: string = $state(
$theme === "light" ? emptyStateTrailLight : emptyStateTrailDark,
);
$: Promise.all(
(log._photos ?? []).map(async (f) => {
return await readAsDataURLAsync(f);
}),
).then((v) => {
if (log.photos.length) {
thumbnail = getFileURL(log, log.photos[0]);
} else if (v.length) {
thumbnail = v[0];
} else {
thumbnail =
$theme === "light" ? emptyStateTrailLight : emptyStateTrailDark;
}
});
interface Props {
log: SummitLog;
mode?: "show" | "edit";
onchange?: (item: DropdownItem) => void;
}
export let log: SummitLog;
export let mode: "show" | "edit" = "show";
let { log, mode = "show", onchange }: Props = $props();
const dropdownItems: DropdownItem[] = [
{ text: $_("edit"), value: "edit" },
{ text: $_("delete"), value: "delete" },
];
$effect(() => fetchPhotos(log._photos ?? []));
function fetchPhotos(photos: File[]) {
Promise.all(
photos.map(async (f) => {
return await readAsDataURLAsync(f);
}),
).then((v) => {
if (log.photos.length) {
thumbnail = getFileURL(log, log.photos[0]);
} else if (v.length) {
thumbnail = v[0];
} else {
thumbnail =
$theme === "light"
? emptyStateTrailLight
: emptyStateTrailDark;
}
});
}
</script>
<div class="p-4 my-2 border border-input-border rounded-xl">
@@ -65,7 +77,7 @@
</h5>
{#if mode == "edit"}
<Dropdown items={dropdownItems} on:change></Dropdown>
<Dropdown items={dropdownItems} {onchange}></Dropdown>
{/if}
</div>
{#if log.distance || log.elevation_gain || log.elevation_loss || log.duration}

View File

@@ -1,13 +1,12 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { type Snippet } from "svelte";
import {
SummitLogCreateSchema,
} from "$lib/models/api/summit_log_schema";
import { SummitLogCreateSchema } from "$lib/models/api/summit_log_schema";
import GPX from "$lib/models/gpx/gpx";
import { summitLog } from "$lib/stores/summit_log_store";
import { createForm } from "felte";
import { cloneDeep } from "$lib/util/deep_util";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
import { z } from "zod";
import Datepicker from "../base/datepicker.svelte";
@@ -15,11 +14,19 @@
import Textarea from "../base/textarea.svelte";
import PhotoPicker from "../trail/photo_picker.svelte";
import TrailPicker from "../trail/trail_picker.svelte";
import { cloneDeep } from "$lib/util/deep_util";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
import type { SummitLog } from "$lib/models/summit_log";
interface Props {
children?: Snippet<[any]>;
onsave?: (summitLog: SummitLog) => void;
}
const dispatch = createEventDispatcher();
let { children, onsave }: Props = $props();
let modal: Modal;
export function openModal() {
modal.openModal();
}
const ClientSummitLogCreateSchema = SummitLogCreateSchema.extend({
_photos: z.array(z.instanceof(File)).optional(),
@@ -36,7 +43,7 @@
>({
initialValues: $summitLog,
extend: validator({ schema: ClientSummitLogCreateSchema }),
onSubmit: async (form) => {
onSubmit: async (form) => {
if (!form.expand?.gpx_data) {
form.gpx = "";
}
@@ -56,15 +63,22 @@
form._photos = [new File([blob], "route")];
}
dispatch("save", form);
closeModal!();
onsave?.(form);
modal.closeModal!();
},
});
$: setFields(cloneDeep($summitLog));
$: if ($summitLog._gpx) {
$data._gpx = $summitLog._gpx;
}
let trailData = $derived($data.expand?.gpx_data);
$effect(() => {
setFields(cloneDeep($summitLog));
});
$effect(() => {
if ($summitLog._gpx) {
$data._gpx = $summitLog._gpx;
}
});
async function handleTrailSelection(trailData: string | null) {
if (!trailData) {
@@ -85,63 +99,69 @@
$data.elevation_gain = totals.elevationGain;
$data.elevation_loss = totals.elevationLoss;
$data.distance = totals.distance;
$data.expand!.gpx_data = trailData;
}
const children_render = $derived(children);
</script>
<Modal
id="summit-log-modal"
title={$data.id ? $_("edit-entry") : $_("add-entry")}
let:openModal
bind:openModal
bind:closeModal
bind:this={modal}
>
<slot {openModal} />
<form
id="summit-log-form"
slot="content"
class="modal-content space-y-4"
use:form
>
<div class="flex">
<Datepicker name="date" label={$_("date")} error={$errors.date}
></Datepicker>
</div>
<div>
<label for="summitlog-photo-input" class="text-sm font-medium pb-1">
{$_("photos")}
</label>
<PhotoPicker
id="summitlog-photo-input"
parent={$data}
bind:photos={$data.photos}
bind:photoFiles={$data._photos}
showThumbnailControls={false}
></PhotoPicker>
</div>
<div class="flex gap-4">
{#if $data.expand}
<TrailPicker
bind:trailFile={$data._gpx}
bind:trailData={$data.expand.gpx_data}
label={$_("trail", { values: { n: 1 } })}
on:change={(e) => handleTrailSelection(e.detail)}
></TrailPicker>
{/if}
<div class="basis-full">
<Textarea
name="text"
extraClasses="h-28"
label={$_("text")}
error={$errors.text}
></Textarea>
{#snippet children({ openModal })}
{@render children_render?.({ openModal })}
{/snippet}
{#snippet content()}
<form id="summit-log-form" class="modal-content space-y-4" use:form>
<div class="flex">
<Datepicker name="date" label={$_("date")} error={$errors.date}
></Datepicker>
</div>
<div>
<label
for="summitlog-photo-input"
class="text-sm font-medium pb-1"
>
{$_("photos")}
</label>
<PhotoPicker
id="summitlog-photo-input"
parent={$data}
bind:photos={$data.photos}
bind:photoFiles={$data._photos}
showThumbnailControls={false}
></PhotoPicker>
</div>
<div class="flex gap-4">
{#if $data.expand}
<TrailPicker
bind:trailFile={$data._gpx}
{trailData}
label={$_("trail", { values: { n: 1 } })}
onchange={(trail) => handleTrailSelection(trail)}
></TrailPicker>
{/if}
<div class="basis-full">
<Textarea
name="text"
extraClasses="h-28"
label={$_("text")}
error={$errors.text}
></Textarea>
</div>
</div>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button class="btn-secondary" onclick={() => modal.closeModal()}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="summit-log-form"
>{$_("save")}</button
>
</div>
</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="summit-log-form"
>{$_("save")}</button
>
</div>
{/snippet}
</Modal>

View File

@@ -10,24 +10,32 @@
import { _ } from "svelte-i18n";
import MapWithElevationMaplibre from "../trail/map_with_elevation_maplibre.svelte";
export let summitLogs: SummitLog[] = [];
export let showCategory: boolean = false;
export let showTrail: boolean = false;
export let showAuthor: boolean = false;
export let showRoute: boolean = false;
export let showPhotos: boolean = false;
interface Props {
summitLogs?: SummitLog[];
showCategory?: boolean;
showTrail?: boolean;
showAuthor?: boolean;
showRoute?: boolean;
showPhotos?: boolean;
}
let openMapModal: () => void;
let closeMapModal: () => void;
let {
summitLogs = [],
showCategory = false,
showTrail = false,
showAuthor = false,
showRoute = false,
showPhotos = false,
}: Props = $props();
let openTextModal: () => void;
let closeTextModal: () => void;
let mapModal: Modal;
let textModal: Modal;
let map: M.Map;
let map: M.Map | undefined = $state();
let trail: Trail | null = null;
let trail: Trail | null = $state(null);
let currentText: string = "";
let currentText: string = $state("");
onMount(async () => {});
@@ -40,21 +48,21 @@
trail.id = log.id;
trail.expand!.gpx_data = log.expand.gpx_data;
openMapModal();
mapModal.openModal();
await tick();
return;
}
async function openText(log: SummitLog) {
currentText = log.text ?? "";
openTextModal();
textModal.openModal();
}
</script>
<table class="w-full">
<thead class="text-left text-gray-500">
<tr class="text-sm">
{#if showPhotos && summitLogs.some(l => l.photos.length)}
{#if showPhotos && summitLogs.some((l) => l.photos.length)}
<th class="w-24"></th>
{/if}
<th>{$_("date")}</th>
@@ -75,7 +83,7 @@
{#if summitLogs.some((l) => l.text?.length)}
<th>{$_("description")}</th>
{/if}
{#if showAuthor && summitLogs.some(l => l.expand?.author)}
{#if showAuthor && summitLogs.some((l) => l.expand?.author)}
<th>
{$_("author", { values: { n: 1 } })}
</th>
@@ -91,13 +99,14 @@
{#each summitLogs as log, i}
<SummitLogTableRow
{log}
on:open={(e) => openMap(e.detail)}
on:text={(e) => openText(e.detail)}
onopen={(log) => openMap(log)}
ontext={(log) => openText(log)}
{showCategory}
{showTrail}
{showAuthor}
showPhotos={showPhotos && summitLogs.some(l => l.photos.length)}
showDescription={summitLogs.some(l => l.text?.length)}
showPhotos={showPhotos &&
summitLogs.some((l) => l.photos.length)}
showDescription={summitLogs.some((l) => l.text?.length)}
showRoute={showRoute && summitLogs.some((l) => l.gpx)}
></SummitLogTableRow>
{/each}
@@ -110,24 +119,29 @@
id="summit-log-table-map-modal"
size="max-w-4xl"
title=""
bind:openModal={openMapModal}
bind:closeModal={closeMapModal}
bind:this={mapModal}
>
<div slot="content" id="summit-log-table-map" class="h-[32rem]">
{#if trail}
<MapWithElevationMaplibre trails={[trail]} bind:map showTerrain
></MapWithElevationMaplibre>
{/if}
</div>
{#snippet content()}
<div id="summit-log-table-map" class="h-[32rem]">
{#if trail}
<MapWithElevationMaplibre
trails={[trail]}
bind:map
showTerrain
></MapWithElevationMaplibre>
{/if}
</div>
{/snippet}
</Modal>
<Modal
id="summit-log-table-text-modal"
size="max-w-xl"
title={$_("description")}
bind:openModal={openTextModal}
bind:closeModal={closeTextModal}
bind:this={textModal}
>
<p slot="content" class="whitespace-pre-wrap">{currentText}</p>
{#snippet content()}
<p class="whitespace-pre-wrap">{currentText}</p>
{/snippet}
</Modal>
<style>

View File

@@ -7,38 +7,53 @@
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
import PhotoGallery from "../photo_gallery.svelte";
export let log: SummitLog;
export let showCategory: boolean = false;
export let showTrail: boolean = false;
export let showRoute: boolean = false;
export let showAuthor: boolean = false;
export let showDescription: boolean = false;
export let showPhotos: boolean = false;
let openGallery: (idx?: number) => void;
let imgSrc: string[] = [];
$: if (log.photos?.length) {
imgSrc = log.photos
.filter((_, i) => i < 3)
.reverse()
.map((p) => getFileURL(log, p));
} else {
imgSrc = [];
interface Props {
log: SummitLog;
showCategory?: boolean;
showTrail?: boolean;
showRoute?: boolean;
showAuthor?: boolean;
showDescription?: boolean;
showPhotos?: boolean;
ontext?: (summitLog: SummitLog) => void;
onopen?: (summitLog: SummitLog) => void;
}
const dispatch = createEventDispatcher();
let {
log,
showCategory = false,
showTrail = false,
showRoute = false,
showAuthor = false,
showDescription = false,
showPhotos = false,
onopen,
ontext
}: Props = $props();
let gallery: PhotoGallery;
let imgSrc: string[] = $state([]);
$effect(() => {
if (log.photos?.length) {
imgSrc = log.photos
.filter((_, i) => i < 3)
.reverse()
.map((p) => getFileURL(log, p));
} else {
imgSrc = [];
}
});
function openText() {
dispatch("text", log);
ontext?.(log);
}
function openRoute() {
dispatch("open", log);
onopen?.(log);
}
function colCount() {
@@ -58,12 +73,12 @@
{#if imgSrc.length}
<PhotoGallery
photos={log.photos.map((p) => getFileURL(log, p))}
bind:open={openGallery}
bind:this={gallery}
></PhotoGallery>
<button
class="relative w-16 aspect-square ml-2 mb-3 shrink-0"
type="button"
on:click={() => openGallery()}
onclick={() => gallery.openGallery()}
>
{#each imgSrc as img, i}
<img
@@ -110,6 +125,7 @@
{#if showTrail}
<td>
<a
aria-label="Go to trail"
class="btn-icon aspect-square"
href="/trail/view/{log.expand?.trails_via_summit_logs?.at(0)
?.id ?? ''}"
@@ -120,7 +136,7 @@
{#if showDescription}
<td>
{#if log.text}
<button on:click={openText}
<button onclick={openText}
><p
class="rounded-full bg-menu-item-background-hover hover:bg-menu-item-background-focus text-ellipsis max-w-28 whitespace-nowrap overflow-hidden px-3 py-1"
>
@@ -164,7 +180,11 @@
{/if}
{#if showRoute && log.expand?.gpx_data}
<td>
<button on:click={openRoute} class="btn-icon">
<button
aria-label="Open route"
onclick={openRoute}
class="btn-icon"
>
<i class="fa fa-map-location-dot px-[3px] text-xl"></i></button
>
</td>