chore(build): refactor codebase for production
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CheckCircle2, RotateCcw, ChevronRight } from 'lucide-react';
|
||||
import { useState } from "react";
|
||||
import { CheckCircle2, RotateCcw, ChevronRight } from "lucide-react";
|
||||
|
||||
export interface VocabOption {
|
||||
id: string;
|
||||
@ -10,7 +10,7 @@ export interface VocabOption {
|
||||
|
||||
export interface VocabExercise {
|
||||
sentence: string;
|
||||
word: string; // the target word — will be highlighted
|
||||
word: string; // the target word — will be highlighted
|
||||
question: string;
|
||||
options: VocabOption[];
|
||||
}
|
||||
@ -20,41 +20,58 @@ interface ContextEliminationWidgetProps {
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export default function ContextEliminationWidget({ exercises, accentColor = 'rose' }: ContextEliminationWidgetProps) {
|
||||
export default function ContextEliminationWidget({
|
||||
exercises,
|
||||
accentColor = "rose",
|
||||
}: ContextEliminationWidgetProps) {
|
||||
const [activeEx, setActiveEx] = useState(0);
|
||||
const [eliminated, setEliminated] = useState<Set<string>>(new Set());
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [triedCorrect, setTriedCorrect] = useState(false);
|
||||
|
||||
const exercise = exercises[activeEx];
|
||||
const wrongIds = exercise.options.filter(o => !o.isCorrect).map(o => o.id);
|
||||
const allWrongEliminated = wrongIds.every(id => eliminated.has(id));
|
||||
const wrongIds = exercise.options
|
||||
.filter((o) => !o.isCorrect)
|
||||
.map((o) => o.id);
|
||||
|
||||
const eliminate = (id: string) => {
|
||||
const opt = exercise.options.find(o => o.id === id)!;
|
||||
const opt = exercise.options.find((o) => o.id === id)!;
|
||||
if (opt.isCorrect) {
|
||||
setTriedCorrect(true);
|
||||
setTimeout(() => setTriedCorrect(false), 1500);
|
||||
} else {
|
||||
const newElim = new Set([...eliminated, id]);
|
||||
setEliminated(newElim);
|
||||
if (wrongIds.every(wid => newElim.has(wid))) {
|
||||
if (wrongIds.every((wid) => newElim.has(wid))) {
|
||||
setRevealed(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => { setEliminated(new Set()); setRevealed(false); setTriedCorrect(false); };
|
||||
const switchEx = (i: number) => { setActiveEx(i); setEliminated(new Set()); setRevealed(false); setTriedCorrect(false); };
|
||||
const reset = () => {
|
||||
setEliminated(new Set());
|
||||
setRevealed(false);
|
||||
setTriedCorrect(false);
|
||||
};
|
||||
const switchEx = (i: number) => {
|
||||
setActiveEx(i);
|
||||
setEliminated(new Set());
|
||||
setRevealed(false);
|
||||
setTriedCorrect(false);
|
||||
};
|
||||
|
||||
// Highlight the target word in the sentence
|
||||
const renderSentence = () => {
|
||||
const idx = exercise.sentence.toLowerCase().indexOf(exercise.word.toLowerCase());
|
||||
const idx = exercise.sentence
|
||||
.toLowerCase()
|
||||
.indexOf(exercise.word.toLowerCase());
|
||||
if (idx === -1) return <>{exercise.sentence}</>;
|
||||
return (
|
||||
<>
|
||||
{exercise.sentence.slice(0, idx)}
|
||||
<mark className={`bg-${accentColor}-200 text-${accentColor}-900 font-bold px-0.5 rounded not-italic`}>
|
||||
<mark
|
||||
className={`bg-${accentColor}-200 text-${accentColor}-900 font-bold px-0.5 rounded not-italic`}
|
||||
>
|
||||
{exercise.sentence.slice(idx, idx + exercise.word.length)}
|
||||
</mark>
|
||||
{exercise.sentence.slice(idx + exercise.word.length)}
|
||||
@ -74,7 +91,7 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
|
||||
className={`px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
i === activeEx
|
||||
? `bg-white border-b-2 border-${accentColor}-600 text-${accentColor}-700`
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Word {i + 1}
|
||||
@ -84,17 +101,27 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
|
||||
)}
|
||||
|
||||
{/* Sentence in context */}
|
||||
<div className={`px-5 py-4 border-b border-gray-100 bg-${accentColor}-50`}>
|
||||
<p className={`text-xs font-semibold uppercase tracking-wider text-${accentColor}-500 mb-2`}>Sentence in Context</p>
|
||||
<p className="text-gray-700 italic leading-relaxed text-sm">{renderSentence()}</p>
|
||||
<div
|
||||
className={`px-5 py-4 border-b border-gray-100 bg-${accentColor}-50`}
|
||||
>
|
||||
<p
|
||||
className={`text-xs font-semibold uppercase tracking-wider text-${accentColor}-500 mb-2`}
|
||||
>
|
||||
Sentence in Context
|
||||
</p>
|
||||
<p className="text-gray-700 italic leading-relaxed text-sm">
|
||||
{renderSentence()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Question + instruction */}
|
||||
<div className="px-5 pt-4 pb-2">
|
||||
<p className="font-medium text-gray-800 text-sm mb-1">{exercise.question}</p>
|
||||
<p className="font-medium text-gray-800 text-sm mb-1">
|
||||
{exercise.question}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 italic">
|
||||
{revealed
|
||||
? 'You found it! The correct definition is highlighted.'
|
||||
? "You found it! The correct definition is highlighted."
|
||||
: 'Click "Eliminate" on definitions that don\'t fit the context. Work by elimination.'}
|
||||
</p>
|
||||
</div>
|
||||
@ -108,40 +135,52 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
|
||||
|
||||
{/* Options */}
|
||||
<div className="px-5 py-3 space-y-2">
|
||||
{exercise.options.map(opt => {
|
||||
{exercise.options.map((opt) => {
|
||||
const isElim = eliminated.has(opt.id);
|
||||
const isAnswer = opt.isCorrect && revealed;
|
||||
|
||||
let wrapCls = 'border-gray-200 bg-white';
|
||||
if (isAnswer) wrapCls = 'border-green-400 bg-green-50';
|
||||
else if (isElim) wrapCls = 'border-gray-100 bg-gray-50';
|
||||
let wrapCls = "border-gray-200 bg-white";
|
||||
if (isAnswer) wrapCls = "border-green-400 bg-green-50";
|
||||
else if (isElim) wrapCls = "border-gray-100 bg-gray-50";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={opt.id}
|
||||
className={`rounded-xl border px-4 py-3 transition-all ${wrapCls} ${isElim ? 'opacity-50' : ''}`}
|
||||
className={`rounded-xl border px-4 py-3 transition-all ${wrapCls} ${isElim ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className={`text-xs font-bold mt-0.5 shrink-0 ${isElim ? 'text-gray-400' : isAnswer ? 'text-green-700' : 'text-gray-500'}`}>
|
||||
<span
|
||||
className={`text-xs font-bold mt-0.5 shrink-0 ${isElim ? "text-gray-400" : isAnswer ? "text-green-700" : "text-gray-500"}`}
|
||||
>
|
||||
{opt.id}.
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm leading-snug ${
|
||||
isElim ? 'text-gray-400 line-through' :
|
||||
isAnswer ? 'text-green-800 font-semibold' :
|
||||
'text-gray-700'
|
||||
}`}>
|
||||
<p
|
||||
className={`text-sm leading-snug ${
|
||||
isElim
|
||||
? "text-gray-400 line-through"
|
||||
: isAnswer
|
||||
? "text-green-800 font-semibold"
|
||||
: "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{opt.definition}
|
||||
</p>
|
||||
{isElim && (
|
||||
<p className="text-xs text-gray-400 mt-0.5 italic">{opt.elimReason}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 italic">
|
||||
{opt.elimReason}
|
||||
</p>
|
||||
)}
|
||||
{isAnswer && (
|
||||
<p className="text-xs text-green-700 mt-1">✓ {opt.elimReason}</p>
|
||||
<p className="text-xs text-green-700 mt-1">
|
||||
✓ {opt.elimReason}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
{isAnswer && <CheckCircle2 className="w-5 h-5 text-green-500" />}
|
||||
{isAnswer && (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500" />
|
||||
)}
|
||||
{!isElim && !isAnswer && !revealed && (
|
||||
<button
|
||||
onClick={() => eliminate(opt.id)}
|
||||
@ -158,7 +197,10 @@ export default function ContextEliminationWidget({ exercises, accentColor = 'ros
|
||||
</div>
|
||||
|
||||
<div className="px-5 pb-5 flex items-center gap-3">
|
||||
<button onClick={reset} className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Reset
|
||||
</button>
|
||||
{revealed && activeEx < exercises.length - 1 && (
|
||||
|
||||
Reference in New Issue
Block a user