summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/_components/command-palette.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/reader/_components/command-palette.tsx')
-rw-r--r--apps/web/app/reader/_components/command-palette.tsx200
1 files changed, 200 insertions, 0 deletions
diff --git a/apps/web/app/reader/_components/command-palette.tsx b/apps/web/app/reader/_components/command-palette.tsx
new file mode 100644
index 0000000..f3ff992
--- /dev/null
+++ b/apps/web/app/reader/_components/command-palette.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import { Command } from "cmdk"
+import { useEffect, useRef, useState } from "react"
+import { useRouter } from "next/navigation"
+import { useUserInterfaceStore } from "@/lib/stores/user-interface-store"
+import { useSubscriptions } from "@/lib/queries/use-subscriptions"
+
+export function CommandPalette() {
+ const isOpen = useUserInterfaceStore((state) => state.isCommandPaletteOpen)
+ const setOpen = useUserInterfaceStore((state) => state.setCommandPaletteOpen)
+ const toggleSidebar = useUserInterfaceStore((state) => state.toggleSidebar)
+ const setEntryListViewMode = useUserInterfaceStore(
+ (state) => state.setEntryListViewMode
+ )
+ const setAddFeedDialogOpen = useUserInterfaceStore(
+ (state) => state.setAddFeedDialogOpen
+ )
+ const router = useRouter()
+ const { data: subscriptionsData } = useSubscriptions()
+ const listReference = useRef<HTMLDivElement>(null)
+ const [inputValue, setInputValue] = useState("")
+
+ useEffect(() => {
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault()
+ setOpen(!isOpen)
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown)
+
+ return () => document.removeEventListener("keydown", handleKeyDown)
+ }, [isOpen, setOpen])
+
+ useEffect(() => {
+ if (!isOpen) return
+
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ setOpen(false)
+ return
+ }
+
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
+ setTimeout(() => {
+ const list = listReference.current
+ if (!list) return
+ const selected = list.querySelector('[aria-selected="true"]') as HTMLElement
+ if (!selected) return
+ const listRect = list.getBoundingClientRect()
+ const selectedRect = selected.getBoundingClientRect()
+ if (selectedRect.bottom > listRect.bottom) {
+ list.scrollTop += selectedRect.bottom - listRect.bottom
+ } else if (selectedRect.top < listRect.top) {
+ list.scrollTop -= listRect.top - selectedRect.top
+ }
+ }, 0)
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown)
+ return () => document.removeEventListener("keydown", handleKeyDown)
+ }, [isOpen, setOpen])
+
+ if (!isOpen) return null
+
+ function handleInputKeyDown(event: React.KeyboardEvent) {
+ if (event.key === "Backspace" && inputValue === "") {
+ event.preventDefault()
+ setOpen(false)
+ }
+ }
+
+ function navigateAndClose(path: string) {
+ router.push(path)
+ setOpen(false)
+ }
+
+ function actionAndClose(action: () => void) {
+ action()
+ setOpen(false)
+ }
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]">
+ <div
+ className="fixed inset-0 bg-background-primary/80"
+ onClick={() => setOpen(false)}
+ />
+ <Command className="relative w-full max-w-lg border border-border bg-background-secondary">
+ <Command.Input
+ placeholder="type a command..."
+ className="w-full border-b border-border bg-transparent px-4 py-3 text-text-primary outline-none placeholder:text-text-dim"
+ autoFocus
+ value={inputValue}
+ onValueChange={setInputValue}
+ onKeyDown={handleInputKeyDown}
+ />
+ <Command.List ref={listReference} className="max-h-80 overflow-auto p-2">
+ <Command.Empty className="p-4 text-center text-text-dim">
+ no results found
+ </Command.Empty>
+
+ <Command.Group
+ heading="navigation"
+ className="mb-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-text-dim"
+ >
+ <Command.Item
+ onSelect={() => navigateAndClose("/reader")}
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ go to all entries
+ </Command.Item>
+ <Command.Item
+ onSelect={() => navigateAndClose("/reader/saved")}
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ go to saved
+ </Command.Item>
+ <Command.Item
+ onSelect={() => navigateAndClose("/reader/settings")}
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ go to settings
+ </Command.Item>
+ </Command.Group>
+
+ {subscriptionsData &&
+ subscriptionsData.subscriptions.length > 0 && (
+ <Command.Group
+ heading="feeds"
+ className="mb-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-text-dim"
+ >
+ {subscriptionsData.subscriptions.map((subscription) => (
+ <Command.Item
+ key={subscription.subscriptionIdentifier}
+ value={`feed-${subscription.subscriptionIdentifier}-${subscription.customTitle ?? subscription.feedTitle}`}
+ onSelect={() =>
+ navigateAndClose(
+ `/reader?feed=${subscription.feedIdentifier}`
+ )
+ }
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ {subscription.customTitle ?? subscription.feedTitle}
+ </Command.Item>
+ ))}
+ </Command.Group>
+ )}
+
+ <Command.Group
+ heading="actions"
+ className="mb-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-text-dim"
+ >
+ <Command.Item
+ onSelect={() =>
+ actionAndClose(() => setAddFeedDialogOpen(true))
+ }
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ add feed
+ </Command.Item>
+ <Command.Item
+ onSelect={() => actionAndClose(toggleSidebar)}
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ toggle sidebar
+ </Command.Item>
+ <Command.Item
+ onSelect={() =>
+ actionAndClose(() => setEntryListViewMode("compact"))
+ }
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ compact view
+ </Command.Item>
+ <Command.Item
+ onSelect={() =>
+ actionAndClose(() => setEntryListViewMode("comfortable"))
+ }
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ comfortable view
+ </Command.Item>
+ <Command.Item
+ onSelect={() =>
+ actionAndClose(() => setEntryListViewMode("expanded"))
+ }
+ className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary"
+ >
+ expanded view
+ </Command.Item>
+ </Command.Group>
+ </Command.List>
+ </Command>
+ </div>
+ )
+}