236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import { useTranslation } from '../../../i18n/useTranslation';
|
||
import type { NamespaceCategory } from '../hooks/useNamespaceCategories';
|
||
import { useModalAnimation } from '../hooks/useModalAnimation';
|
||
|
||
type Props = {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
newCategoryLabel: string;
|
||
setNewCategoryLabel: (value: string) => void;
|
||
onCreateCategory: () => void;
|
||
uncategorizedNamespaces: string[];
|
||
categoriesWithKnownNamespaces: NamespaceCategory[];
|
||
namespaces: string[];
|
||
assignNamespaceByCategory: Record<string, string>;
|
||
setAssignNamespaceByCategory: (next: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)) => void;
|
||
expandedCategoryId: string | null;
|
||
setExpandedCategoryId: (value: string | null | ((prev: string | null) => string | null)) => void;
|
||
dragNamespace: string | null;
|
||
setDragNamespace: (value: string | null) => void;
|
||
addNamespaceToCategory: (categoryId: string, namespace: string) => void;
|
||
removeNamespaceFromCategory: (categoryId: string, namespace: string) => void;
|
||
deleteCategory: (categoryId: string) => void;
|
||
};
|
||
|
||
export default function CategoryManagerModal({
|
||
isOpen,
|
||
onClose,
|
||
newCategoryLabel,
|
||
setNewCategoryLabel,
|
||
onCreateCategory,
|
||
uncategorizedNamespaces,
|
||
categoriesWithKnownNamespaces,
|
||
namespaces,
|
||
assignNamespaceByCategory,
|
||
setAssignNamespaceByCategory,
|
||
expandedCategoryId,
|
||
setExpandedCategoryId,
|
||
dragNamespace,
|
||
setDragNamespace,
|
||
addNamespaceToCategory,
|
||
removeNamespaceFromCategory,
|
||
deleteCategory,
|
||
}: Props) {
|
||
const { t } = useTranslation();
|
||
const { isRendered, isVisible } = useModalAnimation(isOpen);
|
||
|
||
if (!isRendered) return null;
|
||
|
||
return (
|
||
<div className={`fixed inset-0 z-[150] flex items-center justify-center bg-black/30 backdrop-blur-md transition-opacity duration-200 ${
|
||
isVisible ? 'opacity-100' : 'opacity-0'
|
||
}`}>
|
||
<div className={`mx-4 w-full max-w-5xl rounded-[30px] border border-white/80 bg-white/88 backdrop-blur overflow-hidden shadow-[0_32px_80px_-40px_rgba(15,23,42,0.42)] transform transition-all duration-200 ${
|
||
isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-2 scale-[0.98]'
|
||
}`}>
|
||
{/* Header */}
|
||
<div className="px-6 py-5 border-b border-white/60 flex items-start justify-between gap-4 bg-white/40">
|
||
<div>
|
||
<span className="inline-flex items-center rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500 shadow-sm mb-2">
|
||
Admin
|
||
</span>
|
||
<h2 className="text-xl font-black tracking-tight text-slate-950">{t('autofix.kef9de7f0')}</h2>
|
||
<p className="text-xs text-slate-500 mt-0.5">{t('autofix.kc4671abe')}</p>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="rounded-xl border border-slate-200 bg-white/80 px-2.5 py-1.5 text-slate-400 hover:text-slate-700 hover:bg-white transition shadow-sm text-base leading-none shrink-0"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="px-6 py-5 max-h-[70vh] overflow-y-auto space-y-4">
|
||
{/* Create category row */}
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<input
|
||
value={newCategoryLabel}
|
||
onChange={(e) => setNewCategoryLabel(e.target.value)}
|
||
placeholder={t('autofix.ke52ed6e9')}
|
||
className="w-56 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900/20 transition"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={onCreateCategory}
|
||
className="rounded-2xl bg-slate-900 text-white px-4 py-2 text-sm font-semibold shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)] hover:bg-slate-800 transition"
|
||
>
|
||
{t('autofix.k1db86f96')}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{/* Uncategorized pool */}
|
||
<div className="rounded-[20px] border border-white/80 bg-white/70 backdrop-blur p-4 shadow-sm">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-2.5">{t('autofix.k505ebdae')}</p>
|
||
<div className="flex flex-wrap gap-2 min-h-10">
|
||
{uncategorizedNamespaces.map((ns) => (
|
||
<span
|
||
key={ns}
|
||
draggable
|
||
onDragStart={() => setDragNamespace(ns)}
|
||
className="cursor-grab rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 transition"
|
||
title={t('autofix.k66edf1eb')}
|
||
>
|
||
{ns}
|
||
</span>
|
||
))}
|
||
{uncategorizedNamespaces.length === 0 && (
|
||
<span className="text-xs text-slate-400">{t('autofix.k741a01f7')}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Category list */}
|
||
<div className="space-y-2">
|
||
{categoriesWithKnownNamespaces.map((cat) => {
|
||
const availableToAssign = namespaces.filter((ns) => !cat.namespaces.includes(ns));
|
||
const selectValue = assignNamespaceByCategory[cat.id] ?? '';
|
||
const isExpanded = expandedCategoryId === cat.id;
|
||
|
||
return (
|
||
<div
|
||
key={cat.id}
|
||
onDragEnter={() => {
|
||
if (!dragNamespace) return;
|
||
if (expandedCategoryId !== cat.id) setExpandedCategoryId(cat.id);
|
||
}}
|
||
onDragOver={(e) => e.preventDefault()}
|
||
onDrop={() => {
|
||
if (dragNamespace) {
|
||
addNamespaceToCategory(cat.id, dragNamespace);
|
||
setDragNamespace(null);
|
||
}
|
||
}}
|
||
className="rounded-[20px] border border-white/80 bg-white/70 backdrop-blur overflow-hidden shadow-sm"
|
||
>
|
||
{/* Category header row */}
|
||
<div
|
||
className="px-4 py-3 flex items-center justify-between gap-2 cursor-pointer hover:bg-white/60 transition"
|
||
onClick={() => setExpandedCategoryId((prev) => (prev === cat.id ? null : cat.id))}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-bold text-slate-950">{cat.label}</span>
|
||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-semibold text-slate-500 shadow-sm">
|
||
{cat.namespaces.length}
|
||
</span>
|
||
<span className="text-xs text-slate-400">{isExpanded ? t('autofix.k5daa1471') : t('autofix.k893106ba')}</span>
|
||
</div>
|
||
{cat.isCustom && (
|
||
<button
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
deleteCategory(cat.id);
|
||
}}
|
||
className="rounded-2xl border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-600 hover:bg-red-100 transition"
|
||
>
|
||
Delete
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{isExpanded && (
|
||
<div className="border-t border-white/60 p-4 space-y-3 bg-white/40">
|
||
<div className="flex flex-col lg:flex-row items-stretch gap-2">
|
||
<select
|
||
value={selectValue}
|
||
onChange={(e) => setAssignNamespaceByCategory((prev) => ({ ...prev, [cat.id]: e.target.value }))}
|
||
className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2 text-xs shadow-sm focus:outline-none focus:ring-2 focus:ring-slate-900/20 transition"
|
||
>
|
||
<option value="">{t('autofix.k0cdc3ee9')}</option>
|
||
{availableToAssign.map((ns) => (
|
||
<option key={ns} value={ns}>{ns}</option>
|
||
))}
|
||
</select>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (!selectValue) return;
|
||
addNamespaceToCategory(cat.id, selectValue);
|
||
setAssignNamespaceByCategory((prev) => ({ ...prev, [cat.id]: '' }));
|
||
}}
|
||
className="rounded-2xl border border-slate-200 bg-white px-4 py-2 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-2 min-h-10 rounded-2xl border border-dashed border-slate-200 bg-white/60 p-3">
|
||
{cat.namespaces.map((ns) => (
|
||
<span
|
||
key={ns}
|
||
draggable
|
||
onDragStart={() => setDragNamespace(ns)}
|
||
className="group cursor-grab inline-flex items-center rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 hover:border-indigo-300 transition"
|
||
>
|
||
{ns}
|
||
<button
|
||
type="button"
|
||
onClick={() => removeNamespaceFromCategory(cat.id, ns)}
|
||
className="ml-1.5 text-indigo-400 group-hover:text-red-500 transition leading-none"
|
||
title={t('autofix.ka6791a02')}
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
{cat.namespaces.length === 0 && (
|
||
<span className="text-xs text-slate-400">{t('autofix.kf3c3223a')}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="px-6 py-4 border-t border-white/60 bg-white/40 flex justify-end">
|
||
<button
|
||
onClick={onClose}
|
||
className="rounded-2xl bg-slate-900 text-white px-5 py-2 text-sm font-semibold shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)] hover:bg-slate-800 transition"
|
||
>
|
||
Done
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|