Files
Nitro-V3/src/components/catalog/views/admin/CatalogAdminPageTreeItem.tsx
T
Life adb30e6d60 feat(catalog-admin): full catalog admin editor with page/offer management
- New standalone admin editor window (1000x650) with Pages, Offers, Publish tabs
- Pages tab: full page tree with drag-and-drop reorder, identity/layout/content editing
- Icon browser: visual picker for 5500+ catalog icons with search and pagination
- Image browser: browse and select header/teaser images from server
- Offers tab: search, browse and edit catalog offers (cost, amounts, limited edition)
- Publish tab: one-click catalog publish with pending changes indicator
- Page tree: recursive expand/collapse, drag-and-drop reorder, hidden page indicators
- Widened catalog navigation sidebar (classic: 160->220px, modern: 250->280px)
- Improved font sizes and contrast across all admin components
- Support icon editing for root categories (iconId passed to save/create composers)
- Cleaned up inline admin controls from classic/modern catalog views
2026-04-13 00:03:40 +02:00

131 lines
5.2 KiB
TypeScript

import { FC, useCallback, useState } from 'react';
import { FaCaretDown, FaCaretRight, FaEyeSlash, FaGripVertical } from 'react-icons/fa';
import { ICatalogNode } from '../../../../api';
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
export interface CatalogAdminPageTreeItemProps
{
node: ICatalogNode;
depth?: number;
selectedPageId?: number;
onSelect: (node: ICatalogNode) => void;
onReorder: (pageId: number, newParentId: number, newIndex: number) => void;
}
export const CatalogAdminPageTreeItem: FC<CatalogAdminPageTreeItemProps> = props =>
{
const { node, depth = 0, selectedPageId, onSelect, onReorder } = props;
const [ isOpen, setIsOpen ] = useState(node.isOpen);
const [ isDragOver, setIsDragOver ] = useState<'above' | 'on' | 'below' | null>(null);
const isSelected = selectedPageId === node.pageId;
const isHidden = !node.isVisible;
const hasBranch = node.children && node.children.length > 0;
const handleDragStart = useCallback((e: React.DragEvent) =>
{
e.dataTransfer.setData('text/plain', JSON.stringify({ pageId: node.pageId, parentId: node.parent?.pageId ?? -1 }));
e.dataTransfer.effectAllowed = 'move';
}, [ node ]);
const handleDragOver = useCallback((e: React.DragEvent) =>
{
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const y = e.clientY - rect.top;
const third = rect.height / 3;
if(y < third) setIsDragOver('above');
else if(y > third * 2) setIsDragOver('below');
else setIsDragOver('on');
}, []);
const handleDrop = useCallback((e: React.DragEvent) =>
{
e.preventDefault();
setIsDragOver(null);
try
{
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
if(!data.pageId || data.pageId === node.pageId) return;
if(isDragOver === 'on' && hasBranch)
{
onReorder(data.pageId, node.pageId, 0);
}
else
{
const parentId = node.parent?.pageId ?? -1;
const index = node.parent?.children?.indexOf(node) ?? 0;
const targetIndex = isDragOver === 'below' ? index + 1 : index;
onReorder(data.pageId, parentId, targetIndex);
}
}
catch {}
}, [ node, isDragOver, hasBranch, onReorder ]);
return (
<div>
{ isDragOver === 'above' &&
<div className="h-[3px] bg-primary rounded-full mx-1" style={ { marginLeft: depth * 18 + 4 } } /> }
<div
className={ `group/tree flex items-center gap-1.5 px-2 py-1 mx-1 my-[1px] rounded cursor-pointer transition-all text-[13px]
${ isSelected ? 'bg-primary/20 border border-primary/40 font-semibold shadow-sm' : 'border border-transparent hover:bg-white/50' }
${ isHidden ? 'opacity-40' : '' }
${ isDragOver === 'on' ? 'ring-2 ring-primary bg-primary/5' : '' }` }
draggable
style={ { paddingLeft: depth * 18 + 8 } }
onClick={ () => onSelect(node) }
onDragLeave={ () => setIsDragOver(null) }
onDragOver={ handleDragOver }
onDragStart={ handleDragStart }
onDrop={ handleDrop }
>
<FaGripVertical className="text-[12px] text-slate-500 shrink-0 group-hover/tree:text-slate-800 cursor-grab" />
{ hasBranch
? <span
className="text-[15px] text-slate-700 shrink-0 cursor-pointer hover:text-primary w-4 flex items-center justify-center"
onClick={ e => { e.stopPropagation(); setIsOpen(!isOpen); } }
>
{ isOpen ? <FaCaretDown /> : <FaCaretRight /> }
</span>
: <span className="w-4 shrink-0" /> }
<div className="w-6 h-6 flex items-center justify-center shrink-0">
<CatalogIconView icon={ node.iconId } />
</div>
<span className="flex-1 truncate text-dark" title={ `${ node.localization } (ID: ${ node.pageId })` }>
{ node.localization }
</span>
{ isHidden && <FaEyeSlash className="text-[12px] text-danger shrink-0" title="Page is hidden" /> }
<span className="text-[11px] text-slate-500 shrink-0 font-mono">
#{ node.pageId }
</span>
</div>
{ isDragOver === 'below' &&
<div className="h-[3px] bg-primary rounded-full mx-1" style={ { marginLeft: depth * 18 + 4 } } /> }
{ isOpen && hasBranch && node.children.map((child, index) =>
<CatalogAdminPageTreeItem
key={ `${ child.pageId }-${ index }` }
depth={ depth + 1 }
node={ child }
selectedPageId={ selectedPageId }
onReorder={ onReorder }
onSelect={ onSelect }
/>
) }
</div>
);
};