sync: phase 1 backend + frontend from server

This commit is contained in:
2026-06-30 09:26:03 -04:00
parent d4cdd89544
commit 30e159cbdb
31 changed files with 4758 additions and 114 deletions
@@ -0,0 +1,87 @@
import { useState, useMemo } from "react";
import { useConversationStore } from "../stores/conversation.ts";
import { useMemberStore } from "../stores/member.ts";
import { useServerStore } from "../stores/server.ts";
import { useAuthStore } from "../stores/auth.ts";
interface NewConversationModalProps {
onClose: () => void;
}
export function NewConversationModal({ onClose }: NewConversationModalProps) {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<string[]>([]);
const createConversation = useConversationStore((s) => s.createConversation);
const activeServerId = useServerStore((s) => s.activeServerId);
const membersByServer = useMemberStore((s) => s.membersByServer);
const currentUserId = useAuthStore((s) => s.user?.id);
const members = activeServerId ? membersByServer[activeServerId] || [] : [];
const filtered = useMemo(() => {
const q = query.toLowerCase();
return members.filter(
(m) =>
m.id !== currentUserId &&
(m.username.toLowerCase().includes(q) ||
(m.display_name || "").toLowerCase().includes(q)),
);
}, [members, query, currentUserId]);
const toggle = (id: string) => {
setSelected((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
);
};
const handleCreate = async () => {
if (selected.length === 0) return;
await createConversation(selected);
onClose();
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
<div
className="bg-gb-bg border border-gb-bg-t p-4 min-w-[300px] font-mono"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-gb-orange">NEW DIRECT MESSAGE</span>
<button onClick={onClose} className="text-xs text-gb-red">[x]</button>
</div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="search members..."
className="terminal-input w-full mb-3 text-sm"
/>
<div className="max-h-48 overflow-y-auto space-y-1 mb-3">
{filtered.map((m) => (
<button
key={m.id}
onClick={() => toggle(m.id)}
className={`w-full text-left px-2 py-1 text-sm ${
selected.includes(m.id)
? "bg-gb-aqua text-gb-bg"
: "hover:bg-gb-bg-t text-gb-fg-s"
}`}
>
{m.username}
</button>
))}
{filtered.length === 0 && (
<p className="text-gb-fg-f text-xs">[no members found]</p>
)}
</div>
<button
onClick={handleCreate}
disabled={selected.length === 0}
className="w-full px-3 py-1.5 bg-gb-orange text-gb-bg text-xs font-mono hover:bg-gb-yellow transition-colors disabled:opacity-50"
>
[START CONVERSATION]
</button>
</div>
</div>
);
}