2026-07-20 01:33:44 +02:00
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
2026-07-19 21:30:26 +02:00
|
|
|
import clsx from 'clsx';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-07-20 01:33:44 +02:00
|
|
|
import {
|
|
|
|
|
useContactFolders,
|
|
|
|
|
useCreateContactFolder,
|
|
|
|
|
useUpdateContactFolder,
|
|
|
|
|
useDeleteContactFolder,
|
|
|
|
|
useMoveContactToFolder,
|
|
|
|
|
} from '@/api/hooks';
|
|
|
|
|
import { buildFolderTree, type ContactFolderTreeNode } from '@/api/contactFolders';
|
2026-07-19 21:30:26 +02:00
|
|
|
|
2026-07-20 01:33:44 +02:00
|
|
|
export type ContactFilter = 'all' | 'company' | 'person' | `tag:${string}` | `folder:${string}`;
|
2026-07-19 21:30:26 +02:00
|
|
|
|
|
|
|
|
export interface ContactFolderTreeProps {
|
|
|
|
|
selectedFilter: ContactFilter;
|
|
|
|
|
onSelect: (filter: ContactFilter) => void;
|
|
|
|
|
tags: string[];
|
|
|
|
|
loading?: boolean;
|
2026-07-20 01:33:44 +02:00
|
|
|
contacts?: { id: string; displayname: string; type: string; folder_id?: string | null }[];
|
2026-07-19 21:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 03:12:15 +02:00
|
|
|
// Icons
|
2026-07-20 01:33:44 +02:00
|
|
|
|
2026-07-20 00:25:58 +02:00
|
|
|
const icon = (path: string, cls = 'w-4 h-4') => (
|
|
|
|
|
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
2026-07-19 21:30:26 +02:00
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={path} />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-20 00:25:58 +02:00
|
|
|
const chevron = (open: boolean) => (
|
|
|
|
|
<svg
|
|
|
|
|
className={clsx('w-3.5 h-3.5 transition-transform flex-shrink-0', open && 'rotate-90')}
|
|
|
|
|
fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"
|
|
|
|
|
>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ICONS = {
|
|
|
|
|
all: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6-3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z',
|
2026-07-20 07:36:35 +02:00
|
|
|
folder: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z',
|
2026-07-20 00:25:58 +02:00
|
|
|
tag: 'M7 7h.01M7 3h5a1.99 1.99 0 01.832.184l4 2A2 2 0 0118 7v10a2 2 0 01-2 2H7a2 2 0 01-2-2V5a2 2 0 012-2z',
|
2026-07-20 01:33:44 +02:00
|
|
|
edit: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z',
|
|
|
|
|
trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
|
|
|
|
|
plus: 'M12 4v16m8-8H4',
|
2026-07-20 00:25:58 +02:00
|
|
|
};
|
|
|
|
|
|
2026-07-20 03:12:15 +02:00
|
|
|
// Context Menu
|
2026-07-20 01:33:44 +02:00
|
|
|
|
|
|
|
|
interface ContextMenuState {
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
type: 'folder' | 'root';
|
|
|
|
|
id: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ContextMenu({
|
|
|
|
|
state, onClose, onRename, onDelete, onNewFolder, onNewSubfolder,
|
|
|
|
|
}: {
|
|
|
|
|
state: ContextMenuState;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onRename: (id: string) => void;
|
|
|
|
|
onDelete: (id: string) => void;
|
|
|
|
|
onNewFolder: () => void;
|
|
|
|
|
onNewSubfolder: (parentId: string) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handler = (e: MouseEvent) => {
|
|
|
|
|
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('mousedown', handler);
|
|
|
|
|
return () => document.removeEventListener('mousedown', handler);
|
|
|
|
|
}, [onClose]);
|
|
|
|
|
|
|
|
|
|
const items: { label: string; action: () => void; danger?: boolean }[] = [];
|
|
|
|
|
|
|
|
|
|
if (state.type === 'folder') {
|
|
|
|
|
items.push({ label: 'Umbenennen', action: () => { onRename(state.id!); onClose(); } });
|
|
|
|
|
items.push({ label: 'Neuer Unterordner', action: () => { onNewSubfolder(state.id!); onClose(); } });
|
2026-07-20 03:12:15 +02:00
|
|
|
items.push({ label: 'L\u00f6schen', action: () => { onDelete(state.id!); onClose(); }, danger: true });
|
2026-07-20 01:33:44 +02:00
|
|
|
} else {
|
|
|
|
|
items.push({ label: 'Neuer Ordner', action: () => { onNewFolder(); onClose(); } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
className="fixed z-50 bg-white border border-secondary-200 rounded-lg shadow-lg py-1 min-w-[160px]"
|
|
|
|
|
style={{ left: state.x, top: state.y }}
|
|
|
|
|
>
|
|
|
|
|
{items.map((item, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={i}
|
|
|
|
|
onClick={item.action}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'w-full text-left px-3 py-1.5 text-sm hover:bg-secondary-100',
|
|
|
|
|
item.danger && 'text-red-600 hover:bg-red-50'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 03:12:15 +02:00
|
|
|
// Folder Tree Item
|
2026-07-20 01:33:44 +02:00
|
|
|
|
|
|
|
|
function FolderTreeItem({
|
|
|
|
|
node,
|
|
|
|
|
depth,
|
|
|
|
|
selectedFilter,
|
|
|
|
|
onSelectFolder,
|
|
|
|
|
onContextMenu,
|
|
|
|
|
isDragOver,
|
|
|
|
|
onDragOver,
|
|
|
|
|
onDragLeave,
|
|
|
|
|
onDrop,
|
|
|
|
|
}: {
|
|
|
|
|
node: ContactFolderTreeNode;
|
|
|
|
|
depth: number;
|
|
|
|
|
selectedFilter: ContactFilter;
|
|
|
|
|
onSelectFolder: (folderId: string) => void;
|
|
|
|
|
onContextMenu: (e: React.MouseEvent, type: 'folder' | 'root', id: string | null) => void;
|
|
|
|
|
isDragOver: boolean;
|
|
|
|
|
onDragOver: (e: React.DragEvent, folderId: string) => void;
|
|
|
|
|
onDragLeave: (folderId: string) => void;
|
|
|
|
|
onDrop: (e: React.DragEvent, folderId: string) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const [expanded, setExpanded] = useState(true);
|
|
|
|
|
const folderKey = `folder:${node.id}` as ContactFilter;
|
|
|
|
|
const isActive = selectedFilter === folderKey;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div
|
|
|
|
|
onDragOver={(e) => onDragOver(e, node.id)}
|
|
|
|
|
onDragLeave={() => onDragLeave(node.id)}
|
|
|
|
|
onDrop={(e) => onDrop(e, node.id)}
|
|
|
|
|
onContextMenu={(e) => onContextMenu(e, 'folder', node.id)}
|
|
|
|
|
className={clsx(
|
|
|
|
|
'group flex items-center gap-1.5 px-2 py-1.5 text-sm font-medium rounded-md cursor-pointer min-h-touch',
|
|
|
|
|
'transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
|
|
|
isDragOver
|
|
|
|
|
? 'bg-primary-100 ring-2 ring-primary-400'
|
|
|
|
|
: isActive
|
|
|
|
|
? 'bg-primary-50 text-primary-700'
|
|
|
|
|
: 'text-secondary-700 hover:bg-secondary-100',
|
|
|
|
|
)}
|
|
|
|
|
style={{ paddingLeft: depth * 12 + 8 }}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setExpanded(!expanded);
|
|
|
|
|
onSelectFolder(node.id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{chevron(expanded)}
|
2026-07-20 07:36:35 +02:00
|
|
|
{icon(ICONS.folder, 'w-4 h-4 text-secondary-400')}
|
2026-07-20 01:33:44 +02:00
|
|
|
<span className="flex-1 truncate">{node.name}</span>
|
|
|
|
|
{node.contact_count > 0 && (
|
|
|
|
|
<span className="text-xs text-secondary-400 tabular-nums">{node.contact_count}</span>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); onContextMenu(e, 'folder', node.id); }}
|
|
|
|
|
className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-primary-600 p-0.5"
|
|
|
|
|
title="Umbenennen"
|
|
|
|
|
>
|
|
|
|
|
{icon(ICONS.edit, 'w-3.5 h-3.5')}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); onContextMenu(e, 'folder', node.id); }}
|
|
|
|
|
className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-red-600 p-0.5"
|
2026-07-20 03:12:15 +02:00
|
|
|
title="L\u00f6schen"
|
2026-07-20 01:33:44 +02:00
|
|
|
>
|
|
|
|
|
{icon(ICONS.trash, 'w-3.5 h-3.5')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{expanded && node.children.map((child) => (
|
|
|
|
|
<FolderTreeItem
|
|
|
|
|
key={child.id}
|
|
|
|
|
node={child}
|
|
|
|
|
depth={depth + 1}
|
|
|
|
|
selectedFilter={selectedFilter}
|
|
|
|
|
onSelectFolder={onSelectFolder}
|
|
|
|
|
onContextMenu={onContextMenu}
|
|
|
|
|
isDragOver={false}
|
|
|
|
|
onDragOver={onDragOver}
|
|
|
|
|
onDragLeave={onDragLeave}
|
|
|
|
|
onDrop={onDrop}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 03:12:15 +02:00
|
|
|
// Main Component
|
2026-07-20 01:33:44 +02:00
|
|
|
|
2026-07-19 21:30:26 +02:00
|
|
|
export function ContactFolderTree({
|
|
|
|
|
selectedFilter,
|
|
|
|
|
onSelect,
|
|
|
|
|
tags,
|
|
|
|
|
loading,
|
2026-07-20 01:33:44 +02:00
|
|
|
contacts = [],
|
2026-07-19 21:30:26 +02:00
|
|
|
}: ContactFolderTreeProps) {
|
|
|
|
|
const { t } = useTranslation();
|
2026-07-20 00:25:58 +02:00
|
|
|
const [tagsOpen, setTagsOpen] = useState(true);
|
2026-07-20 01:33:44 +02:00
|
|
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
|
|
|
|
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const { data: folders, isLoading: foldersLoading } = useContactFolders();
|
|
|
|
|
const createFolderMut = useCreateContactFolder();
|
|
|
|
|
const updateFolderMut = useUpdateContactFolder();
|
|
|
|
|
const deleteFolderMut = useDeleteContactFolder();
|
|
|
|
|
const moveContactMut = useMoveContactToFolder();
|
|
|
|
|
|
|
|
|
|
const folderList = folders ?? [];
|
|
|
|
|
const tree = buildFolderTree(folderList);
|
2026-07-19 21:30:26 +02:00
|
|
|
|
2026-07-20 00:25:58 +02:00
|
|
|
const itemCls = (active: boolean) =>
|
|
|
|
|
clsx(
|
|
|
|
|
'flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm font-medium min-h-touch text-left',
|
|
|
|
|
'transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
|
|
|
active ? 'bg-primary-50 text-primary-700' : 'text-secondary-700 hover:bg-secondary-100',
|
|
|
|
|
);
|
2026-07-19 21:30:26 +02:00
|
|
|
|
2026-07-20 01:33:44 +02:00
|
|
|
const handleContextMenu = useCallback((e: React.MouseEvent, type: 'folder' | 'root', id: string | null) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setContextMenu({ x: e.clientX, y: e.clientY, type, id });
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleNewFolder = () => {
|
|
|
|
|
const name = prompt('Ordnername:');
|
|
|
|
|
if (!name) return;
|
|
|
|
|
createFolderMut.mutate({ name });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleNewSubfolder = (parentId: string) => {
|
|
|
|
|
const name = prompt('Unterordnername:');
|
|
|
|
|
if (!name) return;
|
|
|
|
|
createFolderMut.mutate({ name, parent_id: parentId });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRename = (id: string) => {
|
|
|
|
|
const folder = folderList.find((f) => f.id === id);
|
|
|
|
|
const newName = prompt('Neuer Name:', folder?.name || '');
|
|
|
|
|
if (!newName) return;
|
|
|
|
|
updateFolderMut.mutate({ id, data: { name: newName } });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = (id: string) => {
|
2026-07-20 03:12:15 +02:00
|
|
|
if (!confirm('Ordner l\u00f6schen? Kontakte bleiben erhalten, werden aber keinem Ordner mehr zugeordnet.')) return;
|
2026-07-20 01:33:44 +02:00
|
|
|
deleteFolderMut.mutate(id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragOver = (e: React.DragEvent, folderId: string) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setDragOverFolderId(folderId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragLeave = (folderId: string) => {
|
|
|
|
|
if (dragOverFolderId === folderId) setDragOverFolderId(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDrop = (e: React.DragEvent, folderId: string) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setDragOverFolderId(null);
|
|
|
|
|
const contactId = e.dataTransfer.getData('text/plain');
|
|
|
|
|
if (!contactId) return;
|
|
|
|
|
moveContactMut.mutate({ contactId, folderId });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRootDrop = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const contactId = e.dataTransfer.getData('text/plain');
|
|
|
|
|
if (!contactId) return;
|
|
|
|
|
moveContactMut.mutate({ contactId, folderId: null });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRootDragOver = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-20 03:12:15 +02:00
|
|
|
const handleSelectFolder = useCallback((folderId: string) => {
|
|
|
|
|
onSelect(`folder:${folderId}` as ContactFilter);
|
|
|
|
|
}, [onSelect]);
|
|
|
|
|
|
2026-07-19 21:30:26 +02:00
|
|
|
return (
|
2026-07-20 00:25:58 +02:00
|
|
|
<nav className="space-y-0.5" aria-label={t('contacts.title')} data-testid="contact-folder-tree">
|
2026-07-20 07:36:35 +02:00
|
|
|
{/* Alle Kontakte */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onSelect('all')}
|
|
|
|
|
className={itemCls(selectedFilter === 'all')}
|
|
|
|
|
aria-current={selectedFilter === 'all' ? 'true' : undefined}
|
|
|
|
|
>
|
|
|
|
|
{icon(ICONS.all)}
|
|
|
|
|
<span>{t('contacts.allContacts')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Folder tree */}
|
2026-07-20 03:12:15 +02:00
|
|
|
<div
|
|
|
|
|
onDragOver={handleRootDragOver}
|
|
|
|
|
onDrop={handleRootDrop}
|
|
|
|
|
onContextMenu={(e) => handleContextMenu(e, 'root', null)}
|
2026-07-20 00:25:58 +02:00
|
|
|
>
|
2026-07-20 07:36:35 +02:00
|
|
|
{tree.map((node) => (
|
|
|
|
|
<FolderTreeItem
|
|
|
|
|
key={node.id}
|
|
|
|
|
node={node}
|
|
|
|
|
depth={0}
|
|
|
|
|
selectedFilter={selectedFilter}
|
|
|
|
|
onSelectFolder={handleSelectFolder}
|
|
|
|
|
onContextMenu={handleContextMenu}
|
|
|
|
|
isDragOver={dragOverFolderId === node.id}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDragLeave={handleDragLeave}
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{/* Root drop zone */}
|
|
|
|
|
<div
|
|
|
|
|
onDragOver={handleRootDragOver}
|
|
|
|
|
onDrop={handleRootDrop}
|
|
|
|
|
className="min-h-[12px] mt-1 rounded-md transition-colors hover:bg-secondary-50"
|
|
|
|
|
onContextMenu={(e) => handleContextMenu(e, 'root', null)}
|
|
|
|
|
/>
|
2026-07-20 01:33:44 +02:00
|
|
|
|
2026-07-20 07:36:35 +02:00
|
|
|
{(foldersLoading || loading) && (
|
|
|
|
|
<div className="px-2 py-1 text-xs text-secondary-400">{t('common.loading')}</div>
|
2026-07-20 01:33:44 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-20 07:36:35 +02:00
|
|
|
{/* Tags */}
|
2026-07-19 21:30:26 +02:00
|
|
|
{tags.length > 0 && (
|
2026-07-20 00:25:58 +02:00
|
|
|
<div className="pt-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setTagsOpen(!tagsOpen)}
|
|
|
|
|
className="flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs font-semibold text-secondary-500 uppercase tracking-wide hover:bg-secondary-100 min-h-touch text-left"
|
|
|
|
|
aria-expanded={tagsOpen}
|
|
|
|
|
>
|
|
|
|
|
{chevron(tagsOpen)}
|
|
|
|
|
{icon(ICONS.tag, 'w-3.5 h-3.5')}
|
|
|
|
|
<span>{t('tags.title')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
{tagsOpen && (
|
|
|
|
|
<div className="mt-0.5 space-y-0.5">
|
|
|
|
|
{tags.map((tag) => {
|
|
|
|
|
const key = `tag:${tag}` as ContactFilter;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={key}
|
|
|
|
|
onClick={() => onSelect(key)}
|
|
|
|
|
style={{ paddingLeft: '32px' }}
|
|
|
|
|
className={itemCls(selectedFilter === key)}
|
|
|
|
|
aria-current={selectedFilter === key ? 'true' : undefined}
|
|
|
|
|
>
|
|
|
|
|
{icon(ICONS.tag, 'w-3.5 h-3.5')}
|
|
|
|
|
<span className="truncate">{tag}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-07-19 21:30:26 +02:00
|
|
|
)}
|
|
|
|
|
|
2026-07-20 01:33:44 +02:00
|
|
|
{contextMenu && (
|
|
|
|
|
<ContextMenu
|
|
|
|
|
state={contextMenu}
|
|
|
|
|
onClose={() => setContextMenu(null)}
|
|
|
|
|
onRename={handleRename}
|
|
|
|
|
onDelete={handleDelete}
|
|
|
|
|
onNewFolder={handleNewFolder}
|
|
|
|
|
onNewSubfolder={handleNewSubfolder}
|
|
|
|
|
/>
|
2026-07-19 21:30:26 +02:00
|
|
|
)}
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
|
|
|
|
}
|