139 lines
3.7 KiB
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>
|