| // src/components/references/ReferenceImportModal.tsx | |
| import { useState } from "react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { useReferenceManager } from "@/hooks/useReferenceManager"; | |
| import { X, Loader2, Globe, FileText, Database, BookOpen } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| interface ReferenceImportModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| docId: string; | |
| onSuccess?: () => void; | |
| } | |
| export function ReferenceImportModal({ | |
| isOpen, | |
| onClose, | |
| docId, | |
| onSuccess, | |
| }: ReferenceImportModalProps) { | |
| const { importFromDOI, importBibTeX, connectZotero, connectMendeley } = | |
| useReferenceManager(docId); | |
| const [mode, setMode] = useState<"doi" | "bibtex" | "zotero" | "mendeley">( | |
| "doi" | |
| ); | |
| const [doi, setDoi] = useState(""); | |
| const [bibtex, setBibtex] = useState(""); | |
| const [zoteroApiKey, setZoteroApiKey] = useState(""); | |
| const [zoteroUserId, setZoteroUserId] = useState(""); | |
| const [mendeleyToken, setMendeleyToken] = useState(""); | |
| const [loading, setLoading] = useState(false); | |
| if (!isOpen) return null; | |
| const handleImport = async () => { | |
| setLoading(true); | |
| try { | |
| if (mode === "doi") { | |
| if (!doi.trim()) throw new Error("Please enter a DOI"); | |
| await importFromDOI(doi.trim()); | |
| toast.success("Reference imported from DOI"); | |
| } else if (mode === "bibtex") { | |
| if (!bibtex.trim()) throw new Error("Please paste a BibTeX entry"); | |
| await importBibTeX(bibtex.trim()); | |
| toast.success("BibTeX imported"); | |
| } else if (mode === "zotero") { | |
| if (!zoteroApiKey.trim() || !zoteroUserId.trim()) | |
| throw new Error("API key and user ID are required"); | |
| await connectZotero(zoteroApiKey.trim(), zoteroUserId.trim()); | |
| toast.success("Zotero references imported"); | |
| } else if (mode === "mendeley") { | |
| if (!mendeleyToken.trim()) | |
| throw new Error("Mendeley access token is required"); | |
| await connectMendeley(mendeleyToken.trim()); | |
| toast.success("Mendeley references imported"); | |
| } | |
| onSuccess?.(); | |
| handleClose(); | |
| } catch (err: any) { | |
| toast.error(err.message || "Import failed"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleClose = () => { | |
| // Reset all fields when closing | |
| setDoi(""); | |
| setBibtex(""); | |
| setZoteroApiKey(""); | |
| setZoteroUserId(""); | |
| setMendeleyToken(""); | |
| onClose(); | |
| }; | |
| const tabs = [ | |
| { key: "doi", label: "DOI", icon: Globe }, | |
| { key: "bibtex", label: "BibTeX", icon: FileText }, | |
| { key: "zotero", label: "Zotero", icon: Database }, | |
| { key: "mendeley", label: "Mendeley", icon: BookOpen }, | |
| ] as const; | |
| return ( | |
| <AnimatePresence> | |
| <div | |
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" | |
| onClick={handleClose} | |
| > | |
| <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-2xl" | |
| 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">Import Reference</h2> | |
| <button | |
| onClick={handleClose} | |
| 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> | |
| {/* Tab buttons */} | |
| <div className="flex gap-2 border-b border-gray-200 dark:border-gray-700 p-4 pb-2"> | |
| {tabs.map((tab) => ( | |
| <button | |
| key={tab.key} | |
| onClick={() => setMode(tab.key)} | |
| className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm transition ${ | |
| mode === tab.key | |
| ? "bg-primary text-white" | |
| : "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600" | |
| }`} | |
| > | |
| <tab.icon className="h-3.5 w-3.5" /> | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Form fields */} | |
| <div className="p-4"> | |
| {mode === "doi" && ( | |
| <div className="mb-4"> | |
| <label className="mb-1 block text-sm font-medium">DOI</label> | |
| <input | |
| type="text" | |
| placeholder="e.g., 10.1038/s41586-020-2649-2" | |
| value={doi} | |
| onChange={(e) => setDoi(e.target.value)} | |
| className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 p-2 dark:bg-gray-700" | |
| /> | |
| <p className="mt-1 text-xs text-gray-400"> | |
| Example: 10.1038/s41586-020-2649-2 | |
| </p> | |
| </div> | |
| )} | |
| {mode === "bibtex" && ( | |
| <div className="mb-4"> | |
| <label className="mb-1 block text-sm font-medium"> | |
| BibTeX Entry | |
| </label> | |
| <textarea | |
| rows={6} | |
| placeholder="@article{key,\n title={...},\n author={...},\n year={...}\n}" | |
| value={bibtex} | |
| onChange={(e) => setBibtex(e.target.value)} | |
| className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 p-2 font-mono text-sm dark:bg-gray-700" | |
| /> | |
| </div> | |
| )} | |
| {mode === "zotero" && ( | |
| <div className="mb-4 space-y-3"> | |
| <div> | |
| <label className="mb-1 block text-sm font-medium"> | |
| Zotero API Key | |
| </label> | |
| <input | |
| type="text" | |
| placeholder="Your Zotero API key" | |
| value={zoteroApiKey} | |
| onChange={(e) => setZoteroApiKey(e.target.value)} | |
| className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 p-2 dark:bg-gray-700" | |
| /> | |
| </div> | |
| <div> | |
| <label className="mb-1 block text-sm font-medium"> | |
| User ID | |
| </label> | |
| <input | |
| type="text" | |
| placeholder="Your Zotero user ID (numeric)" | |
| value={zoteroUserId} | |
| onChange={(e) => setZoteroUserId(e.target.value)} | |
| className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 p-2 dark:bg-gray-700" | |
| /> | |
| </div> | |
| <p className="text-xs text-gray-400"> | |
| Get your API key from{" "} | |
| <a | |
| href="https://www.zotero.org/settings/keys" | |
| target="_blank" | |
| rel="noreferrer" | |
| className="text-primary hover:underline" | |
| > | |
| Zotero settings | |
| </a> | |
| . | |
| </p> | |
| </div> | |
| )} | |
| {mode === "mendeley" && ( | |
| <div className="mb-4"> | |
| <label className="mb-1 block text-sm font-medium"> | |
| Mendeley Access Token | |
| </label> | |
| <input | |
| type="text" | |
| placeholder="Paste your Mendeley access token" | |
| value={mendeleyToken} | |
| onChange={(e) => setMendeleyToken(e.target.value)} | |
| className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 p-2 dark:bg-gray-700" | |
| /> | |
| <p className="mt-1 text-xs text-gray-400"> | |
| Generate a token in your Mendeley developer account. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Action buttons */} | |
| <div className="flex gap-3 border-t border-gray-200 dark:border-gray-700 p-4"> | |
| <button | |
| onClick={handleImport} | |
| disabled={loading} | |
| className="btn-primary flex flex-1 items-center justify-center gap-2 rounded-lg py-2" | |
| > | |
| {loading && <Loader2 className="h-4 w-4 animate-spin" />} | |
| Import | |
| </button> | |
| <button | |
| onClick={handleClose} | |
| className="btn-secondary flex flex-1 rounded-lg py-2" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </motion.div> | |
| </div> | |
| </AnimatePresence> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 8.89 kB
- Xet hash:
- 0e5c8161a8f94c9ea8265cd3478524927cf9398d8989fe5fb75c9965d120250c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.