FeedbackBar: like/dislike/report under every quiz question and lesson

This commit is contained in:
2026-10-12 12:56:52 +03:30
parent 41f1c993ee
commit 3a2a685ed3
4 changed files with 148 additions and 69 deletions

View File

@@ -12,6 +12,7 @@ import { toFa } from '@/lib/format';
import { MathText } from '@/lib/MathText';
import { Diagram } from '@/diagram';
import { StepText } from '@/lib/StepText';
import { FeedbackBar } from '@/components/FeedbackBar';
/** معرفیِ درس، پیش از تمرین. */
export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?: string }) {
@@ -88,6 +89,7 @@ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?:
)}
<div className="mt-auto flex flex-col gap-3 pt-2">
<FeedbackBar kind="lesson" id={String(lessonId)} />
{data.questionCount === 0 ? (
<div className="rounded-2xl border-2 border-dashed border-[#dedced] bg-white py-4 text-center text-sm font-bold text-muted">
تمرینِ این درس به‌زودی اضافه می‌شه

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { Brain, Check, ChevronLeft, Coins, Compass, Flag, Heart, Infinity as InfinityIcon, Lightbulb, Ruler, Sparkles, Star, Target, ThumbsUp, X, Zap, type LucideIcon } from 'lucide-react';
import { api, ApiError, type AnswerResult, type Hearts, type Question, type ReportReason } from '@/api';
import { Brain, Check, ChevronLeft, Coins, Compass, Heart, Infinity as InfinityIcon, Lightbulb, Ruler, Sparkles, Star, Target, ThumbsUp, X, Zap, type LucideIcon } from 'lucide-react';
import { api, ApiError, type AnswerResult, type Hearts, type Question } from '@/api';
import { useAppData } from '@/state/AppData';
import { fetchHome } from '@/state/subject';
import { findNode, findNodeOfLesson, nextLessonOf, unitColor } from '@/state/curriculum';
@@ -17,6 +17,7 @@ import { friendlyError } from '@/lib/errors';
import { toFa } from '@/lib/format';
import { MathText } from '@/lib/MathText';
import { Diagram } from '@/diagram';
import { FeedbackBar } from '@/components/FeedbackBar';
import { sounds } from '@/lib/sound';
import { useGateHold } from '@/lib/useAsync';
import { StreakCelebration, StreakFlame } from '@/components/StreakFlame';
@@ -69,7 +70,6 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
const [submitting, setSubmitting] = useState(false);
const [correctCount, setCorrectCount] = useState(0);
const [confirmExit, setConfirmExit] = useState(false);
const [reporting, setReporting] = useState(false);
const [reload, setReload] = useState(0);
// پشتِ هم درست؛ برای «سه تا پشتِ هم!» (حسِ پیشرفت وسطِ آزمون، نه فقط آخرش).
const [combo, setCombo] = useState(0);
@@ -344,9 +344,6 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
<div className="rounded-3xl border border-line bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium text-muted">سوالِ {toFa(index + 1)}</span>
<button type="button" onClick={() => setReporting(true)} className="flex items-center gap-1 text-[11px] font-bold text-faint">
<Flag size={12} /> گزارشِ ایراد
</button>
</div>
<h2 className="whitespace-pre-line text-base font-bold leading-8 text-ink"><MathText text={question.prompt} /></h2>
{question.figure ? <div className="mt-3"><Diagram spec={question.figure} /></div> : null}
@@ -370,6 +367,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
))}
</div>
<FeedbackBar kind="question" id={question.id} />
</div>
{answer && !answer.correct && look.char === 'pi' && (
@@ -411,7 +409,6 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
</div>
</Sheet>
<ReportSheet open={reporting} questionId={question.id} onClose={() => setReporting(false)} />
</Screen>
);
}
@@ -569,68 +566,6 @@ function TraitChip({ char, color, correct, combo }: { char: string; color: strin
return null;
}
const REASONS: { id: ReportReason; label: string }[] = [
{ id: 'wrong_answer', label: 'جوابِ درست اشتباهه' },
{ id: 'unclear', label: 'صورتِ سوال گنگه' },
{ id: 'typo', label: 'غلطِ تایپی داره' },
{ id: 'too_hard', label: 'برای این درس خیلی سخته' },
{ id: 'other', label: 'یه چیزِ دیگه' },
];
/** گزارشِ سوالِ ایراددار؛ مستقیم به صفِ بررسیِ محتوا می‌رود. */
function ReportSheet({ open, questionId, onClose }: { open: boolean; questionId: string; onClose: () => void }) {
const toast = useToast();
const [reason, setReason] = useState<ReportReason | null>(null);
const [note, setNote] = useState('');
const [sending, setSending] = useState(false);
useEffect(() => {
if (open) {
setReason(null);
setNote('');
}
}, [open]);
async function send() {
if (!reason) return;
setSending(true);
try {
const r = await api.lesson.report(questionId, reason, note.trim());
toast(r.alreadyReported ? 'قبلاً گزارشش رو داده بودی؛ ممنون!' : 'ممنون! گزارشت رسید و بررسی می‌شه.', 'success');
onClose();
} catch (err) {
toast(friendlyError(err, 'گزارش ارسال نشد'), 'error');
} finally {
setSending(false);
}
}
return (
<Sheet open={open} onClose={onClose} title="این سوال چه ایرادی داره؟">
<div className="mb-4 flex flex-col gap-2">
{REASONS.map((r) => (
<button
key={r.id}
type="button"
onClick={() => setReason(r.id)}
className={`rounded-2xl border-2 px-4 py-3 text-right text-sm font-bold ${reason === r.id ? 'border-coral bg-[#fff4f2] text-coral' : 'border-line text-body'}`}
>
{r.label}
</button>
))}
</div>
<textarea
value={note}
onChange={(e) => setNote(e.target.value.slice(0, 500))}
placeholder="توضیحِ بیشتر (اختیاری)"
rows={2}
className="mb-4 w-full resize-none rounded-2xl border-2 border-line px-4 py-3 text-sm outline-none focus:border-coral"
/>
<PrimaryButton disabled={!reason} loading={sending} onClick={() => void send()}>ارسالِ گزارش</PrimaryButton>
</Sheet>
);
}
/** آیکنِ کوچکِ بعد از جواب (همان خانواده‌ی آیکنِ اپ، نه ایموجیِ کیبورد)؛ هر سوال یکی از مجموعه، با یک تکانِ کوتاه. */
const WIN: [LucideIcon, string][] = [[Brain, '#9B7BFF'], [Zap, '#E8A400'], [Target, '#ED6956'], [ThumbsUp, '#3E9E73'], [Sparkles, '#4C5FD5']];
const MISS: [LucideIcon, string][] = [[Lightbulb, '#E8A400'], [Ruler, '#4C5FD5'], [Compass, '#9B7BFF']];