|
| 1 | +import React, { useState } from 'react'; |
| 2 | +import { Download, ChevronDown, Copy, Check } from 'lucide-react'; |
| 3 | +import { Citation } from '../types'; |
| 4 | +import { formatCitation, getFileExtension, FORMAT_LABELS, CitationFormat } from '../logic/citationFormatting'; |
| 5 | + |
| 6 | +// --- Individual Citation Download Component --- |
| 7 | + |
| 8 | +export const CitationDownloadButtons: React.FC<{ citation: Citation; menuDirection?: 'down' | 'up' }> = ({ citation, menuDirection = 'down' }) => { |
| 9 | + const [showMenu, setShowMenu] = useState(false); |
| 10 | + |
| 11 | + const handleDownload = (format: CitationFormat) => { |
| 12 | + const content = formatCitation(citation, format); |
| 13 | + const ext = getFileExtension(format); |
| 14 | + const key = `${citation.authors.split(',')[0].split(' ').pop()?.toLowerCase()}${citation.year}`; |
| 15 | + const blob = new Blob([content], { type: 'text/plain' }); |
| 16 | + const url = URL.createObjectURL(blob); |
| 17 | + const a = document.createElement('a'); |
| 18 | + a.href = url; |
| 19 | + a.download = `${key}.${ext}`; |
| 20 | + document.body.appendChild(a); |
| 21 | + a.click(); |
| 22 | + document.body.removeChild(a); |
| 23 | + URL.revokeObjectURL(url); |
| 24 | + setShowMenu(false); |
| 25 | + }; |
| 26 | + |
| 27 | + const handleCopy = async (format: CitationFormat) => { |
| 28 | + const content = formatCitation(citation, format); |
| 29 | + await navigator.clipboard.writeText(content); |
| 30 | + setShowMenu(false); |
| 31 | + }; |
| 32 | + |
| 33 | + const menuPositionClasses = menuDirection === 'up' |
| 34 | + ? 'bottom-full mb-1' |
| 35 | + : 'top-full mt-1'; |
| 36 | + |
| 37 | + return ( |
| 38 | + <div className="relative"> |
| 39 | + <button |
| 40 | + onClick={() => setShowMenu(!showMenu)} |
| 41 | + className="flex items-center gap-1 px-1.5 py-0.5 text-[9px] font-medium bg-zinc-800 text-zinc-400 hover:text-zinc-200 rounded border border-zinc-700 hover:border-zinc-600 transition-colors" |
| 42 | + > |
| 43 | + <Download className="w-2.5 h-2.5" /> |
| 44 | + Export |
| 45 | + <ChevronDown className={`w-2.5 h-2.5 transition-transform ${showMenu && menuDirection === 'up' ? 'rotate-180' : ''}`} /> |
| 46 | + </button> |
| 47 | + |
| 48 | + {showMenu && ( |
| 49 | + <> |
| 50 | + <div className="fixed inset-0 z-[100]" onClick={() => setShowMenu(false)} /> |
| 51 | + <div className={`absolute right-0 ${menuPositionClasses} z-[101] bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[140px] overflow-hidden`}> |
| 52 | + <div className="text-[8px] text-zinc-500 uppercase tracking-wider px-2 py-1 border-b border-zinc-800"> |
| 53 | + Copy to Clipboard |
| 54 | + </div> |
| 55 | + {(['apa', 'mla', 'chicago', 'vancouver', 'bibtex'] as CitationFormat[]).map(format => ( |
| 56 | + <button |
| 57 | + key={`copy-${format}`} |
| 58 | + onClick={() => handleCopy(format)} |
| 59 | + className="w-full text-left px-2 py-1.5 text-[10px] text-zinc-300 hover:bg-zinc-800 flex items-center gap-2" |
| 60 | + > |
| 61 | + <Copy className="w-2.5 h-2.5 text-zinc-500" /> |
| 62 | + {FORMAT_LABELS[format]} |
| 63 | + </button> |
| 64 | + ))} |
| 65 | + <div className="text-[8px] text-zinc-500 uppercase tracking-wider px-2 py-1 border-t border-zinc-800"> |
| 66 | + Download File |
| 67 | + </div> |
| 68 | + {(['bibtex', 'ris', 'endnote'] as CitationFormat[]).map(format => ( |
| 69 | + <button |
| 70 | + key={`dl-${format}`} |
| 71 | + onClick={() => handleDownload(format)} |
| 72 | + className="w-full text-left px-2 py-1.5 text-[10px] text-zinc-300 hover:bg-zinc-800 flex items-center gap-2" |
| 73 | + > |
| 74 | + <Download className="w-2.5 h-2.5 text-emerald-500" /> |
| 75 | + {FORMAT_LABELS[format]} (.{getFileExtension(format)}) |
| 76 | + </button> |
| 77 | + ))} |
| 78 | + </div> |
| 79 | + </> |
| 80 | + )} |
| 81 | + </div> |
| 82 | + ); |
| 83 | +}; |
| 84 | + |
| 85 | +// --- Bulk Export Dropdown Components --- |
| 86 | + |
| 87 | +export const BulkCopyDropdown: React.FC<{ citations: Citation[] }> = ({ citations }) => { |
| 88 | + const [showMenu, setShowMenu] = useState(false); |
| 89 | + const [copied, setCopied] = useState<string | null>(null); |
| 90 | + |
| 91 | + const handleCopy = async (format: CitationFormat) => { |
| 92 | + const content = citations.map(c => formatCitation(c, format)).join('\n\n'); |
| 93 | + await navigator.clipboard.writeText(content); |
| 94 | + setCopied(format); |
| 95 | + setTimeout(() => setCopied(null), 1500); |
| 96 | + setTimeout(() => setShowMenu(false), 800); |
| 97 | + }; |
| 98 | + |
| 99 | + return ( |
| 100 | + <div className="relative"> |
| 101 | + <button |
| 102 | + onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }} |
| 103 | + className="flex items-center gap-1.5 px-2 py-1 text-[10px] font-bold uppercase tracking-wider bg-zinc-800 text-zinc-300 hover:text-white rounded border border-zinc-700 hover:border-zinc-600 transition-colors" |
| 104 | + > |
| 105 | + {copied ? <Check className="w-3 h-3 text-emerald-400" /> : <Copy className="w-3 h-3" />} |
| 106 | + {copied ? 'Copied!' : 'Copy All'} |
| 107 | + <ChevronDown className="w-3 h-3" /> |
| 108 | + </button> |
| 109 | + |
| 110 | + {showMenu && ( |
| 111 | + <> |
| 112 | + <div className="fixed inset-0 z-[100]" onClick={() => setShowMenu(false)} /> |
| 113 | + <div className="absolute right-0 top-full mt-1 z-[101] bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[120px] overflow-hidden"> |
| 114 | + {(['apa', 'mla', 'chicago', 'vancouver', 'bibtex'] as CitationFormat[]).map(format => ( |
| 115 | + <button |
| 116 | + key={format} |
| 117 | + onClick={() => handleCopy(format)} |
| 118 | + className="w-full text-left px-3 py-1.5 text-[10px] text-zinc-300 hover:bg-zinc-800 flex items-center gap-2" |
| 119 | + > |
| 120 | + {copied === format ? <Check className="w-3 h-3 text-emerald-400" /> : <Copy className="w-3 h-3 text-zinc-500" />} |
| 121 | + {FORMAT_LABELS[format]} |
| 122 | + </button> |
| 123 | + ))} |
| 124 | + </div> |
| 125 | + </> |
| 126 | + )} |
| 127 | + </div> |
| 128 | + ); |
| 129 | +}; |
| 130 | + |
| 131 | +export const BulkDownloadDropdown: React.FC<{ citations: Citation[] }> = ({ citations }) => { |
| 132 | + const [showMenu, setShowMenu] = useState(false); |
| 133 | + |
| 134 | + const handleDownload = (format: CitationFormat) => { |
| 135 | + const content = citations.map(c => formatCitation(c, format)).join('\n\n'); |
| 136 | + const ext = getFileExtension(format); |
| 137 | + const blob = new Blob([content], { type: 'text/plain' }); |
| 138 | + const url = URL.createObjectURL(blob); |
| 139 | + const a = document.createElement('a'); |
| 140 | + a.href = url; |
| 141 | + a.download = `resample-lab-citations.${ext}`; |
| 142 | + document.body.appendChild(a); |
| 143 | + a.click(); |
| 144 | + document.body.removeChild(a); |
| 145 | + URL.revokeObjectURL(url); |
| 146 | + setShowMenu(false); |
| 147 | + }; |
| 148 | + |
| 149 | + return ( |
| 150 | + <div className="relative"> |
| 151 | + <button |
| 152 | + onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }} |
| 153 | + className="flex items-center gap-1.5 px-2 py-1 text-[10px] font-bold uppercase tracking-wider bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 rounded border border-emerald-500/30 transition-colors" |
| 154 | + > |
| 155 | + <Download className="w-3 h-3" /> |
| 156 | + Download All |
| 157 | + <ChevronDown className="w-3 h-3" /> |
| 158 | + </button> |
| 159 | + |
| 160 | + {showMenu && ( |
| 161 | + <> |
| 162 | + <div className="fixed inset-0 z-[100]" onClick={() => setShowMenu(false)} /> |
| 163 | + <div className="absolute right-0 top-full mt-1 z-[101] bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl min-w-[140px] overflow-hidden"> |
| 164 | + {(['bibtex', 'ris', 'endnote', 'apa', 'mla', 'chicago', 'vancouver'] as CitationFormat[]).map(format => ( |
| 165 | + <button |
| 166 | + key={format} |
| 167 | + onClick={() => handleDownload(format)} |
| 168 | + className="w-full text-left px-3 py-1.5 text-[10px] text-zinc-300 hover:bg-zinc-800 flex items-center gap-2" |
| 169 | + > |
| 170 | + <Download className="w-3 h-3 text-emerald-500" /> |
| 171 | + {FORMAT_LABELS[format]} (.{getFileExtension(format)}) |
| 172 | + </button> |
| 173 | + ))} |
| 174 | + </div> |
| 175 | + </> |
| 176 | + )} |
| 177 | + </div> |
| 178 | + ); |
| 179 | +}; |
0 commit comments