Implement local browser caching for shared groups and UI improvements
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
This commit is contained in:
parent
42ed0d275b
commit
7af0935657
37 changed files with 6141 additions and 411 deletions
|
|
@ -47,6 +47,15 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [resultsData, setResultsData] = useState<any | null>(null);
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
|
||||
// Auto-hide toast
|
||||
useEffect(() => {
|
||||
if (toastMessage) {
|
||||
const timer = setTimeout(() => setToastMessage(null), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [toastMessage]);
|
||||
|
||||
// Initialize session
|
||||
useEffect(() => {
|
||||
|
|
@ -67,6 +76,25 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
return;
|
||||
}
|
||||
|
||||
// Shared links: load from localStorage
|
||||
if (isShared) {
|
||||
try {
|
||||
const saved = localStorage.getItem(`quiz_progress_${quiz.id}`);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
setOrder(parsed.order || quiz.questions.map(q => q.id));
|
||||
setCurrentIndex(parsed.currentIndex || 0);
|
||||
setAnswers(parsed.answers || {});
|
||||
setSubmittedAnswers(Object.keys(parsed.answers || {}));
|
||||
setToastMessage("Session restored");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to defaults
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, normal sequential logic checking for progress
|
||||
const prog = quiz.progress?.find((p) => p.mode === "SEQUENTIAL");
|
||||
if (prog) {
|
||||
|
|
@ -96,14 +124,23 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
setSubmittedAnswers([]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [quiz, retakeIds]);
|
||||
}, [quiz, retakeIds, isShared]);
|
||||
|
||||
// Persist progress as you go
|
||||
function saveProgress(
|
||||
idx: number,
|
||||
ans: Record<string, string[]>
|
||||
) {
|
||||
if (retakeIds || isShared) return; // Don't persist retake partial state or shared mode into the main progress row
|
||||
if (retakeIds) return; // Don't persist retake partial state
|
||||
|
||||
if (isShared) {
|
||||
localStorage.setItem(`quiz_progress_${quiz.id}`, JSON.stringify({
|
||||
currentIndex: idx,
|
||||
order: order,
|
||||
answers: ans,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/progress", {
|
||||
method: "PATCH",
|
||||
|
|
@ -172,6 +209,10 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
}
|
||||
|
||||
async function handleFinish() {
|
||||
if (isShared) {
|
||||
localStorage.removeItem(`quiz_progress_${quiz.id}`);
|
||||
}
|
||||
|
||||
// If shared, grade it locally and show results, don't ping backend
|
||||
if (isShared) {
|
||||
// Simulate an attempt object
|
||||
|
|
@ -268,10 +309,18 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center relative">
|
||||
{/* Toast Notification */}
|
||||
{toastMessage && (
|
||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-30 animate-fade-in pointer-events-none">
|
||||
<div className="bg-bg-surface-alt/95 backdrop-blur shadow-[var(--shadow-card)] text-text-heading text-sm font-semibold px-6 py-2.5 rounded-full whitespace-nowrap">
|
||||
{toastMessage}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Unified Card */}
|
||||
<div className="w-full max-w-3xl bg-bg-surface-alt rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8">
|
||||
<div className="w-full max-w-3xl bg-bg-surface-alt rounded-2xl border border-border-light shadow-[var(--shadow-card)] overflow-hidden mb-8 mt-12">
|
||||
|
||||
{/* Progress Header */}
|
||||
<div className="px-6 md:px-8 py-5 flex items-center gap-4">
|
||||
|
|
@ -434,7 +483,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
|
|||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={currentSelections.length === 0}
|
||||
className="w-full py-3.5 rounded-xl bg-primary/60 hover:bg-primary/80 text-white font-bold text-lg tracking-wide disabled:opacity-50 disabled:hover:bg-primary/60 transition-all duration-200 cursor-pointer shadow-sm"
|
||||
className="w-full py-3.5 rounded-xl bg-primary hover:bg-primary-hover text-white font-bold text-lg tracking-wide disabled:opacity-50 disabled:bg-primary/50 disabled:hover:bg-primary/50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm"
|
||||
>
|
||||
Submit Answer
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue