616 lines
22 KiB
TypeScript
616 lines
22 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Search, Folder, FolderPlus, MessageSquare, MoreVertical, Trash2, Edit, FolderInput, ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { chatService, ChatRecord, FolderRecord } from "@/services/chat";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { ptBR } from "date-fns/locale";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
interface ChatSidebarProps {
|
|
isCollapsed: boolean;
|
|
onToggleCollapse: () => void;
|
|
onNewChat: () => void;
|
|
onSelectChat?: (chat: ChatRecord) => void;
|
|
currentChatId?: string;
|
|
}
|
|
|
|
export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelectChat, currentChatId }: ChatSidebarProps) => {
|
|
const { toast } = useToast();
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
|
const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false);
|
|
const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false);
|
|
const [isRenameFolderOpen, setIsRenameFolderOpen] = useState(false);
|
|
const [newFolderName, setNewFolderName] = useState("");
|
|
const [renamingFolder, setRenamingFolder] = useState<FolderRecord | null>(null);
|
|
const [renamedFolderName, setRenamedFolderName] = useState("");
|
|
const [deletingFolder, setDeletingFolder] = useState<FolderRecord | null>(null);
|
|
const [deletingChat, setDeletingChat] = useState<ChatRecord | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// Estado carregado do banco de dados via chatService
|
|
const [chats, setChats] = useState<ChatRecord[]>([]);
|
|
const [folders, setFolders] = useState<FolderRecord[]>([]);
|
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
|
|
|
// Carrega chats e pastas do banco de dados quando o componente monta
|
|
useEffect(() => {
|
|
loadChatsAndFolders();
|
|
|
|
// Escuta evento customizado para recarregar quando chat é atualizado
|
|
const handleChatUpdate = () => {
|
|
loadChatsAndFolders();
|
|
};
|
|
|
|
window.addEventListener('chatUpdated', handleChatUpdate);
|
|
|
|
// Cleanup
|
|
return () => {
|
|
window.removeEventListener('chatUpdated', handleChatUpdate);
|
|
};
|
|
}, []);
|
|
|
|
const loadChatsAndFolders = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await chatService.getChatsAndFolders();
|
|
|
|
console.log('Dados carregados:', data);
|
|
|
|
setChats(data.chats);
|
|
setFolders(data.folders);
|
|
|
|
// Expande todas as pastas por padrão
|
|
setExpandedFolders(new Set(data.folders.map(f => f.id)));
|
|
} catch (error: any) {
|
|
console.error('Erro ao carregar chats e pastas:', error);
|
|
toast({
|
|
title: "Erro ao carregar dados",
|
|
description: error.message || "Não foi possível carregar chats e pastas.",
|
|
variant: "destructive",
|
|
});
|
|
|
|
// Define arrays vazios em caso de erro
|
|
setChats([]);
|
|
setFolders([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateFolder = async () => {
|
|
if (newFolderName.trim()) {
|
|
try {
|
|
await chatService.createFolder(newFolderName);
|
|
await loadChatsAndFolders();
|
|
setNewFolderName("");
|
|
setIsCreateFolderOpen(false);
|
|
|
|
toast({
|
|
title: "Pasta criada",
|
|
description: `Pasta "${newFolderName}" criada com sucesso.`,
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Erro ao criar pasta:', error);
|
|
toast({
|
|
title: "Erro ao criar pasta",
|
|
description: error.message || "Não foi possível criar a pasta. Tente novamente.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDeleteFolder = async () => {
|
|
if (deletingFolder) {
|
|
try {
|
|
await chatService.deleteFolder(deletingFolder.id);
|
|
await loadChatsAndFolders();
|
|
setDeletingFolder(null);
|
|
setIsDeleteFolderOpen(false);
|
|
|
|
toast({
|
|
title: "Pasta excluída",
|
|
description: "A pasta foi excluída com sucesso.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Erro ao excluir pasta:', error);
|
|
toast({
|
|
title: "Erro ao excluir pasta",
|
|
description: error.message || "Não foi possível excluir a pasta. Tente novamente.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const openDeleteFolder = (folder: FolderRecord) => {
|
|
setDeletingFolder(folder);
|
|
setIsDeleteFolderOpen(true);
|
|
};
|
|
|
|
const openRenameFolder = (folder: FolderRecord) => {
|
|
setRenamingFolder(folder);
|
|
setRenamedFolderName(folder.name);
|
|
setIsRenameFolderOpen(true);
|
|
};
|
|
|
|
const handleRenameFolder = async () => {
|
|
if (renamingFolder && renamedFolderName.trim()) {
|
|
try {
|
|
await chatService.renameFolder(renamingFolder.id, renamedFolderName);
|
|
await loadChatsAndFolders();
|
|
setRenamingFolder(null);
|
|
setRenamedFolderName("");
|
|
setIsRenameFolderOpen(false);
|
|
|
|
toast({
|
|
title: "Pasta renomeada",
|
|
description: `Pasta renomeada para "${renamedFolderName}" com sucesso.`,
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Erro ao renomear pasta:', error);
|
|
toast({
|
|
title: "Erro ao renomear pasta",
|
|
description: error.message || "Não foi possível renomear a pasta. Tente novamente.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const toggleFolder = (folderId: string) => {
|
|
const newExpanded = new Set(expandedFolders);
|
|
if (newExpanded.has(folderId)) {
|
|
newExpanded.delete(folderId);
|
|
} else {
|
|
newExpanded.add(folderId);
|
|
}
|
|
setExpandedFolders(newExpanded);
|
|
};
|
|
|
|
const moveToFolder = async (chatId: string, folderId: string) => {
|
|
try {
|
|
await chatService.moveChatToFolder(chatId, folderId);
|
|
await loadChatsAndFolders();
|
|
|
|
const folder = folders.find(f => f.id === folderId);
|
|
toast({
|
|
title: "Chat movido",
|
|
description: `Movido para a pasta "${folder?.name}".`,
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Erro ao mover chat:', error);
|
|
toast({
|
|
title: "Erro ao mover chat",
|
|
description: error.message || "Não foi possível mover o chat. Tente novamente.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDeleteChat = async () => {
|
|
if (deletingChat) {
|
|
try {
|
|
await chatService.deleteChat(deletingChat.id);
|
|
await loadChatsAndFolders();
|
|
setDeletingChat(null);
|
|
setIsDeleteChatOpen(false);
|
|
|
|
toast({
|
|
title: "Chat excluído",
|
|
description: "A conversa foi excluída com sucesso.",
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Erro ao excluir chat:', error);
|
|
toast({
|
|
title: "Erro ao excluir chat",
|
|
description: error.message || "Não foi possível excluir o chat. Tente novamente.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const openDeleteChat = (chat: ChatRecord) => {
|
|
setDeletingChat(chat);
|
|
setIsDeleteChatOpen(true);
|
|
};
|
|
|
|
const handleSelectChat = (chat: ChatRecord) => {
|
|
if (onSelectChat) {
|
|
onSelectChat(chat);
|
|
}
|
|
};
|
|
|
|
// Filtrar chats pela busca
|
|
const filteredChats = chats.filter((chat) => {
|
|
const searchLower = searchQuery.toLowerCase();
|
|
const titleMatch = chat.title.toLowerCase().includes(searchLower);
|
|
return titleMatch;
|
|
});
|
|
|
|
// Separar chats sem pasta (folder_id é null)
|
|
const chatsWithoutFolder = filteredChats.filter((c) => c.folder_id === null);
|
|
|
|
// Agrupar chats por pasta (quando folder_id === folder.id)
|
|
const chatsByFolder = folders.reduce((acc, folder) => {
|
|
acc[folder.id] = filteredChats.filter((c) => c.folder_id === folder.id);
|
|
return acc;
|
|
}, {} as Record<string, ChatRecord[]>);
|
|
|
|
if (isCollapsed) {
|
|
return (
|
|
<div className="w-12 h-full border-r border-border bg-card/30 backdrop-blur-sm flex flex-col items-center py-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onToggleCollapse}
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-80 h-full border-r border-border bg-card/30 backdrop-blur-sm flex flex-col">
|
|
{/* Header with collapse button */}
|
|
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-foreground">Conversas</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onToggleCollapse}
|
|
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="px-4 pb-3 border-b border-border space-y-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Pesquisar conversas..."
|
|
className="pl-9 bg-muted/50"
|
|
/>
|
|
</div>
|
|
|
|
<Button className="w-full gap-2 cyber-glow" variant="default" onClick={onNewChat}>
|
|
<Plus className="w-4 h-4" />
|
|
Novo Chat
|
|
</Button>
|
|
|
|
<Dialog open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" className="w-full gap-2" size="sm">
|
|
<FolderPlus className="w-4 h-4" />
|
|
Nova Pasta
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="glass-effect bg-card border-border z-50">
|
|
<DialogHeader>
|
|
<DialogTitle>Criar Nova Pasta</DialogTitle>
|
|
<DialogDescription>
|
|
Organize suas conversas em pastas personalizadas.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="folder-name">Nome da Pasta</Label>
|
|
<Input
|
|
id="folder-name"
|
|
value={newFolderName}
|
|
onChange={(e) => setNewFolderName(e.target.value)}
|
|
placeholder="Ex: Projetos, Estudos..."
|
|
onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setIsCreateFolderOpen(false)}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={handleCreateFolder} disabled={!newFolderName.trim()}>
|
|
Criar Pasta
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Rename Folder Dialog */}
|
|
<Dialog open={isRenameFolderOpen} onOpenChange={setIsRenameFolderOpen}>
|
|
<DialogContent className="glass-effect bg-card border-border z-50">
|
|
<DialogHeader>
|
|
<DialogTitle>Renomear Pasta</DialogTitle>
|
|
<DialogDescription>
|
|
Digite o novo nome para a pasta "{renamingFolder?.name}".
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="rename-folder-name">Novo Nome</Label>
|
|
<Input
|
|
id="rename-folder-name"
|
|
value={renamedFolderName}
|
|
onChange={(e) => setRenamedFolderName(e.target.value)}
|
|
placeholder="Ex: Projetos, Estudos..."
|
|
onKeyDown={(e) => e.key === "Enter" && handleRenameFolder()}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setIsRenameFolderOpen(false)}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={handleRenameFolder} disabled={!renamedFolderName.trim()}>
|
|
Renomear
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Folder Alert */}
|
|
<AlertDialog open={isDeleteFolderOpen} onOpenChange={setIsDeleteFolderOpen}>
|
|
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Excluir Pasta?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Esta ação não pode ser desfeita. A pasta "{deletingFolder?.name}" será excluída.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDeleteFolder} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
Excluir
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
|
|
{/* Conversations List */}
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-2 space-y-1">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="w-8 h-8 border-4 border-primary/20 border-t-primary rounded-full animate-spin" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Folders */}
|
|
{folders.map((folder) => (
|
|
<div key={folder.id} className="space-y-1">
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => toggleFolder(folder.id)}
|
|
className="flex-1 flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted/50 transition-colors group"
|
|
>
|
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
|
|
<Folder className="w-4 h-4 text-white" />
|
|
</div>
|
|
<span className="font-medium text-sm flex-1 text-left">
|
|
{folder.name}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{chatsByFolder[folder.id]?.length || 0}
|
|
</span>
|
|
</button>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<MoreVertical className="w-3 h-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
|
<DropdownMenuItem
|
|
className="gap-2"
|
|
onClick={() => openRenameFolder(folder)}
|
|
>
|
|
<Edit className="w-3 h-3" />
|
|
Renomear Pasta
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="gap-2 text-destructive"
|
|
onClick={() => openDeleteFolder(folder)}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
Excluir Pasta
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{expandedFolders.has(folder.id) && chatsByFolder[folder.id]?.length > 0 && (
|
|
<div className="ml-6 space-y-1">
|
|
{chatsByFolder[folder.id].map((chat) => (
|
|
<ChatItem
|
|
key={chat.id}
|
|
chat={chat}
|
|
isSelected={currentChatId === chat.id}
|
|
onSelect={() => handleSelectChat(chat)}
|
|
folders={folders}
|
|
onMoveToFolder={moveToFolder}
|
|
onDelete={() => openDeleteChat(chat)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{/* Chats without folder */}
|
|
{chatsWithoutFolder.length > 0 && (
|
|
<div className="space-y-1">
|
|
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
Sem Pasta
|
|
</div>
|
|
{chatsWithoutFolder.map((chat) => (
|
|
<ChatItem
|
|
key={chat.id}
|
|
chat={chat}
|
|
isSelected={currentChatId === chat.id}
|
|
onSelect={() => handleSelectChat(chat)}
|
|
folders={folders}
|
|
onMoveToFolder={moveToFolder}
|
|
onDelete={() => openDeleteChat(chat)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{!isLoading && chats.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
<MessageSquare className="w-12 h-12 text-muted-foreground opacity-50 mb-3" />
|
|
<p className="text-sm text-muted-foreground">
|
|
Nenhuma conversa ainda
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Clique em "Novo Chat" para começar
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Delete Chat Alert */}
|
|
<AlertDialog open={isDeleteChatOpen} onOpenChange={setIsDeleteChatOpen}>
|
|
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Excluir Conversa?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Esta ação não pode ser desfeita. A conversa "{deletingChat?.title}" será permanentemente excluída.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDeleteChat} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
Excluir
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface ChatItemProps {
|
|
chat: ChatRecord;
|
|
isSelected: boolean;
|
|
onSelect: () => void;
|
|
folders: FolderRecord[];
|
|
onMoveToFolder: (chatId: string, folderId: string) => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
const ChatItem = ({
|
|
chat,
|
|
isSelected,
|
|
onSelect,
|
|
folders,
|
|
onMoveToFolder,
|
|
onDelete,
|
|
}: ChatItemProps) => {
|
|
// Formata timestamp relativo
|
|
const timeAgo = formatDistanceToNow(new Date(chat.updated_at), {
|
|
addSuffix: true,
|
|
locale: ptBR,
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className={`group flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all w-full ${
|
|
isSelected
|
|
? "bg-sidebar-accent cyber-border"
|
|
: "hover:bg-muted/50"
|
|
}`}
|
|
onClick={onSelect}
|
|
>
|
|
<MessageSquare className="w-4 h-4 text-primary flex-shrink-0" />
|
|
<div className="flex-1 min-w-0 max-w-[180px]">
|
|
<p className="text-sm font-medium truncate" title={chat.title}>
|
|
{chat.title}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground truncate" title={timeAgo}>
|
|
{timeAgo}
|
|
</p>
|
|
</div>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
|
>
|
|
<MoreVertical className="w-3 h-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
|
{/* Opções para mover para pastas */}
|
|
{folders.filter(f => f.id !== chat.folder_id).map((folder) => (
|
|
<DropdownMenuItem
|
|
key={folder.id}
|
|
className="gap-2"
|
|
onClick={() => onMoveToFolder(chat.id, folder.id)}
|
|
>
|
|
<Folder className="w-3 h-3" />
|
|
Mover para {folder.name}
|
|
</DropdownMenuItem>
|
|
))}
|
|
|
|
<DropdownMenuItem
|
|
className="gap-2 text-destructive"
|
|
onClick={onDelete}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
Excluir
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
};
|