464 lines
13 KiB
TypeScript
464 lines
13 KiB
TypeScript
import { Check, CheckCheck, Crown, Search, SquarePen } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/router";
|
|
import { useCallback, useState } from "react";
|
|
import { Etichetta, EtichetteModal } from "~/components/etichette";
|
|
import { LoadingPage } from "~/components/loading";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "~/components/ui/alert-dialog";
|
|
import { Button, buttonVariants } from "~/components/ui/button";
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "~/components/ui/command";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuSeparator,
|
|
ContextMenuSub,
|
|
ContextMenuSubContent,
|
|
ContextMenuSubTrigger,
|
|
ContextMenuTrigger,
|
|
} from "~/components/ui/context-menu";
|
|
import Input from "~/components/ui/input";
|
|
import { UserAvatar } from "~/components/user_avatar";
|
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
|
import { cn } from "~/lib/utils";
|
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
|
import type { UsersId } from "~/schemas/public/Users";
|
|
import type { ActiveChatsType } from "~/server/controllers/chat.controller";
|
|
import type { NonNullableUser } from "~/server/controllers/user.controller";
|
|
import { api } from "~/utils/api";
|
|
|
|
export const ChatSidebar = ({
|
|
chatId,
|
|
userId,
|
|
}: {
|
|
chatId: ChatsChatid | null;
|
|
userId: UsersId;
|
|
}) => {
|
|
const utils = api.useUtils();
|
|
|
|
api.chatSSE.onSidebarUpdate.useSubscription(undefined, {
|
|
onData() {
|
|
void utils.chat.getActiveChats.invalidate();
|
|
},
|
|
});
|
|
const { data: activeChats, isLoading: loadingActiveChats } =
|
|
api.chat.getActiveChats.useQuery(undefined, {
|
|
refetchOnWindowFocus: true,
|
|
});
|
|
const { data: inactiveUsers, isLoading: loadingInactiveUsers } =
|
|
api.users.getActiveUsersWithoutChat.useQuery();
|
|
|
|
const router = useRouter();
|
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
const [selectedChat, setSelectedChat] = useState<ChatsChatid | null>(null);
|
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
|
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
const [editEtichettaModalOpen, setEditEtichettaModalOpen] = useState(false);
|
|
|
|
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
|
onSuccess: async () => {
|
|
await utils.chat.getActiveChats.invalidate();
|
|
await utils.users.getActiveUsersWithoutChat.invalidate();
|
|
},
|
|
});
|
|
|
|
const handleChatSelection = useCallback(
|
|
async (new_chatid: ChatsChatid) => {
|
|
if (chatId === new_chatid) return;
|
|
await router.push(`/area-riservata/admin/chats/${new_chatid}`);
|
|
setSearchDialogOpen(false);
|
|
},
|
|
[chatId, router],
|
|
);
|
|
|
|
if (loadingActiveChats || loadingInactiveUsers) return <LoadingPage />;
|
|
if (!activeChats || !inactiveUsers) return <div>Errore caricamento</div>;
|
|
|
|
return (
|
|
<div className="group relative flex h-full flex-col gap-4 py-2 data-[collapsed=true]:p-2">
|
|
<div className="mx-2 flex flex-col items-center justify-between gap-2 sm:flex-row sm:gap-0">
|
|
{isDesktop ? (
|
|
<div className="relative">
|
|
<Search className="absolute top-[0.8rem] left-2 size-4 text-muted-foreground" />
|
|
<label className="sr-only" htmlFor="search">
|
|
Search Chats
|
|
</label>
|
|
<Input className="m-0 pl-8" id="search" placeholder="Search" />
|
|
</div>
|
|
) : (
|
|
<Button
|
|
aria-label="Open Search Dialog"
|
|
className="size-8"
|
|
onClick={() => setSearchDialogOpen(true)}
|
|
size="icon"
|
|
variant="ghost"
|
|
>
|
|
<Search size={20} />
|
|
</Button>
|
|
)}
|
|
|
|
<SearchUserComponent
|
|
activeChats={activeChats}
|
|
handleChatSelection={handleChatSelection}
|
|
isOpen={searchDialogOpen}
|
|
OpenChange={setSearchDialogOpen}
|
|
/>
|
|
<Button
|
|
aria-label="Open Add User Dialog"
|
|
className="size-8"
|
|
onClick={() => setAddDialogOpen(true)}
|
|
size="icon"
|
|
variant="ghost"
|
|
>
|
|
<SquarePen size={20} />
|
|
</Button>
|
|
|
|
<NewUserComponent
|
|
inactiveUsers={inactiveUsers}
|
|
isOpen={addDialogOpen}
|
|
OpenChange={setAddDialogOpen}
|
|
/>
|
|
</div>
|
|
|
|
<nav className="grid gap-1 rounded-lg group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:px-2">
|
|
{activeChats.map((chat) => (
|
|
<ContextMenu key={chat.username}>
|
|
<ContextMenuTrigger asChild>
|
|
<Button
|
|
aria-label="Select Chat"
|
|
className={cn(
|
|
buttonVariants({
|
|
size: !isDesktop ? "icon" : "xl",
|
|
variant:
|
|
chat.lastmessage &&
|
|
!chat.lastmessage.isread &&
|
|
chat.lastmessage.sender !== userId
|
|
? "secondary"
|
|
: "grey",
|
|
}),
|
|
|
|
!isDesktop ? "p-1" : "justify-start gap-4",
|
|
"h-fit w-full",
|
|
)}
|
|
onClick={() => handleChatSelection(chat.chatid)}
|
|
>
|
|
{chat.id === userId ? (
|
|
<div className="flex size-10 items-center justify-center rounded-full bg-yellow-500 text-white text-xs">
|
|
<Crown className="size-4" />
|
|
</div>
|
|
) : (
|
|
<UserAvatar
|
|
className="size-10"
|
|
userId={chat.id}
|
|
username={chat.username}
|
|
/>
|
|
)}
|
|
|
|
{isDesktop && (
|
|
<div className="flex h-full max-w-28 flex-col text-left align-top">
|
|
<span>{chat.id === userId ? "Admin Infoalloggi" : chat.username}</span>
|
|
<div className="flex flex-wrap gap-2">
|
|
{chat.etichette.map((etichetta) => (
|
|
<Etichetta
|
|
className="text-xs"
|
|
color_hex={etichetta.color_hex}
|
|
key={etichetta.id_etichetta}
|
|
title={etichetta.title}
|
|
/>
|
|
))}
|
|
</div>
|
|
{chat.lastmessage && (
|
|
<>
|
|
<div className="flex items-center gap-2">
|
|
<span className="truncate text-muted-foreground text-xs">
|
|
{chat.lastmessage.sender === userId ? "Tu: " : ""}
|
|
{chat.lastmessage.message}
|
|
</span>
|
|
|
|
{chat.lastmessage.sender === userId &&
|
|
(chat.lastmessage.isread ? (
|
|
<CheckCheck className="size-4 text-green-500" />
|
|
) : (
|
|
<Check className="size-4" />
|
|
))}
|
|
</div>
|
|
<span className="truncate text-muted-foreground text-xs">
|
|
{new Date(chat.lastmessage.time).toLocaleString(
|
|
"it",
|
|
{
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
},
|
|
)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Button>
|
|
</ContextMenuTrigger>
|
|
<ChatSidebarContextMenu
|
|
chatid={chat.chatid}
|
|
setDeleteModalOpen={setDeleteModalOpen}
|
|
setEditEtichettaModalOpen={setEditEtichettaModalOpen}
|
|
setSelectedChat={setSelectedChat}
|
|
/>
|
|
</ContextMenu>
|
|
))}
|
|
{selectedChat && (
|
|
<>
|
|
<AlertDialog
|
|
onOpenChange={setDeleteModalOpen}
|
|
open={deleteModalOpen}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Questa azione non può essere annullata. Questo eliminerà
|
|
definitivamente la chat e rimuoverà i dati dai nostri
|
|
server.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
aria-label="Delete Chat"
|
|
className={cn(buttonVariants({ variant: "destructive" }))}
|
|
onClick={() => {
|
|
if (selectedChat) {
|
|
deletechat({
|
|
chatId: selectedChat,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
Continua
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
<EtichetteModal
|
|
chatId={selectedChat}
|
|
onOpenChange={setEditEtichettaModalOpen}
|
|
open={editEtichettaModalOpen}
|
|
/>
|
|
</>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SearchUserComponent = ({
|
|
isOpen,
|
|
OpenChange,
|
|
activeChats,
|
|
handleChatSelection,
|
|
}: {
|
|
isOpen: boolean;
|
|
OpenChange: (status: boolean) => void;
|
|
activeChats: ActiveChatsType[];
|
|
handleChatSelection: (chatId: ChatsChatid) => void;
|
|
}) => {
|
|
return (
|
|
<CommandDialog onOpenChange={OpenChange} open={isOpen}>
|
|
<CommandInput
|
|
className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
|
|
placeholder="Digita il nome utente..."
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty>Nessun risultato</CommandEmpty>
|
|
<CommandGroup
|
|
className="pb-2"
|
|
heading={activeChats.length > 0 ? "Suggerimenti" : undefined}
|
|
>
|
|
{activeChats?.map((chat) => (
|
|
<CommandItem
|
|
className="cursor-pointer rounded-md"
|
|
key={chat.id}
|
|
onSelect={() => {
|
|
handleChatSelection(chat.chatid);
|
|
}}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<UserAvatar
|
|
className="size-8"
|
|
userId={chat.id}
|
|
username={chat.username}
|
|
/>
|
|
|
|
<span>
|
|
<span className="block font-medium">{chat.username}</span>
|
|
</span>
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</CommandDialog>
|
|
);
|
|
};
|
|
|
|
const NewUserComponent = ({
|
|
isOpen,
|
|
OpenChange,
|
|
inactiveUsers,
|
|
}: {
|
|
isOpen: boolean;
|
|
OpenChange: (status: boolean) => void;
|
|
inactiveUsers: NonNullableUser[];
|
|
}) => {
|
|
const utils = api.useUtils();
|
|
const { mutate: createNewChat } = api.chatSSE.createChat.useMutation({
|
|
onSuccess: async () => {
|
|
await utils.chat.getActiveChats.invalidate();
|
|
OpenChange(false);
|
|
},
|
|
});
|
|
return (
|
|
<CommandDialog onOpenChange={OpenChange} open={isOpen}>
|
|
<CommandInput
|
|
className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
|
|
placeholder="Digita il nome utente..."
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty>Nessun risultato</CommandEmpty>
|
|
<CommandGroup
|
|
className="pb-2"
|
|
heading={inactiveUsers.length > 0 ? "Suggerimenti" : undefined}
|
|
>
|
|
{inactiveUsers?.map((user) => (
|
|
<CommandItem
|
|
className="cursor-pointer rounded-md"
|
|
key={user.id}
|
|
onSelect={() => {
|
|
createNewChat({
|
|
userId: user.id,
|
|
});
|
|
}}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<UserAvatar
|
|
className="size-8"
|
|
userId={user.id}
|
|
username={user.username}
|
|
/>
|
|
|
|
<span>
|
|
<span className="block font-medium">{user.username}</span>
|
|
</span>
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</CommandDialog>
|
|
);
|
|
};
|
|
|
|
const ChatSidebarContextMenu = ({
|
|
chatid,
|
|
setSelectedChat,
|
|
setDeleteModalOpen,
|
|
setEditEtichettaModalOpen,
|
|
}: {
|
|
chatid: ChatsChatid;
|
|
setSelectedChat: (chat: ChatsChatid | null) => void;
|
|
setDeleteModalOpen: (status: boolean) => void;
|
|
setEditEtichettaModalOpen: (status: boolean) => void;
|
|
}) => {
|
|
const { data: chatData } = api.chat.getChatInfobyId.useQuery({
|
|
chatId: chatid,
|
|
});
|
|
if (!chatData) return null;
|
|
|
|
return (
|
|
<>
|
|
<ContextMenuContent>
|
|
<ContextMenuSub>
|
|
<ContextMenuSubTrigger>Etichette</ContextMenuSubTrigger>
|
|
<ContextMenuSubContent className="w-48">
|
|
<ContextMenuItem asChild>
|
|
<button
|
|
aria-label="Edit Chat Labels"
|
|
className="w-full cursor-pointer"
|
|
onClick={() => {
|
|
setSelectedChat(chatid);
|
|
setEditEtichettaModalOpen(true);
|
|
}}
|
|
type="button"
|
|
>
|
|
Modifica
|
|
</button>
|
|
</ContextMenuItem>
|
|
</ContextMenuSubContent>
|
|
</ContextMenuSub>
|
|
<ContextMenuSeparator />
|
|
<ContextMenuItem>
|
|
<Link
|
|
aria-label="View User Profile"
|
|
href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`}
|
|
target="_blank"
|
|
>
|
|
Profilo
|
|
</Link>
|
|
</ContextMenuItem>
|
|
|
|
<ContextMenuItem>
|
|
<Link
|
|
aria-label="View User Orders"
|
|
href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`}
|
|
target="_blank"
|
|
>
|
|
Ordini
|
|
</Link>
|
|
</ContextMenuItem>
|
|
<ContextMenuItem>
|
|
<Link
|
|
aria-label="View User Attachments"
|
|
href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`}
|
|
target="_blank"
|
|
>
|
|
Allegati
|
|
</Link>
|
|
</ContextMenuItem>
|
|
<ContextMenuSub>
|
|
<ContextMenuSubTrigger>Altro</ContextMenuSubTrigger>
|
|
<ContextMenuSubContent className="w-48">
|
|
<ContextMenuItem
|
|
aria-label="Edit Chat"
|
|
className="cursor-pointer rounded-md bg-red-500 text-white hover:bg-red-500/90 focus:bg-red-500/90 focus:text-white"
|
|
onClick={() => {
|
|
setSelectedChat(chatid);
|
|
setDeleteModalOpen(true);
|
|
}}
|
|
>
|
|
Elimina Chat
|
|
</ContextMenuItem>
|
|
</ContextMenuSubContent>
|
|
</ContextMenuSub>
|
|
</ContextMenuContent>
|
|
</>
|
|
);
|
|
};
|