* narrow-wide trail list view mode added * change icon, change pos, add i18n * change icon --------- Co-authored-by: Christian Beutel <> Co-authored-by: Flomp <Flomp@users.noreply.github.com>
255 lines
7.8 KiB
Svelte
255 lines
7.8 KiB
Svelte
<script lang="ts">
|
|
import { browser } from "$app/environment";
|
|
import { beforeNavigate, goto } from "$app/navigation";
|
|
import { page } from "$app/state";
|
|
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
|
import TrailList from "$lib/components/trail/trail_list.svelte";
|
|
import type { Trail, TrailFilter } from "$lib/models/trail";
|
|
import { trails_search_filter } from "$lib/stores/trail_store";
|
|
import type { Snapshot } from "@sveltejs/kit";
|
|
import { onMount } from "svelte";
|
|
import { _ } from "svelte-i18n";
|
|
import { APIError } from "$lib/util/api_util";
|
|
import { sanitizeTrailFilter } from "$lib/util/trail_filter_util";
|
|
|
|
const TRAIL_LIST_FILTER_STORAGE_KEY = "trailListFilter";
|
|
|
|
function restoreStoredFilter(defaultFilter: TrailFilter): TrailFilter {
|
|
if (!browser) {
|
|
return defaultFilter;
|
|
}
|
|
|
|
const stored = localStorage.getItem(TRAIL_LIST_FILTER_STORAGE_KEY);
|
|
if (!stored) {
|
|
return defaultFilter;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(stored) as Record<string, unknown>;
|
|
return sanitizeTrailFilter(parsed, defaultFilter);
|
|
} catch {
|
|
localStorage.removeItem(TRAIL_LIST_FILTER_STORAGE_KEY);
|
|
return defaultFilter;
|
|
}
|
|
}
|
|
|
|
function persistFilter() {
|
|
if (!browser) {
|
|
return;
|
|
}
|
|
localStorage.setItem(
|
|
TRAIL_LIST_FILTER_STORAGE_KEY,
|
|
JSON.stringify(filter),
|
|
);
|
|
}
|
|
|
|
let filterExpanded: boolean = $state(true);
|
|
|
|
let loading: boolean = $state(true);
|
|
|
|
let filter: TrailFilter = $state(restoreStoredFilter(page.data.filter));
|
|
const pagination: { page: number; totalPages: number; items: number } =
|
|
$state({
|
|
page: page.url.searchParams.has("page")
|
|
? parseInt(page.url.searchParams.get("page")!)
|
|
: 1,
|
|
totalPages: 1,
|
|
items: 25,
|
|
});
|
|
let trails: Trail[] = $state([]);
|
|
let trailsFullWidth = $state(false);
|
|
let widthPreferences: Record<string, boolean> = $state({});
|
|
let currentDisplayMode: string = $state("cards");
|
|
const widthPreferenceStorageKey = "trailWidthPreferences";
|
|
|
|
export const snapshot: Snapshot<TrailFilter> = {
|
|
capture: () => filter,
|
|
restore: (value) => {
|
|
filter = sanitizeTrailFilter(value, page.data.filter);
|
|
handleFilterUpdate();
|
|
},
|
|
};
|
|
|
|
onMount(() => {
|
|
if (window.innerWidth < 768) {
|
|
filterExpanded = false;
|
|
}
|
|
loadWidthPreferences();
|
|
});
|
|
|
|
function loadWidthPreferences() {
|
|
if (typeof localStorage === "undefined") {
|
|
return;
|
|
}
|
|
try {
|
|
const stored = localStorage.getItem(widthPreferenceStorageKey);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
if (parsed && typeof parsed === "object") {
|
|
widthPreferences = parsed;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("Failed to parse trail width preferences", err);
|
|
}
|
|
applyWidthPreference(currentDisplayMode);
|
|
}
|
|
|
|
function applyWidthPreference(mode: string) {
|
|
trailsFullWidth = widthPreferences[mode] ?? false;
|
|
}
|
|
|
|
function persistWidthPreferences() {
|
|
if (typeof localStorage === "undefined") {
|
|
return;
|
|
}
|
|
try {
|
|
localStorage.setItem(
|
|
widthPreferenceStorageKey,
|
|
JSON.stringify(widthPreferences),
|
|
);
|
|
} catch (err) {
|
|
console.warn("Failed to persist trail width preferences", err);
|
|
}
|
|
}
|
|
|
|
function updateWidthPreference(mode: string, value: boolean) {
|
|
widthPreferences = { ...widthPreferences, [mode]: value };
|
|
persistWidthPreferences();
|
|
}
|
|
|
|
function handleDisplayModeChange(mode: string) {
|
|
currentDisplayMode = mode;
|
|
applyWidthPreference(mode);
|
|
}
|
|
|
|
function toggleTrailWidth() {
|
|
trailsFullWidth = !trailsFullWidth;
|
|
updateWidthPreference(currentDisplayMode, trailsFullWidth);
|
|
}
|
|
|
|
beforeNavigate(({ to }) => {
|
|
if (!browser || !to?.url) {
|
|
return;
|
|
}
|
|
|
|
// Keep filter for in-page navigation (e.g. pagination on /trails).
|
|
if (to.url.pathname.startsWith("/trails")) {
|
|
return;
|
|
}
|
|
|
|
localStorage.removeItem(TRAIL_LIST_FILTER_STORAGE_KEY);
|
|
});
|
|
|
|
async function handleFilterUpdate(resetPagination: boolean = true) {
|
|
loading = true;
|
|
persistFilter();
|
|
|
|
await paginate(resetPagination ? 1 : pagination.page, pagination.items);
|
|
|
|
loading = false;
|
|
}
|
|
|
|
async function paginate(
|
|
newPage: number,
|
|
items: number,
|
|
scrollToTop: boolean = true,
|
|
) {
|
|
pagination.page = newPage;
|
|
|
|
try {
|
|
await doPaginate(newPage, items);
|
|
} catch (err: any) {
|
|
let apiError: APIError = err;
|
|
if (apiError.status == 413) {
|
|
// content too large
|
|
|
|
let newItems = 10;
|
|
|
|
if (items == 12 || items == 24 || items == 48 || items == 96) {
|
|
// cards view
|
|
if (items > 96) {
|
|
newItems = 96;
|
|
} else if (items > 48) {
|
|
newItems = 48;
|
|
} else if (items > 24) {
|
|
newItems = 24;
|
|
} else {
|
|
newItems = 12;
|
|
}
|
|
} else {
|
|
if (items > 100) {
|
|
newItems = 100;
|
|
} else if (items > 50) {
|
|
newItems = 50;
|
|
} else if (items > 25) {
|
|
newItems = 25;
|
|
} else {
|
|
newItems = 10;
|
|
}
|
|
}
|
|
|
|
await doPaginate(newPage, newItems);
|
|
}
|
|
}
|
|
|
|
page.url.searchParams.set("page", newPage.toString());
|
|
goto(`?${page.url.searchParams.toString()}`, {
|
|
keepFocus: true,
|
|
noScroll: !scrollToTop,
|
|
});
|
|
}
|
|
|
|
async function doPaginate(newPage: number, items: number) {
|
|
const response = await trails_search_filter(filter, newPage, items);
|
|
if (items) {
|
|
pagination.items = items;
|
|
}
|
|
trails = response.items;
|
|
pagination.page = response.page;
|
|
pagination.totalPages = response.totalPages;
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{$_("trail", { values: { n: 2 } })} | wanderer</title>
|
|
</svelte:head>
|
|
|
|
<main
|
|
class={`grid grid-cols-1 md:grid-cols-[300px_1fr] items-start gap-8 mx-6 ${trailsFullWidth ? "md:mx-6 max-w-full" : "md:mx-auto max-w-7xl"}`}
|
|
>
|
|
<TrailFilterPanel
|
|
categories={page.data.categories}
|
|
bind:filter
|
|
{filterExpanded}
|
|
onupdate={() => handleFilterUpdate()}
|
|
></TrailFilterPanel>
|
|
<TrailList
|
|
bind:filter
|
|
{loading}
|
|
{trails}
|
|
{pagination}
|
|
onupdate={() => handleFilterUpdate(false)}
|
|
onpagination={paginate}
|
|
ondisplaychange={handleDisplayModeChange}
|
|
>
|
|
{#snippet trailWidthToggleSnippet()}
|
|
<button
|
|
type="button"
|
|
class="btn-icon"
|
|
onclick={toggleTrailWidth}
|
|
aria-pressed={trailsFullWidth}
|
|
aria-label={trailsFullWidth
|
|
? $_("collapse-trail-list")
|
|
: $_("expand-trail-list")}
|
|
title={trailsFullWidth
|
|
? $_("collapse-trail-list")
|
|
: $_("expand-trail-list")}
|
|
>
|
|
<i class="fa {trailsFullWidth ? 'fa-compress' : 'fa-expand'}"
|
|
></i>
|
|
</button>
|
|
{/snippet}
|
|
</TrailList>
|
|
</main>
|