perf(admin): lazy-load response navigation
This commit is contained in:
@@ -7,7 +7,7 @@ import { getSessionDiscordId } from "@/lib/auth/auth";
|
||||
import { getDiscordMemberProfile } from "@/lib/discord/discord";
|
||||
import { db } from "@/db";
|
||||
import { submissions } from "@/db/schema";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { isFormAcceptingSubmissions } from "@/lib/form/form-availability";
|
||||
|
||||
@@ -25,43 +25,21 @@ export default async function AdminFormLayout(
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const [session, form] = await Promise.all([
|
||||
const [session, form, [{ responseCount }]] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
db.query.forms.findFirst({
|
||||
where: (record, { eq }) => eq(record.id, params.id),
|
||||
}),
|
||||
db
|
||||
.select({ responseCount: count() })
|
||||
.from(submissions)
|
||||
.where(eq(submissions.formId, params.id)),
|
||||
]);
|
||||
|
||||
if (!form) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const responseItems = await db
|
||||
.select({
|
||||
id: submissions.id,
|
||||
userName: submissions.userName,
|
||||
userDiscordId: submissions.userDiscordId,
|
||||
})
|
||||
.from(submissions)
|
||||
.where(eq(submissions.formId, params.id))
|
||||
.orderBy(asc(submissions.submittedAt), asc(submissions.id));
|
||||
|
||||
const responseDiscordIds = [
|
||||
...new Set(
|
||||
responseItems
|
||||
.map((response) => response.userDiscordId)
|
||||
.filter((value): value is string => Boolean(value))
|
||||
),
|
||||
];
|
||||
const responseProfiles = Object.fromEntries(
|
||||
await Promise.all(
|
||||
responseDiscordIds.map(async (discordId) => [
|
||||
discordId,
|
||||
await getDiscordMemberProfile(discordId),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
let profile = null;
|
||||
const discordId = getSessionDiscordId(session);
|
||||
if (discordId) {
|
||||
@@ -74,18 +52,7 @@ export default async function AdminFormLayout(
|
||||
<FormAdminSidebar
|
||||
formId={params.id}
|
||||
formTitle={form.title}
|
||||
responseItems={responseItems.map((response, index) => ({
|
||||
id: response.id,
|
||||
displayName:
|
||||
(response.userDiscordId
|
||||
? responseProfiles[response.userDiscordId]?.globalName ||
|
||||
responseProfiles[response.userDiscordId]?.username
|
||||
: null) ||
|
||||
response.userName ||
|
||||
response.userDiscordId ||
|
||||
"Anonymous",
|
||||
queueNumber: index + 1,
|
||||
}))}
|
||||
responseCount={responseCount}
|
||||
isOpen={isFormAcceptingSubmissions(form)}
|
||||
userName={session?.user?.name || ""}
|
||||
userImage={session?.user?.image || ""}
|
||||
|
||||
@@ -7,6 +7,8 @@ import SubmissionDetailClient from "./client";
|
||||
import { DeleteSubmissionButton } from "@/components/delete-buttons";
|
||||
import { RealtimeRefresh } from "@/components/realtime-refresh";
|
||||
import { getDiscordMemberProfile } from "@/lib/discord/discord";
|
||||
import { submissions } from "@/db/schema";
|
||||
import { and, asc, desc, eq, gt, lt, or } from "drizzle-orm";
|
||||
|
||||
interface SubmissionDetailPageProps {
|
||||
params: Promise<{
|
||||
@@ -39,18 +41,53 @@ export default async function SubmissionDetailPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const respondentProfile = submission.userDiscordId
|
||||
? await getDiscordMemberProfile(submission.userDiscordId)
|
||||
: null;
|
||||
|
||||
const questions = await db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, submission.formId),
|
||||
orderBy: (q, { asc }) => [asc(q.displayOrder)],
|
||||
});
|
||||
|
||||
const answersList = await db.query.answers.findMany({
|
||||
where: (a, { eq }) => eq(a.submissionId, submission.id),
|
||||
});
|
||||
const [respondentProfile, questions, answersList, [previous], [next]] =
|
||||
await Promise.all([
|
||||
submission.userDiscordId
|
||||
? getDiscordMemberProfile(submission.userDiscordId)
|
||||
: null,
|
||||
db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, submission.formId),
|
||||
orderBy: (q, { asc }) => [asc(q.displayOrder)],
|
||||
}),
|
||||
db.query.answers.findMany({
|
||||
where: (a, { eq }) => eq(a.submissionId, submission.id),
|
||||
}),
|
||||
db
|
||||
.select({ id: submissions.id })
|
||||
.from(submissions)
|
||||
.where(
|
||||
and(
|
||||
eq(submissions.formId, formId),
|
||||
or(
|
||||
gt(submissions.submittedAt, submission.submittedAt),
|
||||
and(
|
||||
eq(submissions.submittedAt, submission.submittedAt),
|
||||
gt(submissions.id, submission.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(submissions.submittedAt), asc(submissions.id))
|
||||
.limit(1),
|
||||
db
|
||||
.select({ id: submissions.id })
|
||||
.from(submissions)
|
||||
.where(
|
||||
and(
|
||||
eq(submissions.formId, formId),
|
||||
or(
|
||||
lt(submissions.submittedAt, submission.submittedAt),
|
||||
and(
|
||||
eq(submissions.submittedAt, submission.submittedAt),
|
||||
lt(submissions.id, submission.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(submissions.submittedAt), desc(submissions.id))
|
||||
.limit(1),
|
||||
]);
|
||||
|
||||
const answersMap: Record<string, { value: string; imageCdnId: string | null }> = {};
|
||||
for (const ans of answersList) {
|
||||
@@ -59,14 +96,8 @@ export default async function SubmissionDetailPage({
|
||||
imageCdnId: ans.imageCdnId ?? null,
|
||||
};
|
||||
}
|
||||
const submissionsList = await db.query.submissions.findMany({
|
||||
where: (s, { eq }) => eq(s.formId, formId),
|
||||
orderBy: (s, { desc }) => [desc(s.submittedAt)],
|
||||
});
|
||||
|
||||
const currentIndex = submissionsList.findIndex((s) => s.id === submissionId);
|
||||
const prevId = currentIndex > 0 ? submissionsList[currentIndex - 1].id : null;
|
||||
const nextId = currentIndex >= 0 && currentIndex < submissionsList.length - 1 ? submissionsList[currentIndex + 1].id : null;
|
||||
const prevId = previous?.id ?? null;
|
||||
const nextId = next?.id ?? null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-6xl mx-auto p-4 md:p-6 pb-10">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { db } from "@/db";
|
||||
import { forms, submissions } from "@/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, asc, count, eq, ilike, or } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { requireAdmin } from "@/lib/auth/auth";
|
||||
@@ -10,6 +10,59 @@ import { sanitizeHtml } from "@/lib/sanitize-html";
|
||||
import { sse } from "@/lib/realtime/sse";
|
||||
import { isValidDiscordWebhookUrl } from "@/lib/discord/discord-webhook-settings";
|
||||
|
||||
const RESPONSE_NAVIGATION_PAGE_SIZE = 50;
|
||||
|
||||
export async function getResponseNavigation(
|
||||
formId: string,
|
||||
page = 1,
|
||||
query = "",
|
||||
) {
|
||||
await requireAdmin();
|
||||
const safeQuery = query.trim().slice(0, 100);
|
||||
const requestedPage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
|
||||
const where = and(
|
||||
eq(submissions.formId, formId),
|
||||
safeQuery
|
||||
? or(
|
||||
ilike(submissions.userName, `%${safeQuery}%`),
|
||||
ilike(submissions.userDiscordId, `%${safeQuery}%`),
|
||||
)
|
||||
: undefined,
|
||||
);
|
||||
const [{ total }] = await db
|
||||
.select({ total: count() })
|
||||
.from(submissions)
|
||||
.where(where);
|
||||
const pageCount = Math.max(
|
||||
1,
|
||||
Math.ceil(total / RESPONSE_NAVIGATION_PAGE_SIZE),
|
||||
);
|
||||
const currentPage = Math.min(requestedPage, pageCount);
|
||||
const rows = await db
|
||||
.select({
|
||||
id: submissions.id,
|
||||
userName: submissions.userName,
|
||||
userDiscordId: submissions.userDiscordId,
|
||||
})
|
||||
.from(submissions)
|
||||
.where(where)
|
||||
.orderBy(asc(submissions.submittedAt), asc(submissions.id))
|
||||
.limit(RESPONSE_NAVIGATION_PAGE_SIZE)
|
||||
.offset((currentPage - 1) * RESPONSE_NAVIGATION_PAGE_SIZE);
|
||||
|
||||
return {
|
||||
items: rows.map((row, index) => ({
|
||||
id: row.id,
|
||||
displayName: row.userName || row.userDiscordId || "Anonymous",
|
||||
queueNumber:
|
||||
(currentPage - 1) * RESPONSE_NAVIGATION_PAGE_SIZE + index + 1,
|
||||
})),
|
||||
page: currentPage,
|
||||
pageCount,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createForm(formData: FormData) {
|
||||
await requireAdmin();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user