feat:update form
This commit is contained in:
@@ -26,6 +26,7 @@ import Image from 'next/image';
|
||||
import { stripHtml } from "@/lib/utils";
|
||||
import { HtmlDisplay } from "@/components/html-display";
|
||||
import { formatStoredAnswer } from "@/lib/form-answer-values";
|
||||
import { normalizeStoredAnswerSnapshot } from "@/lib/submission-intake";
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
@@ -40,10 +41,10 @@ interface SubmissionDetailClientProps {
|
||||
userName: string | null;
|
||||
userDiscordId: string | null;
|
||||
submittedAt: Date;
|
||||
editHistory?: { editedAt: string; oldAnswers: Record<string, string> }[] | null;
|
||||
editHistory?: { editedAt: string; oldAnswers: Record<string, string | { value: string; imageCdnId: string | null }> }[] | null;
|
||||
};
|
||||
questions: Question[];
|
||||
answers: Record<string, string>;
|
||||
answers: Record<string, { value: string; imageCdnId: string | null }>;
|
||||
}
|
||||
|
||||
export default function SubmissionDetailClient({
|
||||
@@ -103,16 +104,25 @@ export default function SubmissionDetailClient({
|
||||
{/* Answers List */}
|
||||
<div className="space-y-4">
|
||||
{questions.map((q) => {
|
||||
const rawAnswerValue = answers[q.id] || "";
|
||||
const rawAnswer = answers[q.id] ?? { value: "", imageCdnId: null };
|
||||
const rawAnswerValue = rawAnswer.value;
|
||||
const answerValue = formatStoredAnswer(rawAnswerValue, q.type);
|
||||
const imageUrl = rawAnswer.imageCdnId ? `/cdn/${rawAnswer.imageCdnId}` : "";
|
||||
|
||||
const wasEdited = editHistory.some(
|
||||
(edit) => (edit.oldAnswers[q.id] ?? "") !== rawAnswerValue
|
||||
(edit) => {
|
||||
const oldSnapshot = normalizeStoredAnswerSnapshot(edit.oldAnswers[q.id]);
|
||||
return oldSnapshot.value !== rawAnswerValue || oldSnapshot.imageCdnId !== rawAnswer.imageCdnId;
|
||||
}
|
||||
);
|
||||
|
||||
const qAnswerHistory = editHistory.map((edit) => ({
|
||||
editedAt: edit.editedAt,
|
||||
answer: formatStoredAnswer(edit.oldAnswers[q.id], q.type),
|
||||
answer: (() => {
|
||||
const snapshot = normalizeStoredAnswerSnapshot(edit.oldAnswers[q.id]);
|
||||
const text = formatStoredAnswer(snapshot.value, q.type);
|
||||
return text || (snapshot.imageCdnId ? `/cdn/${snapshot.imageCdnId}` : "");
|
||||
})(),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -171,8 +181,8 @@ export default function SubmissionDetailClient({
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{answerValue ? (
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{answerValue && (
|
||||
<div className="flex items-start justify-between gap-4 p-3 bg-muted rounded-md group">
|
||||
<div className="flex flex-col min-w-0">
|
||||
{wasEdited && (
|
||||
@@ -198,7 +208,11 @@ export default function SubmissionDetailClient({
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
{imageUrl && (
|
||||
<AnswerImagePreview src={imageUrl} label={stripHtml(q.label)} />
|
||||
)}
|
||||
{!answerValue && !imageUrl && (
|
||||
<span className="text-sm text-muted-foreground italic pl-3 border-l-2 border-muted block">
|
||||
No response provided
|
||||
</span>
|
||||
@@ -255,3 +269,53 @@ export default function SubmissionDetailClient({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnswerImagePreview({ src, label }: { src: string; label: string }) {
|
||||
const [unavailable, setUnavailable] = useState(false);
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<span className="text-sm text-muted-foreground italic">
|
||||
Image unavailable
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-auto w-fit p-2"
|
||||
aria-label={`Open image answer for ${label}`}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
width={224}
|
||||
height={160}
|
||||
unoptimized
|
||||
className="max-h-40 max-w-56 rounded object-contain"
|
||||
onError={() => setUnavailable(true)}
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Image answer</DialogTitle>
|
||||
<DialogDescription>{label}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
width={1200}
|
||||
height={800}
|
||||
unoptimized
|
||||
className="max-h-[70vh] w-full rounded-md object-contain"
|
||||
onError={() => setUnavailable(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,9 +47,12 @@ export default async function SubmissionDetailPage({
|
||||
where: (a, { eq }) => eq(a.submissionId, submission.id),
|
||||
});
|
||||
|
||||
const answersMap: Record<string, string> = {};
|
||||
const answersMap: Record<string, { value: string; imageCdnId: string | null }> = {};
|
||||
for (const ans of answersList) {
|
||||
answersMap[ans.questionId] = ans.value;
|
||||
answersMap[ans.questionId] = {
|
||||
value: ans.value,
|
||||
imageCdnId: ans.imageCdnId ?? null,
|
||||
};
|
||||
}
|
||||
const submissionsList = await db.query.submissions.findMany({
|
||||
where: (s, { eq }) => eq(s.formId, formId),
|
||||
|
||||
Reference in New Issue
Block a user