mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-12 13:28:55 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
cfa181f3c7
commit
1940f02cf3
2 changed files with 67 additions and 9 deletions
|
|
@ -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<HomeData> {
|
||||
const res = await fetch("/api/me/portfolio-home");
|
||||
if (!res.ok) throw new Error("Failed to load portfolios");
|
||||
|
|
@ -68,14 +72,32 @@ function useOptimisticHomeMutation<V>(
|
|||
});
|
||||
}
|
||||
|
||||
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<RailView>("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<HomeSort>("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() {
|
|||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => 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() {
|
|||
<b className="text-gray-900">{query.trim()}</b>”.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuery("")}
|
||||
onClick={() => chooseQuery("")}
|
||||
className="font-semibold text-brandmidblue hover:underline"
|
||||
>
|
||||
Clear search
|
||||
|
|
@ -421,7 +457,7 @@ export default function PortfolioHome() {
|
|||
)
|
||||
) : display === "grid" ? (
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{rows.map((portfolio) => (
|
||||
{visibleRows.map((portfolio) => (
|
||||
<PortfolioGridCard
|
||||
key={portfolio.id}
|
||||
portfolio={portfolio}
|
||||
|
|
@ -436,13 +472,28 @@ export default function PortfolioHome() {
|
|||
</div>
|
||||
) : (
|
||||
<PortfolioTable
|
||||
portfolios={rows}
|
||||
portfolios={visibleRows}
|
||||
actions={cardActions}
|
||||
folderNameOf={
|
||||
view === "all" || view === "starred" ? folderNameOf : () => null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{hiddenCount > 0 && (
|
||||
<div className="mt-5 flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVisibleCount((count) => count + PAGE_SIZE)}
|
||||
className="rounded-lg border border-[#c3ccd8] bg-white px-5 py-2 text-sm font-semibold text-gray-900 hover:border-brandmidblue hover:text-brandmidblue"
|
||||
>
|
||||
Show {Math.min(PAGE_SIZE, hiddenCount)} more
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
Showing <span className="tabular-nums">{visibleRows.length}</span>{" "}
|
||||
of <span className="tabular-nums">{rows.length}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <PortfolioHome />;
|
||||
// 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 <PortfolioHome initialDisplay={initialDisplay} />;
|
||||
};
|
||||
export default Home;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue