adds list
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
|
||||
--footer-background: 36, 39, 52;
|
||||
|
||||
--separator: 148, 163, 184;
|
||||
--separator: 209, 213, 219;
|
||||
|
||||
background: rgba(var(--background));
|
||||
color: rgba(var(--content));
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
--footer-background: 26, 29, 51;
|
||||
|
||||
--separator: 108, 115, 150;
|
||||
--separator: 43, 46, 61;
|
||||
|
||||
background: rgba(var(--background));
|
||||
color: rgba(var(--content));
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createInstance, pb } from '$lib/pocketbase'
|
||||
import { regenerateInstance } from '$lib/meilisearch'
|
||||
import { pb } from '$lib/pocketbase'
|
||||
import type { Handle } from '@sveltejs/kit'
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// load the store data from the request cookie string
|
||||
pb.authStore.loadFromCookie(event.request.headers.get('cookie') || '')
|
||||
regenerateInstance();
|
||||
try {
|
||||
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
|
||||
if (pb.authStore.isValid) {
|
||||
|
||||
29
web/src/lib/components/list/list_card.svelte
Normal file
29
web/src/lib/components/list/list_card.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { List } from "$lib/models/list";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
export let list: List;
|
||||
export let active: boolean = false;
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-6 p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
|
||||
class:bg-menu-item-background-hover={active}
|
||||
>
|
||||
<img
|
||||
class="w-24 aspect-square rounded-full"
|
||||
src={list.avatar}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div class="basis-full self-start">
|
||||
<div class="flex justify-between items-center">
|
||||
<h5 class="text-xl font-semibold">{list.name}</h5>
|
||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mr-8">{list.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
114
web/src/lib/components/list/list_modal.svelte
Normal file
114
web/src/lib/components/list/list_modal.svelte
Normal file
@@ -0,0 +1,114 @@
|
||||
<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 Modal from "../base/modal.svelte";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
|
||||
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) => {
|
||||
const htmlForm = document.getElementById(
|
||||
"list-form",
|
||||
) as HTMLFormElement;
|
||||
const formData = new FormData(htmlForm);
|
||||
|
||||
dispatch("save", { list: submittedList, formData: formData });
|
||||
(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 = $list.avatar ?? "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
id="list-modal"
|
||||
title="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"
|
||||
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">Save</button>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -52,6 +52,9 @@
|
||||
case "/map":
|
||||
childPosition = 3;
|
||||
break;
|
||||
case "/lists":
|
||||
childPosition = 4;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
|
||||
export let trail: Trail;
|
||||
@@ -13,7 +14,7 @@
|
||||
role="listitem"
|
||||
>
|
||||
<div class="w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl">
|
||||
<img src={trail.thumbnail} alt="" />
|
||||
<img src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
|
||||
112
web/src/lib/components/trail/trail_list.svelte
Normal file
112
web/src/lib/components/trail/trail_list.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import type { Trail, TrailFilter } from "$lib/models/trail";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import Select, { type SelectItem } from "../base/select.svelte";
|
||||
import EmptyStateSearch from "../empty_states/empty_state_search.svelte";
|
||||
import TrailCard from "./trail_card.svelte";
|
||||
import TrailListItem from "./trail_list_item.svelte";
|
||||
|
||||
export let filter: TrailFilter;
|
||||
export let trails: Trail[];
|
||||
|
||||
const displayOptions: SelectItem[] = [
|
||||
{ text: "Cards", value: "cards" },
|
||||
{ text: "List", value: "list" },
|
||||
];
|
||||
|
||||
let selectedDisplayOption = displayOptions[0].value;
|
||||
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
const sortOptions: SelectItem[] = [
|
||||
{ text: "Alphabetical", value: "name" },
|
||||
{ text: "Creation date", value: "created" },
|
||||
{ text: "Distance", value: "distance" },
|
||||
{ text: "Elevation gain", value: "elevation_gain" },
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
const storedDisplayOption = localStorage.getItem("displayOption");
|
||||
if (storedDisplayOption) {
|
||||
selectedDisplayOption = storedDisplayOption;
|
||||
}
|
||||
});
|
||||
|
||||
function setDisplayOption() {
|
||||
localStorage.setItem("displayOption", selectedDisplayOption);
|
||||
}
|
||||
|
||||
async function setSort() {
|
||||
dispatch("update", filter);
|
||||
}
|
||||
|
||||
async function setSortOrder() {
|
||||
if (filter.sortOrder === "+") {
|
||||
filter.sortOrder = "-";
|
||||
} else {
|
||||
filter.sortOrder = "+";
|
||||
}
|
||||
dispatch("update", filter);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-start gap-8 justify-end mx-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Sort</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
bind:value={filter.sort}
|
||||
items={sortOptions}
|
||||
on:change={setSort}
|
||||
></Select>
|
||||
<button
|
||||
id="sort-order-btn"
|
||||
class="btn-icon"
|
||||
class:rotated={filter.sortOrder == "-"}
|
||||
on:click={() => setSortOrder()}
|
||||
><i class="fa fa-arrow-up"></i></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Display as</p>
|
||||
|
||||
<Select
|
||||
bind:value={selectedDisplayOption}
|
||||
items={displayOptions}
|
||||
on:change={() => setDisplayOption()}
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
|
||||
{#if trails.length == 0}
|
||||
<div class="flex flex-col basis-full items-center">
|
||||
<EmptyStateSearch></EmptyStateSearch>
|
||||
</div>
|
||||
{/if}
|
||||
{#each trails as trail}
|
||||
<a
|
||||
class="max-w-full"
|
||||
class:basis-full={selectedDisplayOption === "list"}
|
||||
href="/trail/view/{trail.id}"
|
||||
>
|
||||
{#if selectedDisplayOption === "cards"}
|
||||
<TrailCard {trail}></TrailCard>
|
||||
{:else}
|
||||
<TrailListItem {trail}></TrailListItem>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#sort-order-btn {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
:global(.rotated) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
|
||||
export let trail: Trail;
|
||||
@@ -11,7 +12,7 @@
|
||||
<div class="shrink-0">
|
||||
<img
|
||||
class="h-28 w-28 object-cover rounded-xl"
|
||||
src={trail.thumbnail}
|
||||
src={getFileURL(trail, trail.thumbnail)}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MeiliSearch } from 'meilisearch'
|
||||
import { pb } from './pocketbase'
|
||||
|
||||
export function createInstance() {
|
||||
if(pb.authStore.model) {
|
||||
if (pb.authStore.model) {
|
||||
return new MeiliSearch({ host: PUBLIC_MEILISEARCH_URL, apiKey: pb.authStore.model.token })
|
||||
}
|
||||
return new MeiliSearch({ host: PUBLIC_MEILISEARCH_URL, apiKey: PUBLIC_MEILISEARCH_API_TOKEN })
|
||||
|
||||
34
web/src/lib/models/list.ts
Normal file
34
web/src/lib/models/list.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { object, string } from "yup";
|
||||
import type { Trail } from "./trail";
|
||||
|
||||
export class List {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
expand?: {
|
||||
trails: Trail[]
|
||||
}
|
||||
author?: string;
|
||||
|
||||
constructor(name: string, trails: Trail[], params?: { description?: string, avatar?: string, author?: string }) {
|
||||
this.name = name;
|
||||
this.expand = { trails: trails };
|
||||
this.description = params?.description;
|
||||
this.avatar = params?.description;
|
||||
this.author = params?.author;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ListFilter {
|
||||
q: string,
|
||||
sort: "name" | "size" | "created";
|
||||
sortOrder: "+" | "-"
|
||||
}
|
||||
|
||||
export const listSchema = object<List>({
|
||||
id: string().optional(),
|
||||
name: string().required(),
|
||||
description: string().optional(),
|
||||
avatar: string().optional()
|
||||
});
|
||||
@@ -75,7 +75,7 @@ class Trail {
|
||||
}
|
||||
}
|
||||
|
||||
const trailSchema = object<SummitLog>({
|
||||
const trailSchema = object<Trail>({
|
||||
id: string().optional(),
|
||||
name: string().required("Required"),
|
||||
location: string().optional(),
|
||||
|
||||
54
web/src/lib/stores/list_store.ts
Normal file
54
web/src/lib/stores/list_store.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { List, type ListFilter } from "$lib/models/list";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export const lists: Writable<List[]> = writable([])
|
||||
export const list: Writable<List> = writable(new List("", []))
|
||||
|
||||
|
||||
export async function lists_index(filter?: ListFilter) {
|
||||
const dbResponse: List[] = (await pb.collection('lists').getList<List>(1, 5, {
|
||||
expand: "trails",
|
||||
sort: `${filter?.sortOrder ?? "+"}${filter?.sort ?? "name"}`
|
||||
})).items
|
||||
|
||||
for (const list of dbResponse) {
|
||||
list.avatar = getFileURL(list, list.avatar);
|
||||
}
|
||||
|
||||
lists.set(dbResponse);
|
||||
|
||||
if(dbResponse.length > 0) {
|
||||
list.set(dbResponse[0])
|
||||
}
|
||||
|
||||
return dbResponse;
|
||||
}
|
||||
|
||||
export async function lists_create(formData: { [key: string]: any; } | FormData) {
|
||||
if (!pb.authStore.model) {
|
||||
throw new Error("Unauthenticated");
|
||||
}
|
||||
|
||||
formData.append("author", pb.authStore.model!.id);
|
||||
|
||||
let model = await pb
|
||||
.collection("lists")
|
||||
.create<List>(formData);
|
||||
}
|
||||
|
||||
export async function lists_update(list:List, formData: { [key: string]: any; } | FormData) {
|
||||
if((formData.get("avatar") as File).size == 0) {
|
||||
formData.delete("avatar");
|
||||
}
|
||||
let model = await pb
|
||||
.collection("lists")
|
||||
.update<List>(list.id! ,formData);
|
||||
}
|
||||
|
||||
export async function lists_delete(list:List) {
|
||||
let success = await pb
|
||||
.collection("lists")
|
||||
.delete(list.id!);
|
||||
}
|
||||
@@ -137,7 +137,6 @@ export async function trails_create(trail: Trail, formData: { [key: string]: any
|
||||
|
||||
if (!pb.authStore.model) {
|
||||
throw new Error("Unauthenticated");
|
||||
;
|
||||
}
|
||||
formData.set("category", trail.expand.category!.id);
|
||||
|
||||
@@ -302,7 +301,6 @@ async function fetchGPX(trail: Trail) {
|
||||
}
|
||||
|
||||
function setFileURLs(trail: Trail) {
|
||||
trail.thumbnail = getFileURL(trail, trail.thumbnail);
|
||||
for (let i = 0; i < trail.photos.length; i++) {
|
||||
const photo = trail.photos[i];
|
||||
trail.photos[i] = getFileURL(trail, photo)
|
||||
|
||||
88
web/src/routes/lists/+page.svelte
Normal file
88
web/src/routes/lists/+page.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||
import ListCard from "$lib/components/list/list_card.svelte";
|
||||
import ListModal from "$lib/components/list/list_modal.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
import { List } from "$lib/models/list";
|
||||
import type { TrailFilter } from "$lib/models/trail";
|
||||
import {
|
||||
list,
|
||||
lists,
|
||||
lists_create,
|
||||
lists_delete,
|
||||
lists_index,
|
||||
lists_update,
|
||||
} from "$lib/stores/list_store";
|
||||
|
||||
let openListModal: () => void;
|
||||
|
||||
let filter: TrailFilter = $page.data.filter;
|
||||
|
||||
function beforeListModalOpen() {
|
||||
list.set(new List("", []));
|
||||
openListModal();
|
||||
}
|
||||
|
||||
async function saveList(
|
||||
e: CustomEvent<{ list: List; formData: FormData }>,
|
||||
) {
|
||||
const result = e.detail;
|
||||
if (result.list.id) {
|
||||
await lists_update(result.list, result.formData);
|
||||
await lists_index();
|
||||
} else {
|
||||
await lists_create(result.formData);
|
||||
await lists_index();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDropdownClick(
|
||||
e: CustomEvent<DropdownItem>,
|
||||
currentList: List,
|
||||
) {
|
||||
const item = e.detail;
|
||||
|
||||
if (item.value == "edit") {
|
||||
list.set(currentList);
|
||||
openListModal();
|
||||
} else if (item.value == "delete") {
|
||||
await lists_delete(currentList);
|
||||
await lists_index();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto min-h-screen"
|
||||
>
|
||||
<ul
|
||||
class="list-list max-w-xl mx-2 md:mx-auto mt-8 rounded-xl border border-input-border p-4"
|
||||
>
|
||||
<button
|
||||
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
||||
on:click={beforeListModalOpen}
|
||||
>
|
||||
<i class="fa fa-plus text-xl aspect-square"></i>
|
||||
<h5 class="text-xl font-semibold">Create new list</h5>
|
||||
</button>
|
||||
<hr class="border-separator my-2" />
|
||||
{#each $lists as item, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<li on:click={() => list.set(item)}>
|
||||
<ListCard list={item} on:change={(e) => handleDropdownClick(e, item)} active={item.id === $list.id}
|
||||
></ListCard>
|
||||
{#if i != $lists.length - 1}
|
||||
<hr class="border-separator my-2" />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<TrailList
|
||||
bind:filter
|
||||
trails={$list.expand?.trails ?? []}
|
||||
on:update={async () => await lists_index()}
|
||||
></TrailList>
|
||||
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
||||
</main>
|
||||
22
web/src/routes/lists/+page.ts
Normal file
22
web/src/routes/lists/+page.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { TrailFilter } from "$lib/models/trail";
|
||||
import { lists_index } from "$lib/stores/list_store";
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
|
||||
export const load: Load = async ({ params }) => {
|
||||
const filter: TrailFilter = {
|
||||
q: "",
|
||||
category: [],
|
||||
near: {
|
||||
radius: 2000,
|
||||
},
|
||||
distanceMin: 0,
|
||||
distanceMax: 20000,
|
||||
elevationGainMin: 0,
|
||||
elevationGainMax: 4000,
|
||||
sort: "created",
|
||||
sortOrder: "+",
|
||||
};
|
||||
await lists_index();
|
||||
|
||||
return {filter};
|
||||
};
|
||||
@@ -1,63 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import Select, {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
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 TrailListItem from "$lib/components/trail/trail_list_item.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
import type { TrailFilter } from "$lib/models/trail";
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import { trails, trails_search_filter } from "$lib/stores/trail_store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const displayOptions: SelectItem[] = [
|
||||
{ text: "Cards", value: "cards" },
|
||||
{ text: "List", value: "list" },
|
||||
];
|
||||
let selectedDisplayOption = displayOptions[0].value;
|
||||
|
||||
const sortOptions: SelectItem[] = [
|
||||
{ text: "Alphabetical", value: "name" },
|
||||
{ text: "Creation date", value: "created" },
|
||||
{ text: "Distance", value: "distance" },
|
||||
{ text: "Elevation gain", value: "elevation_gain" },
|
||||
];
|
||||
|
||||
let filterExpanded: boolean = true;
|
||||
|
||||
const filter: TrailFilter = $page.data.filter;
|
||||
|
||||
onMount(() => {
|
||||
const storedDisplayOption = localStorage.getItem("displayOption");
|
||||
if (storedDisplayOption) {
|
||||
selectedDisplayOption = storedDisplayOption;
|
||||
}
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
filterExpanded = false;
|
||||
}
|
||||
});
|
||||
|
||||
function setDisplayOption() {
|
||||
localStorage.setItem("displayOption", selectedDisplayOption);
|
||||
}
|
||||
|
||||
async function setSort() {
|
||||
await trails_search_filter(filter);
|
||||
}
|
||||
|
||||
async function setSortOrder() {
|
||||
if (filter.sortOrder === "+") {
|
||||
filter.sortOrder = "-";
|
||||
} else {
|
||||
filter.sortOrder = "+";
|
||||
}
|
||||
|
||||
await trails_search_filter(filter);
|
||||
}
|
||||
|
||||
async function handleFilterUpdate() {
|
||||
await trails_search_filter(filter);
|
||||
}
|
||||
@@ -72,67 +32,9 @@
|
||||
{filterExpanded}
|
||||
on:update={() => handleFilterUpdate()}
|
||||
></TrailFilterPanel>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-start gap-8 justify-end mx-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Sort</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
bind:value={filter.sort}
|
||||
items={sortOptions}
|
||||
on:change={setSort}
|
||||
></Select>
|
||||
<button
|
||||
id="sort-order-btn"
|
||||
class="btn-icon"
|
||||
class:rotated={filter.sortOrder == "-"}
|
||||
on:click={() => setSortOrder()}
|
||||
><i class="fa fa-arrow-up"></i></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 pb-2">Display as</p>
|
||||
|
||||
<Select
|
||||
bind:value={selectedDisplayOption}
|
||||
items={displayOptions}
|
||||
on:change={() => setDisplayOption()}
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="trails"
|
||||
class="flex items-start flex-wrap gap-8 py-8 max-w-full"
|
||||
>
|
||||
{#if $trails.length == 0}
|
||||
<div class="flex flex-col basis-full items-center">
|
||||
<EmptyStateSearch></EmptyStateSearch>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $trails as trail}
|
||||
<a
|
||||
class="max-w-full"
|
||||
class:basis-full={selectedDisplayOption === "list"}
|
||||
href="/trail/view/{trail.id}"
|
||||
>
|
||||
{#if selectedDisplayOption === "cards"}
|
||||
<TrailCard {trail}></TrailCard>
|
||||
{:else}
|
||||
<TrailListItem {trail}></TrailListItem>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<TrailList
|
||||
{filter}
|
||||
trails={$trails}
|
||||
on:update={async () => await trails_search_filter(filter)}
|
||||
></TrailList>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#sort-order-btn {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
:global(.rotated) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user