From 34d2b48578532d387b1466c82ae4a761cd1d1a4f Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sun, 2 Nov 2025 20:13:57 +0000
Subject: feat: Add view options to show tag/title and control image fit. Fixes
 #1960

---
 apps/web/components/dashboard/ViewOptions.tsx | 154 ++++++++++++++++++++++++--
 1 file changed, 147 insertions(+), 7 deletions(-)

(limited to 'apps/web/components/dashboard/ViewOptions.tsx')

diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx
index c86f43fb..e7b459fe 100644
--- a/apps/web/components/dashboard/ViewOptions.tsx
+++ b/apps/web/components/dashboard/ViewOptions.tsx
@@ -1,6 +1,12 @@
 "use client";
 
-import { createElement, useEffect, useState } from "react";
+import {
+  createElement,
+  useEffect,
+  useOptimistic,
+  useState,
+  useTransition,
+} from "react";
 import { ButtonWithTooltip } from "@/components/ui/button";
 import {
   DropdownMenu,
@@ -21,10 +27,15 @@ import {
 import {
   updateBookmarksLayout,
   updateGridColumns,
+  updateImageFit,
   updateShowNotes,
+  updateShowTags,
+  updateShowTitle,
 } from "@/lib/userLocalSettings/userLocalSettings";
 import {
   Check,
+  Heading,
+  Image,
   LayoutDashboard,
   LayoutGrid,
   LayoutList,
@@ -32,6 +43,7 @@ import {
   LucideIcon,
   NotepadText,
   Settings,
+  Tag,
 } from "lucide-react";
 
 type LayoutType = "masonry" | "grid" | "list" | "compact";
@@ -47,16 +59,73 @@ export default function ViewOptions() {
   const { t } = useTranslation();
   const layout = useBookmarkLayout();
   const gridColumns = useGridColumns();
-  const { showNotes } = useBookmarkDisplaySettings();
+  const actualDisplaySettings = useBookmarkDisplaySettings();
   const [tempColumns, setTempColumns] = useState(gridColumns);
+  const [, startTransition] = useTransition();
+
+  // Optimistic state for all toggles
+  const [optimisticDisplaySettings, setOptimisticDisplaySettings] =
+    useOptimistic(actualDisplaySettings);
+
+  const [optimisticLayout, setOptimisticLayout] = useOptimistic(layout);
 
-  const showColumnSlider = layout === "grid" || layout === "masonry";
+  const [optimisticImageFit, setOptimisticImageFit] = useOptimistic(
+    actualDisplaySettings.imageFit,
+  );
+
+  const showColumnSlider =
+    optimisticLayout === "grid" || optimisticLayout === "masonry";
 
   // Update temp value when actual value changes
   useEffect(() => {
     setTempColumns(gridColumns);
   }, [gridColumns]);
 
+  // Handlers with optimistic updates
+  const handleLayoutChange = (newLayout: LayoutType) => {
+    startTransition(async () => {
+      setOptimisticLayout(newLayout);
+      await updateBookmarksLayout(newLayout);
+    });
+  };
+
+  const handleShowNotesChange = (checked: boolean) => {
+    startTransition(async () => {
+      setOptimisticDisplaySettings({
+        ...optimisticDisplaySettings,
+        showNotes: checked,
+      });
+      await updateShowNotes(checked);
+    });
+  };
+
+  const handleShowTagsChange = (checked: boolean) => {
+    startTransition(async () => {
+      setOptimisticDisplaySettings({
+        ...optimisticDisplaySettings,
+        showTags: checked,
+      });
+      await updateShowTags(checked);
+    });
+  };
+
+  const handleShowTitleChange = (checked: boolean) => {
+    startTransition(async () => {
+      setOptimisticDisplaySettings({
+        ...optimisticDisplaySettings,
+        showTitle: checked,
+      });
+      await updateShowTitle(checked);
+    });
+  };
+
+  const handleImageFitChange = (fit: "cover" | "contain") => {
+    startTransition(async () => {
+      setOptimisticImageFit(fit);
+      await updateImageFit(fit);
+    });
+  };
+
   return (
     <DropdownMenu>
       <DropdownMenuTrigger asChild>
@@ -76,13 +145,16 @@ export default function ViewOptions() {
           <DropdownMenuItem
             key={key}
             className="cursor-pointer justify-between"
-            onClick={async () => await updateBookmarksLayout(key as LayoutType)}
+            onSelect={(e) => {
+              e.preventDefault();
+              handleLayoutChange(key);
+            }}
           >
             <div className="flex items-center gap-2">
               {createElement(iconMap[key as LayoutType], { size: 18 })}
               <span>{t(`layouts.${key}`)}</span>
             </div>
-            {layout === key && <Check className="ml-2 size-4" />}
+            {optimisticLayout === key && <Check className="ml-2 size-4" />}
           </DropdownMenuItem>
         ))}
 
@@ -131,11 +203,79 @@ export default function ViewOptions() {
             </Label>
             <Switch
               id="show-notes"
-              checked={showNotes}
-              onCheckedChange={updateShowNotes}
+              checked={optimisticDisplaySettings.showNotes}
+              onCheckedChange={handleShowNotesChange}
+            />
+          </div>
+
+          <div className="flex items-center justify-between">
+            <Label
+              htmlFor="show-tags"
+              className="flex cursor-pointer items-center gap-2 text-sm"
+            >
+              <Tag size={16} />
+              <span>{t("view_options.show_tags")}</span>
+            </Label>
+            <Switch
+              id="show-tags"
+              checked={optimisticDisplaySettings.showTags}
+              onCheckedChange={handleShowTagsChange}
+            />
+          </div>
+
+          <div className="flex items-center justify-between">
+            <Label
+              htmlFor="show-title"
+              className="flex cursor-pointer items-center gap-2 text-sm"
+            >
+              <Heading size={16} />
+              <span>{t("view_options.show_title")}</span>
+            </Label>
+            <Switch
+              id="show-title"
+              checked={optimisticDisplaySettings.showTitle}
+              onCheckedChange={handleShowTitleChange}
             />
           </div>
         </div>
+
+        <DropdownMenuSeparator />
+        <div className="px-2 py-1.5 text-sm font-semibold">
+          {t("view_options.image_options")}
+        </div>
+
+        <div className="space-y-1 px-2 py-2">
+          <DropdownMenuItem
+            className="cursor-pointer justify-between"
+            onSelect={(e) => {
+              e.preventDefault();
+              handleImageFitChange("cover");
+            }}
+          >
+            <div className="flex items-center gap-2">
+              <Image size={16} />
+              <span>{t("view_options.image_fit_cover")}</span>
+            </div>
+            {optimisticImageFit === "cover" && (
+              <Check className="ml-2 size-4" />
+            )}
+          </DropdownMenuItem>
+          <DropdownMenuItem
+            className="cursor-pointer justify-between"
+            onSelect={(e) => {
+              e.preventDefault();
+              handleImageFitChange("contain");
+            }}
+          >
+            <div className="flex items-center gap-2">
+              <Image size={16} />
+              <span>{t("view_options.image_fit_contain")}</span>
+            </div>
+            {optimisticImageFit === "contain" && (
+              <Check className="ml-2 size-4" />
+            )}
+          </DropdownMenuItem>
+        </div>
       </DropdownMenuContent>
     </DropdownMenu>
   );
-- 
cgit v1.2.3-70-g09d2

