Implement local browser caching for shared groups and UI improvements
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s

This commit is contained in:
Elijah 2026-07-10 16:01:22 -07:00
parent 42ed0d275b
commit 7af0935657
37 changed files with 6141 additions and 411 deletions

View file

@ -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>