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
+99
View File
@@ -0,0 +1,99 @@
import { useState, useRef, useEffect } from "react";
import { useMessageStore } from "../stores/message.ts";
interface MessageSearchProps {
channelId: string;
channelName: string;
onClose: () => void;
onJump?: (messageId: string) => void;
}
export function MessageSearch({ channelId, channelName, onClose, onJump }: MessageSearchProps) {
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const results = useMessageStore((s) => s.searchResultsByChannel[channelId] || []);
const searchMessages = useMessageStore((s) => s.searchMessages);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
const t = setTimeout(() => {
if (query.trim()) {
searchMessages(channelId, query.trim());
setSelectedIndex(0);
}
}, 200);
return () => clearTimeout(t);
}, [query, channelId, searchMessages]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
return;
}
if (results.length === 0) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex((i) => (i + 1) % results.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex((i) => (i - 1 + results.length) % results.length);
} else if (e.key === "Enter") {
e.preventDefault();
const msg = results[selectedIndex];
if (msg && onJump) {
onJump(msg.id);
}
onClose();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [results, selectedIndex, onClose, onJump]);
return (
<div className="absolute inset-x-0 top-10 z-20 bg-gb-bg border border-gb-bg-t shadow-lg mx-3">
<div className="px-3 py-2 border-b border-gb-bg-t flex items-center justify-between">
<span className="text-xs text-gb-orange font-mono">SEARCH #{channelName.toUpperCase()}</span>
<button onClick={onClose} className="text-xs text-gb-red hover:text-gb-orange">[x]</button>
</div>
<div className="p-2">
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="search messages..."
className="terminal-input w-full text-sm"
/>
</div>
<div className="max-h-64 overflow-y-auto font-mono text-xs">
{results.length === 0 && query.trim() && (
<div className="px-3 py-2 text-gb-fg-f">[no results]</div>
)}
{results.map((msg, idx) => (
<button
key={msg.id}
onClick={() => {
onJump?.(msg.id);
onClose();
}}
className={`w-full text-left px-3 py-2 border-b border-gb-bg-t last:border-b-0 ${
idx === selectedIndex ? "bg-gb-bg-s" : "hover:bg-gb-bg-s/50"
}`}
>
<div className="flex items-center gap-2 text-gb-fg-f">
<span className="text-gb-aqua">&lt;{msg.author_username}&gt;</span>
<span className="text-gb-fg-s">{new Date(msg.created_at).toLocaleString()}</span>
</div>
<div className="text-gb-fg truncate">{msg.content}</div>
</button>
))}
</div>
</div>
);
}