This commit is contained in:
2026-05-03 17:01:46 +03:30
parent b5ad5420b2
commit 9c30295b4b
76 changed files with 7891 additions and 461 deletions

View File

@@ -0,0 +1,254 @@
"use client";
import { useState, useEffect, useCallback } from "react";
type Question = { id: string; questionText: string; options: string[]; order: number };
type Quiz = {
id: string;
windowStart: string | Date;
windowEnd: string | Date;
isProcessed: boolean;
questions: Question[];
};
function useCountdown(target: Date) {
const calc = useCallback(() => Math.max(0, target.getTime() - Date.now()), [target]);
const [ms, setMs] = useState(calc);
useEffect(() => {
const t = setInterval(() => setMs(calc()), 1000);
return () => clearInterval(t);
}, [calc]);
const s = Math.floor(ms / 1000);
return { hours: Math.floor(s / 3600), minutes: Math.floor((s % 3600) / 60), seconds: s % 60, done: ms === 0 };
}
function CountdownUnit({ value, label }: { value: number; label: string }) {
return (
<div className="flex flex-col items-center">
<div className="bg-white/10 backdrop-blur border border-white/20 rounded-xl w-16 h-16 flex items-center justify-center text-2xl font-bold tabular-nums">
{String(value).padStart(2, "0")}
</div>
<span className="text-xs text-gray-400 mt-1">{label}</span>
</div>
);
}
export default function DailyQuizClient({
quiz,
alreadySubmitted,
}: {
quiz: Quiz | null;
alreadySubmitted: boolean;
}) {
const [answers, setAnswers] = useState<(number | null)[]>(
quiz ? Array(quiz.questions.length).fill(null) : []
);
const [step, setStep] = useState(0);
const [submitted, setSubmitted] = useState(alreadySubmitted);
const [result, setResult] = useState<{
score: number;
correct: number;
total: number;
rewardTier: "GOLD" | "SILVER" | "BRONZE" | null;
rewardTierLabel: string | null;
} | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const windowEnd = quiz ? new Date(quiz.windowEnd) : new Date();
const windowStart = quiz ? new Date(quiz.windowStart) : new Date();
const countdown = useCountdown(windowEnd);
const startCountdown = useCountdown(windowStart);
const now = new Date();
const isProcessed = quiz?.isProcessed ?? false;
const isActive = quiz ? !isProcessed && now >= windowStart && now <= windowEnd : false;
const notStarted = quiz ? !isProcessed && now < windowStart : false;
async function handleSubmit() {
if (!quiz) return;
if (answers.some((a) => a === null)) {
setError("لطفاً به همه سوالات پاسخ دهید");
return;
}
setLoading(true);
setError("");
const res = await fetch("/api/quiz/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ quizId: quiz.id, answers }),
});
const data = await res.json();
if (res.ok) {
setResult(data);
setSubmitted(true);
} else {
setError(data.error ?? "خطا");
}
setLoading(false);
}
if (!quiz) {
return (
<div className="max-w-lg mx-auto text-center py-20">
<div className="text-6xl mb-4">*</div>
<h1 className="text-2xl font-bold mb-2">کوییزی برای امروز وجود ندارد</h1>
<p className="text-gray-400">فردا دوباره بیا</p>
</div>
);
}
if (submitted) {
return (
<div className="max-w-lg mx-auto text-center py-20">
<div className="bg-white/5 backdrop-blur border border-white/10 rounded-2xl p-8">
{result ? (
<>
<div className="text-6xl mb-4">{result.score === 100 ? "*" : result.score >= 50 ? "+" : "-"}</div>
<h2 className="text-2xl font-bold mb-2">نتیجه شما</h2>
<div className="text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-amber-500 my-4">
{result.score}%
</div>
<p className="text-gray-300">{result.correct} از {result.total} سوال صحیح</p>
{result.score === 100 && (
<p className="mt-4 text-green-400 font-medium">شما در قرعهکشی Golden Card شرکت دارید</p>
)}
</>
) : (
<>
<div className="text-5xl mb-4">OK</div>
<h2 className="text-xl font-bold">پاسخهای شما ثبت شد</h2>
</>
)}
</div>
</div>
);
}
if (notStarted) {
return (
<div className="max-w-lg mx-auto text-center py-20">
<div className="bg-white/5 backdrop-blur border border-white/10 rounded-2xl p-8">
<p className="text-gray-400 mb-4">کوییز هنوز شروع نشده و در این زمان باز میشود:</p>
<div className="flex justify-center gap-3">
<CountdownUnit value={startCountdown.hours} label="ساعت" />
<CountdownUnit value={startCountdown.minutes} label="دقیقه" />
<CountdownUnit value={startCountdown.seconds} label="ثانیه" />
</div>
</div>
</div>
);
}
if (!isActive) {
return (
<div className="max-w-lg mx-auto text-center py-20">
<div className="text-5xl mb-4">!</div>
<h2 className="text-xl font-bold">
{isProcessed ? "این کوییز بعد از قرعه‌کشی بسته شده است" : "بازه زمانی کوییز به پایان رسیده"}
</h2>
</div>
);
}
const q = quiz.questions[step];
const progress = ((step + 1) / quiz.questions.length) * 100;
return (
<div className="max-w-xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold mb-1">کوییز روزانه</h1>
<p className="text-gray-400 text-sm">پاسخ صحیح = شانس برنده شدن Golden Card</p>
</div>
<div className="flex justify-center gap-3 mb-8">
<CountdownUnit value={countdown.hours} label="ساعت" />
<CountdownUnit value={countdown.minutes} label="دقیقه" />
<CountdownUnit value={countdown.seconds} label="ثانیه" />
</div>
<div className="mb-6">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>سوال {step + 1} از {quiz.questions.length}</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-yellow-400 to-amber-500 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
</div>
<div className="bg-white/5 backdrop-blur border border-white/10 rounded-2xl p-6 mb-4">
<p className="text-lg font-medium mb-6 leading-relaxed">{q.questionText}</p>
<div className="flex flex-col gap-3">
{q.options.map((opt, oi) => (
<button
key={oi}
type="button"
onClick={() => {
const updated = [...answers];
updated[step] = oi;
setAnswers(updated);
}}
className={`text-right px-4 py-3 rounded-xl border transition-all text-sm ${
answers[step] === oi
? "border-yellow-400 bg-yellow-400/10 text-yellow-300"
: "border-white/10 hover:border-white/30 hover:bg-white/5"
}`}
>
<span className="font-bold ml-2 text-gray-400">{["الف", "ب", "ج", "د"][oi]})</span>
{opt}
</button>
))}
</div>
</div>
{error && <p className="text-red-400 text-sm text-center mb-3">{error}</p>}
<div className="flex gap-3">
{step > 0 && (
<button
type="button"
onClick={() => setStep(step - 1)}
className="flex-1 py-3 rounded-xl border border-white/20 hover:bg-white/5 transition text-sm"
>
قبلی
</button>
)}
{step < quiz.questions.length - 1 ? (
<button
type="button"
onClick={() => {
if (answers[step] === null) {
setError("لطفاً یک گزینه انتخاب کنید");
return;
}
setError("");
setStep(step + 1);
}}
className="flex-1 py-3 rounded-xl bg-gradient-to-r from-yellow-500 to-amber-500 text-black font-bold hover:opacity-90 transition text-sm"
>
بعدی
</button>
) : (
<button
type="button"
onClick={handleSubmit}
disabled={loading}
className="flex-1 py-3 rounded-xl bg-gradient-to-r from-green-500 to-emerald-500 text-white font-bold hover:opacity-90 transition text-sm disabled:opacity-50"
>
{loading ? "در حال ارسال..." : "ثبت پاسخ‌ها"}
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,115 @@
import { requireAuth } from "@/lib/session";
import { db } from "@/lib/db";
import { formatPersianDate, formatPersianDateTime } from "@/lib/persianDate";
export default async function QuizHistoryPage() {
const session = await requireAuth();
const userId = (session.user as any).id;
const submissions = await db.quizSubmission.findMany({
where: { userId },
include: {
quiz: {
include: { questions: { orderBy: { order: "asc" } } },
},
},
orderBy: { submittedAt: "desc" },
});
return (
<div className="min-h-screen bg-gray-950 text-white py-10 px-4">
<div className="max-w-3xl mx-auto">
<h1 className="text-2xl font-bold mb-6">تاریخچه کوییزها</h1>
{submissions.length === 0 && (
<div className="text-center py-20 text-gray-500">
<div className="text-5xl mb-4">📋</div>
<p>هنوز در هیچ کوییزی شرکت نکردهاید</p>
</div>
)}
<div className="flex flex-col gap-4">
{submissions.map((sub) => {
const correct = sub.answers.filter((ans, i) => ans === sub.quiz.questions[i]?.correctAnswer).length;
const total = sub.quiz.questions.length;
return (
<div
key={sub.id}
className={`bg-white/5 backdrop-blur border rounded-2xl p-5 ${
sub.score === 100 ? "border-yellow-400/30" : "border-white/10"
}`}
>
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-bold text-lg">
{formatPersianDate(new Date(sub.quiz.date))}
</p>
<p className="text-xs text-gray-400">
{formatPersianDateTime(new Date(sub.submittedAt))}
</p>
</div>
<div className="text-left">
<div
className={`text-2xl font-black ${
sub.score === 100
? "text-yellow-400"
: sub.score >= 50
? "text-green-400"
: "text-red-400"
}`}
>
{sub.score}%
</div>
<p className="text-xs text-gray-400">
{correct} از {total}
</p>
</div>
</div>
{sub.score === 100 && (
<div className="bg-yellow-400/10 border border-yellow-400/30 rounded-lg px-3 py-2 text-xs text-yellow-300 flex items-center gap-2">
<span>🏆</span>
<span>واجد شرایط قرعهکشی Golden Card</span>
</div>
)}
{/* Show answers */}
<details className="mt-4">
<summary className="cursor-pointer text-sm text-gray-400 hover:text-white transition">
مشاهده جزئیات
</summary>
<div className="mt-3 flex flex-col gap-3">
{sub.quiz.questions.map((q, i) => {
const userAnswer = sub.answers[i];
const isCorrect = userAnswer === q.correctAnswer;
return (
<div
key={q.id}
className={`border rounded-lg p-3 text-sm ${
isCorrect ? "border-green-500/30 bg-green-500/5" : "border-red-500/30 bg-red-500/5"
}`}
>
<p className="font-medium mb-2">{q.questionText}</p>
<div className="flex flex-col gap-1 text-xs">
<p className={isCorrect ? "text-green-400" : "text-red-400"}>
پاسخ شما: {q.options[userAnswer ?? 0]} {isCorrect ? "✓" : "✗"}
</p>
{!isCorrect && (
<p className="text-green-400">پاسخ صحیح: {q.options[q.correctAnswer]}</p>
)}
</div>
</div>
);
})}
</div>
</details>
</div>
);
})}
</div>
</div>
</div>
);
}

36
app/(user)/quiz/page.tsx Normal file
View File

@@ -0,0 +1,36 @@
import { requireAuth } from "@/lib/session";
import { db } from "@/lib/db";
import DailyQuizClient from "./DailyQuizClient";
export default async function QuizPage() {
const session = await requireAuth();
const userId = (session.user as any).id;
const now = new Date();
const todayStart = new Date(now);
todayStart.setHours(0, 0, 0, 0);
const todayEnd = new Date(now);
todayEnd.setHours(23, 59, 59, 999);
const quiz = await db.dailyQuiz.findFirst({
where: { date: { gte: todayStart, lte: todayEnd } },
include: {
questions: {
orderBy: { order: "asc" },
select: { id: true, questionText: true, options: true, order: true },
},
},
});
const alreadySubmitted = quiz
? !!(await db.quizSubmission.findUnique({
where: { userId_quizId: { userId, quizId: quiz.id } },
}))
: false;
return (
<div className="min-h-screen bg-gray-950 text-white py-10 px-4">
<DailyQuizClient quiz={quiz} alreadySubmitted={alreadySubmitted} />
</div>
);
}