Files
vontor-cz/frontend/src/components/social/chat/ChatSidebar.tsx
Brunobrno f19375254f Create chat modal UI & session login for WS auth
Backend: initialize a Django session on token obtain (login) so AuthMiddlewareStack can authenticate WebSocket connections; validate the token serializer and map TokenError -> InvalidToken; call django_logout on logout to destroy the session. Frontend: add a CreateChatModal component (DM/group creation) with user search, selection, validation, API mutation and cache invalidation; wire modal into ChatSidebar and add Czech translations for the new UI strings.
2026-05-28 08:40:55 +02:00

103 lines
3.6 KiB
TypeScript

import { useState, useMemo } from "react";
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { FiSearch, FiPlus } from "react-icons/fi";
import { useApiSocialChatsList } from "@/api/generated/private/chat/chat";
import Avatar from "@/components/ui/Avatar";
import Spinner from "@/components/ui/Spinner";
import EmptyState from "@/components/ui/EmptyState";
import IconButton from "@/components/ui/IconButton";
import CreateChatModal from "./CreateChatModal";
export default function ChatSidebar() {
const { t } = useTranslation("social");
const [query, setQuery] = useState("");
const [createOpen, setCreateOpen] = useState(false);
const { data, isLoading } = useApiSocialChatsList(
query ? { search: query } : undefined,
);
const chats = useMemo(() => data?.results ?? [], [data]);
return (
<aside className="flex h-full flex-col border-r border-brand-lines/15">
<header className="flex items-center justify-between gap-2 border-b border-brand-lines/10 px-3 py-3">
<h2 className="text-sm font-semibold text-brand-text">
{t("chat.sidebar.title")}
</h2>
<IconButton
icon={<FiPlus size={16} />}
label={t("chat.sidebar.new")}
onClick={() => setCreateOpen(true)}
/>
</header>
<div className="relative px-3 py-2">
<FiSearch
className="absolute left-5 top-1/2 -translate-y-1/2 text-brand-text/50"
size={14}
/>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("chat.sidebar.search")}
className="w-full rounded-xl bg-brand-bgLight/40 border border-brand-lines/20 py-2 pl-8 pr-2 text-sm text-brand-text placeholder:text-brand-text/40 focus:outline-none focus:border-brand-accent"
/>
</div>
<div className="flex-1 overflow-y-auto">
{isLoading && (
<div className="flex justify-center py-6">
<Spinner size={20} />
</div>
)}
{!isLoading && chats.length === 0 && (
<EmptyState message={t("chat.sidebar.empty")} />
)}
<ul>
{chats.map((chat) => (
<li key={chat.id}>
<NavLink
to={`/social/chats/${chat.id}`}
className={({ isActive }) =>
[
"flex items-center gap-3 px-3 py-2.5 hover:bg-brand-lines/10 transition-colors",
isActive ? "bg-brand-lines/10" : "",
].join(" ")
}
>
<Avatar
name={chat.name ?? `chat ${chat.id}`}
src={chat.icon ?? undefined}
size={36}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-brand-text">
{chat.name || `Chat #${chat.id}`}
</div>
<div className="truncate text-xs text-brand-text/60">
{chat.chat_type}
</div>
</div>
{(chat.unread_count ?? 0) > 0 && (
<span className="ml-auto shrink-0 rounded-full bg-brand-accent px-1.5 py-0.5 text-[10px] font-bold leading-none text-brand-bg">
{(chat.unread_count ?? 0) > 99 ? "99+" : chat.unread_count}
</span>
)}
</NavLink>
</li>
))}
</ul>
</div>
<CreateChatModal
open={createOpen}
onClose={() => setCreateOpen(false)}
/>
</aside>
);
}