feat: project tabs (board/chat), fullscreen chat mode
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s
All checks were successful
Deploy Web Client / deploy (push) Successful in 35s
This commit is contained in:
parent
be5e71d42e
commit
e3c4d528a8
@ -7,10 +7,16 @@ import Sidebar from "@/components/Sidebar";
|
||||
import KanbanBoard from "@/components/KanbanBoard";
|
||||
import ChatPanel from "@/components/ChatPanel";
|
||||
|
||||
const TABS = [
|
||||
{ key: "board", label: "📋 Доска" },
|
||||
{ key: "chat", label: "💬 Чат" },
|
||||
];
|
||||
|
||||
export default function ProjectPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState("board");
|
||||
|
||||
useEffect(() => {
|
||||
getProjects()
|
||||
@ -33,17 +39,44 @@ export default function ProjectPage() {
|
||||
<div className="flex h-screen">
|
||||
<Sidebar projects={projects} activeSlug={slug} />
|
||||
<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>
|
||||
<h1 className="text-xl font-bold">{project.name}</h1>
|
||||
{project.description && (
|
||||
<p className="text-sm text-[var(--muted)]">{project.description}</p>
|
||||
)}
|
||||
</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>
|
||||
{project.chat_id && <ChatPanel chatId={project.chat_id} />}
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{activeTab === "board" && (
|
||||
<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>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -12,9 +12,10 @@ const AUTHOR_ICON: Record<string, string> = {
|
||||
|
||||
interface Props {
|
||||
chatId: string;
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatPanel({ chatId }: Props) {
|
||||
export default function ChatPanel({ chatId, fullscreen = false }: Props) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -27,7 +28,6 @@ export default function ChatPanel({ chatId }: Props) {
|
||||
}
|
||||
}, [chatId]);
|
||||
|
||||
// Listen for new messages via WS
|
||||
useEffect(() => {
|
||||
const unsub = wsClient.on("message.new", (data: any) => {
|
||||
if (data.chat_id === chatId) {
|
||||
@ -51,12 +51,53 @@ export default function ChatPanel({ chatId }: Props) {
|
||||
setMessages((prev) => [...prev, msg]);
|
||||
} catch (e) {
|
||||
console.error("Failed to send:", e);
|
||||
setInput(text); // restore on error
|
||||
setInput(text);
|
||||
} finally {
|
||||
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 (
|
||||
<div className={`border-b border-[var(--border)] transition-all ${open ? "h-64" : "h-10"}`}>
|
||||
<button
|
||||
|
||||
Loading…
Reference in New Issue
Block a user