import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { useAuth } from "@/lib/hooks/useAuth"; import { useState, useRef, useEffect } from "react"; import { Button } from "./ui/button"; import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; import { ChevronDown, ExternalLink, LoaderCircle } from "lucide-react"; import { useBoardsStore } from "@/lib/stores/boards"; import { BoardsPicker } from "./BoardPicker"; import { toast } from "sonner"; import { AtUri } from "@atproto/api"; import { LIST_COLLECTION, LIST_ITEM_COLLECTION } from "@/constants"; import { FeedItem } from "./Feed"; import { BoardItem, useBoardItemsStore } from "@/lib/stores/boardItems"; import { useRecentBoardsStore } from "@/lib/stores/recentBoards"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; export function SaveButton({ post, image, onDropdownOpenChange, }: { post: PostView; image: number; onDropdownOpenChange?: (isOpen: boolean) => void; }) { const { agent } = useAuth(); const [isLoading, setLoading] = useState(false); const [isOpen, setOpen] = useState(false); const [selectedBoard, setSelectedBoard] = useState(""); const boardsStore = useBoardsStore(); const { setBoardItem } = useBoardItemsStore(); const [isDropdownOpen, setDropdownOpen] = useState(false); const { recentBoards, addRecentBoard } = useRecentBoardsStore(); const dropdownRef = useRef(null); // Handle closing dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setDropdownOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Update parent component when dropdown state changes useEffect(() => { onDropdownOpenChange?.(isDropdownOpen); }, [isDropdownOpen, onDropdownOpenChange]); const saveToBoard = async (boardId: string) => { setLoading(true); try { if (!agent || !agent.assertDid) { toast("Unable to save - not logged in properly"); return; } const record: BoardItem = { url: post.uri + `?image=${image}`, list: AtUri.make(agent.assertDid, LIST_COLLECTION, boardId).toString(), $type: LIST_ITEM_COLLECTION, createdAt: new Date().toISOString(), }; const result = await agent?.com.atproto.repo.createRecord({ collection: LIST_ITEM_COLLECTION, record, repo: agent?.assertDid || "", }); if (result?.success) { const rkey = new AtUri(result.data.uri).rkey; setBoardItem(rkey, record); addRecentBoard(boardId); toast("Image saved"); setOpen(false); setDropdownOpen(false); } else { toast("Failed to save image"); } } finally { setLoading(false); } }; if (agent == null) return
not logged in :(
; // Get board names for recent boards with correct board structure const recentBoardsWithNames = recentBoards .map((boardId) => { // Look through all DIDs in the boards store for (const did in boardsStore.boards) { // Check if this DID has the board we're looking for if (boardsStore.boards[did]?.[boardId]) { return { id: boardId, name: boardsStore.boards[did][boardId].name || "Unnamed Board", }; } } // If board not found return { id: boardId, name: "Unnamed Board" }; }) .slice(0, 5); // Show only top 5 recent boards return (
{/* Main Save button - always opens dialog */} {/* Dropdown arrow for recents */} { setDropdownOpen(open); onDropdownOpenChange?.(open); }} > Recent Boards {recentBoardsWithNames.length > 0 ? ( recentBoardsWithNames.map((board) => ( { e.stopPropagation(); saveToBoard(board.id); }} className="cursor-pointer" > {board.name} )) ) : ( No recent boards )}
Save post to board { const record = { name: name, $type: LIST_COLLECTION, createdAt: new Date().toISOString(), description: "", }; const result = await agent?.com.atproto.repo.createRecord({ collection: LIST_COLLECTION, record, repo: agent.assertDid, }); if (result?.success) { toast("Board created"); const rkey = new AtUri(result.data.uri).rkey; boardsStore.setBoard(agent.assertDid, rkey, record); setSelectedBoard(rkey); } else { toast("Failed to create board"); } }} />

Saved posts use{" "} scrapboard.org's standard format, making them interoperable.

); }