328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { ChevronLeft } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DIMENSION_CONFIGS,
|
|
DIMENSION_ORDER,
|
|
type DimensionConfig,
|
|
getDescriptiveTitleForScore,
|
|
} from '@/config/resultImages';
|
|
import { DimensionCard } from './DimensionCard';
|
|
import { SmoothTextAppearance } from '@/components/motion';
|
|
import type { AssessmentSubmission } from '@/services/assessmentService';
|
|
import type { DimensionName } from '@/lib/assessment/scoring/dimensions';
|
|
import { cn } from '@/lib/utils';
|
|
import { MOTION_EASE } from '@/lib/motion';
|
|
|
|
export interface AssessmentResultViewProps {
|
|
phase: 'pre' | 'post';
|
|
currentSubmission: AssessmentSubmission;
|
|
baselineSubmission?: AssessmentSubmission | null;
|
|
onContinue?: () => void;
|
|
continueLabel?: string;
|
|
showActions?: boolean;
|
|
}
|
|
|
|
export function AssessmentResultView({
|
|
phase,
|
|
currentSubmission,
|
|
baselineSubmission,
|
|
onContinue,
|
|
continueLabel = 'مشاهده آموزشها و تمرینها',
|
|
showActions = true,
|
|
}: AssessmentResultViewProps) {
|
|
const navigate = useNavigate();
|
|
const isPre = phase === 'pre';
|
|
const scores = currentSubmission.scores;
|
|
const baselineScores = baselineSubmission?.scores;
|
|
|
|
// Active step: 0, 1, 2, 3 = dimension steps; 4 = final summary screen
|
|
const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
|
|
const [isTransitioning, setIsTransitioning] = useState<boolean>(false);
|
|
|
|
// Track revealed status for each dimension so it's preserved on back/forward
|
|
const [revealed, setRevealed] = useState<Record<DimensionName, boolean>>({
|
|
efficacy: false,
|
|
hope: false,
|
|
resilience: false,
|
|
optimism: false,
|
|
});
|
|
|
|
const totalScore = scores.total;
|
|
const maxTotalScore = 144;
|
|
const totalPercent = Math.round(
|
|
(Math.max(0, Math.min(maxTotalScore, totalScore)) / maxTotalScore) * 100
|
|
);
|
|
|
|
const hasComparison = Boolean(
|
|
!isPre && baselineScores && typeof baselineScores.total === 'number'
|
|
);
|
|
const totalDelta = hasComparison ? totalScore - (baselineScores?.total ?? 0) : 0;
|
|
|
|
const currentDimensionKey = DIMENSION_ORDER[currentStepIndex];
|
|
const isSummaryStep = currentStepIndex === 4;
|
|
|
|
const handleRevealCurrent = () => {
|
|
if (currentDimensionKey) {
|
|
setRevealed((prev) => ({ ...prev, [currentDimensionKey]: true }));
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (isTransitioning) return;
|
|
setIsTransitioning(true);
|
|
setCurrentStepIndex((prev) => Math.min(prev + 1, 4));
|
|
window.setTimeout(() => setIsTransitioning(false), 220);
|
|
};
|
|
|
|
const handlePrev = () => {
|
|
if (isTransitioning) return;
|
|
setIsTransitioning(true);
|
|
setCurrentStepIndex((prev) => Math.max(prev - 1, 0));
|
|
window.setTimeout(() => setIsTransitioning(false), 220);
|
|
};
|
|
|
|
const handleJumpToStep = (index: number) => {
|
|
if (isTransitioning) return;
|
|
const targetDimension = DIMENSION_ORDER[index];
|
|
if (
|
|
index < currentStepIndex ||
|
|
(index <= 3 && targetDimension && revealed[targetDimension]) ||
|
|
index === currentStepIndex
|
|
) {
|
|
setIsTransitioning(true);
|
|
setCurrentStepIndex(index);
|
|
window.setTimeout(() => setIsTransitioning(false), 220);
|
|
}
|
|
};
|
|
|
|
const handleFinalContinue = () => {
|
|
if (onContinue) {
|
|
onContinue();
|
|
} else if (isPre) {
|
|
navigate('/chat');
|
|
} else {
|
|
navigate('/course-status');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="mx-auto flex h-full min-h-0 w-full max-w-[430px] flex-1 flex-col" dir="rtl">
|
|
{/* 1. Low-Height, Minimal 4-Segment Progress Indicator */}
|
|
<div className="shrink-0 space-y-1.5 px-1 pb-2">
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span className="font-semibold text-foreground">
|
|
{isSummaryStep ? 'جمعبندی نهایی' : `مؤلفه ${currentStepIndex + 1} از ۴`}
|
|
</span>
|
|
<span>
|
|
{isSummaryStep
|
|
? 'کارنامه نهایی'
|
|
: DIMENSION_CONFIGS[currentDimensionKey]?.titleFa}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Clean, low-height segment bar */}
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
{DIMENSION_ORDER.map((dimKey, idx) => {
|
|
const isCompleted =
|
|
revealed[dimKey] && (currentStepIndex > idx || isSummaryStep);
|
|
const isCurrent = currentStepIndex === idx;
|
|
|
|
return (
|
|
<button
|
|
key={dimKey}
|
|
type="button"
|
|
onClick={() => handleJumpToStep(idx)}
|
|
disabled={!revealed[dimKey] && idx > currentStepIndex}
|
|
className={cn(
|
|
'h-1.5 rounded-full transition-all duration-300 focus:outline-none',
|
|
isCurrent
|
|
? 'bg-sky-500 dark:bg-sky-400'
|
|
: isCompleted
|
|
? 'bg-sky-500/50 hover:bg-sky-500 dark:bg-sky-400/50'
|
|
: 'bg-slate-200/80 dark:bg-slate-800'
|
|
)}
|
|
aria-label={DIMENSION_CONFIGS[dimKey]?.titleFa}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 2. Main Content (Dimension Card OR Final Summary) */}
|
|
<div className="flex-1 min-h-0 flex flex-col">
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
{!isSummaryStep && currentDimensionKey ? (
|
|
/* Step-by-Step Dimension Card */
|
|
<DimensionCard
|
|
key={currentDimensionKey}
|
|
config={DIMENSION_CONFIGS[currentDimensionKey]}
|
|
score={scores[currentDimensionKey]}
|
|
baselineScore={
|
|
hasComparison ? baselineScores?.[currentDimensionKey] : undefined
|
|
}
|
|
stepNumber={currentStepIndex + 1}
|
|
totalSteps={4}
|
|
isRevealed={revealed[currentDimensionKey]}
|
|
onReveal={handleRevealCurrent}
|
|
onNext={handleNext}
|
|
onPrev={currentStepIndex > 0 ? handlePrev : undefined}
|
|
isLastDimension={currentStepIndex === 3}
|
|
/>
|
|
) : (
|
|
/* Final Summary Screen */
|
|
<motion.div
|
|
key="final-summary"
|
|
initial={{ opacity: 0, y: 5 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -5 }}
|
|
transition={{ duration: 0.22, ease: MOTION_EASE }}
|
|
style={{ willChange: 'opacity, transform' }}
|
|
className="flex h-full min-h-0 flex-1 flex-col"
|
|
>
|
|
{/* Main Summary Glass Card */}
|
|
<div className="glass-panel flex h-full min-h-0 flex-1 flex-col justify-between overflow-y-auto space-y-4 rounded-3xl border border-white/50 p-4 sm:p-5 shadow-sm dark:border-white/10 dark:bg-slate-900/60">
|
|
{/* Header Title */}
|
|
<div className="text-center sm:text-right">
|
|
<h1 className="text-xl font-bold tracking-tight text-foreground sm:text-2xl">
|
|
{isPre
|
|
? 'کارنامه اولیه سرمایه روانشناختی'
|
|
: 'کارنامه نهایی دوره نویا'}
|
|
</h1>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
خلاصه وضعیت چهار مؤلفه شما
|
|
</p>
|
|
</div>
|
|
|
|
{/* Total Score Section */}
|
|
<div className="space-y-1.5 rounded-2xl bg-sky-500/10 p-4 dark:bg-sky-500/15">
|
|
<div className="flex items-baseline justify-between">
|
|
<span className="text-xs font-semibold text-sky-800 dark:text-sky-300">
|
|
نمره کل آزمون:
|
|
</span>
|
|
<div className="flex items-baseline gap-1 font-bold tabular-nums">
|
|
<span className="text-2xl font-black text-sky-900 dark:text-sky-100 sm:text-3xl">
|
|
{totalScore.toLocaleString('fa-IR')}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">از ۱۴۴</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-slate-800">
|
|
<div
|
|
className="h-full rounded-full bg-neon-gradient transition-all duration-600"
|
|
style={{ width: `${totalPercent}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{hasComparison && baselineScores && (
|
|
<div className="flex items-center justify-between pt-1 text-xs text-muted-foreground">
|
|
<span>
|
|
پیشآزمون: {baselineScores.total.toLocaleString('fa-IR')} ➔
|
|
پسآزمون: {totalScore.toLocaleString('fa-IR')}
|
|
</span>
|
|
<span
|
|
className={
|
|
totalDelta > 0
|
|
? 'font-bold text-emerald-600 dark:text-emerald-400'
|
|
: totalDelta < 0
|
|
? 'font-bold text-amber-600 dark:text-amber-400'
|
|
: 'text-muted-foreground'
|
|
}
|
|
>
|
|
{totalDelta > 0
|
|
? `+${totalDelta.toLocaleString('fa-IR')} رشد کل`
|
|
: totalDelta < 0
|
|
? `${totalDelta.toLocaleString('fa-IR')} تغییر`
|
|
: 'بدون تغییر'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 4-Dimension Comparative Bars (Simple & Readable) */}
|
|
<div className="space-y-2.5 pt-1">
|
|
{DIMENSION_ORDER.map((dimKey) => {
|
|
const config: DimensionConfig = DIMENSION_CONFIGS[dimKey];
|
|
const dimScore = scores[dimKey];
|
|
const dimPercent = Math.round((dimScore / 36) * 100);
|
|
|
|
return (
|
|
<div key={dimKey} className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="font-semibold text-foreground">
|
|
{config.titleFa}
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground">
|
|
{getDescriptiveTitleForScore(dimKey, dimScore)}
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 w-full overflow-hidden rounded-full bg-slate-200/80 dark:bg-slate-800">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-500"
|
|
style={{
|
|
width: `${dimPercent}%`,
|
|
backgroundColor: config.accentColor,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Non-diagnostic notice */}
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
این نتیجه بازتاب پاسخهای فعلی شماست و تشخیص قطعی روانشناختی نیست.
|
|
</p>
|
|
|
|
{/* Mandatory Closing Text */}
|
|
<SmoothTextAppearance contentKey="final-summary-closing" distance={5} duration={0.22}>
|
|
<div className="rounded-2xl bg-white/50 p-4 text-xs leading-7 text-foreground/90 dark:bg-slate-800/40 sm:text-sm sm:leading-8">
|
|
<p className="whitespace-pre-line text-body">
|
|
حالا تصویر روشنتری از چهار مؤلفه سرمایه روانشناختی خودت داری.
|
|
{'\n\n'}
|
|
این نتیجه، نقطه شروعیه برای شناخت بهتر خودت؛ نه تعریف کامل شخصیت تو.
|
|
{'\n\n'}
|
|
برای آشنایی بیشتر با این مؤلفهها و تقویت اونها، پیشنهاد میکنیم آموزشهای نویا رو ببینی و تمرینهای مرتبط رو انجام بدی.
|
|
</p>
|
|
</div>
|
|
</SmoothTextAppearance>
|
|
|
|
{/* Single Prominent Button */}
|
|
{showActions && (
|
|
<div className="space-y-2 pt-1">
|
|
<Button
|
|
type="button"
|
|
variant="neon"
|
|
size="lg"
|
|
onClick={handleFinalContinue}
|
|
className="w-full gap-2 font-bold shadow-md"
|
|
>
|
|
<span>{continueLabel}</span>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
|
|
{/* Subtle Secondary Back Control */}
|
|
<div className="text-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCurrentStepIndex(0)}
|
|
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
>
|
|
مرور دوباره مؤلفهها
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|