sync: phase 1 backend + frontend from server
This commit is contained in:
@@ -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"><{msg.author_username}></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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user