Files
goal-planner/frontend/components/TaskCompleteModal.tsx
T
2026-11-02 16:43:15 +08:00

177 lines
7.8 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { Task, TaskPriority } from '../types';
import { useLanguage } from '../i18n/LanguageContext';
import { MessageKey } from '../i18n/translations';
interface TaskCompleteModalProps {
task: Task;
level: number;
onClose: () => void;
}
const ENCOURAGE_KEYS: Record<TaskPriority, MessageKey[]> = {
high: ['encourageHigh1', 'encourageHigh2', 'encourageHigh3', 'encourageHigh4'],
medium: ['encourageMid1', 'encourageMid2', 'encourageMid3', 'encourageMid4'],
low: ['encourageLow1', 'encourageLow2', 'encourageLow3', 'encourageLow4'],
};
const LEVEL_KEYS: Record<number, MessageKey> = {
0: 'levelPhase',
1: 'levelTask',
2: 'levelSubtask',
};
const EMOJIS_BY_LEVEL: Record<number, string[]> = {
0: ['🏆', '🎊', '🎉', '🥇'],
1: ['⭐', '✨', '💫', '🌟'],
2: ['👍', '👏', '💪', '🎯'],
};
const TaskCompleteModal: React.FC<TaskCompleteModalProps> = ({ task, level, onClose }) => {
const { t } = useLanguage();
const [visible, setVisible] = useState(false);
useEffect(() => {
requestAnimationFrame(() => setVisible(true));
const timer = setTimeout(() => handleClose(), 4000);
return () => clearTimeout(timer);
}, []);
const handleClose = () => {
setVisible(false);
setTimeout(onClose, 300);
};
const encouragePool = ENCOURAGE_KEYS[task.priority] || ENCOURAGE_KEYS.medium;
const encouragement = t(encouragePool[(task.priority.charCodeAt(0) + level) % encouragePool.length]);
const levelLabel = t(LEVEL_KEYS[level] || 'levelTask');
const emojis = EMOJIS_BY_LEVEL[level] || EMOJIS_BY_LEVEL[2];
const mainEmoji = emojis[Math.floor(Math.random() * emojis.length)];
return createPortal(
<div
className="fixed inset-0 z-[150] flex items-center justify-center"
onClick={handleClose}
>
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm transition-opacity duration-300"
style={{ opacity: visible ? 1 : 0 }}
/>
<div
className="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-600 max-w-sm w-full mx-4 overflow-hidden"
style={{
opacity: visible ? 1 : 0,
transform: visible ? 'scale(1)' : 'scale(0.8)',
transition: 'all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)',
}}
onClick={(e) => e.stopPropagation()}
>
{/* Confetti strip */}
<div className="h-1.5 flex">
{['#f59e0b', '#ef4444', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899', '#f97316'].map((c, i) => (
<div key={i} className="flex-1" style={{ backgroundColor: c }} />
))}
</div>
<div className="px-6 pt-6 pb-5 text-center">
{/* Emoji with bounce */}
<div
className="text-5xl mb-3"
style={{
animation: visible ? 'taskCompleteEmoji 0.6s ease-out' : 'none',
}}
>
{mainEmoji}
</div>
{/* Level badge */}
<span className="inline-block text-[10px] font-bold px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300 mb-2">
{t('levelDone', { label: levelLabel })}
</span>
{/* Task title */}
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-100 mt-2 mb-1 line-clamp-2">
{task.title}
</h3>
{/* Encouragement */}
<p className="text-sm text-slate-500 dark:text-slate-400 mb-5">
{encouragement}
</p>
{/* Progress ring */}
<div className="flex items-center justify-center gap-3 mb-5">
<svg className="w-12 h-12" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="15.5" fill="none" stroke="currentColor" className="text-slate-100 dark:text-slate-700" strokeWidth="3" />
<circle
cx="18" cy="18" r="15.5" fill="none" stroke="currentColor"
className="text-emerald-500"
strokeWidth="3"
strokeDasharray="97.4"
strokeDashoffset="0"
strokeLinecap="round"
style={{
animation: visible ? 'taskCompleteRing 0.8s ease-out 0.2s both' : 'none',
}}
/>
<text x="18" y="18" textAnchor="middle" dominantBaseline="central"
className="fill-emerald-600 dark:fill-emerald-400 text-[9px] font-bold"
>✓</text>
</svg>
<span className="text-xs text-slate-400 dark:text-slate-500">{t('completed')}</span>
</div>
{/* Close button */}
<button
onClick={handleClose}
className="w-full bg-gradient-to-r from-emerald-500 to-green-500 text-white font-semibold py-2.5 rounded-xl hover:from-emerald-600 hover:to-green-600 transition-all shadow-md shadow-emerald-200 dark:shadow-emerald-900/30 active:scale-[0.97]"
>
{t('keepGoing')}
</button>
</div>
{/* Floating particles */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{Array.from({ length: 12 }).map((_, i) => (
<div
key={i}
className="absolute rounded-full"
style={{
left: `${10 + Math.random() * 80}%`,
bottom: '-5%',
width: `${4 + Math.random() * 5}px`,
height: `${4 + Math.random() * 5}px`,
backgroundColor: ['#f59e0b', '#ef4444', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899'][i % 6],
animation: visible ? `taskCompleteParticle ${1 + Math.random() * 1.5}s ease-out ${Math.random() * 0.3}s forwards` : 'none',
opacity: 0,
}}
/>
))}
</div>
</div>
<style>{`
@keyframes taskCompleteEmoji {
0% { opacity: 0; transform: scale(0) rotate(-30deg); }
50% { transform: scale(1.3) rotate(5deg); }
70% { transform: scale(0.9) rotate(-2deg); }
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
@keyframes taskCompleteRing {
from { stroke-dashoffset: 97.4; }
to { stroke-dashoffset: 0; }
}
@keyframes taskCompleteParticle {
0% { transform: translateY(0) scale(0); opacity: 0; }
20% { opacity: 1; transform: translateY(-20px) scale(1); }
100% { transform: translateY(-120px) scale(0.3); opacity: 0; }
}
`}</style>
</div>,
document.body
);
};
export default TaskCompleteModal;