From ea1d0023bfee55358ebb1a96f3d06e783a219c0d Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 1 Jun 2025 20:46:41 +0100 Subject: feat: Add support for public lists (#1511) * WIP: public lists * Drop viewing modes * Add the public endpoint for assets * regen the openapi spec * proper handling for different asset types * Add num bookmarks and a no bookmark banner * Correctly set page title * Add a not-found page * merge the RSS and public list endpoints * Add e2e tests for the public endpoints * Redesign the share list modal * Make NEXTAUTH_SECRET not required * propery render text bookmarks * rebase migration * fix public token tests * Add more tests --- .../components/public/lists/PublicBookmarkGrid.tsx | 247 +++++++++++++++++++++ .../components/public/lists/PublicListHeader.tsx | 17 ++ 2 files changed, 264 insertions(+) create mode 100644 apps/web/components/public/lists/PublicBookmarkGrid.tsx create mode 100644 apps/web/components/public/lists/PublicListHeader.tsx (limited to 'apps/web/components/public') diff --git a/apps/web/components/public/lists/PublicBookmarkGrid.tsx b/apps/web/components/public/lists/PublicBookmarkGrid.tsx new file mode 100644 index 00000000..038ac3ae --- /dev/null +++ b/apps/web/components/public/lists/PublicBookmarkGrid.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useEffect, useMemo } from "react"; +import Link from "next/link"; +import BookmarkFormattedCreatedAt from "@/components/dashboard/bookmarks/BookmarkFormattedCreatedAt"; +import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent"; +import FooterLinkURL from "@/components/dashboard/bookmarks/FooterLinkURL"; +import { ActionButton } from "@/components/ui/action-button"; +import { badgeVariants } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { api } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; +import tailwindConfig from "@/tailwind.config"; +import { Expand, FileIcon, ImageIcon } from "lucide-react"; +import { useInView } from "react-intersection-observer"; +import Masonry from "react-masonry-css"; +import resolveConfig from "tailwindcss/resolveConfig"; + +import { + BookmarkTypes, + ZPublicBookmark, +} from "@karakeep/shared/types/bookmarks"; +import { ZCursor } from "@karakeep/shared/types/pagination"; + +function TagPill({ tag }: { tag: string }) { + return ( +
+ {tag} +
+ ); +} + +function BookmarkCard({ bookmark }: { bookmark: ZPublicBookmark }) { + const renderContent = () => { + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + return ( +
+ {bookmark.bannerImageUrl && ( +
+ + {bookmark.title + +
+ )} +
+ + {bookmark.title} + +
+
+ ); + + case BookmarkTypes.TEXT: + return ( +
+ {bookmark.title && ( +

+ {bookmark.title} +

+ )} +
+ + {{ + id: bookmark.id, + content: { + text: bookmark.content.text, + }, + }} + + + + + + + + {{ + id: bookmark.id, + content: { + text: bookmark.content.text, + }, + }} + + + +
+
+ ); + + case BookmarkTypes.ASSET: + return ( +
+ {bookmark.bannerImageUrl ? ( +
+ + {bookmark.title + +
+ ) : ( +
+ {bookmark.content.assetType === "image" ? ( + + ) : ( + + )} +
+ )} +
+ + {bookmark.title} + +
+
+ ); + } + }; + + return ( + + + {renderContent()} + + {/* Tags */} + {bookmark.tags.length > 0 && ( +
+ {bookmark.tags.map((tag, index) => ( + + ))} +
+ )} + + {/* Footer */} +
+
+ {bookmark.content.type === BookmarkTypes.LINK && ( + <> + + + + )} + +
+
+
+
+ ); +} + +function getBreakpointConfig() { + const fullConfig = resolveConfig(tailwindConfig); + + const breakpointColumnsObj: { [key: number]: number; default: number } = { + default: 3, + }; + breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2; + breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1; + breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1; + return breakpointColumnsObj; +} + +export default function PublicBookmarkGrid({ + bookmarks: initialBookmarks, + nextCursor, + list, +}: { + list: { + id: string; + name: string; + description: string | null | undefined; + icon: string; + numItems: number; + }; + bookmarks: ZPublicBookmark[]; + nextCursor: ZCursor | null; +}) { + const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView(); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + api.publicBookmarks.getPublicBookmarksInList.useInfiniteQuery( + { listId: list.id }, + { + initialData: () => ({ + pages: [{ bookmarks: initialBookmarks, nextCursor, list }], + pageParams: [null], + }), + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + refetchOnMount: true, + }, + ); + + useEffect(() => { + if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [loadMoreButtonInView]); + + const breakpointConfig = useMemo(() => getBreakpointConfig(), []); + + const bookmarks = useMemo(() => { + return data.pages.flatMap((b) => b.bookmarks); + }, [data]); + return ( + <> + + {bookmarks.map((bookmark) => ( + + ))} + + {hasNextPage && ( +
+ fetchNextPage()} + variant="ghost" + > + Load More + +
+ )} + + ); +} diff --git a/apps/web/components/public/lists/PublicListHeader.tsx b/apps/web/components/public/lists/PublicListHeader.tsx new file mode 100644 index 00000000..1f016351 --- /dev/null +++ b/apps/web/components/public/lists/PublicListHeader.tsx @@ -0,0 +1,17 @@ +export default function PublicListHeader({ + list, +}: { + list: { + id: string; + numItems: number; + }; +}) { + return ( +
+ +

+ {list.numItems} bookmarks +

+
+ ); +} -- cgit v1.2.3-70-g09d2