perf(admin): lazy-load response navigation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -12,11 +12,13 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarInput,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
FilePenLine,
|
||||
LayoutDashboard,
|
||||
Settings,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import {
|
||||
@@ -36,6 +39,10 @@ import {
|
||||
import { GuardedLink } from "@/components/guarded-link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getResponseNavigation } from "@/app/admin/form/actions";
|
||||
|
||||
type ResponseNavigationData = Awaited<ReturnType<typeof getResponseNavigation>>;
|
||||
|
||||
export function FormAdminSidebar({
|
||||
formId,
|
||||
@@ -44,7 +51,7 @@ export function FormAdminSidebar({
|
||||
userName,
|
||||
userImage,
|
||||
profile,
|
||||
responseItems,
|
||||
responseCount,
|
||||
}: {
|
||||
formId: string;
|
||||
formTitle: string;
|
||||
@@ -52,15 +59,34 @@ export function FormAdminSidebar({
|
||||
userName?: string;
|
||||
userImage?: string;
|
||||
profile?: DiscordMemberProfile | null;
|
||||
responseItems?: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
queueNumber: number;
|
||||
}[];
|
||||
responseCount: number;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const responseList = responseItems ?? [];
|
||||
const [responsesOpen, setResponsesOpen] = useState(true);
|
||||
const [responsesOpen, setResponsesOpen] = useState(
|
||||
pathname.startsWith(`/admin/form/${formId}/result/`),
|
||||
);
|
||||
const [responseQuery, setResponseQuery] = useState("");
|
||||
const [responseData, setResponseData] =
|
||||
useState<ResponseNavigationData | null>(null);
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
const [isLoadingResponses, startResponseTransition] = useTransition();
|
||||
|
||||
const loadResponses = useCallback((page: number, query = responseQuery) => {
|
||||
startResponseTransition(async () => {
|
||||
try {
|
||||
setResponseError(null);
|
||||
setResponseData(await getResponseNavigation(formId, page, query));
|
||||
} catch {
|
||||
setResponseError("Could not load responses.");
|
||||
}
|
||||
});
|
||||
}, [formId, responseQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (responsesOpen && responseCount > 0 && !responseData) {
|
||||
loadResponses(1, "");
|
||||
}
|
||||
}, [loadResponses, responsesOpen, responseCount, responseData]);
|
||||
const currentSection = pathname.startsWith(`/admin/form/${formId}/result`)
|
||||
? "Responses"
|
||||
: pathname.startsWith(`/admin/form/${formId}/extra`)
|
||||
@@ -154,8 +180,12 @@ export function FormAdminSidebar({
|
||||
<span>{label}</span>
|
||||
</GuardedLink>
|
||||
</SidebarMenuButton>
|
||||
{label === "Responses" && responseList.length > 0 && (
|
||||
{label === "Responses" && responseCount > 0 && (
|
||||
<SidebarMenuBadge>{responseCount}</SidebarMenuBadge>
|
||||
)}
|
||||
{label === "Responses" && responseCount > 0 && (
|
||||
<SidebarMenuAction
|
||||
className="right-8"
|
||||
onClick={() => setResponsesOpen((open) => !open)}
|
||||
aria-label={`${responsesOpen ? "Collapse" : "Expand"} responses`}
|
||||
aria-expanded={responsesOpen}
|
||||
@@ -170,10 +200,34 @@ export function FormAdminSidebar({
|
||||
)}
|
||||
{label === "Responses" &&
|
||||
responsesOpen &&
|
||||
responseList.length > 0 && (
|
||||
<ScrollArea>
|
||||
<SidebarMenuSub>
|
||||
{responseList.map((response) => {
|
||||
responseCount > 0 && (
|
||||
<div className="mt-1 flex flex-col gap-2 group-data-[collapsible=icon]:hidden">
|
||||
<form
|
||||
className="flex items-center gap-1 px-2"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
loadResponses(1);
|
||||
}}
|
||||
>
|
||||
<SidebarInput
|
||||
value={responseQuery}
|
||||
onChange={(event) => setResponseQuery(event.target.value)}
|
||||
placeholder="Search responses"
|
||||
aria-label="Search response navigation"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
disabled={isLoadingResponses}
|
||||
aria-label="Search responses"
|
||||
>
|
||||
<Search />
|
||||
</Button>
|
||||
</form>
|
||||
<ScrollArea className="max-h-[min(50vh,28rem)]">
|
||||
<SidebarMenuSub>
|
||||
{responseData?.items.map((response) => {
|
||||
const responseHref = `/admin/form/${formId}/result/${response.id}`;
|
||||
|
||||
return (
|
||||
@@ -191,8 +245,42 @@ export function FormAdminSidebar({
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</ScrollArea>
|
||||
{isLoadingResponses && !responseData && (
|
||||
<SidebarMenuSubItem className="px-2 py-1 text-xs text-muted-foreground">
|
||||
Loading responses…
|
||||
</SidebarMenuSubItem>
|
||||
)}
|
||||
{responseError && (
|
||||
<SidebarMenuSubItem className="px-2 py-1 text-xs text-destructive">
|
||||
{responseError}
|
||||
</SidebarMenuSubItem>
|
||||
)}
|
||||
</SidebarMenuSub>
|
||||
</ScrollArea>
|
||||
{responseData && responseData.pageCount > 1 && (
|
||||
<div className="flex items-center justify-between gap-1 px-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isLoadingResponses || responseData.page === 1}
|
||||
onClick={() => loadResponses(responseData.page - 1)}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{responseData.page} / {responseData.pageCount}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isLoadingResponses || responseData.page === responseData.pageCount}
|
||||
onClick={() => loadResponses(responseData.page + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user