"use client"; import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types"; import type { ReactNode } from "react"; import { useCallback, useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useSession } from "@/lib/auth/client"; import { BOOKMARK_DRAG_MIME } from "@/lib/bookmark-drag"; import useBulkActionsStore from "@/lib/bulkActions"; import { bookmarkLayoutSwitch, useBookmarkDisplaySettings, useBookmarkLayout, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { Check, GripVertical, Image as ImageIcon, NotebookPen, } from "lucide-react"; import { useTheme } from "next-themes"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; import { useBookmarkListContext } from "@karakeep/shared-react/hooks/bookmark-list-context"; import { useTRPC } from "@karakeep/shared-react/trpc"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { getBookmarkTitle, isBookmarkStillTagging, } from "@karakeep/shared/utils/bookmarkUtils"; import { switchCase } from "@karakeep/shared/utils/switch"; import BookmarkActionBar from "./BookmarkActionBar"; import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt"; import BookmarkOwnerIcon from "./BookmarkOwnerIcon"; import { NotePreview } from "./NotePreview"; import TagList from "./TagList"; interface Props { bookmark: ZBookmark; image: (layout: BookmarksLayoutTypes, className: string) => ReactNode; title?: ReactNode; content?: ReactNode; footer?: ReactNode; className?: string; fitHeight?: boolean; wrapTags: boolean; } function BottomRow({ footer, bookmark, }: { footer?: ReactNode; bookmark: ZBookmark; }) { return (
{footer && <>{footer}•}
); } function OwnerIndicator({ bookmark }: { bookmark: ZBookmark }) { const api = useTRPC(); const listContext = useBookmarkListContext(); const collaborators = useQuery( api.lists.getCollaborators.queryOptions( { listId: listContext?.id ?? "", }, { refetchOnWindowFocus: false, enabled: !!listContext?.hasCollaborators, }, ), ); if (!listContext || listContext.userRole === "owner" || !collaborators.data) { return null; } let owner = undefined; if (bookmark.userId === collaborators.data.owner?.id) { owner = collaborators.data.owner; } else { owner = collaborators.data.collaborators.find( (c) => c.userId === bookmark.userId, )?.user; } if (!owner) return null; return (
); } function MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) { const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark); const [isSelected, setIsSelected] = useState(false); const { theme } = useTheme(); const { data: session } = useSession(); useEffect(() => { setIsSelected(selectedBookmarks.some((item) => item.id === bookmark.id)); }, [selectedBookmarks]); // Don't show selector for non-owned bookmarks or when bulk edit is disabled const isOwner = session?.user?.id === bookmark.userId; if (!isBulkEditEnabled || !isOwner) return null; const getIconColor = () => { if (theme === "dark") { return isSelected ? "black" : "white"; } return isSelected ? "white" : "black"; }; const getIconBackgroundColor = () => { if (theme === "dark") { return isSelected ? "bg-white" : "bg-white bg-opacity-10"; } return isSelected ? "bg-black" : "bg-white bg-opacity-40"; }; return ( ); } function DragHandle({ bookmark, className, }: { bookmark: ZBookmark; className?: string; }) { const { isBulkEditEnabled } = useBulkActionsStore(); const handleDragStart = useCallback( (e: React.DragEvent) => { e.stopPropagation(); e.dataTransfer.setData(BOOKMARK_DRAG_MIME, bookmark.id); e.dataTransfer.effectAllowed = "copy"; // Create a small pill element as the drag preview const pill = document.createElement("div"); const title = getBookmarkTitle(bookmark) ?? "Untitled"; pill.textContent = title.length > 40 ? title.substring(0, 40) + "\u2026" : title; Object.assign(pill.style, { position: "fixed", left: "-9999px", top: "-9999px", padding: "6px 12px", borderRadius: "8px", backgroundColor: "hsl(var(--card))", border: "1px solid hsl(var(--border))", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", fontSize: "13px", fontFamily: "inherit", color: "hsl(var(--foreground))", maxWidth: "240px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", }); document.body.appendChild(pill); e.dataTransfer.setDragImage(pill, 0, 0); requestAnimationFrame(() => pill.remove()); }, [bookmark], ); if (isBulkEditEnabled) return null; return (
); } function ListView({ bookmark, image, title, content, footer, className, }: Props) { const { showNotes, showTags, showTitle, imageFit } = useBookmarkDisplaySettings(); const imgFitClass = switchCase(imageFit, { cover: "object-cover", contain: "object-contain", }); const note = showNotes ? bookmark.note?.trim() : undefined; return (
{image("list", cn("size-32 rounded-lg", imgFitClass))}
{showTitle && title && (
{title}
)} {content &&
{content}
} {note && } {showTags && (
)}
); } function GridView({ bookmark, image, title, content, footer, className, wrapTags, layout, fitHeight = false, }: Props & { layout: BookmarksLayoutTypes }) { const { showNotes, showTags, showTitle, imageFit } = useBookmarkDisplaySettings(); const imgFitClass = switchCase(imageFit, { cover: "object-cover", contain: "object-contain", }); const note = showNotes ? bookmark.note?.trim() : undefined; const img = image( "grid", cn("h-56 min-h-56 w-full rounded-t-lg", imgFitClass), ); return (
{img &&
{img}
}
{showTitle && title && (
{title}
)} {content &&
{content}
} {note && } {showTags && (
)}
); } function CompactView({ bookmark, title, footer, className }: Props) { const { showTitle } = useBookmarkDisplaySettings(); return (
{bookmark.content.type === BookmarkTypes.LINK && bookmark.content.favicon && ( favicon )} {bookmark.content.type === BookmarkTypes.TEXT && ( )} {bookmark.content.type === BookmarkTypes.ASSET && ( )} {showTitle && (
{title ?? "Untitled"}
)} {footer && (

•{footer}

)}

); } export function BookmarkLayoutAdaptingCard(props: Props) { const layout = useBookmarkLayout(); return bookmarkLayoutSwitch(layout, { masonry: , grid: , list: , compact: , }); }