177 lines
7.8 KiB
TypeScript
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;
|