tahamajs/IE / Projects /front /src /components /references /ReferencePicker.tsx
tahamajs's picture
download
raw
4.7 kB
// src/components/references/ReferencePicker.tsx
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useReferenceManager } from "@/hooks/useReferenceManager";
import { X, Search, Loader2 } from "lucide-react";
interface ReferencePickerProps {
isOpen: boolean;
onClose: () => void;
docId: string;
}
export function ReferencePicker({ isOpen, onClose, docId }: ReferencePickerProps) {
const { references, loading } = useReferenceManager(docId);
const [search, setSearch] = useState("");
const [filtered, setFiltered] = useState(references);
useEffect(() => {
if (!search.trim()) {
setFiltered(references);
return;
}
const q = search.toLowerCase();
setFiltered(
references.filter(
(ref) =>
ref.title.toLowerCase().includes(q) ||
ref.authors.some((a) => `${a.given} ${a.family}`.toLowerCase().includes(q))
)
);
}, [search, references]);
const insertCitation = (ref: any) => {
window.dispatchEvent(
new CustomEvent("insertCitation", {
detail: { key: ref.key, text: `\\cite{${ref.key}}` },
})
);
onClose();
};
if (!isOpen) return null;
return (
<AnimatePresence>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="w-full max-w-lg rounded-2xl bg-white dark:bg-gray-800 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 p-4">
<h2 className="text-lg font-bold">Insert Citation</h2>
<button
onClick={onClose}
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Search bar */}
<div className="p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search by title or author..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 py-2 pl-9 pr-3 focus:outline-none focus:ring-2 focus:ring-primary"
autoFocus
/>
</div>
</div>
{/* Results */}
<div className="max-h-96 overflow-y-auto px-4 pb-4">
{loading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
</div>
) : filtered.length === 0 ? (
<div className="py-8 text-center text-gray-400">
<p>No references found.</p>
<p className="mt-1 text-sm">
Add references via the <strong>Reference Manager</strong> page.
</p>
</div>
) : (
<div className="space-y-2">
{filtered.map((ref) => (
<button
key={ref.id}
onClick={() => insertCitation(ref)}
className="w-full rounded-lg border border-gray-100 dark:border-gray-700 p-3 text-left transition hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div className="font-medium text-gray-900 dark:text-gray-100">
{ref.title}
</div>
<div className="mt-1 text-xs text-gray-500">
{ref.authors.map((a: any) => `${a.given} ${a.family}`).join(", ")} ({ref.year})
</div>
{ref.journal && (
<div className="mt-0.5 text-xs text-gray-400">{ref.journal}</div>
)}
</button>
))}
</div>
)}
</div>
{/* Footer hint */}
<div className="border-t border-gray-100 dark:border-gray-700 p-3 text-center text-xs text-gray-400">
Click a reference to insert <code className="rounded bg-gray-100 px-1">{"\\cite{key}"}</code>
</div>
</motion.div>
</div>
</AnimatePresence>
);
}

Xet Storage Details

Size:
4.7 kB
·
Xet hash:
e77cb99d8bf222e28966e110ebe2cfffb7ecb00c57f89b2d44cf9c8556299f5b

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.