qnote/web/src/lib/components/Sidebar.svelte

139 lines
3.7 KiB
Svelte

<script lang="ts">
import type { NoteMetadata, FullNote } from "$lib/client"
import Close from "$lib/icons/Close.svelte"
import CreateNew from "$lib/icons/CreateNew.svelte"
import Logout from "$lib/icons/Logout.svelte"
import Search from "$lib/icons/Search.svelte"
import Settings from "$lib/icons/Settings.svelte"
// Props
export let sidebarOpen = true
export let notes: NoteMetadata[] = []
export let currentNote: FullNote | null = null
export let toggleSettings: () => void
export let logout: () => Promise<void>
export let createNewNote: () => Promise<void>
export let selectNote: (noteId: string, fetchRemote: boolean) => Promise<void>
const formatDate = (dateString: string | Date): string => {
if (!dateString) {
return ""
}
const d = new Date(dateString)
return d.toLocaleDateString(undefined, {
weekday: "short",
year: "2-digit",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric"
})
}
const closeSidebar = () => {
sidebarOpen = false
}
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault() // Prevent page scroll on space
selectNote(noteID, true) // 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: add admin modal button (+ component similar to the settings modal) if the user is an admin -->
<!-- TODO: component-level paging support (via the implementation in `pages.ts`) -->
<aside
class="sidebar z-10"
class:translate-x-0={sidebarOpen}
class:translate-x-[-100%]={!sidebarOpen}
class:fixed={true}
class:md:relative={true}
>
<!-- Sidebar header -->
<div class="sidebar-header">
<div class="flex items-center justify-between">
<button
on:click={createNewNote}
class="btn-primary rounded-full p-2"
aria-label="Create new note"
>
<CreateNew />
</button>
<!-- Search bar -->
<div class="relative pl-4">
<input type="text" placeholder="Search" bind:value={searchQuery} class="search-bar" />
<Search />
</div>
<!-- Close button visible only on mobile -->
<div class="relative pl-4">
<button
on:click={closeSidebar}
class="btn-secondary rounded-full p-2 md:hidden"
aria-label="Close sidebar"
>
<Close />
</button>
</div>
</div>
</div>
<!-- Notes list -->
<div class="flex-1 overflow-y-auto">
{#if filteredNotes.length > 0}
<ul class="sidebar-divider" role="listbox">
{#each filteredNotes as note}
<li
class="sidebar-item"
class:sidebar-item-active={currentNote && note.id === currentNote.id}
on:click={() => selectNote(note.id, false)}
on:keydown={(e) => handleNoteKeydown(e, note.id)}
tabindex="0"
role="option"
aria-selected={currentNote && note.id === currentNote.id}
>
<h3 class="truncate font-bold">{note.title || "Untitled Note"}</h3>
<p class="sidebar-item-text mt-1 italic">
{formatDate(note.updatedAt)}
</p>
</li>
{/each}
</ul>
{:else}
<div class="sidebar-item-text p-4 text-center">
{searchQuery ? "No notes match your search" : "No notes yet"}
</div>
{/if}
</div>
<!-- Sidebar footer -->
<div class="sidebar-footer">
<div class="flex justify-between">
<button on:click={logout} class="btn-secondary rounded-full p-2" aria-label="Logout">
<Logout />
</button>
<!-- It's better for UX that the logout button isn't on the right side -->
<button
on:click={toggleSettings}
class="btn-secondary rounded-full p-2"
aria-label="Toggle settings"
>
<Settings />
</button>
</div>
</div>
</aside>