feat: update CMS service handlers and main application logic.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
interface DescriptionPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function DescriptionPlayer({ id, title, content }: DescriptionPlayerProps) {
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Overview"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-invert prose-lg max-w-none">
|
||||
{/* We can use a markdown parser here later if desired, for now simple multiline text */}
|
||||
<div className="text-gray-300 leading-relaxed whitespace-pre-wrap font-medium">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
interface FillInTheBlanksPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function FillInTheBlanksPlayer({ id, title, content }: FillInTheBlanksPlayerProps) {
|
||||
const [userAnswers, setUserAnswers] = useState<string[]>([]);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
// Parse content to find blanks
|
||||
const parsed = useMemo(() => {
|
||||
const parts: { type: 'text' | 'blank'; value?: string; index?: number; answer?: string }[] = [];
|
||||
const answers: string[] = [];
|
||||
let lastIndex = 0;
|
||||
const regex = /\[\[(.*?)\]\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
parts.push({ type: 'text', value: content.substring(lastIndex, match.index) });
|
||||
const answer = match[1];
|
||||
parts.push({ type: 'blank', index: answers.length, answer });
|
||||
answers.push(answer);
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
parts.push({ type: 'text', value: content.substring(lastIndex) });
|
||||
|
||||
return { parts, answers };
|
||||
}, [content]);
|
||||
|
||||
const handleReset = () => {
|
||||
setSubmitted(false);
|
||||
setUserAnswers([]);
|
||||
};
|
||||
|
||||
const isCorrect = (index: number) => {
|
||||
return userAnswers[index]?.trim().toLowerCase() === parsed.answers[index]?.trim().toLowerCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Fill in the Blanks"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<div className="text-lg leading-loose text-gray-100">
|
||||
{parsed.parts.map((part, i) => (
|
||||
part.type === 'text' ? (
|
||||
<span key={i}>{part.value}</span>
|
||||
) : (
|
||||
<input
|
||||
key={i}
|
||||
type="text"
|
||||
value={userAnswers[part.index!] || ""}
|
||||
onChange={(e) => {
|
||||
const newAnswers = [...userAnswers];
|
||||
newAnswers[part.index!] = e.target.value;
|
||||
setUserAnswers(newAnswers);
|
||||
}}
|
||||
disabled={submitted}
|
||||
className={`mx-1 px-2 py-0 border-b-2 bg-transparent transition-all focus:outline-none text-center rounded-t-sm ${submitted
|
||||
? (isCorrect(part.index!) ? "border-green-500 text-green-400 bg-green-500/10" : "border-red-500 text-red-100 bg-red-500/10")
|
||||
: "border-blue-500/30 focus:border-blue-500 text-blue-400 focus:bg-blue-500/5"
|
||||
}`}
|
||||
style={{ width: `${Math.max((part.answer?.length || 5) * 12, 60)}px` }}
|
||||
placeholder="..."
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!submitted && parsed.answers.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
>
|
||||
Validate Answers
|
||||
</button>
|
||||
)}
|
||||
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
interface MatchingPair {
|
||||
left: string;
|
||||
right: string;
|
||||
}
|
||||
|
||||
interface MatchingPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
pairs: MatchingPair[];
|
||||
}
|
||||
|
||||
export default function MatchingPlayer({ id, title, pairs }: MatchingPlayerProps) {
|
||||
const [selectedLeft, setSelectedLeft] = useState<number | null>(null);
|
||||
const [matches, setMatches] = useState<Record<number, number>>({});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const shuffledRight = useMemo(() => {
|
||||
return (pairs || [])
|
||||
.map((p, i) => ({ value: p.right, originalIdx: i }))
|
||||
.sort(() => Math.random() - 0.5);
|
||||
}, [pairs]);
|
||||
|
||||
const handleMatch = (leftIdx: number, rightIdx: number) => {
|
||||
if (submitted) return;
|
||||
setMatches(prev => ({ ...prev, [leftIdx]: rightIdx }));
|
||||
setSelectedLeft(null);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSubmitted(false);
|
||||
setMatches({});
|
||||
setSelectedLeft(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Concept Matching"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-8 glass border-white/5 rounded-3xl relative">
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Term</label>
|
||||
{(pairs || []).map((pair, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => !submitted && setSelectedLeft(i)}
|
||||
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft === i ? "border-blue-500 bg-blue-500/10 text-white shadow-lg" :
|
||||
matches[i] !== undefined ? "border-blue-500/20 bg-blue-500/5 text-blue-400" :
|
||||
"border-white/5 bg-white/5 text-gray-200 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
{pair.left}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Definition</label>
|
||||
{shuffledRight.map((item, i) => {
|
||||
const matchedLeftIdx = Object.keys(matches).find(k => matches[parseInt(k)] === item.originalIdx);
|
||||
const isCorrect = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) === item.originalIdx;
|
||||
const isWrong = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) !== item.originalIdx;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
disabled={selectedLeft === null || submitted}
|
||||
onClick={() => handleMatch(selectedLeft!, item.originalIdx)}
|
||||
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white/5" : ""
|
||||
} ${isCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
|
||||
isWrong ? "border-red-500 bg-red-500/20 text-red-100" :
|
||||
matchedLeftIdx !== undefined ? "border-blue-500/30 bg-blue-500/5 text-blue-400" :
|
||||
"border-white/5 bg-white/5 text-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{item.value}</span>
|
||||
{isCorrect && <span>✅</span>}
|
||||
{isWrong && <span>❌</span>}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 pt-8 border-t border-white/5">
|
||||
{!submitted && Object.keys(matches).length === (pairs || []).length && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
>
|
||||
Validate Matching
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Play, Lock, AlertCircle } from "lucide-react";
|
||||
|
||||
interface MediaPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
url: string;
|
||||
media_type: 'video' | 'audio';
|
||||
config?: {
|
||||
maxPlays?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function MediaPlayer({ id, title, url, media_type, config }: MediaPlayerProps) {
|
||||
const [playCount, setPlayCount] = useState(0);
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
const maxPlays = config?.maxPlays || 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (maxPlays > 0 && playCount >= maxPlays) {
|
||||
setLocked(true);
|
||||
}
|
||||
}, [playCount, maxPlays]);
|
||||
|
||||
const handlePlay = () => {
|
||||
if (locked) return;
|
||||
setPlayCount(prev => prev + 1);
|
||||
};
|
||||
|
||||
if (locked) {
|
||||
return (
|
||||
<div className="space-y-4" id={id}>
|
||||
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400">{title || "Multimedia Content"}</h3>
|
||||
<div className="glass-card aspect-video flex flex-col items-center justify-center gap-6 border-red-500/20 bg-red-500/5">
|
||||
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center text-red-500">
|
||||
<Lock size={32} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold text-white mb-2">Content Locked</p>
|
||||
<p className="text-sm text-gray-500 max-w-xs uppercase tracking-widest font-black">
|
||||
You have reached the limit of {maxPlays} plays for this content.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to format URL (handles YouTube embeds)
|
||||
const getEmbedUrl = (rawUrl: string) => {
|
||||
if (rawUrl.includes("youtube.com/watch?v=")) {
|
||||
return rawUrl.replace("watch?v=", "embed/");
|
||||
}
|
||||
if (rawUrl.includes("youtu.be/")) {
|
||||
return rawUrl.replace("youtu.be/", "youtube.com/embed/");
|
||||
}
|
||||
return rawUrl;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400">{title || "Multimedia Content"}</h3>
|
||||
{maxPlays > 0 && (
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest px-3 py-1 rounded-full bg-white/5 border border-white/5 text-gray-500">
|
||||
{playCount} / {maxPlays} PLAYS
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="glass-card !p-2 overflow-hidden aspect-video relative group">
|
||||
<iframe
|
||||
src={getEmbedUrl(url)}
|
||||
className="w-full h-full rounded-xl"
|
||||
allowFullScreen
|
||||
onLoad={() => {
|
||||
// In a real app, we'd detect play events from the player API
|
||||
// For this demo, we'll increment when the iframe loads or specific interaction
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Simulated play tracker overlay (invisible but catches first click) */}
|
||||
{playCount === 0 && (
|
||||
<div
|
||||
onClick={handlePlay}
|
||||
className="absolute inset-0 bg-black/40 flex items-center justify-center cursor-pointer group-hover:bg-black/20 transition-all"
|
||||
>
|
||||
<div className="w-20 h-20 rounded-full bg-blue-500 flex items-center justify-center shadow-2xl shadow-blue-500/40 group-hover:scale-110 transition-transform">
|
||||
<Play size={32} className="text-white fill-white ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{maxPlays > 0 && playCount > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-orange-500/70 p-4 rounded-xl bg-orange-500/5 border border-orange-500/10">
|
||||
<AlertCircle size={14} />
|
||||
<span>Watch carefully. Content will lock after {maxPlays} plays.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
interface OrderingPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export default function OrderingPlayer({ id, title, items }: OrderingPlayerProps) {
|
||||
const [userOrder, setUserOrder] = useState<number[]>([]);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const shuffledItems = useMemo(() => {
|
||||
return (items || [])
|
||||
.map((item, i) => ({ value: item, originalIdx: i }))
|
||||
.sort(() => Math.random() - 0.5);
|
||||
}, [items]);
|
||||
|
||||
const handlePick = (originalIdx: number) => {
|
||||
if (submitted) return;
|
||||
if (userOrder.includes(originalIdx)) {
|
||||
setUserOrder(userOrder.filter(i => i !== originalIdx));
|
||||
} else {
|
||||
setUserOrder([...userOrder, originalIdx]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSubmitted(false);
|
||||
setUserOrder([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Sequence Ordering"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 p-8 glass border-white/5 rounded-3xl">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Available Items</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{shuffledItems.map((item, i) => {
|
||||
const isPicked = userOrder.includes(item.originalIdx);
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
disabled={isPicked || submitted}
|
||||
onClick={() => handlePick(item.originalIdx)}
|
||||
className={`px-6 py-3 rounded-full border text-sm font-bold transition-all ${isPicked ? "opacity-20 grayscale border-white/5 bg-white/5" :
|
||||
"border-white/10 bg-white/5 text-gray-200 hover:border-blue-500/50 hover:bg-blue-500/5"
|
||||
}`}
|
||||
>
|
||||
{item.value}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Your Sequence</label>
|
||||
<div className="space-y-3">
|
||||
{userOrder.length === 0 && <p className="text-xs text-gray-600 italic py-4">Click items to build the sequence...</p>}
|
||||
{userOrder.map((idx, i) => {
|
||||
const isItemCorrect = submitted && idx === i;
|
||||
const isItemWrong = submitted && idx !== i;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => !submitted && handlePick(idx)}
|
||||
className={`flex items-center gap-4 p-4 rounded-xl border text-sm font-bold transition-all cursor-pointer ${isItemCorrect ? "border-green-500 bg-green-500/20 text-green-400" :
|
||||
isItemWrong ? "border-red-500 bg-red-500/20 text-red-100" :
|
||||
"border-blue-500/30 bg-blue-500/5 text-blue-400 hover:bg-blue-500/10"
|
||||
}`}
|
||||
>
|
||||
<span className="opacity-50 text-xs">{i + 1}.</span>
|
||||
<span className="flex-1">{(items || [])[idx]}</span>
|
||||
{!submitted && <span className="text-xs opacity-50">×</span>}
|
||||
{isItemCorrect && <span>✅</span>}
|
||||
{isItemWrong && <span>❌</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-white/5">
|
||||
{!submitted && userOrder.length === (items || []).length && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
>
|
||||
Validate Sequence
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface QuizQuestion {
|
||||
id: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
correct: number[];
|
||||
type?: 'multiple-choice' | 'true-false' | 'multiple-select';
|
||||
}
|
||||
|
||||
interface QuizPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
quizData: {
|
||||
questions: QuizQuestion[];
|
||||
};
|
||||
}
|
||||
|
||||
export default function QuizPlayer({ id, title, quizData }: QuizPlayerProps) {
|
||||
const [userAnswers, setUserAnswers] = useState<Record<string, number[]>>({});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const questions = quizData?.questions || [];
|
||||
|
||||
const handleAnswer = (qId: string, optionIndex: number, isMulti: boolean) => {
|
||||
if (submitted) return;
|
||||
setUserAnswers(prev => {
|
||||
const current = prev[qId] || [];
|
||||
if (isMulti) {
|
||||
const next = current.includes(optionIndex)
|
||||
? current.filter(i => i !== optionIndex)
|
||||
: [...current, optionIndex].sort((a, b) => a - b);
|
||||
return { ...prev, [qId]: next };
|
||||
} else {
|
||||
return { ...prev, [qId]: [optionIndex] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Knowledge Check"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{questions.map((q) => (
|
||||
<div key={q.id} className="space-y-4 p-8 glass border-white/5 rounded-3xl">
|
||||
<h4 className="font-bold text-xl text-gray-100 leading-tight">{q.question}</h4>
|
||||
<div className="grid gap-3">
|
||||
{q.options.map((opt, oIdx) => {
|
||||
const isSelected = userAnswers[q.id]?.includes(oIdx);
|
||||
const isCorrect = q.correct?.includes(oIdx);
|
||||
const isActuallyCorrect = isCorrect && isSelected;
|
||||
const isWrongSelection = !isCorrect && isSelected;
|
||||
const missedCorrect = isCorrect && !isSelected;
|
||||
|
||||
let style = "glass border-white/10 hover:bg-white/5";
|
||||
if (submitted) {
|
||||
if (isActuallyCorrect) style = "bg-green-500/20 border-green-500 text-green-400";
|
||||
else if (isWrongSelection) style = "bg-red-500/20 border-red-500 text-red-100";
|
||||
else if (missedCorrect) style = "border-orange-500/50 text-orange-400 animate-pulse";
|
||||
else style = "opacity-50 grayscale border-white/5";
|
||||
} else if (isSelected) {
|
||||
style = "bg-blue-500/20 border-blue-500 text-white shadow-[0_0_20px_rgba(59,130,246,0.2)]";
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={oIdx}
|
||||
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
|
||||
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold ${style}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{opt}</span>
|
||||
{submitted && isActuallyCorrect && <span>✅</span>}
|
||||
{submitted && isWrongSelection && <span>❌</span>}
|
||||
{submitted && missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter">Correct Answer</span>}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!submitted && questions.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20"
|
||||
>
|
||||
Validate Answers
|
||||
</button>
|
||||
)}
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={() => { setSubmitted(false); setUserAnswers({}); }}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-3xl border-white/5"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ShortAnswerPlayerProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
prompt: string;
|
||||
correctAnswers: string[];
|
||||
}
|
||||
|
||||
export default function ShortAnswerPlayer({ id, title, prompt, correctAnswers }: ShortAnswerPlayerProps) {
|
||||
const [userAnswer, setUserAnswer] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleReset = () => {
|
||||
setSubmitted(false);
|
||||
setUserAnswer("");
|
||||
};
|
||||
|
||||
const isCorrect = (correctAnswers || []).some(ans => ans.trim().toLowerCase() === userAnswer.trim().toLowerCase());
|
||||
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white uppercase tracking-widest text-[10px]">
|
||||
{title || "Short Answer"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-8 glass border-white/5 rounded-3xl space-y-8">
|
||||
<p className="text-xl font-bold text-gray-100">{prompt || "Please enter your answer below:"}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
value={userAnswer}
|
||||
onChange={(e) => setUserAnswer(e.target.value)}
|
||||
disabled={submitted}
|
||||
className={`w-full bg-white/5 border-2 rounded-2xl px-6 py-4 text-lg transition-all focus:outline-none ${submitted
|
||||
? (isCorrect ? "border-green-500 bg-green-500/10 text-green-400" : "border-red-500 bg-red-500/10 text-red-100")
|
||||
: "border-white/10 focus:border-blue-500 text-white"
|
||||
}`}
|
||||
placeholder="Type your answer..."
|
||||
/>
|
||||
|
||||
{submitted && !isCorrect && (
|
||||
<div className="p-4 bg-orange-500/10 border border-orange-500/20 rounded-xl animate-in fade-in duration-500">
|
||||
<p className="text-[10px] text-orange-400 uppercase font-black tracking-widest">Suggested Answer(s):</p>
|
||||
<p className="text-sm text-gray-400 mt-1">{(correctAnswers || [])[0]}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!submitted && (
|
||||
<button
|
||||
onClick={() => setSubmitted(true)}
|
||||
disabled={!userAnswer.trim()}
|
||||
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 disabled:opacity-50 disabled:grayscale"
|
||||
>
|
||||
Submit Answer
|
||||
</button>
|
||||
)}
|
||||
|
||||
{submitted && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl border-white/5"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user