cpns / apps /web /src /components /attempt /QuestionReviewCard.tsx
pramudya-yb
Rename project from Labas to Pram
2b62b0c
Raw
History Blame Contribute Delete
11.6 kB
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
import { Button } from "@pram/ui/components/button";
import { Input } from "@pram/ui/components/input";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { formatTime } from "@/lib/time";
import type { Question } from "@/lib/types";
import { OptionDisplay } from "./OptionDisplay";
interface QuestionReviewCardProps {
q: Question;
secIdx: number;
qIdx: number;
ans: { questionId: string; userAnswer?: string; answer?: string; isCorrect?: boolean | null; partialScore?: number | null; timeSpentSec?: number } | null;
userId?: string;
isExpanded: boolean;
onToggleExpand: () => void;
}
export function QuestionReviewCard({
q,
secIdx,
qIdx,
ans,
userId,
isExpanded,
onToggleExpand,
}: QuestionReviewCardProps) {
const isCorrect = ans?.isCorrect;
const userAnswer = ans?.userAnswer ?? "Tidak dijawab";
const isOwner = q.creatorUserId === userId;
const hasOptions = Array.isArray(q.options) && q.options.length > 0;
const [isEditing, setIsEditing] = useState(false);
const [editPassage, setEditPassage] = useState(q.passageText ?? "");
const [editCorrectAnswer, setEditCorrectAnswer] = useState(q.correctAnswer ?? "");
const [editExplanation, setEditExplanation] = useState(q.explanation ?? "");
const feedbackQuery = useQuery(
trpc.feedback.getQuestionFeedback.queryOptions(
{ questionId: q.id },
{ enabled: !!q.id },
),
);
const voteMutation = useMutation({
...trpc.feedback.voteQuestion.mutationOptions(),
onSuccess: () => feedbackQuery.refetch(),
});
const updateQuestionMutation = useMutation({
...trpc.question.update.mutationOptions(),
onSuccess: () => {
setIsEditing(false);
window.location.reload();
},
});
const handleSaveEdit = () => {
updateQuestionMutation.mutate({
id: q.id,
passageText: editPassage,
correctAnswer: editCorrectAnswer,
explanation: editExplanation,
});
};
return (
<div
className={`bg-[var(--pure-white)] border-2 rounded-[var(--radius-xl)] clay-shadow transition-all ${
isCorrect === true
? "border-[var(--matcha-400)]"
: isCorrect === false
? "border-[var(--pomegranate-400)]"
: "border-[var(--oat-border)]"
}`}
>
{/* Header — always visible, clickable to expand */}
<button
onClick={onToggleExpand}
className="w-full text-left p-4 flex items-start gap-3 cursor-pointer"
>
<span
className={`w-8 h-8 rounded-full text-xs flex items-center justify-center font-bold shrink-0 ${
isCorrect === true
? "bg-[var(--matcha-600)] text-[var(--pure-white)]"
: isCorrect === false
? "bg-[var(--pomegranate-400)] text-[var(--pure-white)]"
: "bg-[var(--oat-light)] text-[var(--warm-charcoal)]"
}`}
>
{secIdx + 1}.{qIdx + 1}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[var(--clay-black)] truncate">
{q.questionText}
</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--oat-light)] text-[var(--warm-charcoal)]">
{(q.format ?? "").replace(/_/g, " ")}
</span>
{ans?.timeSpentSec != null && (
<span className="text-[10px] text-[var(--warm-silver)] flex items-center gap-0.5">
<MaterialIcon name="timer" className="text-[10px]" />
{formatTime(ans.timeSpentSec)}
</span>
)}
</div>
</div>
<span className="shrink-0 flex items-center gap-1.5 text-xs text-[var(--warm-silver)]">
{isCorrect === true && (
<MaterialIcon name="check_circle" className="text-sm text-[var(--matcha-600)]" />
)}
{isCorrect === false && (
<MaterialIcon name="cancel" className="text-sm text-[var(--pomegranate-400)]" />
)}
<MaterialIcon
name={isExpanded ? "expand_less" : "expand_more"}
className="text-sm"
/>
</span>
</button>
{/* Expanded content */}
{isExpanded && (
<div className="px-4 pb-4 pl-11 space-y-3">
{/* Passage Text */}
{q.passageText && (
<div className="bg-[var(--oat-light)] rounded-[var(--radius-lg)] p-3 text-sm text-[var(--warm-charcoal)] whitespace-pre-wrap leading-relaxed">
<span className="font-semibold text-[var(--clay-black)] block mb-1">Teks Bacaan:</span>
{q.passageText}
</div>
)}
{/* Interactive Option Display */}
{hasOptions && (
<OptionDisplay
format={q.format ?? ""}
options={q.options}
correctAnswer={q.correctAnswer ?? ""}
userAnswer={userAnswer}
/>
)}
{/* Owner Inline Edit Form */}
{isEditing && isOwner && (
<div className="space-y-3 bg-[var(--badge-blue-bg)] rounded-[var(--radius-lg)] p-4 border-2 border-[var(--badge-blue-bg)]">
<p className="text-sm font-semibold text-[var(--badge-blue-text)] flex items-center gap-2">
<MaterialIcon name="edit_note" className="text-sm" />
Koreksi Soal
</p>
<div>
<label className="text-xs font-medium text-[var(--warm-charcoal)] mb-1 block">Teks Bacaan</label>
<textarea
value={editPassage}
onChange={(e) => setEditPassage(e.target.value)}
className="w-full min-h-[80px] p-2 rounded-[var(--radius-md)] border-2 border-[var(--oat-border)] bg-[var(--pure-white)] text-sm text-[var(--clay-black)] resize-y"
/>
</div>
<div>
<label className="text-xs font-medium text-[var(--warm-charcoal)] mb-1 block">Jawaban Benar</label>
<Input
value={editCorrectAnswer}
onChange={(e) => setEditCorrectAnswer(e.target.value)}
className="h-9 text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-[var(--warm-charcoal)] mb-1 block">Penjelasan</label>
<textarea
value={editExplanation}
onChange={(e) => setEditExplanation(e.target.value)}
className="w-full min-h-[60px] p-2 rounded-[var(--radius-md)] border-2 border-[var(--oat-border)] bg-[var(--pure-white)] text-sm text-[var(--clay-black)] resize-y"
/>
</div>
<div className="flex gap-2">
<Button
onClick={handleSaveEdit}
disabled={updateQuestionMutation.isPending}
className="h-9 text-sm bg-[var(--clay-black)] text-[var(--pure-white)] hover:bg-[var(--warm-charcoal)] rounded-[var(--radius-lg)]"
>
<MaterialIcon name="save" className="text-sm mr-1" />
Simpan
</Button>
<Button
variant="outline"
onClick={() => setIsEditing(false)}
className="h-9 text-sm rounded-[var(--radius-lg)] border-2 border-[var(--oat-border)]"
>
Batal
</Button>
</div>
</div>
)}
{/* Answer Comparison */}
<div className="flex flex-wrap gap-4 text-sm">
<div>
<span className="text-[var(--warm-silver)]">Jawaban Anda:</span>{" "}
<span
className={`font-semibold ${
isCorrect === true
? "text-[var(--matcha-700)]"
: isCorrect === false
? "text-[var(--pomegranate-600)]"
: "text-[var(--warm-charcoal)]"
}`}
>
{userAnswer}
</span>
</div>
{ans?.partialScore != null && ans?.partialScore < 100 && (
<div>
<span className="text-[var(--warm-silver)]">Skor Parsial:</span>{" "}
<span className="font-semibold text-[var(--lemon-700)]">
{ans.partialScore}%
</span>
</div>
)}
{(isCorrect === false || isCorrect === null) && (
<div>
<span className="text-[var(--warm-silver)]">Jawaban Benar:</span>{" "}
<span className="font-semibold text-[var(--matcha-700)]">
{q.correctAnswer}
</span>
</div>
)}
{ans?.timeSpentSec != null && (
<div className="flex items-center gap-1 text-[var(--warm-silver)]">
<MaterialIcon name="timer" className="text-xs" />
<span>{formatTime(ans.timeSpentSec)}</span>
</div>
)}
</div>
{/* Explanation */}
{q.explanation && !isEditing && (
<div className="bg-[var(--oat-light)] rounded-[var(--radius-lg)] p-3 text-sm text-[var(--warm-charcoal)]">
<span className="font-semibold text-[var(--clay-black)]">Penjelasan:</span>{" "}
{q.explanation}
</div>
)}
{/* Thumbs Feedback */}
<div className="flex items-center gap-3 pt-1">
<button
onClick={() => voteMutation.mutate({ questionId: q.id, type: "up" })}
className={`flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium transition-all ${
feedbackQuery.data?.myFeedback === "up"
? "bg-[var(--matcha-300)] text-[var(--matcha-800)]"
: "bg-[var(--oat-light)] text-[var(--warm-charcoal)] hover:bg-[var(--matcha-300)] hover:text-[var(--matcha-800)]"
}`}
>
<MaterialIcon name="thumb_up" className="text-sm" />
<span>{feedbackQuery.data?.up ?? 0}</span>
</button>
<button
onClick={() => voteMutation.mutate({ questionId: q.id, type: "down" })}
className={`flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium transition-all ${
feedbackQuery.data?.myFeedback === "down"
? "bg-[var(--pomegranate-100)] text-[var(--pomegranate-600)]"
: "bg-[var(--oat-light)] text-[var(--warm-charcoal)] hover:bg-[var(--pomegranate-100)] hover:text-[var(--pomegranate-600)]"
}`}
>
<MaterialIcon name="thumb_down" className="text-sm" />
<span>{feedbackQuery.data?.down ?? 0}</span>
</button>
{isOwner && (
<button
onClick={() => setIsEditing(true)}
className="ml-auto flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium bg-[var(--oat-light)] text-[var(--warm-charcoal)] hover:bg-[var(--badge-blue-bg)] hover:text-[var(--badge-blue-text)] transition-all"
>
<MaterialIcon name="edit" className="text-sm" />
Koreksi
</button>
)}
</div>
</div>
)}
</div>
);
}