From 6a15cfe643cb9f1078dc6dca3bf6aec4dc797fce Mon Sep 17 00:00:00 2001 From: Lemon Date: Sun, 12 Apr 2026 12:04:06 -0700 Subject: [PATCH] Add sidebar destination to save popovers and restore NoteCard click guards - Add 'Sidebar' option (PanelLeft icon) to save popover on SpellRunPage and SearchPage - SearchPage publishes a kind:777 spell before adding to sidebar (same as Share flow) - SpellRunPage adds directly since nevent is already available - Restore target.closest('button') and target.closest('a') guards in NoteCard click handlers to prevent double-navigation when clicking interactive elements --- src/components/NoteCard.tsx | 8 ++++++-- src/pages/SearchPage.tsx | 36 ++++++++++++++++++++++++++++++++++++ src/pages/SpellRunPage.tsx | 26 +++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 40dcceb5..23eed397 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -255,7 +255,9 @@ export const NoteCard = memo(function NoteCard({ target.closest("[data-radix-dialog-content]") || target.closest("[data-vaul-drawer]") || target.closest("[data-vaul-drawer-overlay]") || - target.closest('[data-testid="zap-modal"]') + target.closest('[data-testid="zap-modal"]') || + target.closest("button") || + target.closest("a") ) { return; } @@ -270,7 +272,9 @@ export const NoteCard = memo(function NoteCard({ target.closest("[data-radix-dialog-content]") || target.closest("[data-vaul-drawer]") || target.closest("[data-vaul-drawer-overlay]") || - target.closest('[data-testid="zap-modal"]') + target.closest('[data-testid="zap-modal"]') || + target.closest("button") || + target.closest("a") ) { return; } diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index e7da32a8..5ba7a56a 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -10,6 +10,7 @@ import { BookmarkPlus, Check, Loader2, + PanelLeft, Globe, Users, UserSearch, Clock, Flame, TrendingUp, Share2, @@ -38,6 +39,7 @@ import { useSearchProfiles } from '@/hooks/useSearchProfiles'; import { useAuthor } from '@/hooks/useAuthor'; import { useStreamPosts } from '@/hooks/useStreamPosts'; import { useFeed, type FeedItem } from '@/hooks/useFeed'; +import { useFeedSettings } from '@/hooks/useFeedSettings'; import { useSavedFeeds } from '@/hooks/useSavedFeeds'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useProfileTabs } from '@/hooks/useProfileTabs'; @@ -355,6 +357,7 @@ export function SearchPage() { const { lists } = useUserLists(); const { data: followPacks = [] } = useFollowPacks(); const { savedFeeds, addSavedFeed, isPending: isSavingFeed } = useSavedFeeds(); + const { addToSidebar } = useFeedSettings(); const profileTabsQuery = useProfileTabs(user?.pubkey); const { publishProfileTabs, isPending: isPublishingTabs } = usePublishProfileTabs(); const { mutateAsync: publishEvent } = useNostrPublish(); @@ -363,6 +366,7 @@ export function SearchPage() { const [saveFeedLabel, setSaveFeedLabel] = useState(''); const [savedJustNow, setSavedJustNow] = useState(false); const [isSharing, setIsSharing] = useState(false); + const [isAddingToSidebar, setIsAddingToSidebar] = useState(false); const listPickerValue = useMatchedListId(authorPubkeys); @@ -467,6 +471,30 @@ export function SearchPage() { } }; + const handleAddToSidebar = async () => { + if (!saveFeedLabel.trim() || isAddingToSidebar || !user) return; + setIsAddingToSidebar(true); + try { + const tags = currentSpellTags.map(([t, ...rest]) => + t === 'name' ? ['name', saveFeedLabel.trim()] : + t === 'alt' ? ['alt', `Spell: ${saveFeedLabel.trim()}`] : + [t, ...rest] + ); + const event = await publishEvent({ kind: 777, content: '', tags, created_at: Math.floor(Date.now() / 1000) }); + const neventId = nip19.neventEncode({ id: event.id, author: event.pubkey, kind: event.kind }); + addToSidebar(`nostr:${neventId}`); + setSavePopoverOpen(false); + setSaveFeedLabel(''); + setSavedJustNow(true); + setTimeout(() => setSavedJustNow(false), 2000); + toast({ title: 'Added to sidebar' }); + } catch (err) { + toast({ title: 'Failed to add to sidebar', description: err instanceof Error ? err.message : undefined, variant: 'destructive' }); + } finally { + setIsAddingToSidebar(false); + } + }; + // Resolve author pubkeys for the stream const streamAuthorPubkeys = authorScope === 'follows' ? followPubkeys @@ -598,6 +626,14 @@ export function SearchPage() { loading={isPublishingTabs} /> )} + } + label="Sidebar" + description="Pin to your sidebar" + onClick={() => handleAddToSidebar()} + disabled={!saveFeedLabel.trim() || isSavingFeed || isPublishingTabs || isAddingToSidebar} + loading={isAddingToSidebar} + /> } label="Share" diff --git a/src/pages/SpellRunPage.tsx b/src/pages/SpellRunPage.tsx index ac60a2db..01c95e42 100644 --- a/src/pages/SpellRunPage.tsx +++ b/src/pages/SpellRunPage.tsx @@ -5,7 +5,7 @@ import { nip19 } from 'nostr-tools'; import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import { useSeoMeta } from '@unhead/react'; -import { AlertCircle, BookmarkPlus, Check, Loader2, Share2, User, WandSparkles } from 'lucide-react'; +import { AlertCircle, BookmarkPlus, Check, Loader2, PanelLeft, Share2, User, WandSparkles } from 'lucide-react'; import { NoteCard } from '@/components/NoteCard'; import { PageHeader } from '@/components/PageHeader'; @@ -21,6 +21,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowList } from '@/hooks/useFollowActions'; import { useProfileTabs } from '@/hooks/useProfileTabs'; import { usePublishProfileTabs } from '@/hooks/usePublishProfileTabs'; +import { useFeedSettings } from '@/hooks/useFeedSettings'; import { useSavedFeeds } from '@/hooks/useSavedFeeds'; import { useStreamPosts } from '@/hooks/useStreamPosts'; import { useToast } from '@/hooks/useToast'; @@ -99,6 +100,7 @@ export function SpellRunPage() { // ── Save popover state ─────────────────────────────────────────────── const { savedFeeds, addSavedFeed, removeSavedFeed } = useSavedFeeds(); + const { addToSidebar, orderedItems } = useFeedSettings(); const profileTabsQuery = useProfileTabs(user?.pubkey); const { publishProfileTabs, isPending: isPublishingTabs } = usePublishProfileTabs(); const { toast } = useToast(); @@ -127,6 +129,10 @@ export function SpellRunPage() { const alreadySaved = !!matchingSavedFeed; + /** The nostr: URI for this spell (used for sidebar). */ + const sidebarId = nevent ? `nostr:${nevent}` : undefined; + const alreadyInSidebar = sidebarId ? orderedItems.includes(sidebarId) : false; + const handleSaveHomeFeed = useCallback(async () => { if (!spellAsFilter || !saveFeedLabel.trim()) return; await addSavedFeed(saveFeedLabel.trim(), spellAsFilter as Record, []); @@ -168,6 +174,16 @@ export function SpellRunPage() { } }, [spellEvent, nevent, saveFeedLabel, toast]); + const handleAddToSidebar = useCallback(() => { + if (!sidebarId) return; + addToSidebar(sidebarId); + setSavePopoverOpen(false); + setSaveFeedLabel(''); + setSavedJustNow(true); + setTimeout(() => setSavedJustNow(false), 2000); + toast({ title: 'Added to sidebar' }); + }, [sidebarId, addToSidebar, toast]); + const handleRemoveSaved = useCallback(async () => { if (!matchingSavedFeed) return; await removeSavedFeed(matchingSavedFeed.id); @@ -264,6 +280,14 @@ export function SpellRunPage() { disabled={!saveFeedLabel.trim() || isPublishingTabs} loading={isPublishingTabs} /> + } + label="Sidebar" + description="Pin to your sidebar" + onClick={handleAddToSidebar} + disabled={!sidebarId || alreadyInSidebar} + loading={false} + /> } label="Share"