fix: add load-more button and pagination to attachments page

This commit is contained in:
gitkeniwo 2025-11-18 00:06:07 +01:00
parent 4de8712cb0
commit c9d44c0526
1 changed files with 99 additions and 56 deletions

View File

@ -8,6 +8,7 @@ import Empty from "@/components/Empty";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { attachmentServiceClient } from "@/grpcweb"; import { attachmentServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import useResponsiveWidth from "@/hooks/useResponsiveWidth";
@ -42,18 +43,51 @@ const Attachments = observer(() => {
searchQuery: "", searchQuery: "",
}); });
const [attachments, setAttachments] = useState<Attachment[]>([]); const [attachments, setAttachments] = useState<Attachment[]>([]);
const [nextPageToken, setNextPageToken] = useState("");
const [isLoadingMore, setIsLoadingMore] = useState(false);
const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery)); const filteredAttachments = attachments.filter((attachment) => includes(attachment.filename, state.searchQuery));
const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo)); const groupedAttachments = groupAttachmentsByDate(filteredAttachments.filter((attachment) => attachment.memo));
const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo); const unusedAttachments = filteredAttachments.filter((attachment) => !attachment.memo);
useEffect(() => { useEffect(() => {
attachmentServiceClient.listAttachments({}).then(({ attachments }) => { const fetchInitialAttachments = async () => {
setAttachments(attachments); try {
loadingState.setFinish(); const { attachments: fetchedAttachments, nextPageToken } = await attachmentServiceClient.listAttachments({
Promise.all(attachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null))); pageSize: 50,
}); });
setAttachments(fetchedAttachments);
setNextPageToken(nextPageToken ?? "");
await Promise.all(
fetchedAttachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null)),
);
} finally {
loadingState.setFinish();
}
};
fetchInitialAttachments();
}, []); }, []);
const handleLoadMore = async () => {
if (!nextPageToken) {
return;
}
setIsLoadingMore(true);
try {
const { attachments: fetchedAttachments, nextPageToken: newPageToken } = await attachmentServiceClient.listAttachments({
pageSize: 50,
pageToken: nextPageToken,
});
setAttachments((prevAttachments) => [...prevAttachments, ...fetchedAttachments]);
setNextPageToken(newPageToken ?? "");
await Promise.all(
fetchedAttachments.map((attachment) => (attachment.memo ? memoStore.getOrFetchMemoByName(attachment.memo) : null)),
);
} finally {
setIsLoadingMore(false);
}
};
return ( return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"> <section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
{!md && <MobileHeader />} {!md && <MobileHeader />}
@ -89,61 +123,70 @@ const Attachments = observer(() => {
<p className="mt-4 text-muted-foreground">{t("message.no-data")}</p> <p className="mt-4 text-muted-foreground">{t("message.no-data")}</p>
</div> </div>
) : ( ) : (
<div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}> <>
{Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => { <div className={"w-full h-auto px-2 flex flex-col justify-start items-start gap-y-8"}>
return ( {Array.from(groupedAttachments.entries()).map(([monthStr, attachments]) => {
<div key={monthStr} className="w-full flex flex-row justify-start items-start"> return (
<div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start"> <div key={monthStr} className="w-full flex flex-row justify-start items-start">
<span className="text-sm opacity-60">{dayjs(monthStr).year()}</span> <div className="w-16 sm:w-24 pt-4 sm:pl-4 flex flex-col justify-start items-start">
<span className="font-medium text-xl"> <span className="text-sm opacity-60">{dayjs(monthStr).year()}</span>
{dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })} <span className="font-medium text-xl">
</span> {dayjs(monthStr).toDate().toLocaleString(i18n.language, { month: "short" })}
</div> </span>
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
{attachments.map((attachment) => {
return (
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
</div>
</div>
);
})}
</div>
</div>
);
})}
{unusedAttachments.length > 0 && (
<>
<Separator />
<div className="w-full flex flex-row justify-start items-start">
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
<div className="w-full flex flex-row justify-start items-center gap-2">
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
</div> </div>
{unusedAttachments.map((attachment) => { <div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
return ( {attachments.map((attachment) => {
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start"> return (
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"> <div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
<AttachmentIcon attachment={attachment} strokeWidth={0.5} /> <div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
</div>
</div> </div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"> );
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p> })}
</div> </div>
</div>
);
})}
</div> </div>
</div> );
</> })}
{unusedAttachments.length > 0 && (
<>
<Separator />
<div className="w-full flex flex-row justify-start items-start">
<div className="w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"></div>
<div className="w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap">
<div className="w-full flex flex-row justify-start items-center gap-2">
<span className="text-muted-foreground">{t("resource.unused-resources")}</span>
<span className="text-muted-foreground opacity-80">({unusedAttachments.length})</span>
</div>
{unusedAttachments.map((attachment) => {
return (
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-border overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
<p className="text-xs shrink text-muted-foreground truncate">{attachment.filename}</p>
</div>
</div>
);
})}
</div>
</div>
</>
)}
</div>
{nextPageToken && (
<div className="w-full flex flex-row justify-center items-center mt-4">
<Button variant="outline" size="sm" onClick={handleLoadMore} disabled={isLoadingMore}>
{isLoadingMore ? t("resource.fetching-data") : t("memo.load-more")}
</Button>
</div>
)} )}
</div> </>
)} )}
</> </>
)} )}