- Text: kasra/fatha/damma/shadda/sukun/hamza-above and «هی» ezafe removed from every user-visible string (comments untouched); tanwin kept. Stale copy fixed (question packs are no longer sold), awkward onboarding sentences rewritten, سؤال → سوال. plainPersian() in MathText cleans the live tutor stream. - Home: the mascot + speech bubble header is gone; the selected companion hops beside the current island and sits at every third bend (CSS transform animations only). - Checkout: order summary, payment-method rows with Zarinpal/Bazaar icons and radio, amount on the pay button, lock trust line, darker overlay. paymentMethods in config.js per channel (web: zarinpal, app: zarinpal+bazaar by default). - Profile: «ورود با حساب دیگر» removed (it was logout); new «حساب کاربری» sheet shows phone/ID and holds «حذف حساب کاربری»; logout is a red row. Delete sheet tells Bazaar subscribers to cancel renewal in Bazaar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.4 KiB
TypeScript
132 lines
5.4 KiB
TypeScript
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<Kind, { id: string; label: string }[]> = {
|
||
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 (
|
||
<>
|
||
<div className="flex gap-2" role="group" aria-label="نظر و گزارش">
|
||
<button type="button" aria-pressed={vote === 1} onClick={() => void cast(1)} className={`${base} ${vote === 1 ? 'border-[#5FBF8F] bg-[#EEF9F3] text-[#2f8a5f]' : idle}`}>
|
||
<ThumbsUp size={16} strokeWidth={2.4} /> خوب بود
|
||
</button>
|
||
<button type="button" aria-pressed={vote === -1} onClick={() => void cast(-1)} className={`${base} ${vote === -1 ? 'border-[#ED6956] bg-[#fff4f2] text-coral' : idle}`}>
|
||
<ThumbsDown size={16} strokeWidth={2.4} /> خوب نبود
|
||
</button>
|
||
<button type="button" onClick={() => setOpen(true)} className={`${base} ${idle}`}>
|
||
<Flag size={16} strokeWidth={2.4} /> گزارش ایراد
|
||
</button>
|
||
</div>
|
||
<ReportSheet open={open} kind={kind} id={id} onClose={() => setOpen(false)} />
|
||
</>
|
||
);
|
||
}
|
||
|
||
function ReportSheet({ open, kind, id, onClose }: { open: boolean; kind: Kind; id: string; onClose: () => void }) {
|
||
const toast = useToast();
|
||
const [reason, setReason] = useState<string | 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.feedback.report(kind, id, reason, note.trim());
|
||
toast(r.alreadyReported ? 'قبلاً گزارشش رو داده بودی؛ ممنون!' : 'ممنون! گزارشت رسید و بررسی میشه.', 'success');
|
||
onClose();
|
||
} catch (err) {
|
||
toast(friendlyError(err, 'گزارش ارسال نشد'), 'error');
|
||
} finally {
|
||
setSending(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Sheet open={open} onClose={onClose} title={kind === 'question' ? 'این سوال چه ایرادی داره؟' : 'این درس چه ایرادی داره؟'}>
|
||
<div className="mb-4 flex flex-col gap-2">
|
||
{REASONS[kind].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>
|
||
);
|
||
}
|