diff --git a/src/api/index.ts b/src/api/index.ts index 1125131..edda0f6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -121,6 +121,17 @@ export const api = { ), }, + /** لایک/دیسلایک و گزارشِ ایراد روی سوال و درس (۰ = پس‌گرفتنِ رأی). */ + feedback: { + vote: (kind: 'question' | 'lesson', id: string, value: 1 | -1 | 0) => + http.post<{ value: number }>(`/${kind === 'question' ? 'questions' : 'lessons'}/${seg(id)}/vote`, { value }), + report: (kind: 'question' | 'lesson', id: string, reason: string, note?: string) => + http.post<{ success: boolean; reportId: number; alreadyReported: boolean }>( + `/${kind === 'question' ? 'questions' : 'lessons'}/${seg(id)}/report`, + { reason, ...(note ? { note } : {}) }, + ), + }, + leaderboard: (period: T.LeaderboardPeriod) => http.get<{ period: string; entries: T.LeaderboardEntry[] }>(`/leaderboard${q({ period })}`).then((r) => r.entries), diff --git a/src/components/FeedbackBar.tsx b/src/components/FeedbackBar.tsx new file mode 100644 index 0000000..1402e76 --- /dev/null +++ b/src/components/FeedbackBar.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState } from 'react'; +import { Flag, ThumbsDown, ThumbsUp } from 'lucide-react'; +import { api } from '@/api'; +import { friendlyError } from '@/lib/errors'; +import { Sheet } from '@/components/Sheet'; +import { useToast } from '@/components/Toast'; +import { PrimaryButton } from '@/components/ui'; + +type Kind = 'question' | 'lesson'; + +const REASONS: Record = { + question: [ + { id: 'wrong_answer', label: 'جوابِ درست اشتباهه' }, + { id: 'unclear', label: 'صورتِ سوال گنگه' }, + { id: 'typo', label: 'غلطِ تایپی داره' }, + { id: 'too_hard', label: 'برای این درس خیلی سخته' }, + { id: 'other', label: 'یه چیزِ دیگه' }, + ], + lesson: [ + { id: 'wrong_content', label: 'مطلبِ درس اشتباهه' }, + { id: 'unclear', label: 'توضیح گنگه' }, + { id: 'typo', label: 'غلطِ تایپی داره' }, + { id: 'bad_figure', label: 'شکل ایراد داره' }, + { id: 'other', label: 'یه چیزِ دیگه' }, + ], +}; + +const memoKey = (kind: Kind, id: string) => `tadrisino.vote.${kind}.${id}`; +function readVote(kind: Kind, id: string): 1 | -1 | 0 { + try { + const v = Number(sessionStorage.getItem(memoKey(kind, id))); + return v === 1 || v === -1 ? v : 0; + } catch { + return 0; + } +} + +/** + * زیرِ هر سوال و هر درس: لایک، دیسلایک و گزارشِ ایراد. به ادمین می‌رسد + * (ستونِ 👍/👎 و صفِ گزارش‌ها) تا ایرادِ محتوا را خودِ دانش‌آموزها خبر بدهند. + */ +export function FeedbackBar({ kind, id }: { kind: Kind; id: string }) { + const toast = useToast(); + const [vote, setVote] = useState<1 | -1 | 0>(0); + const [open, setOpen] = useState(false); + + useEffect(() => setVote(readVote(kind, id)), [kind, id]); + + async function cast(next: 1 | -1) { + const value = vote === next ? 0 : next; + const before = vote; + setVote(value); + try { + await api.feedback.vote(kind, id, value); + try { sessionStorage.setItem(memoKey(kind, id), String(value)); } catch { /* حافظه‌ی مرورگر بسته است؛ فقط یادآوریِ همین نشست */ } + if (value !== 0) toast('ممنون از نظرت!', 'success'); + } catch (err) { + setVote(before); + toast(friendlyError(err, 'نظرت ثبت نشد'), 'error'); + } + } + + const base = 'flex min-h-11 flex-1 items-center justify-center gap-1.5 rounded-2xl border-2 px-2 text-xs font-black transition-colors'; + const idle = 'border-line bg-white text-muted'; + return ( + <> +
+ + + +
+ setOpen(false)} /> + + ); +} + +function ReportSheet({ open, kind, id, onClose }: { open: boolean; kind: Kind; id: string; onClose: () => void }) { + const toast = useToast(); + const [reason, setReason] = useState(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.feedback.report(kind, id, reason, note.trim()); + toast(r.alreadyReported ? 'قبلاً گزارشش رو داده بودی؛ ممنون!' : 'ممنون! گزارشت رسید و بررسی می‌شه.', 'success'); + onClose(); + } catch (err) { + toast(friendlyError(err, 'گزارش ارسال نشد'), 'error'); + } finally { + setSending(false); + } + } + + return ( + +
+ {REASONS[kind].map((r) => ( + + ))} +
+