learning_ai_notes/web/src/components/LinkNoteModal.tsx

161 lines
5.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { searchNoteSummaries, createNoteRelationship } from "@/lib/notes-client";
import type { NoteSummary } from "@/lib/types";
interface LinkNoteModalProps {
noteId: string;
workspaceId: string;
existingLinkedIds: string[];
onLinked: () => void;
onClose: () => void;
}
const RELATIONSHIP_TYPES = ["related", "parent", "child", "blocks", "blocked_by", "duplicate"] as const;
export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked, onClose }: LinkNoteModalProps) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<NoteSummary[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [relationshipType, setRelationshipType] = useState<string>("related");
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searched, setSearched] = useState(false);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (!query.trim()) return;
try {
const notes = await searchNoteSummaries(query.trim());
const filtered = notes.filter((n) => n.id !== noteId && !existingLinkedIds.includes(n.id));
setResults(filtered);
setSearched(true);
setSelectedId(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Search failed");
}
}
async function handleLink() {
if (!selectedId || saving) return;
setSaving(true);
setError(null);
try {
await createNoteRelationship({
id: crypto.randomUUID(),
workspaceId,
fromNoteId: noteId,
toNoteId: selectedId,
relationshipType,
});
onLinked();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to link note");
} finally {
setSaving(false);
}
}
return (
<div
className="modal-overlay"
style={{
position: "fixed",
inset: 0,
background: "var(--nl-overlay-scrim)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
className="surface-card"
style={{
padding: "var(--nl-space-6)",
width: "100%",
maxWidth: 520,
display: "grid",
gap: "var(--nl-space-4)",
}}
>
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Link Note</div>
{error && <div style={{ color: "var(--nl-danger)", fontSize: "0.875rem" }}>{error}</div>}
<form onSubmit={handleSearch} style={{ display: "flex", gap: "var(--nl-space-2)" }}>
<input
className="input"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search notes..."
aria-label="Search notes to link"
style={{ flex: 1 }}
autoFocus
/>
<button type="submit" className="btn btn-secondary">
Search
</button>
</form>
{searched && results.length === 0 && (
<div style={{ color: "var(--nl-text-secondary)", fontSize: "0.875rem" }}>No matching notes found.</div>
)}
{results.length > 0 && (
<div style={{ maxHeight: 200, overflowY: "auto", display: "grid", gap: "var(--nl-space-2)" }}>
{results.map((note) => (
<button
key={note.id}
type="button"
className={selectedId === note.id ? "surface-card" : "surface-muted"}
style={{
padding: "var(--nl-space-3)",
textAlign: "left",
cursor: "pointer",
border: selectedId === note.id ? "2px solid var(--nl-accent-primary)" : "2px solid transparent",
}}
onClick={() => setSelectedId(note.id)}
aria-label={`Select note: ${note.title}`}
>
<div style={{ fontWeight: 600 }}>{note.title}</div>
<div style={{ color: "var(--nl-text-secondary)", fontSize: "0.875rem" }}>{note.excerpt}</div>
</button>
))}
</div>
)}
{selectedId && (
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Relationship type</span>
<select value={relationshipType} onChange={(e) => setRelationshipType(e.target.value)} className="input" aria-label="Relationship type">
{RELATIONSHIP_TYPES.map((rt) => (
<option key={rt} value={rt}>
{rt.replace("_", " ")}
</option>
))}
</select>
</label>
)}
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Cancel
</button>
<button type="button" className="btn btn-primary" disabled={!selectedId || saving} onClick={handleLink}>
{saving ? "Linking..." : "Link"}
</button>
</div>
</div>
</div>
);
}