| // 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.