feat: project tabs (board/chat), fullscreen chat mode
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s

This commit is contained in:
Markov 2026-02-23 12:13:45 +01:00
parent be5e71d42e
commit e3c4d528a8
2 changed files with 85 additions and 11 deletions

View File

@ -7,10 +7,16 @@ import Sidebar from "@/components/Sidebar";
import KanbanBoard from "@/components/KanbanBoard"; import KanbanBoard from "@/components/KanbanBoard";
import ChatPanel from "@/components/ChatPanel"; import ChatPanel from "@/components/ChatPanel";
const TABS = [
{ key: "board", label: "📋 Доска" },
{ key: "chat", label: "💬 Чат" },
];
export default function ProjectPage() { export default function ProjectPage() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState("board");
useEffect(() => { useEffect(() => {
getProjects() getProjects()
@ -33,17 +39,44 @@ export default function ProjectPage() {
<div className="flex h-screen"> <div className="flex h-screen">
<Sidebar projects={projects} activeSlug={slug} /> <Sidebar projects={projects} activeSlug={slug} />
<main className="flex-1 flex flex-col overflow-hidden"> <main className="flex-1 flex flex-col overflow-hidden">
<header className="border-b border-[var(--border)] px-6 py-4 flex items-center gap-4 pl-14 md:pl-6"> <header className="border-b border-[var(--border)] px-6 py-3 pl-14 md:pl-6">
<div className="flex items-center justify-between mb-2">
<div> <div>
<h1 className="text-xl font-bold">{project.name}</h1> <h1 className="text-xl font-bold">{project.name}</h1>
{project.description && ( {project.description && (
<p className="text-sm text-[var(--muted)]">{project.description}</p> <p className="text-sm text-[var(--muted)]">{project.description}</p>
)} )}
</div> </div>
</div>
<div className="flex gap-1">
{TABS.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-3 py-1.5 rounded text-sm transition-colors ${
activeTab === tab.key
? "bg-[var(--accent)]/10 text-[var(--accent)]"
: "text-[var(--muted)] hover:bg-white/5 hover:text-[var(--fg)]"
}`}
>
{tab.label}
</button>
))}
</div>
</header> </header>
{project.chat_id && <ChatPanel chatId={project.chat_id} />}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{activeTab === "board" && (
<KanbanBoard projectId={project.id} projectSlug={project.slug} /> <KanbanBoard projectId={project.id} projectSlug={project.slug} />
)}
{activeTab === "chat" && project.chat_id && (
<ChatPanel chatId={project.chat_id} fullscreen />
)}
{activeTab === "chat" && !project.chat_id && (
<div className="flex items-center justify-center h-full text-[var(--muted)] text-sm">
Чат недоступен
</div>
)}
</div> </div>
</main> </main>
</div> </div>

View File

@ -12,9 +12,10 @@ const AUTHOR_ICON: Record<string, string> = {
interface Props { interface Props {
chatId: string; chatId: string;
fullscreen?: boolean;
} }
export default function ChatPanel({ chatId }: Props) { export default function ChatPanel({ chatId, fullscreen = false }: Props) {
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -27,7 +28,6 @@ export default function ChatPanel({ chatId }: Props) {
} }
}, [chatId]); }, [chatId]);
// Listen for new messages via WS
useEffect(() => { useEffect(() => {
const unsub = wsClient.on("message.new", (data: any) => { const unsub = wsClient.on("message.new", (data: any) => {
if (data.chat_id === chatId) { if (data.chat_id === chatId) {
@ -51,12 +51,53 @@ export default function ChatPanel({ chatId }: Props) {
setMessages((prev) => [...prev, msg]); setMessages((prev) => [...prev, msg]);
} catch (e) { } catch (e) {
console.error("Failed to send:", e); console.error("Failed to send:", e);
setInput(text); // restore on error setInput(text);
} finally { } finally {
setSending(false); setSending(false);
} }
}; };
if (fullscreen) {
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-3">
{messages.map((msg) => (
<div key={msg.id} className="text-sm">
<div className="flex items-center gap-1 text-xs text-[var(--muted)] mb-0.5">
<span>{AUTHOR_ICON[msg.author_type] || "👤"}</span>
<span className="font-medium">{msg.author_slug}</span>
<span>·</span>
<span>{new Date(msg.created_at).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit" })}</span>
</div>
<div className="ml-5 whitespace-pre-wrap">{msg.content}</div>
</div>
))}
{messages.length === 0 && (
<div className="text-sm text-[var(--muted)] italic py-8 text-center">Нет сообщений</div>
)}
<div ref={bottomRef} />
</div>
<div className="px-6 py-3 border-t border-[var(--border)] flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
placeholder="Сообщение..."
className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded-lg px-4 py-2 text-sm outline-none focus:border-[var(--accent)]"
/>
<button
onClick={handleSend}
disabled={sending}
className="px-4 py-2 bg-[var(--accent)] text-white rounded-lg text-sm hover:opacity-90 disabled:opacity-50"
>
</button>
</div>
</div>
);
}
// Compact mode (collapsible)
return ( return (
<div className={`border-b border-[var(--border)] transition-all ${open ? "h-64" : "h-10"}`}> <div className={`border-b border-[var(--border)] transition-all ${open ? "h-64" : "h-10"}`}>
<button <button