151 lines
6.5 KiB
TypeScript
151 lines
6.5 KiB
TypeScript
|
|
import React, { useState, useEffect, useRef } from 'react';
|
||
|
|
import type { GeIdItem } from './QueueStatus';
|
||
|
|
import DragHandleIcon from './DragHandleIcon';
|
||
|
|
|
||
|
|
interface QueueManagementModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
queueItems: GeIdItem[];
|
||
|
|
onReorder: (reorderedItems: GeIdItem[]) => void;
|
||
|
|
onDelete: (key: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QueueManagementModal: React.FC<QueueManagementModalProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
queueItems,
|
||
|
|
onReorder,
|
||
|
|
onDelete,
|
||
|
|
}) => {
|
||
|
|
const [localItems, setLocalItems] = useState<GeIdItem[]>([]);
|
||
|
|
|
||
|
|
const dragItem = useRef<number | null>(null);
|
||
|
|
const dragOverItem = useRef<number | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen) {
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
setLocalItems(queueItems.filter(item => item.status === 'waiting'));
|
||
|
|
} else {
|
||
|
|
document.body.style.overflow = 'auto';
|
||
|
|
}
|
||
|
|
return () => {
|
||
|
|
document.body.style.overflow = 'auto';
|
||
|
|
};
|
||
|
|
}, [isOpen, queueItems]);
|
||
|
|
|
||
|
|
if (!isOpen) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, position: number) => {
|
||
|
|
dragItem.current = position;
|
||
|
|
e.dataTransfer.effectAllowed = 'move';
|
||
|
|
// Add a delay to allow the ghost image to be created before styling
|
||
|
|
setTimeout(() => {
|
||
|
|
e.currentTarget.classList.add('dragging');
|
||
|
|
}, 0)
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>, position: number) => {
|
||
|
|
dragOverItem.current = position;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||
|
|
if (dragItem.current === null || dragOverItem.current === null) return;
|
||
|
|
const newItems = [...localItems];
|
||
|
|
const dragItemContent = newItems[dragItem.current];
|
||
|
|
newItems.splice(dragItem.current, 1);
|
||
|
|
newItems.splice(dragOverItem.current, 0, dragItemContent);
|
||
|
|
dragItem.current = null;
|
||
|
|
dragOverItem.current = null;
|
||
|
|
setLocalItems(newItems);
|
||
|
|
onReorder(newItems); // Update parent state
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
|
||
|
|
e.currentTarget.classList.remove('dragging');
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="fixed inset-0 bg-black bg-opacity-70 z-50 flex justify-center items-center p-4 animate-fade-in-fast"
|
||
|
|
onClick={onClose}
|
||
|
|
role="dialog"
|
||
|
|
aria-modal="true"
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className="bg-slate-800 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col p-6 animate-slide-up"
|
||
|
|
onClick={e => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
<div className="flex justify-between items-center mb-4 pb-4 border-b border-slate-700">
|
||
|
|
<h2 className="text-xl font-semibold text-white">Quản lý Hàng đợi</h2>
|
||
|
|
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors text-2xl leading-none">×</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex-grow overflow-y-auto pr-2 -mr-2">
|
||
|
|
{localItems.length > 0 ? (
|
||
|
|
<div className="space-y-2">
|
||
|
|
{localItems.map((item, index) => (
|
||
|
|
<div
|
||
|
|
key={item.key}
|
||
|
|
className="flex items-start justify-between p-3 bg-slate-700/50 rounded-lg group transition-shadow"
|
||
|
|
draggable
|
||
|
|
onDragStart={(e) => handleDragStart(e, index)}
|
||
|
|
onDragEnter={(e) => handleDragEnter(e, index)}
|
||
|
|
onDragEnd={handleDragEnd}
|
||
|
|
onDragOver={(e) => e.preventDefault()}
|
||
|
|
onDrop={handleDrop}
|
||
|
|
>
|
||
|
|
<div className="flex items-start flex-1 min-w-0">
|
||
|
|
<div className="cursor-move text-slate-500 group-hover:text-slate-300 mr-4 pt-1 flex-shrink-0">
|
||
|
|
<DragHandleIcon />
|
||
|
|
</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="font-mono text-sm text-slate-200 font-semibold truncate" title={`${item.id} (${item.lang})`}>
|
||
|
|
{item.id} ({item.lang})
|
||
|
|
</div>
|
||
|
|
<pre className="text-slate-400 text-xs font-mono mt-1 whitespace-pre-wrap break-words">{item.usernames}</pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => onDelete(item.key)}
|
||
|
|
className="text-xs font-medium text-rose-500 hover:text-rose-400 transition-colors opacity-0 group-hover:opacity-100 ml-4 flex-shrink-0"
|
||
|
|
>
|
||
|
|
Xoá
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="flex justify-center items-center h-full">
|
||
|
|
<p className="text-slate-500">Không có submit nào đang chờ.</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<style>{`
|
||
|
|
.dragging {
|
||
|
|
opacity: 0.5;
|
||
|
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.2), 0 4px 6px -4px rgb(0 0 0 / 0.2);
|
||
|
|
}
|
||
|
|
@keyframes fade-in-fast {
|
||
|
|
from { opacity: 0; }
|
||
|
|
to { opacity: 1; }
|
||
|
|
}
|
||
|
|
.animate-fade-in-fast {
|
||
|
|
animation: fade-in-fast 0.2s ease-out forwards;
|
||
|
|
}
|
||
|
|
@keyframes slide-up {
|
||
|
|
from { opacity: 0; transform: translateY(20px); }
|
||
|
|
to { opacity: 1; transform: translateY(0); }
|
||
|
|
}
|
||
|
|
.animate-slide-up {
|
||
|
|
animation: slide-up 0.3s ease-out forwards;
|
||
|
|
}
|
||
|
|
`}</style>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default QueueManagementModal;
|