From 1940f02cf36af4cc8544bcd07024a861ff7fa7ac Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 7 Jul 2026 16:32:19 +0000 Subject: [PATCH] feat(home): delete-folder confirm, view preference cookie, show-more batching Folder removal now confirms with copy stating portfolios are unfiled, not deleted. Grid/list preference persists in a cookie read by the server component, so the chosen view renders on first paint. Long lists render the first 24 rows with a show-more button; the reveal count resets when the view or search changes, and starred float keeps pinned work on the first screen. Co-Authored-By: Claude Fable 5 --- src/app/components/home/PortfolioHome.tsx | 67 ++++++++++++++++++++--- src/app/home/page.tsx | 9 ++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/app/components/home/PortfolioHome.tsx b/src/app/components/home/PortfolioHome.tsx index 14758f1a..feb883e9 100644 --- a/src/app/components/home/PortfolioHome.tsx +++ b/src/app/components/home/PortfolioHome.tsx @@ -22,6 +22,10 @@ import NewPortfolioModal from "./NewPortfolioModal"; const HOME_KEY = ["portfolio-home"]; +// "Show more" batch size: bounds the initial wall without pagination chrome. +// Divisible by 2 and 3 so grid rows stay full at every breakpoint. +const PAGE_SIZE = 24; + async function fetchHome(): Promise { const res = await fetch("/api/me/portfolio-home"); if (!res.ok) throw new Error("Failed to load portfolios"); @@ -68,14 +72,32 @@ function useOptimisticHomeMutation( }); } -export default function PortfolioHome() { +export default function PortfolioHome({ + initialDisplay = "grid", +}: { + initialDisplay?: "grid" | "list"; +}) { const queryClient = useQueryClient(); const { data, isLoading, isError, refetch } = useQuery(HOME_KEY, fetchHome); const [view, setView] = useState("all"); - const [display, setDisplay] = useState<"grid" | "list">("grid"); + const [display, setDisplay] = useState<"grid" | "list">(initialDisplay); + const chooseDisplay = (value: "grid" | "list") => { + setDisplay(value); + document.cookie = `home-display=${value}; path=/; max-age=31536000; samesite=lax`; + }; const [sort, setSort] = useState("updated"); const [query, setQuery] = useState(""); + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); + // Changing what's listed resets how much of it is revealed. + const chooseView = (next: RailView) => { + setView(next); + setVisibleCount(PAGE_SIZE); + }; + const chooseQuery = (next: string) => { + setQuery(next); + setVisibleCount(PAGE_SIZE); + }; const [announcement, setAnnouncement] = useState(""); const [newPortfolioOpen, setNewPortfolioOpen] = useState(false); @@ -209,6 +231,9 @@ export default function PortfolioHome() { ); } + const visibleRows = rows.slice(0, visibleCount); + const hiddenCount = rows.length - visibleRows.length; + const viewTitle = view === "all" ? "All portfolios" @@ -256,7 +281,7 @@ export default function PortfolioHome() { setQuery(e.target.value)} + onChange={(e) => chooseQuery(e.target.value)} placeholder="Search portfolios" aria-label="Search portfolios by name, status, goal or folder" className="w-60 rounded-lg border border-[#c3ccd8] bg-white py-2 pl-8 pr-3 text-sm" @@ -290,7 +315,7 @@ export default function PortfolioHome() { aria-pressed={display === value} aria-label={label} title={label} - onClick={() => setDisplay(value)} + onClick={() => chooseDisplay(value)} className={`grid place-items-center px-2.5 py-2 ${ display === value ? "bg-brandblue text-white" @@ -326,7 +351,7 @@ export default function PortfolioHome() { (p) => folderIdOf(p.id) === folderId && needsAttention(p), ).length } - onSelect={setView} + onSelect={chooseView} onReorder={(orderedIds) => { reorderMutation.mutate(orderedIds); setAnnouncement("Folder order saved"); @@ -336,6 +361,17 @@ export default function PortfolioHome() { renameMutation.mutate({ folderId, name }) } onDelete={(folderId) => { + const folder = folderById.get(folderId); + const count = data.portfolios.filter( + (p) => folderIdOf(p.id) === folderId, + ).length; + const inside = + count === 0 + ? "It is empty." + : `Its ${count} portfolio${count === 1 ? "" : "s"} will be unfiled, not deleted.`; + if (!window.confirm(`Remove folder “${folder?.name}”? ${inside}`)) { + return; + } deleteMutation.mutate(folderId); setAnnouncement("Folder removed — its portfolios are unfiled"); }} @@ -386,7 +422,7 @@ export default function PortfolioHome() { {query.trim()}”.{" "} + + Showing {visibleRows.length}{" "} + of {rows.length} + + + )} diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index 85ce4c92..4111b06d 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -1,5 +1,6 @@ import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getServerSession } from "next-auth"; +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import PortfolioHome from "../components/home/PortfolioHome"; @@ -10,6 +11,12 @@ const Home = async () => { redirect("/"); } - return ; + // Device-level display preference travels as a cookie so the server renders + // the chosen view on first paint (no client-side flicker). + const cookieStore = await cookies(); + const initialDisplay = + cookieStore.get("home-display")?.value === "list" ? "list" : "grid"; + + return ; }; export default Home;