267 lines
8.1 KiB
Svelte
267 lines
8.1 KiB
Svelte
<script lang="ts">
|
|
import Create from "$lib/icons/sidebar/Create.svelte"
|
|
import Delete from "$lib/icons/sidebar/Delete.svelte"
|
|
import ChevronLeft from "$lib/icons/sidebar/ChevronLeft.svelte"
|
|
import ChevronRight from "$lib/icons/sidebar/ChevronRight.svelte"
|
|
import User from "$lib/icons/sidebar/User.svelte"
|
|
import ChevronDown from "$lib/icons/ChevronDown.svelte"
|
|
import ChevronUp from "$lib/icons/ChevronUp.svelte"
|
|
import Tag from "$lib/icons/sidebar/Tag.svelte"
|
|
import WebhookTruck from "$lib/icons/sidebar/WebhookTruck.svelte"
|
|
import Search from "$lib/icons/sidebar/Search.svelte"
|
|
import SettingsGear from "$lib/icons/sidebar/SettingsGear.svelte"
|
|
import AdminWrench from "$lib/icons/sidebar/AdminShield.svelte"
|
|
import Exit from "$lib/icons/sidebar/Exit.svelte"
|
|
import ThemeToggle from "./ThemeToggle.svelte"
|
|
import { formatDateLong, formatDateShort, parseExpirationPrefix } from "$lib/util/contentVisual"
|
|
import type { FullNote, NoteMetadata } from "$lib/logic/model"
|
|
|
|
// props
|
|
export let isSidebarOpen: boolean
|
|
export let isUserMenuOpen: boolean
|
|
export let username: string
|
|
export let notes: NoteMetadata[] = []
|
|
export let currentNote: FullNote | null = null
|
|
export let toggleSettingsModal: () => void
|
|
export let toggleAdminModal: () => void
|
|
export let toggleWebhookModal: () => void
|
|
export let toggleTagModal: () => void
|
|
export let logout: () => Promise<void>
|
|
export let createNewNote: () => Promise<void>
|
|
export let selectNote: (
|
|
noteID: string,
|
|
fetchRemote: boolean,
|
|
clearSelection: boolean
|
|
) => Promise<void>
|
|
export let deleteNote: (noteID: string) => Promise<void>
|
|
|
|
const toggleSidebar = () => {
|
|
isUserMenuOpen = false
|
|
isSidebarOpen = !isSidebarOpen
|
|
}
|
|
|
|
const toggleUserMenu = () => {
|
|
isUserMenuOpen = !isUserMenuOpen
|
|
}
|
|
|
|
const handleDeleteNote = (event: MouseEvent, noteID: string) => {
|
|
event.stopPropagation()
|
|
|
|
if (confirm("Are you sure you want to delete this note?")) {
|
|
deleteNote(noteID)
|
|
}
|
|
}
|
|
|
|
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault() // prevent page scroll on space
|
|
selectNote(noteID, true, false) // sync with API due to pushing updates
|
|
}
|
|
}
|
|
|
|
// client-side search
|
|
let searchQuery = ""
|
|
$: filteredNotes = searchQuery
|
|
? notes.filter((note) => note.title.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
: notes
|
|
</script>
|
|
|
|
<!-- TODO: handle pagination support later if it becomes an issue (`util/itemPagination.ts`) -->
|
|
|
|
<!-- Outmost sidebar container with collapsible functionality -->
|
|
<div
|
|
class="outer-sidebar-container"
|
|
class:sidebar-expanded={isSidebarOpen}
|
|
class:sidebar-collapsed={!isSidebarOpen}
|
|
>
|
|
<!-- Collapsed mini sidebar (always visible) -->
|
|
<div class="mini-sidebar">
|
|
<button
|
|
on:click={toggleSidebar}
|
|
class="sidebar-button"
|
|
aria-label={isSidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
|
>
|
|
{#if isSidebarOpen}
|
|
<ChevronLeft classString="h-5 w-5" />
|
|
{:else}
|
|
<ChevronRight classString="h-5 w-5" />
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Mini sidebar icons -->
|
|
{#if !isSidebarOpen}
|
|
<div class="mini-sidebar-button-container">
|
|
<button on:click={createNewNote} class="sidebar-button" aria-label="Create new note">
|
|
<Create classString="h-5 w-5" />
|
|
</button>
|
|
|
|
<button on:click={toggleWebhookModal} class="sidebar-button" aria-label="View webhooks">
|
|
<WebhookTruck classString="h-5 w-5" />
|
|
</button>
|
|
|
|
<button
|
|
on:click={toggleTagModal}
|
|
class="sidebar-button"
|
|
aria-label="View tags & expiration"
|
|
>
|
|
<Tag classString="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Should be roughly in the center of the mini sidebar despite the absolute positioning -->
|
|
<div class="absolute bottom-4 left-3">
|
|
<ThemeToggle />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expanded sidebar -->
|
|
<aside
|
|
class="sidebar"
|
|
class:translate-x-0={isSidebarOpen}
|
|
class:translate-x-[-100%]={!isSidebarOpen}
|
|
class:opacity-100={isSidebarOpen}
|
|
class:opacity-0={!isSidebarOpen}
|
|
class:pointer-events-none={!isSidebarOpen}
|
|
class:pointer-events-auto={isSidebarOpen}
|
|
class:md:relative={true}
|
|
>
|
|
<!-- Header with app logo and display name -->
|
|
<div class="sidebar-header-container">
|
|
<!-- svelte-ignore a11y_invalid_attribute -->
|
|
<a href="#" class="sidebar-header-link" on:click={() => selectNote("", false, true)}>
|
|
<img src="favicon.svg" alt="Logo" class="sidebar-header-logo" />
|
|
<span class="sidebar-header-text">QueNote</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="sidebar-section-divider"></div>
|
|
|
|
<!-- Action buttons (same functionality as mini sidebar icons) -->
|
|
<div class="flex flex-col px-2 py-3">
|
|
<button on:click={createNewNote} class="sidebar-action-button mb-1">
|
|
<Create classString="mr-3 h-5 w-5" />
|
|
Create note
|
|
</button>
|
|
|
|
<button on:click={toggleWebhookModal} class="sidebar-action-button mb-1">
|
|
<WebhookTruck classString="mr-3 h-5 w-5" />
|
|
Webhooks
|
|
</button>
|
|
|
|
<button on:click={toggleTagModal} class="sidebar-action-button">
|
|
<Tag classString="mr-3 h-5 w-5" />
|
|
Note tags
|
|
</button>
|
|
</div>
|
|
|
|
<div class="relative flex flex-col px-2 pb-3">
|
|
<input type="text" placeholder="Search" bind:value={searchQuery} class="sidebar-search-bar" />
|
|
<Search classString="sidebar-search-bar-icon" />
|
|
</div>
|
|
|
|
<div class="sidebar-section-divider"></div>
|
|
|
|
<!-- Notes list -->
|
|
<div class="flex-1 overflow-y-auto p-2">
|
|
{#if filteredNotes.length > 0}
|
|
<ul class="space-y-1" role="listbox">
|
|
{#each filteredNotes as note}
|
|
<li
|
|
class="sidebar-list"
|
|
class:sidebar-list-active={currentNote && note.id === currentNote.id}
|
|
on:click={() => selectNote(note.id, false, false)}
|
|
on:keydown={(e) => handleNoteKeydown(e, note.id)}
|
|
tabindex="0"
|
|
role="option"
|
|
aria-selected={currentNote && note.id === currentNote.id}
|
|
>
|
|
<div class="flex w-full items-start justify-between">
|
|
<h3 class="sidebar-list-item-title">
|
|
{parseExpirationPrefix(note.title)[0] || "Untitled Note"}
|
|
</h3>
|
|
|
|
<!-- Delete button -->
|
|
<button
|
|
on:click={(e) => handleDeleteNote(e, note.id)}
|
|
class="sidebar-button sidebar-list-item-delete-button"
|
|
aria-label="Delete note"
|
|
>
|
|
<Delete classString="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<p class="sidebar-list-item-metadata">
|
|
{formatDateLong(note.updatedAt)}
|
|
</p>
|
|
|
|
<p class="sidebar-list-item-metadata">
|
|
Expires {note.expiresAt !== null ? formatDateShort(note.expiresAt) : "never"}
|
|
</p>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{:else}
|
|
<div class="sidebar-search-info">
|
|
{searchQuery ? "No notes match your search" : "No notes yet"}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="sidebar-section-divider"></div>
|
|
|
|
<!-- User section (single button) with dropdown -->
|
|
<div class="relative flex flex-col px-2 py-3">
|
|
<button
|
|
on:click={toggleUserMenu}
|
|
class="sidebar-action-button flex-wrap justify-between px-4 py-4"
|
|
>
|
|
<div class="sidebar-user-button-content-container">
|
|
<User classString="h-6 w-6 flex-shrink-0" />
|
|
|
|
<div class="sidebar-user-button-username-container">
|
|
<span class="sidebar-user-button-username-text">Logged in as {username}</span>
|
|
</div>
|
|
|
|
{#if isUserMenuOpen}
|
|
<ChevronDown classString="h-6 w-6 flex-shrink-0" />
|
|
{:else}
|
|
<ChevronUp classString="h-6 w-6 flex-shrink-0" />
|
|
{/if}
|
|
</div>
|
|
</button>
|
|
|
|
<!-- User actions dropdown menu -->
|
|
{#if isUserMenuOpen}
|
|
<div class="sidebar-user-dropdown">
|
|
<div class="flex flex-col p-0">
|
|
<button
|
|
on:click={toggleSettingsModal}
|
|
class="sidebar-action-button rounded-none rounded-t-lg border-0 border-b"
|
|
>
|
|
<SettingsGear classString="mr-3 h-5 w-5" />
|
|
Settings
|
|
</button>
|
|
|
|
<button
|
|
on:click={toggleAdminModal}
|
|
class="sidebar-action-button rounded-none border-0 border-y"
|
|
>
|
|
<AdminWrench classString="mr-3 h-5 w-5" />
|
|
Admin view
|
|
</button>
|
|
|
|
<button
|
|
on:click={logout}
|
|
class="sidebar-action-button rounded-none rounded-b-lg border-0 border-t"
|
|
>
|
|
<Exit classString="mr-3 h-5 w-5" />
|
|
Log out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</aside>
|
|
</div>
|