1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
import { useEffect, useMemo } from "react";
import NoBookmarksBanner from "@/components/dashboard/bookmarks/NoBookmarksBanner";
import { ActionButton } from "@/components/ui/action-button";
import useBulkActionsStore from "@/lib/bulkActions";
import { useInBookmarkGridStore } from "@/lib/store/useInBookmarkGridStore";
import {
bookmarkLayoutSwitch,
useBookmarkLayout,
useGridColumns,
} from "@/lib/userLocalSettings/bookmarksLayout";
import tailwindConfig from "@/tailwind.config";
import { Slot } from "@radix-ui/react-slot";
import { ErrorBoundary } from "react-error-boundary";
import { useInView } from "react-intersection-observer";
import Masonry from "react-masonry-css";
import resolveConfig from "tailwindcss/resolveConfig";
import type { ZBookmark } from "@karakeep/shared/types/bookmarks";
import BookmarkCard from "./BookmarkCard";
import EditorCard from "./EditorCard";
import UnknownCard from "./UnknownCard";
function StyledBookmarkCard({ children }: { children: React.ReactNode }) {
return (
<Slot className="mb-4 border border-border bg-card duration-300 ease-in hover:shadow-lg hover:transition-all">
{children}
</Slot>
);
}
function getBreakpointConfig(userColumns: number) {
const fullConfig = resolveConfig(tailwindConfig);
const breakpointColumnsObj: { [key: number]: number; default: number } = {
default: userColumns,
};
// Responsive behavior: reduce columns on smaller screens
const lgColumns = Math.max(1, Math.min(userColumns, userColumns - 1));
const mdColumns = Math.max(1, Math.min(userColumns, 2));
const smColumns = 1;
breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = lgColumns;
breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = mdColumns;
breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = smColumns;
return breakpointColumnsObj;
}
export default function BookmarksGrid({
bookmarks,
hasNextPage = false,
fetchNextPage = () => ({}),
isFetchingNextPage = false,
showEditorCard = false,
}: {
bookmarks: ZBookmark[];
showEditorCard?: boolean;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
fetchNextPage?: () => void;
}) {
const layout = useBookmarkLayout();
const gridColumns = useGridColumns();
const bulkActionsStore = useBulkActionsStore();
const inBookmarkGrid = useInBookmarkGridStore();
const breakpointConfig = useMemo(
() => getBreakpointConfig(gridColumns),
[gridColumns],
);
const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();
useEffect(() => {
bulkActionsStore.setVisibleBookmarks(bookmarks);
return () => {
bulkActionsStore.setVisibleBookmarks([]);
};
}, [bookmarks]);
useEffect(() => {
inBookmarkGrid.setInBookmarkGrid(true);
return () => {
inBookmarkGrid.setInBookmarkGrid(false);
};
}, []);
useEffect(() => {
if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [loadMoreButtonInView]);
if (bookmarks.length == 0 && !showEditorCard) {
return <NoBookmarksBanner />;
}
const children = [
showEditorCard && (
<StyledBookmarkCard key={"editor"}>
<EditorCard />
</StyledBookmarkCard>
),
...bookmarks.map((b) => (
<ErrorBoundary key={b.id} fallback={<UnknownCard bookmark={b} />}>
<StyledBookmarkCard>
<BookmarkCard bookmark={b} />
</StyledBookmarkCard>
</ErrorBoundary>
)),
];
return (
<>
{bookmarkLayoutSwitch(layout, {
masonry: (
<Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
{children}
</Masonry>
),
grid: (
<Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
{children}
</Masonry>
),
list: <div className="grid grid-cols-1">{children}</div>,
compact: <div className="grid grid-cols-1">{children}</div>,
})}
{hasNextPage && (
<div className="flex justify-center">
<ActionButton
ref={loadMoreRef}
ignoreDemoMode={true}
loading={isFetchingNextPage}
onClick={() => fetchNextPage()}
variant="ghost"
>
Load More
</ActionButton>
</div>
)}
</>
);
}
|