fix: stabilize list loading and show correct list count
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<script lang="ts">
|
||||
import {type TrustedEvent} from "@welshman/util"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||
import Folder from "@assets/icons/folder.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
|
||||
type BookmarkList = {
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
type Props = {
|
||||
lists: BookmarkList[]
|
||||
selectedKey: string
|
||||
totalCount: number
|
||||
onOpenManager: () => void
|
||||
onSelect: (key: string) => void
|
||||
onRename: (key: string, label: string, nextLabel: string) => Promise<void>
|
||||
onDelete: (key: string, label: string) => Promise<void>
|
||||
}
|
||||
|
||||
const {lists, selectedKey, totalCount, onOpenManager, onSelect, onRename, onDelete}: Props =
|
||||
$props()
|
||||
|
||||
let menuOpen = $state(false)
|
||||
let menuX = $state(0)
|
||||
let menuY = $state(0)
|
||||
let menuListKey = $state("")
|
||||
let menuListLabel = $state("")
|
||||
let listDialogMode: "rename" | "delete" | undefined = $state()
|
||||
let dialogListKey = $state("")
|
||||
let dialogListLabel = $state("")
|
||||
let nextListLabel = $state("")
|
||||
let dialogPending = $state(false)
|
||||
|
||||
const closeMenu = () => {
|
||||
menuOpen = false
|
||||
}
|
||||
|
||||
const openListMenu = (event: MouseEvent, key: string, label: string) => {
|
||||
event.preventDefault()
|
||||
menuOpen = true
|
||||
menuX = event.clientX
|
||||
menuY = event.clientY
|
||||
menuListKey = key
|
||||
menuListLabel = label
|
||||
}
|
||||
|
||||
const openMenuList = () => {
|
||||
closeMenu()
|
||||
onSelect(menuListKey)
|
||||
}
|
||||
|
||||
const openRenameDialog = () => {
|
||||
closeMenu()
|
||||
listDialogMode = "rename"
|
||||
dialogListKey = menuListKey
|
||||
dialogListLabel = menuListLabel
|
||||
nextListLabel = menuListLabel
|
||||
}
|
||||
|
||||
const openDeleteDialog = () => {
|
||||
closeMenu()
|
||||
listDialogMode = "delete"
|
||||
dialogListKey = menuListKey
|
||||
dialogListLabel = menuListLabel
|
||||
nextListLabel = menuListLabel
|
||||
}
|
||||
|
||||
const closeDialog = (force = false) => {
|
||||
if (!force && dialogPending) {
|
||||
return
|
||||
}
|
||||
|
||||
listDialogMode = undefined
|
||||
dialogListKey = ""
|
||||
dialogListLabel = ""
|
||||
nextListLabel = ""
|
||||
}
|
||||
|
||||
const submitRename = async () => {
|
||||
if (listDialogMode !== "rename") {
|
||||
return
|
||||
}
|
||||
|
||||
dialogPending = true
|
||||
|
||||
try {
|
||||
await onRename(dialogListKey, dialogListLabel, nextListLabel)
|
||||
closeDialog(true)
|
||||
} finally {
|
||||
dialogPending = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitDelete = async () => {
|
||||
if (listDialogMode !== "delete") {
|
||||
return
|
||||
}
|
||||
|
||||
dialogPending = true
|
||||
|
||||
try {
|
||||
await onDelete(dialogListKey, dialogListLabel)
|
||||
closeDialog(true)
|
||||
} finally {
|
||||
dialogPending = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleWindowKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
closeMenu()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={closeMenu} onkeydown={handleWindowKeydown} />
|
||||
|
||||
<SecondaryNav class="relative">
|
||||
<SecondaryNavSection>
|
||||
<SecondaryNavHeader>
|
||||
<span class="flex items-center gap-2 uppercase tracking-wide">
|
||||
<Icon icon={Bookmark} />
|
||||
Bookmarks
|
||||
</span>
|
||||
<span class="badge badge-sm badge-neutral">{totalCount}</span>
|
||||
</SecondaryNavHeader>
|
||||
<div
|
||||
class="flex items-center justify-between px-1 pt-1 text-xs uppercase tracking-wide opacity-70">
|
||||
<span>My Lists</span>
|
||||
<Button class="btn btn-ghost btn-xs btn-square" onclick={onOpenManager}>
|
||||
<Icon size={3.5} icon={Add} />
|
||||
</Button>
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
|
||||
<div class="col-2 gap-2 overflow-y-auto px-2 pb-2">
|
||||
{#each lists as list (list.key)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
oncontextmenu={event => openListMenu(event, list.key, list.label)}>
|
||||
<Link href={`/bookmarks?list=${encodeURIComponent(list.key)}`}>
|
||||
<div
|
||||
class={`card2 card2-sm bg-alt col-2 gap-1 transition-colors hover:bg-base-100 ${selectedKey === list.key ? "bg-base-100" : ""}`}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="truncate font-medium">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<Icon size={4} icon={list.key.startsWith("10003:") ? Bookmark : Folder} />
|
||||
{list.label}
|
||||
</span>
|
||||
</p>
|
||||
<span class="badge badge-sm badge-neutral">{list.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card2 card2-sm bg-alt text-sm opacity-70">No lists found yet.</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if menuOpen}
|
||||
<div class="fixed inset-0 z-popover" role="presentation">
|
||||
<div
|
||||
class="menu rounded-box bg-base-100 shadow-xl"
|
||||
role="menu"
|
||||
style={`position: fixed; left: ${menuX}px; top: ${menuY}px; min-width: 11rem;`}>
|
||||
<li>
|
||||
<Button class="justify-start" onclick={openMenuList}>Open List</Button>
|
||||
</li>
|
||||
{#if menuListKey !== selectedKey}
|
||||
<li>
|
||||
<Button class="justify-start" onclick={openRenameDialog}>Rename List</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button class="justify-start text-error" onclick={openDeleteDialog}>Delete List</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if listDialogMode}
|
||||
<div class="fixed inset-0 z-modal bg-black/45 p-4 md:p-6">
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-0"
|
||||
aria-label="Close dialog"
|
||||
onclick={() => closeDialog()}></button>
|
||||
<div class="center relative h-full w-full">
|
||||
<div
|
||||
class="card2 bg-base-100 w-full max-w-md gap-4 p-5 shadow-2xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1">
|
||||
{#if listDialogMode === "rename"}
|
||||
<div class="col-2 gap-1">
|
||||
<h3 class="text-lg font-semibold">Rename List</h3>
|
||||
<p class="text-sm opacity-70">Choose a new name for "{dialogListLabel}".</p>
|
||||
</div>
|
||||
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<Icon icon={Folder} />
|
||||
<input bind:value={nextListLabel} class="grow" type="text" placeholder="List name" />
|
||||
</label>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button class="btn btn-ghost" onclick={() => closeDialog()} disabled={dialogPending}
|
||||
>Cancel</Button>
|
||||
<Button
|
||||
class="btn btn-neutral"
|
||||
onclick={submitRename}
|
||||
disabled={dialogPending || !nextListLabel.trim()}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="col-2 gap-1">
|
||||
<h3 class="text-lg font-semibold">Delete List?</h3>
|
||||
<p class="text-sm opacity-70">
|
||||
This will remove "{dialogListLabel}" and cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button class="btn btn-ghost" onclick={() => closeDialog()} disabled={dialogPending}
|
||||
>Cancel</Button>
|
||||
<Button class="btn btn-error" onclick={submitDelete} disabled={dialogPending}
|
||||
>Delete</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</SecondaryNav>
|
||||
Reference in New Issue
Block a user