adds profile page
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
|
||||||
function toggleMenu(e: MouseEvent) {
|
export function toggleMenu(e: MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
@@ -35,15 +35,16 @@
|
|||||||
<svelte:window on:mouseup={() => (isOpen = false)} />
|
<svelte:window on:mouseup={() => (isOpen = false)} />
|
||||||
|
|
||||||
<div class="dropdown relative">
|
<div class="dropdown relative">
|
||||||
<button
|
<slot {toggleMenu}>
|
||||||
class="btn-icon flex items-center justify-center"
|
<button
|
||||||
on:click={toggleMenu}
|
class="btn-icon flex items-center justify-center"
|
||||||
type="button"
|
on:click={toggleMenu}
|
||||||
>
|
type="button"
|
||||||
<slot>
|
>
|
||||||
<i class="fa fa-ellipsis-vertical text-{size}"></i>
|
<i class="fa fa-ellipsis-vertical text-{size}"></i>
|
||||||
</slot>
|
</button>
|
||||||
</button>
|
</slot>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<ul
|
<ul
|
||||||
class="menu absolute bg-menu-background border border-input-border rounded-l-xl rounded-b-xl shadow-md right-0 overflow-hidden mt-2"
|
class="menu absolute bg-menu-background border border-input-border rounded-l-xl rounded-b-xl shadow-md right-0 overflow-hidden mt-2"
|
||||||
@@ -51,13 +52,10 @@
|
|||||||
style="z-index: 1001"
|
style="z-index: 1001"
|
||||||
>
|
>
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
||||||
<li
|
<li
|
||||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||||
tabindex="0"
|
role="presentation"
|
||||||
on:mouseup|stopPropagation={() => handleItemClick(item)}
|
on:mouseup|stopPropagation={() => handleItemClick(item)}
|
||||||
on:keydown|stopPropagation={() => handleItemClick(item)}
|
|
||||||
>
|
>
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
<i class="fa fa-{item.icon} mr-3"></i>
|
<i class="fa fa-{item.icon} mr-3"></i>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let title: string;
|
export let title: string;
|
||||||
|
export let size: string = "2xl";
|
||||||
export function openModal() {
|
export function openModal() {
|
||||||
document.body.style.position = "fixed";
|
document.body.style.position = "fixed";
|
||||||
document.body.style.width = "100%";
|
document.body.style.width = "100%";
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
{id}
|
{id}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="relative w-full max-w-2xl max-h-full rounded-xl text-content"
|
class="relative w-full max-w-{size} max-h-full rounded-xl text-content"
|
||||||
>
|
>
|
||||||
<!-- Modal content -->
|
<!-- Modal content -->
|
||||||
<div class="relative bg-background shadow">
|
<div class="relative bg-background shadow">
|
||||||
|
|||||||
@@ -9,18 +9,19 @@
|
|||||||
|
|
||||||
{#if $toast}
|
{#if $toast}
|
||||||
<div
|
<div
|
||||||
class="fixed px-4 py-3 shadow-xl rounded-xl border bottom-4 right-4 bg-white text-gray-500 flex items-center"
|
class="fixed px-4 py-3 shadow-xl rounded-xl border bottom-4 right-4 bg-white text-gray-500 flex items-center max-w-md"
|
||||||
style="z-index: 1001;"
|
style="z-index: 1001;"
|
||||||
in:fade={{ duration: 250 }}
|
in:fade={{ duration: 250 }}
|
||||||
out:fade={{ duration: 250 }}
|
out:fade={{ duration: 250 }}
|
||||||
>
|
>
|
||||||
<span>
|
<i
|
||||||
<i
|
class="fa fa-{$toast.icon} p-3 mr-2 rounded-lg"
|
||||||
class="fa fa-{$toast.icon} p-3 mr-2 rounded-lg"
|
class:success-toast={$toast.type == "success"}
|
||||||
class:success-toast={$toast.type == "success"}
|
class:error-toast={$toast.type == "error"}
|
||||||
class:error-toast={$toast.type == "error"}
|
></i>
|
||||||
></i>{$toast.text}</span
|
<p class="mr-4">
|
||||||
>
|
{$toast.text}
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white ml-6"
|
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white ml-6"
|
||||||
|
|||||||
@@ -14,11 +14,17 @@
|
|||||||
class="flex items-center gap-6 p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
|
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}
|
class:bg-menu-item-background-hover={active}
|
||||||
>
|
>
|
||||||
<img
|
{#if list.avatar}
|
||||||
class="w-24 aspect-square rounded-full"
|
<img
|
||||||
src={list.avatar}
|
class="w-16 md:w-24 aspect-square rounded-full"
|
||||||
alt="avatar"
|
src={list.avatar}
|
||||||
/>
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center">
|
||||||
|
<i class="fa fa-table-list text-5xl"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="basis-full self-start">
|
<div class="basis-full self-start">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h5 class="text-xl font-semibold">{list.name}</h5>
|
<h5 class="text-xl font-semibold">{list.name}</h5>
|
||||||
|
|||||||
64
web/src/lib/components/list/list_select_modal.svelte
Normal file
64
web/src/lib/components/list/list_select_modal.svelte
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
|
|
||||||
|
import { lists, lists_index } from "$lib/stores/list_store";
|
||||||
|
import Modal from "../base/modal.svelte";
|
||||||
|
import type { List } from "$lib/models/list";
|
||||||
|
import { trail } from "$lib/stores/trail_store";
|
||||||
|
|
||||||
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleSelect(list: List) {
|
||||||
|
dispatch("change", list);
|
||||||
|
closeModal!();
|
||||||
|
}
|
||||||
|
|
||||||
|
function listContainsCurrentTrail(list: List) {
|
||||||
|
return list.trails?.includes($trail.id!);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="list-modal"
|
||||||
|
title="Select List"
|
||||||
|
size="md"
|
||||||
|
let:openModal
|
||||||
|
bind:openModal
|
||||||
|
bind:closeModal
|
||||||
|
>
|
||||||
|
<slot {openModal} />
|
||||||
|
|
||||||
|
<ul slot="content">
|
||||||
|
{#each $lists as list}
|
||||||
|
<li
|
||||||
|
class="flex gap-4 items-center p-4 hover:bg-menu-item-background-hover rounded-xl transition-colors cursor-pointer"
|
||||||
|
on:click={() => handleSelect(list)}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
{#if list.avatar}
|
||||||
|
<img
|
||||||
|
class="w-12 aspect-square rounded-full"
|
||||||
|
src={list.avatar}
|
||||||
|
alt="avatar"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex w-12 aspect-square shrink-0 items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="fa fa-table-list text-5xl"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<h5 class="text-md font-semibold">{list.name}</h5>
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="fa fa-{listContainsCurrentTrail(list)
|
||||||
|
? 'minus'
|
||||||
|
: 'plus'} rounded-full border border-input-border p-2"
|
||||||
|
></i>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
@@ -78,6 +78,8 @@
|
|||||||
if (item.value == "logout") {
|
if (item.value == "logout") {
|
||||||
logout();
|
logout();
|
||||||
goto("/");
|
goto("/");
|
||||||
|
} else if (item.value == "profile") {
|
||||||
|
goto("/profile");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -104,24 +106,25 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-6 border-input-border" />
|
<hr class="my-6 border-input-border" />
|
||||||
<div class="flex flex-col basis-full gap-y-3">
|
<div class="flex flex-col basis-full">
|
||||||
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
|
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
|
||||||
><i class="fa fa-plus mr-2"></i>New Trail</a
|
><i class="fa fa-plus mr-2"></i>New Trail</a
|
||||||
>
|
>
|
||||||
{#if $currentUser}
|
{#if $currentUser}
|
||||||
<div class="basis-full"></div>
|
<div class="basis-full"></div>
|
||||||
<hr class="border-input-border" />
|
<hr class="border-input-border" />
|
||||||
<div class="flex gap-4 items-center mx-4">
|
<div class="flex gap-4 items-center m-4">
|
||||||
<img
|
<img
|
||||||
class="rounded-full w-8 aspect-square"
|
class="rounded-full w-8 aspect-square"
|
||||||
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
|
src={$currentUser.avatar ||
|
||||||
|
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm">{$currentUser.username}</p>
|
<p class="text-sm">{$currentUser.username}</p>
|
||||||
<p class="text-sm text-gray-500">{$currentUser.email}</p>
|
<p class="text-sm text-gray-500">{$currentUser.email}</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-icon tooltip" data-title="Logout"
|
<button class="btn-icon"
|
||||||
><i class="fa-solid fa-arrow-right-from-bracket"
|
><i class="fa-solid fa-arrow-right-from-bracket"
|
||||||
></i></button
|
></i></button
|
||||||
>
|
>
|
||||||
@@ -165,12 +168,18 @@
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
on:change={(e) => handleDropdownClick(e.detail)}
|
on:change={(e) => handleDropdownClick(e.detail)}
|
||||||
|
let:toggleMenu={openDropdown}
|
||||||
>
|
>
|
||||||
<img
|
<button
|
||||||
class="rounded-full"
|
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 w-10"
|
||||||
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
|
on:click={openDropdown}
|
||||||
alt=""
|
>
|
||||||
/>
|
<img
|
||||||
|
class="rounded-full"
|
||||||
|
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export class List {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
trails?: string[];
|
||||||
expand?: {
|
expand?: {
|
||||||
trails: Trail[]
|
trails: Trail[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { List, type ListFilter } from "$lib/models/list";
|
import { List, type ListFilter } from "$lib/models/list";
|
||||||
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb } from "$lib/pocketbase";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
@@ -8,10 +9,10 @@ export const list: Writable<List> = writable(new List("", []))
|
|||||||
|
|
||||||
|
|
||||||
export async function lists_index(filter?: ListFilter) {
|
export async function lists_index(filter?: ListFilter) {
|
||||||
const dbResponse: List[] = (await pb.collection('lists').getList<List>(1, 5, {
|
const dbResponse: List[] = (await pb.collection('lists').getFullList<List>({
|
||||||
expand: "trails",
|
expand: "trails",
|
||||||
sort: `${filter?.sortOrder ?? "+"}${filter?.sort ?? "name"}`
|
sort: `${filter?.sortOrder ?? "+"}${filter?.sort ?? "name"}`
|
||||||
})).items
|
}))
|
||||||
|
|
||||||
for (const list of dbResponse) {
|
for (const list of dbResponse) {
|
||||||
list.avatar = getFileURL(list, list.avatar);
|
list.avatar = getFileURL(list, list.avatar);
|
||||||
@@ -19,7 +20,7 @@ export async function lists_index(filter?: ListFilter) {
|
|||||||
|
|
||||||
lists.set(dbResponse);
|
lists.set(dbResponse);
|
||||||
|
|
||||||
if(dbResponse.length > 0) {
|
if (dbResponse.length > 0) {
|
||||||
list.set(dbResponse[0])
|
list.set(dbResponse[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,17 +39,29 @@ export async function lists_create(formData: { [key: string]: any; } | FormData)
|
|||||||
.create<List>(formData);
|
.create<List>(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lists_update(list:List, formData: { [key: string]: any; } | FormData) {
|
export async function lists_update(list: List, formData: { [key: string]: any; } | FormData) {
|
||||||
if((formData.get("avatar") as File).size == 0) {
|
if ((formData.get("avatar") as File).size == 0) {
|
||||||
formData.delete("avatar");
|
formData.delete("avatar");
|
||||||
}
|
}
|
||||||
let model = await pb
|
let model = await pb
|
||||||
.collection("lists")
|
.collection("lists")
|
||||||
.update<List>(list.id! ,formData);
|
.update<List>(list.id!, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lists_delete(list:List) {
|
export async function lists_delete(list: List) {
|
||||||
let success = await pb
|
let success = await pb
|
||||||
.collection("lists")
|
.collection("lists")
|
||||||
.delete(list.id!);
|
.delete(list.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lists_add_trail(list: List, trail: Trail) {
|
||||||
|
let model = await pb
|
||||||
|
.collection("lists")
|
||||||
|
.update(list.id!, { "trails+": trail.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lists_remove_trail(list: List, trail: Trail) {
|
||||||
|
let model = await pb
|
||||||
|
.collection("lists")
|
||||||
|
.update(list.id!, { "trails-": trail.id });
|
||||||
}
|
}
|
||||||
@@ -54,10 +54,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto min-h-screen"
|
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-8 max-w-7xl mx-4 md:mx-auto"
|
||||||
|
style="min-height: calc(100vh - 124px)"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
class="list-list max-w-xl mx-2 md:mx-auto mt-8 rounded-xl border border-input-border p-4"
|
class="list-list max-w-xl mx-2 md:mx-auto mt-8 rounded-xl border border-input-border p-4"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
class="flex w-full items-center gap-4 hover:bg-menu-item-background-hover transition-colors rounded-xl p-4 cursor-pointer"
|
||||||
@@ -67,17 +68,20 @@
|
|||||||
<h5 class="text-xl font-semibold">Create new list</h5>
|
<h5 class="text-xl font-semibold">Create new list</h5>
|
||||||
</button>
|
</button>
|
||||||
<hr class="border-separator my-2" />
|
<hr class="border-separator my-2" />
|
||||||
{#each $lists as item, i}
|
<div class="max-h-[512px] md:max-h-none overflow-y-auto">
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
{#each $lists as item, i}
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<li on:click={() => list.set(item)} role="presentation">
|
||||||
<li on:click={() => list.set(item)}>
|
<ListCard
|
||||||
<ListCard list={item} on:change={(e) => handleDropdownClick(e, item)} active={item.id === $list.id}
|
list={item}
|
||||||
></ListCard>
|
on:change={(e) => handleDropdownClick(e, item)}
|
||||||
{#if i != $lists.length - 1}
|
active={item.id === $list.id}
|
||||||
<hr class="border-separator my-2" />
|
></ListCard>
|
||||||
{/if}
|
{#if i != $lists.length - 1}
|
||||||
</li>
|
<hr class="border-separator my-2" />
|
||||||
{/each}
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
<TrailList
|
<TrailList
|
||||||
bind:filter
|
bind:filter
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
|
||||||
let L: any;
|
let L: any;
|
||||||
let map: Map;
|
let map: Map;
|
||||||
@@ -188,7 +189,7 @@
|
|||||||
`<a href="/trail/view/${trail.id}">
|
`<a href="/trail/view/${trail.id}">
|
||||||
<li class="flex items-center gap-4 cursor-pointer text-black">
|
<li class="flex items-center gap-4 cursor-pointer text-black">
|
||||||
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${
|
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${
|
||||||
trail.thumbnail
|
getFileURL(trail, trail.thumbnail)
|
||||||
}" alt="">
|
}" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
102
web/src/routes/profile/+page.svelte
Normal file
102
web/src/routes/profile/+page.svelte
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Search, {
|
||||||
|
type SearchItem,
|
||||||
|
} from "$lib/components/base/search.svelte";
|
||||||
|
import Select, {
|
||||||
|
type SelectItem,
|
||||||
|
} from "$lib/components/base/select.svelte";
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import { ms } from "$lib/meilisearch";
|
||||||
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
|
import { country_codes } from "$lib/util/country_code_util";
|
||||||
|
import RadioGroup, {
|
||||||
|
type RadioItem,
|
||||||
|
} from "$lib/components/base/radio_group.svelte";
|
||||||
|
|
||||||
|
const languages: SelectItem[] = [
|
||||||
|
{ text: "English", value: "en" },
|
||||||
|
{ text: "German", value: "de" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const units: RadioItem[] = [
|
||||||
|
{ text: "Metric", value: "metric" },
|
||||||
|
{ text: "Imperial", value: "imperial" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedLanguage = "en";
|
||||||
|
|
||||||
|
let searchDropdownItems: SearchItem[] = [];
|
||||||
|
let citySearchQuery: string = "";
|
||||||
|
|
||||||
|
async function searchCities(q: string) {
|
||||||
|
const result = await ms.index("cities500").search(q, { limit: 5 });
|
||||||
|
searchDropdownItems = result.hits.map((h) => ({
|
||||||
|
text: h.name,
|
||||||
|
description:
|
||||||
|
country_codes[h["country code"] as keyof typeof country_codes],
|
||||||
|
value: h,
|
||||||
|
icon: "city",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchClick(item: SearchItem) {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class="grid grid-cols-1 md:grid-cols-[356px_1fr] max-w-4xl mx-4 md:mx-auto gap-x-8 items-start"
|
||||||
|
>
|
||||||
|
{#if $currentUser}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center rounded-xl border border-input-border p-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="rounded-full w-32 aspect-square"
|
||||||
|
src={$currentUser.avatar ||
|
||||||
|
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<h3 class="text-2xl mt-4 font-semibold">{$currentUser.username}</h3>
|
||||||
|
<h5 class="font-medium">{$currentUser.email}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6 rounded-xl p-4">
|
||||||
|
<h3 class="text-2xl font-semibold">Settings</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5 class="font-medium mb-1">Default Location</h5>
|
||||||
|
<Search
|
||||||
|
items={searchDropdownItems}
|
||||||
|
placeholder="Search cities..."
|
||||||
|
bind:value={citySearchQuery}
|
||||||
|
on:update={(e) => searchCities(e.detail)}
|
||||||
|
on:click={(e) => handleSearchClick(e.detail)}
|
||||||
|
></Search>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-medium mb-1">Language</h5>
|
||||||
|
<Select items={languages} bind:value={selectedLanguage}
|
||||||
|
></Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-medium mb-1">Units</h5>
|
||||||
|
<RadioGroup
|
||||||
|
name="unit"
|
||||||
|
items={units}
|
||||||
|
selected={0}
|
||||||
|
on:change={() => {}}
|
||||||
|
></RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h5 class="font-medium mb-1">Set new password</h5>
|
||||||
|
<TextField placeholder="New password" label="" type="password"
|
||||||
|
></TextField>
|
||||||
|
<TextField placeholder="Repeat password" type="password"
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
<hr class="border-input-border" />
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h4 class="text-xl text-red-400">Danger zone</h4>
|
||||||
|
<button class="btn-danger">Delete Account</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import Dropdown from "$lib/components/base/dropdown.svelte";
|
||||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
|
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
|
||||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||||
import Tabs from "$lib/components/tabs.svelte";
|
import Tabs from "$lib/components/tabs.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
|
import type { List } from "$lib/models/list";
|
||||||
|
import {
|
||||||
|
lists_add_trail,
|
||||||
|
lists_index,
|
||||||
|
lists_remove_trail,
|
||||||
|
} from "$lib/stores/list_store";
|
||||||
|
import { show_toast } from "$lib/stores/toast_store";
|
||||||
import { trail, trails_delete } from "$lib/stores/trail_store";
|
import { trail, trails_delete } from "$lib/stores/trail_store";
|
||||||
import { currentUser } from "$lib/stores/user_store";
|
import { currentUser } from "$lib/stores/user_store";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
import type { Icon, Map, Marker } from "leaflet";
|
import type { Icon, Map, Marker } from "leaflet";
|
||||||
@@ -29,11 +39,15 @@
|
|||||||
let lightboxDataSource: DataSource;
|
let lightboxDataSource: DataSource;
|
||||||
|
|
||||||
let openConfirmModal: () => void;
|
let openConfirmModal: () => void;
|
||||||
|
let openListSelectModal: () => void;
|
||||||
|
|
||||||
let mapFullScreen: boolean = false;
|
let mapFullScreen: boolean = false;
|
||||||
|
|
||||||
const dropdownItems: DropdownItem[] = [
|
const dropdownItems: DropdownItem[] = [
|
||||||
{ text: "Show on map", value: "map", icon: "map" },
|
{ text: "Show on map", value: "map", icon: "map" },
|
||||||
|
{ text: "Directions", value: "direction", icon: "car" },
|
||||||
|
...($trail.gpx ? [{ text: "Download GPX", value: "download", icon: "download" }] : []),
|
||||||
|
{ text: "Add to list", value: "list", icon: "bookmark" },
|
||||||
{ text: "Edit", value: "edit", icon: "pen" },
|
{ text: "Edit", value: "edit", icon: "pen" },
|
||||||
{ text: "Delete", value: "delete", icon: "trash" },
|
{ text: "Delete", value: "delete", icon: "trash" },
|
||||||
];
|
];
|
||||||
@@ -129,6 +143,18 @@
|
|||||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||||
if (item.value == "map") {
|
if (item.value == "map") {
|
||||||
goto(`/map/?lat=${$trail.lat}&lon=${$trail.lon}`);
|
goto(`/map/?lat=${$trail.lat}&lon=${$trail.lon}`);
|
||||||
|
} else if (item.value == "list") {
|
||||||
|
openListSelectModal();
|
||||||
|
} else if (item.value == "direction") {
|
||||||
|
window
|
||||||
|
.open(
|
||||||
|
`https://www.google.com/maps/dir/Current+Location/${$trail.lat},${$trail.lon}`,
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
|
} else if (item.value == "download") {
|
||||||
|
downloadURI(getFileURL($trail, $trail.gpx), $trail.gpx!);
|
||||||
|
|
||||||
} else if (item.value == "edit") {
|
} else if (item.value == "edit") {
|
||||||
goto(`/trail/edit/${$trail.id}`);
|
goto(`/trail/edit/${$trail.id}`);
|
||||||
} else if (item.value == "delete") {
|
} else if (item.value == "delete") {
|
||||||
@@ -136,6 +162,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadURI(uri: string, name: string) {
|
||||||
|
var link = document.createElement("a");
|
||||||
|
link.setAttribute("download", name);
|
||||||
|
link.href = uri;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTrail() {
|
async function deleteTrail() {
|
||||||
trails_delete($trail).then(() => history.back());
|
trails_delete($trail).then(() => history.back());
|
||||||
}
|
}
|
||||||
@@ -145,13 +180,46 @@
|
|||||||
await tick();
|
await tick();
|
||||||
map.invalidateSize();
|
map.invalidateSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleListSelection(list: List) {
|
||||||
|
try {
|
||||||
|
if (list.trails?.includes($trail.id!)) {
|
||||||
|
await lists_remove_trail(list, $trail);
|
||||||
|
show_toast({
|
||||||
|
type: "success",
|
||||||
|
icon: "check",
|
||||||
|
text: `Removed trail from "${list.name}"`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await lists_add_trail(list, $trail);
|
||||||
|
show_toast({
|
||||||
|
type: "success",
|
||||||
|
icon: "check",
|
||||||
|
text: `Added trail to "${list.name}"`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await lists_index();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
show_toast({
|
||||||
|
type: "error",
|
||||||
|
icon: "close",
|
||||||
|
text: "Error adding trail to list.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="trail-panel max-w-5xl mx-auto border border-input-border rounded-3xl overflow-hidden"
|
class="trail-panel max-w-5xl mx-auto border border-input-border rounded-3xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<section class="relative h-80">
|
<section class="relative h-80">
|
||||||
<img class="w-full h-80" src={$trail.thumbnail} alt="" />
|
<img
|
||||||
|
class="w-full h-80"
|
||||||
|
src={getFileURL($trail, $trail.thumbnail)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
></div>
|
></div>
|
||||||
@@ -168,18 +236,17 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{#if $currentUser && $currentUser.id == $trail.author}
|
{#if $currentUser && $currentUser.id == $trail.author}
|
||||||
<div
|
<Dropdown
|
||||||
class="px-4 py-2 bg-menu-background rounded-full space-x-2"
|
items={dropdownItems}
|
||||||
|
on:change={(e) => handleDropdownClick(e.detail)}
|
||||||
|
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
|
||||||
>
|
>
|
||||||
{#each dropdownItems as item}
|
|
||||||
<button
|
|
||||||
data-title={item.text}
|
|
||||||
class="tooltip btn-icon"
|
|
||||||
on:click={() => handleDropdownClick(item)}
|
|
||||||
><i class="fa fa-{item.icon}"></i></button
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -288,6 +355,10 @@
|
|||||||
bind:openModal={openConfirmModal}
|
bind:openModal={openConfirmModal}
|
||||||
on:confirm={deleteTrail}
|
on:confirm={deleteTrail}
|
||||||
></ConfirmModal>
|
></ConfirmModal>
|
||||||
|
<ListSelectModal
|
||||||
|
bind:openModal={openListSelectModal}
|
||||||
|
on:change={(e) => handleListSelection(e.detail)}
|
||||||
|
></ListSelectModal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { lists_index } from "$lib/stores/list_store";
|
||||||
import { trails_show } from "$lib/stores/trail_store";
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
import type { Load } from "@sveltejs/kit";
|
import type { Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
export const load: Load = async ({ params }) => {
|
||||||
await trails_show(params.id!, true)
|
await trails_show(params.id!, true)
|
||||||
|
await lists_index();
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user