adds profile page

This commit is contained in:
Christian Beutel
2024-02-27 22:59:22 +01:00
parent 965e4196ae
commit 0d3683a319
13 changed files with 342 additions and 69 deletions

View File

@@ -16,7 +16,7 @@
let isOpen = false;
function toggleMenu(e: MouseEvent) {
export function toggleMenu(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
isOpen = !isOpen;
@@ -35,15 +35,16 @@
<svelte:window on:mouseup={() => (isOpen = false)} />
<div class="dropdown relative">
<button
class="btn-icon flex items-center justify-center"
on:click={toggleMenu}
type="button"
>
<slot>
<slot {toggleMenu}>
<button
class="btn-icon flex items-center justify-center"
on:click={toggleMenu}
type="button"
>
<i class="fa fa-ellipsis-vertical text-{size}"></i>
</slot>
</button>
</button>
</slot>
{#if isOpen}
<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"
@@ -51,13 +52,10 @@
style="z-index: 1001"
>
{#each items as item}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<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"
tabindex="0"
role="presentation"
on:mouseup|stopPropagation={() => handleItemClick(item)}
on:keydown|stopPropagation={() => handleItemClick(item)}
>
{#if item.icon}
<i class="fa fa-{item.icon} mr-3"></i>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
export let id: string;
export let title: string;
export let size: string = "2xl";
export function openModal() {
document.body.style.position = "fixed";
document.body.style.width = "100%";
@@ -24,7 +25,7 @@
{id}
tabindex="-1"
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 -->
<div class="relative bg-background shadow">

View File

@@ -9,18 +9,19 @@
{#if $toast}
<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;"
in:fade={{ duration: 250 }}
out:fade={{ duration: 250 }}
>
<span>
<i
class="fa fa-{$toast.icon} p-3 mr-2 rounded-lg"
class:success-toast={$toast.type == "success"}
class:error-toast={$toast.type == "error"}
></i>{$toast.text}</span
>
<i
class="fa fa-{$toast.icon} p-3 mr-2 rounded-lg"
class:success-toast={$toast.type == "success"}
class:error-toast={$toast.type == "error"}
></i>
<p class="mr-4">
{$toast.text}
</p>
<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"

View File

@@ -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:bg-menu-item-background-hover={active}
>
<img
class="w-24 aspect-square rounded-full"
src={list.avatar}
alt="avatar"
/>
{#if list.avatar}
<img
class="w-16 md:w-24 aspect-square rounded-full"
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="flex justify-between items-center">
<h5 class="text-xl font-semibold">{list.name}</h5>

View 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>

View File

@@ -78,6 +78,8 @@
if (item.value == "logout") {
logout();
goto("/");
} else if (item.value == "profile") {
goto("/profile");
}
}
</script>
@@ -104,24 +106,25 @@
{/each}
</div>
<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"
><i class="fa fa-plus mr-2"></i>New Trail</a
>
{#if $currentUser}
<div class="basis-full"></div>
<hr class="border-input-border" />
<div class="flex gap-4 items-center mx-4">
<div class="flex gap-4 items-center m-4">
<img
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=""
/>
<div>
<p class="text-sm">{$currentUser.username}</p>
<p class="text-sm text-gray-500">{$currentUser.email}</p>
</div>
<button class="btn-icon tooltip" data-title="Logout"
<button class="btn-icon"
><i class="fa-solid fa-arrow-right-from-bracket"
></i></button
>
@@ -165,12 +168,18 @@
<Dropdown
items={dropdownItems}
on:change={(e) => handleDropdownClick(e.detail)}
let:toggleMenu={openDropdown}
>
<img
class="rounded-full"
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
alt=""
/>
<button
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-10 w-10"
on:click={openDropdown}
>
<img
class="rounded-full"
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
alt=""
/>
</button>
</Dropdown>
</div>
{:else}

View File

@@ -6,6 +6,7 @@ export class List {
name: string;
description?: string;
avatar?: string;
trails?: string[];
expand?: {
trails: Trail[]
}

View File

@@ -1,4 +1,5 @@
import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase";
import { getFileURL } from "$lib/util/file_util";
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) {
const dbResponse: List[] = (await pb.collection('lists').getList<List>(1, 5, {
const dbResponse: List[] = (await pb.collection('lists').getFullList<List>({
expand: "trails",
sort: `${filter?.sortOrder ?? "+"}${filter?.sort ?? "name"}`
})).items
}))
for (const list of dbResponse) {
list.avatar = getFileURL(list, list.avatar);
@@ -19,7 +20,7 @@ export async function lists_index(filter?: ListFilter) {
lists.set(dbResponse);
if(dbResponse.length > 0) {
if (dbResponse.length > 0) {
list.set(dbResponse[0])
}
@@ -38,17 +39,29 @@ export async function lists_create(formData: { [key: string]: any; } | FormData)
.create<List>(formData);
}
export async function lists_update(list:List, formData: { [key: string]: any; } | FormData) {
if((formData.get("avatar") as File).size == 0) {
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);
.update<List>(list.id!, formData);
}
export async function lists_delete(list:List) {
export async function lists_delete(list: List) {
let success = await pb
.collection("lists")
.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 });
}