Compare commits

...

5 Commits

2 changed files with 74 additions and 86 deletions

View File

@ -1,4 +1,5 @@
import { useEffect, useRef, useState, useCallback } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
import { createPortal } from "react-dom";
import { X } from "lucide-react"; import { X } from "lucide-react";
import type { InventoryItem, ActiveEffect } from "../types/quest"; import type { InventoryItem, ActiveEffect } from "../types/quest";
import { import {
@ -43,7 +44,6 @@ const STYLES = `
to { transform: translateY(0); opacity:1; } to { transform: translateY(0); opacity:1; }
} }
/* Sea shimmer bg */
.inv-sheet::before { .inv-sheet::before {
content: ''; content: '';
position: absolute; inset: 0; pointer-events: none; z-index: 0; position: absolute; inset: 0; pointer-events: none; z-index: 0;
@ -58,7 +58,6 @@ const STYLES = `
100% { background-position: 100% 100%, 0% 100%; } 100% { background-position: 100% 100%, 0% 100%; }
} }
/* Gold orb top-right */
.inv-sheet::after { .inv-sheet::after {
content: ''; content: '';
position: absolute; top: -60px; right: -40px; z-index: 0; position: absolute; top: -60px; right: -40px; z-index: 0;
@ -67,7 +66,6 @@ const STYLES = `
pointer-events: none; pointer-events: none;
} }
/* ── Handle ── */
.inv-handle-row { .inv-handle-row {
display: flex; justify-content: center; display: flex; justify-content: center;
padding: 0.75rem 0 0; flex-shrink: 0; position: relative; z-index: 2; padding: 0.75rem 0 0; flex-shrink: 0; position: relative; z-index: 2;
@ -77,7 +75,6 @@ const STYLES = `
background: rgba(255,255,255,0.1); background: rgba(255,255,255,0.1);
} }
/* ── Header ── */
.inv-header { .inv-header {
position: relative; z-index: 2; position: relative; z-index: 2;
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
@ -108,7 +105,6 @@ const STYLES = `
background: rgba(251,191,36,0.1); background: rgba(251,191,36,0.1);
} }
/* ── Active effects banner ── */
.inv-active-bar { .inv-active-bar {
position: relative; z-index: 2; position: relative; z-index: 2;
display: flex; gap: 0.5rem; overflow-x: auto; scrollbar-width: none; display: flex; gap: 0.5rem; overflow-x: auto; scrollbar-width: none;
@ -140,7 +136,6 @@ const STYLES = `
margin-left: 0.1rem; margin-left: 0.1rem;
} }
/* ── Divider ── */
.inv-divider { .inv-divider {
position: relative; z-index: 2; position: relative; z-index: 2;
height: 1px; margin: 0.85rem 1.3rem 0; height: 1px; margin: 0.85rem 1.3rem 0;
@ -154,7 +149,6 @@ const STYLES = `
text-transform: uppercase; color: rgba(255,255,255,0.25); text-transform: uppercase; color: rgba(255,255,255,0.25);
} }
/* ── Scrollable item grid ── */
.inv-scroll { .inv-scroll {
position: relative; z-index: 2; position: relative; z-index: 2;
flex: 1; overflow-y: auto; scrollbar-width: none; flex: 1; overflow-y: auto; scrollbar-width: none;
@ -162,7 +156,6 @@ const STYLES = `
} }
.inv-scroll::-webkit-scrollbar { display: none; } .inv-scroll::-webkit-scrollbar { display: none; }
/* ── Empty state ── */
.inv-empty { .inv-empty {
display: flex; flex-direction: column; align-items: center; display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 0.6rem; justify-content: center; gap: 0.6rem;
@ -173,7 +166,6 @@ const STYLES = `
} }
.inv-empty-icon { font-size: 2.5rem; opacity: 0.4; } .inv-empty-icon { font-size: 2.5rem; opacity: 0.4; }
/* ── Loading skeleton ── */
.inv-skeleton-grid { .inv-skeleton-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
} }
@ -187,12 +179,10 @@ const STYLES = `
50% { opacity: 1; } 50% { opacity: 1; }
} }
/* ── Item grid ── */
.inv-grid { .inv-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
} }
/* ── Item card ── */
.inv-card { .inv-card {
border-radius: 20px; padding: 1rem; border-radius: 20px; padding: 1rem;
border: 1.5px solid rgba(255,255,255,0.07); border: 1.5px solid rgba(255,255,255,0.07);
@ -213,8 +203,6 @@ const STYLES = `
transform: translateY(-2px); transform: translateY(-2px);
} }
.inv-card:active { transform: translateY(0) scale(0.98); } .inv-card:active { transform: translateY(0) scale(0.98); }
/* Active card styling */
.inv-card.is-active { .inv-card.is-active {
border-color: rgba(251,191,36,0.4); border-color: rgba(251,191,36,0.4);
background: rgba(251,191,36,0.06); background: rgba(251,191,36,0.06);
@ -223,26 +211,17 @@ const STYLES = `
border-color: rgba(251,191,36,0.6); border-color: rgba(251,191,36,0.6);
background: rgba(251,191,36,0.09); background: rgba(251,191,36,0.09);
} }
/* Just-activated flash */
@keyframes invActivateFlash { @keyframes invActivateFlash {
0% { background: rgba(251,191,36,0.25); border-color: rgba(251,191,36,0.8); } 0% { background: rgba(251,191,36,0.25); border-color: rgba(251,191,36,0.8); }
100%{ background: rgba(251,191,36,0.06); border-color: rgba(251,191,36,0.4); } 100%{ background: rgba(251,191,36,0.06); border-color: rgba(251,191,36,0.4); }
} }
.inv-card.just-activated { .inv-card.just-activated { animation: invActivateFlash 0.9s ease forwards; }
animation: invActivateFlash 0.9s ease forwards;
}
/* Card shimmer overlay */
.inv-card-sheen { .inv-card-sheen {
position: absolute; inset: 0; pointer-events: none; position: absolute; inset: 0; pointer-events: none;
background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.04) 50%, transparent 70%); background: linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.04) 50%, transparent 70%);
transform: translateX(-100%); transform: translateX(-100%); transition: transform 0.5s ease;
transition: transform 0.5s ease;
} }
.inv-card:hover .inv-card-sheen { transform: translateX(100%); } .inv-card:hover .inv-card-sheen { transform: translateX(100%); }
/* Icon area */
.inv-card-icon-wrap { .inv-card-icon-wrap {
width: 44px; height: 44px; border-radius: 14px; width: 44px; height: 44px; border-radius: 14px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
@ -258,30 +237,23 @@ const STYLES = `
.inv-card-active-dot { .inv-card-active-dot {
position: absolute; top: -3px; right: -3px; position: absolute; top: -3px; right: -3px;
width: 10px; height: 10px; border-radius: 50%; width: 10px; height: 10px; border-radius: 50%;
background: #fbbf24; background: #fbbf24; border: 2px solid #08111f;
border: 2px solid #08111f;
animation: invDotPulse 2s ease-in-out infinite; animation: invDotPulse 2s ease-in-out infinite;
} }
@keyframes invDotPulse { @keyframes invDotPulse {
0%,100% { box-shadow: 0 0 0 0 rgba(251,191,36,0.6); } 0%,100% { box-shadow: 0 0 0 0 rgba(251,191,36,0.6); }
50% { box-shadow: 0 0 0 5px rgba(251,191,36,0); } 50% { box-shadow: 0 0 0 5px rgba(251,191,36,0); }
} }
/* Card text */
.inv-card-name { .inv-card-name {
font-family: 'Nunito', sans-serif; font-family: 'Nunito', sans-serif;
font-size: 0.82rem; font-weight: 900; color: #fff; font-size: 0.82rem; font-weight: 900; color: #fff; line-height: 1.2;
line-height: 1.2;
} }
.inv-card.is-active .inv-card-name { color: #fbbf24; } .inv-card.is-active .inv-card-name { color: #fbbf24; }
.inv-card-desc { .inv-card-desc {
font-family: 'Nunito Sans', sans-serif; font-family: 'Nunito Sans', sans-serif;
font-size: 0.63rem; font-weight: 600; font-size: 0.63rem; font-weight: 600;
color: rgba(255,255,255,0.38); line-height: 1.4; color: rgba(255,255,255,0.38); line-height: 1.4; flex: 1;
flex: 1;
} }
/* Qty + type row */
.inv-card-meta { .inv-card-meta {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
gap: 0.4rem; margin-top: auto; gap: 0.4rem; margin-top: auto;
@ -299,11 +271,8 @@ const STYLES = `
letter-spacing: 0.1em; text-transform: uppercase; letter-spacing: 0.1em; text-transform: uppercase;
color: rgba(255,255,255,0.22); color: rgba(255,255,255,0.22);
} }
/* Activate button */
.inv-activate-btn { .inv-activate-btn {
width: 100%; width: 100%; padding: 0.48rem;
padding: 0.48rem;
border-radius: 10px; border: none; cursor: pointer; border-radius: 10px; border: none; cursor: pointer;
font-family: 'Nunito', sans-serif; font-family: 'Nunito', sans-serif;
font-size: 0.7rem; font-weight: 900; font-size: 0.7rem; font-weight: 900;
@ -315,10 +284,7 @@ const STYLES = `
border: 1px solid rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1);
color: rgba(255,255,255,0.6); color: rgba(255,255,255,0.6);
} }
.inv-activate-btn.idle:hover { .inv-activate-btn.idle:hover { background: rgba(255,255,255,0.12); color: white; }
background: rgba(255,255,255,0.12);
color: white;
}
.inv-activate-btn.activating { .inv-activate-btn.activating {
background: rgba(251,191,36,0.1); background: rgba(251,191,36,0.1);
border: 1px solid rgba(251,191,36,0.25); border: 1px solid rgba(251,191,36,0.25);
@ -330,8 +296,7 @@ const STYLES = `
.inv-activate-btn.active-state { .inv-activate-btn.active-state {
background: rgba(251,191,36,0.12); background: rgba(251,191,36,0.12);
border: 1px solid rgba(251,191,36,0.3); border: 1px solid rgba(251,191,36,0.3);
color: #fbbf24; color: #fbbf24; cursor: default;
cursor: default;
} }
.inv-activate-btn.success-flash { .inv-activate-btn.success-flash {
background: rgba(74,222,128,0.18); background: rgba(74,222,128,0.18);
@ -339,24 +304,17 @@ const STYLES = `
color: #4ade80; color: #4ade80;
animation: invSuccessScale 0.35s cubic-bezier(0.34,1.56,0.64,1) both; animation: invSuccessScale 0.35s cubic-bezier(0.34,1.56,0.64,1) both;
} }
@keyframes invSuccessScale { @keyframes invSuccessScale { from{transform:scale(0.94)} to{transform:scale(1)} }
from { transform: scale(0.94); }
to { transform: scale(1); }
}
.inv-activate-btn:disabled { pointer-events: none; } .inv-activate-btn:disabled { pointer-events: none; }
/* Time remaining on active button */
.inv-active-time { .inv-active-time {
font-family: 'Nunito Sans', sans-serif; font-family: 'Nunito Sans', sans-serif;
font-size: 0.55rem; font-weight: 700; font-size: 0.55rem; font-weight: 700; color: rgba(251,191,36,0.5);
color: rgba(251,191,36,0.5);
} }
/* ── Toast ── */
.inv-toast { .inv-toast {
position: fixed; bottom: calc(1.5rem + env(safe-area-inset-bottom)); position: fixed; bottom: calc(1.5rem + env(safe-area-inset-bottom));
left: 50%; transform: translateX(-50%); left: 50%; transform: translateX(-50%);
z-index: 90; z-index: 9999;
display: flex; align-items: center; gap: 0.55rem; display: flex; align-items: center; gap: 0.55rem;
padding: 0.7rem 1.2rem; padding: 0.7rem 1.2rem;
background: linear-gradient(135deg, #1a3a1a, #0d2010); background: linear-gradient(135deg, #1a3a1a, #0d2010);
@ -380,13 +338,11 @@ const ITEM_ICON: Record<string, string> = {
title: "🏴‍☠️", title: "🏴‍☠️",
coin_boost: "🪙", coin_boost: "🪙",
}; };
const ITEM_ICON_DEFAULT = "📦";
function itemIcon(effectType: string): string { function itemIcon(effectType: string): string {
return ITEM_ICON[effectType] ?? ITEM_ICON_DEFAULT; return ITEM_ICON[effectType] ?? "📦";
} }
// ─── Check if an item is currently active ─────────────────────────────────────
function isItemActive( function isItemActive(
item: InventoryItem, item: InventoryItem,
activeEffects: ActiveEffect[], activeEffects: ActiveEffect[],
@ -438,33 +394,23 @@ const ItemCard = ({
style={{ "--ci-delay": `${index * 0.045}s` } as React.CSSProperties} style={{ "--ci-delay": `${index * 0.045}s` } as React.CSSProperties}
> >
<div className="inv-card-sheen" /> <div className="inv-card-sheen" />
{/* Icon */}
<div className="inv-card-icon-wrap"> <div className="inv-card-icon-wrap">
{itemIcon(inv.item.effect_type)} {itemIcon(inv.item.effect_type)}
{isActive && <div className="inv-card-active-dot" />} {isActive && <div className="inv-card-active-dot" />}
</div> </div>
{/* Name + description */}
<p className="inv-card-name">{inv.item.name}</p> <p className="inv-card-name">{inv.item.name}</p>
<p className="inv-card-desc">{inv.item.description}</p> <p className="inv-card-desc">{inv.item.description}</p>
{/* Qty + type */}
<div className="inv-card-meta"> <div className="inv-card-meta">
<span className="inv-card-qty">×{inv.quantity}</span> <span className="inv-card-qty">×{inv.quantity}</span>
<span className="inv-card-type"> <span className="inv-card-type">
{inv.item.type.replace(/_/g, " ")} {inv.item.type.replace(/_/g, " ")}
</span> </span>
</div> </div>
{/* Time remaining if active */}
{isActive && activeEffect && ( {isActive && activeEffect && (
<div className="inv-active-time"> <div className="inv-active-time">
{formatTimeLeft(activeEffect.expires_at)} remaining {formatTimeLeft(activeEffect.expires_at)} remaining
</div> </div>
)} )}
{/* Activate button */}
<button <button
className={`inv-activate-btn ${btnState}`} className={`inv-activate-btn ${btnState}`}
onClick={() => !isActive && !isActivating && onActivate(inv.id)} onClick={() => !isActive && !isActivating && onActivate(inv.id)}
@ -504,11 +450,9 @@ export const InventoryModal = ({ onClose }: Props) => {
const [toastMsg, setToastMsg] = useState(""); const [toastMsg, setToastMsg] = useState("");
const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const toastTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// ── Fetch on open ──────────────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
if (!token) return; if (!token) return;
let cancelled = false; let cancelled = false;
const fetchInv = async () => { const fetchInv = async () => {
setLoading(true); setLoading(true);
try { try {
@ -520,31 +464,24 @@ export const InventoryModal = ({ onClose }: Props) => {
if (!cancelled) setLoading(false); if (!cancelled) setLoading(false);
} }
}; };
fetchInv(); fetchInv();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [token]); }, [token]);
// ── Activate ──────────────────────────────────────────────────────────────
const handleActivate = useCallback( const handleActivate = useCallback(
async (itemId: string) => { async (itemId: string) => {
if (!token) return; if (!token) return;
activateItemOptimistic(itemId); activateItemOptimistic(itemId);
try { try {
const updatedInv = await api.activateItem(token, itemId); const updatedInv = await api.activateItem(token, itemId);
activateItemSuccess(updatedInv, itemId); activateItemSuccess(updatedInv, itemId);
// Find item name for toast
const name = items.find((i) => i.id === itemId)?.item.name ?? "Item"; const name = items.find((i) => i.id === itemId)?.item.name ?? "Item";
setToastMsg( setToastMsg(
`${itemIcon(items.find((i) => i.id === itemId)?.item.effect_type ?? "")} ${name} activated!`, `${itemIcon(items.find((i) => i.id === itemId)?.item.effect_type ?? "")} ${name} activated!`,
); );
setShowToast(true); setShowToast(true);
// Auto-clear success state + toast
if (toastTimer.current) clearTimeout(toastTimer.current); if (toastTimer.current) clearTimeout(toastTimer.current);
toastTimer.current = setTimeout(() => { toastTimer.current = setTimeout(() => {
setShowToast(false); setShowToast(false);
@ -560,7 +497,6 @@ export const InventoryModal = ({ onClose }: Props) => {
[token, items], [token, items],
); );
// Cleanup timer on unmount
useEffect( useEffect(
() => () => { () => () => {
if (toastTimer.current) clearTimeout(toastTimer.current); if (toastTimer.current) clearTimeout(toastTimer.current);
@ -570,18 +506,19 @@ export const InventoryModal = ({ onClose }: Props) => {
const liveEffects = getLiveEffects(activeEffects); const liveEffects = getLiveEffects(activeEffects);
return ( // Portal the entire modal to document.body so it always
// renders at the top of the DOM tree, escaping any parent
// stacking context, overflow:hidden, or z-index constraints.
return createPortal(
<> <>
<style>{STYLES}</style> <style>{STYLES}</style>
<div className="inv-overlay" onClick={onClose}> <div className="inv-overlay" onClick={onClose}>
<div className="inv-sheet" onClick={(e) => e.stopPropagation()}> <div className="inv-sheet" onClick={(e) => e.stopPropagation()}>
{/* Handle */}
<div className="inv-handle-row"> <div className="inv-handle-row">
<div className="inv-handle" /> <div className="inv-handle" />
</div> </div>
{/* Header */}
<div className="inv-header"> <div className="inv-header">
<div className="inv-header-left"> <div className="inv-header-left">
<span className="inv-eyebrow"> Pirate's Hold</span> <span className="inv-eyebrow"> Pirate's Hold</span>
@ -592,7 +529,6 @@ export const InventoryModal = ({ onClose }: Props) => {
</button> </button>
</div> </div>
{/* Active effects bar */}
{liveEffects.length > 0 && ( {liveEffects.length > 0 && (
<div className="inv-active-bar"> <div className="inv-active-bar">
{liveEffects.map((e) => ( {liveEffects.map((e) => (
@ -616,7 +552,6 @@ export const InventoryModal = ({ onClose }: Props) => {
: "Your hold"} : "Your hold"}
</p> </p>
{/* Scroll area */}
<div className="inv-scroll"> <div className="inv-scroll">
{loading && items.length === 0 ? ( {loading && items.length === 0 ? (
<div className="inv-skeleton-grid"> <div className="inv-skeleton-grid">
@ -649,7 +584,6 @@ export const InventoryModal = ({ onClose }: Props) => {
</div> </div>
)} )}
{/* Error inline */}
{error && ( {error && (
<p <p
style={{ style={{
@ -668,8 +602,8 @@ export const InventoryModal = ({ onClose }: Props) => {
</div> </div>
</div> </div>
{/* Success toast */}
{showToast && <div className="inv-toast">{toastMsg}</div>} {showToast && <div className="inv-toast">{toastMsg}</div>}
</> </>,
document.body,
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { QuizData } from "../../types/lesson"; import { type QuizData } from "../../types/lesson";
import { CheckCircle2, XCircle, ChevronRight } from "lucide-react"; import { CheckCircle2, XCircle, ChevronRight } from "lucide-react";
interface QuizProps { interface QuizProps {
@ -22,11 +22,13 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
if (!selectedId) return; if (!selectedId) return;
setIsSubmitted(true); setIsSubmitted(true);
const selectedOption = data.options.find((opt) => opt.id === selectedId); const selectedOption = data.options.find((opt) => opt.id === selectedId);
const selectedOption = data.options.find((opt) => opt.id === selectedId);
if (selectedOption?.isCorrect && onComplete) { if (selectedOption?.isCorrect && onComplete) {
onComplete(); onComplete();
} }
}; };
const selectedOption = data.options.find((opt) => opt.id === selectedId);
const selectedOption = data.options.find((opt) => opt.id === selectedId); const selectedOption = data.options.find((opt) => opt.id === selectedId);
const isCorrect = selectedOption?.isCorrect; const isCorrect = selectedOption?.isCorrect;
@ -39,6 +41,12 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
<p className="text-lg font-medium text-slate-900 mb-6 whitespace-pre-line"> <p className="text-lg font-medium text-slate-900 mb-6 whitespace-pre-line">
{data.question} {data.question}
</p> </p>
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-wider mb-2">
Concept Check
</h4>
<p className="text-lg font-medium text-slate-900 mb-6 whitespace-pre-line">
{data.question}
</p>
<div className="space-y-3"> <div className="space-y-3">
{data.options.map((option) => { {data.options.map((option) => {
@ -58,9 +66,12 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
} else if (option.isCorrect) { } else if (option.isCorrect) {
// Highlight correct answer if wrong one was picked // Highlight correct answer if wrong one was picked
borderClass = "border-green-200 bg-green-50/50"; borderClass = "border-green-200 bg-green-50/50";
// Highlight correct answer if wrong one was picked
borderClass = "border-green-200 bg-green-50/50";
} }
} else if (selectedId === option.id) { } else if (selectedId === option.id) {
borderClass = "border-indigo-600 bg-indigo-50"; borderClass = "border-indigo-600 bg-indigo-50";
borderClass = "border-indigo-600 bg-indigo-50";
} }
return ( return (
@ -71,6 +82,17 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
className={`w-full text-left p-4 rounded-lg border-2 transition-all duration-200 flex items-center justify-between group ${borderClass} ${bgClass}`} className={`w-full text-left p-4 rounded-lg border-2 transition-all duration-200 flex items-center justify-between group ${borderClass} ${bgClass}`}
> >
<div className="flex items-center"> <div className="flex items-center">
<span
className={`w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold mr-3 ${
isSubmitted && option.isCorrect
? "bg-green-200 text-green-800"
: isSubmitted && option.id === selectedId
? "bg-red-200 text-red-800"
: selectedId === option.id
? "bg-indigo-600 text-white"
: "bg-slate-100 text-slate-500"
}`}
>
<span <span
className={`w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold mr-3 ${ className={`w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold mr-3 ${
isSubmitted && option.isCorrect isSubmitted && option.isCorrect
@ -87,6 +109,9 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
<span className="text-slate-700 group-hover:text-slate-900"> <span className="text-slate-700 group-hover:text-slate-900">
{option.text} {option.text}
</span> </span>
<span className="text-slate-700 group-hover:text-slate-900">
{option.text}
</span>
</div> </div>
{icon} {icon}
</button> </button>
@ -97,6 +122,9 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
{/* Feedback Section */} {/* Feedback Section */}
{isSubmitted && ( {isSubmitted && (
<div
className={`p-6 border-t ${isCorrect ? "bg-green-50 border-green-100" : "bg-slate-50 border-slate-100"}`}
>
<div <div
className={`p-6 border-t ${isCorrect ? "bg-green-50 border-green-100" : "bg-slate-50 border-slate-100"}`} className={`p-6 border-t ${isCorrect ? "bg-green-50 border-green-100" : "bg-slate-50 border-slate-100"}`}
> >
@ -124,6 +152,29 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
{data.explanation} {data.explanation}
</div> </div>
</div> </div>
<div
className={`mt-1 p-1 rounded-full ${isCorrect ? "bg-green-200" : "bg-slate-200"}`}
>
{isCorrect ? (
<CheckCircle2 className="w-4 h-4 text-green-700" />
) : (
<div className="w-4 h-4 text-slate-500 font-bold text-center leading-4">
i
</div>
)}
</div>
<div>
<p
className={`font-bold ${isCorrect ? "text-green-800" : "text-slate-800"} mb-1`}
>
{isCorrect ? "That's right!" : "Not quite."}
</p>
<p className="text-slate-600 mb-2">{selectedOption?.feedback}</p>
<div className="text-sm text-slate-500 bg-white p-3 rounded border border-slate-200">
<span className="font-semibold block mb-1">Explanation:</span>
{data.explanation}
</div>
</div>
</div> </div>
</div> </div>
)} )}
@ -137,6 +188,9 @@ const Quiz: React.FC<QuizProps> = ({ data, onComplete }) => {
selectedId selectedId
? "bg-slate-900 text-white hover:bg-slate-800 shadow-md transform hover:-translate-y-0.5" ? "bg-slate-900 text-white hover:bg-slate-800 shadow-md transform hover:-translate-y-0.5"
: "bg-slate-200 text-slate-400 cursor-not-allowed" : "bg-slate-200 text-slate-400 cursor-not-allowed"
selectedId
? "bg-slate-900 text-white hover:bg-slate-800 shadow-md transform hover:-translate-y-0.5"
: "bg-slate-200 text-slate-400 cursor-not-allowed"
}`} }`}
> >
Check Answer <ChevronRight className="w-4 h-4 ml-1" /> Check Answer <ChevronRight className="w-4 h-4 ml-1" />