rcgit

/ karakeep

Commit 154efe17

SHA 154efe17421ca96d433fcc1f820ad460e1675bdc
Author Mohamed Bassem <me at mbassem dot com>
Author Date 2025-07-26 12:58 +0000
Committer Mohamed Bassem <me at mbassem dot com>
Commit Date 2025-07-26 12:58 +0000
Parent(s) 8b4fb49cc066 (diff)
Tree 4336090648fe

patch snapshot

feat: Configurable number of grid columns. Fixes #1713
File + - Graph
D apps/web/components/dashboard/ChangeLayout.tsx +0 -64
M apps/web/components/dashboard/GlobalActions.tsx +2 -2
A apps/web/components/dashboard/ViewOptions.tsx +115 -0
M apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx +17 -6
M apps/web/lib/userLocalSettings/bookmarksLayout.tsx +6 -0
M apps/web/lib/userLocalSettings/types.ts +1 -0
M apps/web/lib/userLocalSettings/userLocalSettings.ts +11 -0
7 file(s) changed, 152 insertions(+), 72 deletions(-)

apps/web/components/dashboard/ChangeLayout.tsx

diff --git a/apps/web/components/dashboard/ChangeLayout.tsx b/apps/web/components/dashboard/ChangeLayout.tsx
deleted file mode 100644
index c7f44a73..00000000
--- a/apps/web/components/dashboard/ChangeLayout.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import React from "react";
-import { ButtonWithTooltip } from "@/components/ui/button";
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { useTranslation } from "@/lib/i18n/client";
-import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
-import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
-import {
-  Check,
-  LayoutDashboard,
-  LayoutGrid,
-  LayoutList,
-  List,
-  LucideIcon,
-} from "lucide-react";
-
-type LayoutType = "masonry" | "grid" | "list" | "compact";
-
-const iconMap: Record<LayoutType, LucideIcon> = {
-  masonry: LayoutDashboard,
-  grid: LayoutGrid,
-  list: LayoutList,
-  compact: List,
-};
-
-export default function ChangeLayout() {
-  const { t } = useTranslation();
-  const layout = useBookmarkLayout();
-
-  return (
-    <DropdownMenu>
-      <DropdownMenuTrigger asChild>
-        <ButtonWithTooltip
-          tooltip={t("actions.change_layout")}
-          delayDuration={100}
-          variant="ghost"
-        >
-          {React.createElement(iconMap[layout], { size: 18 })}
-        </ButtonWithTooltip>
-      </DropdownMenuTrigger>
-      <DropdownMenuContent className="w-fit">
-        {(Object.keys(iconMap) as LayoutType[]).map((key) => (
-          <DropdownMenuItem
-            key={key}
-            className="cursor-pointer justify-between"
-            onClick={async () => await updateBookmarksLayout(key as LayoutType)}
-          >
-            <div className="flex items-center gap-2">
-              {React.createElement(iconMap[key as LayoutType], { size: 18 })}
-              <span>{t(`layouts.${key}`)}</span>
-            </div>
-            {layout == key && <Check className="ml-2 size-4" />}
-          </DropdownMenuItem>
-        ))}
-      </DropdownMenuContent>
-    </DropdownMenu>
-  );
-}

apps/web/components/dashboard/GlobalActions.tsx

diff --git a/apps/web/components/dashboard/GlobalActions.tsx b/apps/web/components/dashboard/GlobalActions.tsx
index ecbb70bf..d36b93d9 100644
--- a/apps/web/components/dashboard/GlobalActions.tsx
+++ b/apps/web/components/dashboard/GlobalActions.tsx
@@ -1,13 +1,13 @@
 "use client";
 
 import BulkBookmarksAction from "@/components/dashboard/BulkBookmarksAction";
-import ChangeLayout from "@/components/dashboard/ChangeLayout";
 import SortOrderToggle from "@/components/dashboard/SortOrderToggle";
+import ViewOptions from "@/components/dashboard/ViewOptions";
 
 export default function GlobalActions() {
   return (
     <div className="flex min-w-max flex-wrap overflow-hidden">
-      <ChangeLayout />
+      <ViewOptions />
       <SortOrderToggle />
       <BulkBookmarksAction />
     </div>

apps/web/components/dashboard/ViewOptions.tsx

diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx
new file mode 100644
index 00000000..6367421f
--- /dev/null
+++ b/apps/web/components/dashboard/ViewOptions.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import React from "react";
+import { ButtonWithTooltip } from "@/components/ui/button";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Slider } from "@/components/ui/slider";
+import {
+  useBookmarkLayout,
+  useGridColumns,
+} from "@/lib/userLocalSettings/bookmarksLayout";
+import {
+  updateBookmarksLayout,
+  updateGridColumns,
+} from "@/lib/userLocalSettings/userLocalSettings";
+import {
+  Check,
+  LayoutDashboard,
+  LayoutGrid,
+  LayoutList,
+  List,
+  LucideIcon,
+  Settings,
+} from "lucide-react";
+
+type LayoutType = "masonry" | "grid" | "list" | "compact";
+
+const iconMap: Record<LayoutType, LucideIcon> = {
+  masonry: LayoutDashboard,
+  grid: LayoutGrid,
+  list: LayoutList,
+  compact: List,
+};
+
+const layoutNames: Record<LayoutType, string> = {
+  masonry: "Masonry",
+  grid: "Grid",
+  list: "List",
+  compact: "Compact",
+};
+
+export default function ViewOptions() {
+  const layout = useBookmarkLayout();
+  const gridColumns = useGridColumns();
+  const [tempColumns, setTempColumns] = React.useState(gridColumns);
+
+  const showColumnSlider = layout === "grid" || layout === "masonry";
+
+  // Update temp value when actual value changes
+  React.useEffect(() => {
+    setTempColumns(gridColumns);
+  }, [gridColumns]);
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <ButtonWithTooltip
+          tooltip="View Options"
+          delayDuration={100}
+          variant="ghost"
+        >
+          <Settings size={18} />
+        </ButtonWithTooltip>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent className="w-56">
+        <div className="px-2 py-1.5 text-sm font-semibold">Layout</div>
+        {(Object.keys(iconMap) as LayoutType[]).map((key) => (
+          <DropdownMenuItem
+            key={key}
+            className="cursor-pointer justify-between"
+            onClick={async () => await updateBookmarksLayout(key as LayoutType)}
+          >
+            <div className="flex items-center gap-2">
+              {React.createElement(iconMap[key as LayoutType], { size: 18 })}
+              <span>{layoutNames[key]}</span>
+            </div>
+            {layout === key && <Check className="ml-2 size-4" />}
+          </DropdownMenuItem>
+        ))}
+
+        {showColumnSlider && (
+          <>
+            <DropdownMenuSeparator />
+            <div className="px-2 py-3">
+              <div className="mb-2 flex items-center justify-between">
+                <span className="text-sm font-semibold">Columns</span>
+                <span className="text-sm text-muted-foreground">
+                  {tempColumns}
+                </span>
+              </div>
+              <Slider
+                value={[tempColumns]}
+                onValueChange={([value]) => setTempColumns(value)}
+                onValueCommit={([value]) => updateGridColumns(value)}
+                min={1}
+                max={6}
+                step={1}
+                className="w-full"
+              />
+              <div className="mt-1 flex justify-between text-xs text-muted-foreground">
+                <span>1</span>
+                <span>6</span>
+              </div>
+            </div>
+          </>
+        )}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}

apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx

diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
index 21bc5fed..954a7751 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
@@ -5,6 +5,7 @@ import useBulkActionsStore from "@/lib/bulkActions";
 import {
   bookmarkLayoutSwitch,
   useBookmarkLayout,
+  useGridColumns,
 } from "@/lib/userLocalSettings/bookmarksLayout";
 import tailwindConfig from "@/tailwind.config";
 import { Slot } from "@radix-ui/react-slot";
@@ -27,15 +28,21 @@ function StyledBookmarkCard({ children }: { children: React.ReactNode }) {
   );
 }
 
-function getBreakpointConfig() {
+function getBreakpointConfig(userColumns: number) {
   const fullConfig = resolveConfig(tailwindConfig);
 
   const breakpointColumnsObj: { [key: number]: number; default: number } = {
-    default: 3,
+    default: userColumns,
   };
-  breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2;
-  breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1;
-  breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1;
+
+  // 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;
 }
 
@@ -53,8 +60,12 @@ export default function BookmarksGrid({
   fetchNextPage?: () => void;
 }) {
   const layout = useBookmarkLayout();
+  const gridColumns = useGridColumns();
   const bulkActionsStore = useBulkActionsStore();
-  const breakpointConfig = useMemo(() => getBreakpointConfig(), []);
+  const breakpointConfig = useMemo(
+    () => getBreakpointConfig(gridColumns),
+    [gridColumns],
+  );
   const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();
 
   useEffect(() => {

apps/web/lib/userLocalSettings/bookmarksLayout.tsx

diff --git a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
index a122c6e7..346c85e0 100644
--- a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
+++ b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
@@ -13,6 +13,7 @@ export const UserLocalSettingsCtx = createContext<
 >({
   bookmarkGridLayout: defaultLayout,
   lang: fallbackLng,
+  gridColumns: 3,
 });
 
 function useUserLocalSettings() {
@@ -29,6 +30,11 @@ export function useInterfaceLang() {
   return settings.lang;
 }
 
+export function useGridColumns() {
+  const settings = useUserLocalSettings();
+  return settings.gridColumns;
+}
+
 export function bookmarkLayoutSwitch<T>(
   layout: BookmarksLayoutTypes,
   data: Record<BookmarksLayoutTypes, T>,

apps/web/lib/userLocalSettings/types.ts

diff --git a/apps/web/lib/userLocalSettings/types.ts b/apps/web/lib/userLocalSettings/types.ts
index 94c9bb21..c87c8c33 100644
--- a/apps/web/lib/userLocalSettings/types.ts
+++ b/apps/web/lib/userLocalSettings/types.ts
@@ -8,6 +8,7 @@ export type BookmarksLayoutTypes = z.infer<typeof zBookmarkGridLayout>;
 export const zUserLocalSettings = z.object({
   bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
   lang: z.string().optional().default("en"),
+  gridColumns: z.number().min(1).max(6).optional().default(3),
 });
 
 export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;

apps/web/lib/userLocalSettings/userLocalSettings.ts

diff --git a/apps/web/lib/userLocalSettings/userLocalSettings.ts b/apps/web/lib/userLocalSettings/userLocalSettings.ts
index 311ad99f..85ec69a6 100644
--- a/apps/web/lib/userLocalSettings/userLocalSettings.ts
+++ b/apps/web/lib/userLocalSettings/userLocalSettings.ts
@@ -37,3 +37,14 @@ export async function updateInterfaceLang(lang: string) {
     sameSite: "lax",
   });
 }
+
+export async function updateGridColumns(gridColumns: number) {
+  const userSettings = cookies().get(USER_LOCAL_SETTINGS_COOKIE_NAME);
+  const parsed = parseUserLocalSettings(userSettings?.value);
+  cookies().set({
+    name: USER_LOCAL_SETTINGS_COOKIE_NAME,
+    value: JSON.stringify({ ...parsed, gridColumns }),
+    maxAge: 34560000, // Chrome caps max age to 400 days
+    sameSite: "lax",
+  });
+}