Skip to content

Commit 86175df

Browse files
committed
Refactor citation logic, improve header UI, and add 'Regime-Matched' indicator
1 parent b13372b commit 86175df

5 files changed

Lines changed: 456 additions & 413 deletions

File tree

App.tsx

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
TotalVsMinorityOverlay,
1111
FoldsVsMinorityOverlay
1212
} from './components/ChartOverlays';
13-
import { FlaskConical, ChevronDown, ChevronUp, MousePointerClick, Layers, Info, Github, Heart, Quote, Check, Copy } from 'lucide-react';
13+
import { FlaskConical, ChevronDown, ChevronUp, MousePointerClick, Layers, Info, Github, Heart, Quote, Check, Copy, Download } from 'lucide-react';
14+
import { CITATIONS } from './types';
15+
import { formatCitation, getFileExtension, CitationFormat, FORMAT_LABELS } from './logic/citationFormatting';
1416

1517
// --- Static Configurations ---
1618

@@ -58,35 +60,38 @@ const MissionBrief = () => {
5860
};
5961

6062
const CitationButton = () => {
61-
const [copiedBib, setCopiedBib] = useState(false);
62-
const [copiedAPA, setCopiedAPA] = useState(false);
63-
64-
const handleCopy = (text: string, type: 'bib' | 'apa') => {
65-
navigator.clipboard.writeText(text);
66-
if (type === 'bib') {
67-
setCopiedBib(true);
68-
setTimeout(() => setCopiedBib(false), 2000);
69-
} else {
70-
setCopiedAPA(true);
71-
setTimeout(() => setCopiedAPA(false), 2000);
72-
}
73-
};
63+
const [copiedFormat, setCopiedFormat] = useState<string | null>(null);
64+
const citation = CITATIONS.resampleLab2025;
7465

75-
const bibtex = `@software{ResampleLab2025,
76-
author = {mr-september},
77-
title = {Resample Lab: Advanced Data Resampling Heuristics},
78-
year = {2025},
79-
publisher = {GitHub},
80-
journal = {GitHub repository},
81-
howpublished = {\\url{https://github.com/mr-september/Resample-Lab}}
82-
}`;
66+
// Guard against missing citation (though it should be in types)
67+
if (!citation) return null;
8368

84-
const apa = `mr-september. (2025). Resample Lab: Advanced Data Resampling Heuristics [Computer software]. GitHub. https://github.com/mr-september/Resample-Lab`;
69+
const handleCopy = async (format: CitationFormat) => {
70+
const content = formatCitation(citation, format);
71+
await navigator.clipboard.writeText(content);
72+
setCopiedFormat(format);
73+
setTimeout(() => setCopiedFormat(null), 2000);
74+
};
75+
76+
const handleDownload = (format: CitationFormat) => {
77+
const content = formatCitation(citation, format);
78+
const ext = getFileExtension(format);
79+
const key = `${citation.authors.split(',')[0].split(' ').pop()?.toLowerCase()}${citation.year}`;
80+
const blob = new Blob([content], { type: 'text/plain' });
81+
const url = URL.createObjectURL(blob);
82+
const a = document.createElement('a');
83+
a.href = url;
84+
a.download = `${key}.${ext}`;
85+
document.body.appendChild(a);
86+
a.click();
87+
document.body.removeChild(a);
88+
URL.revokeObjectURL(url);
89+
};
8590

8691
return (
87-
<div className="relative group">
92+
<div className="relative group flex items-center">
8893
<button
89-
className="text-zinc-500 hover:text-indigo-400 transition-colors"
94+
className="text-zinc-500 hover:text-indigo-400 transition-colors flex items-center justify-center h-full"
9095
aria-label="Cite this Lab"
9196
>
9297
<Quote className="w-5 h-5" />
@@ -103,15 +108,16 @@ const CitationButton = () => {
103108
<div className="flex items-center justify-between">
104109
<span className="text-xs text-zinc-400">BibTeX</span>
105110
<button
106-
onClick={() => handleCopy(bibtex, 'bib')}
111+
onClick={() => handleCopy('bibtex')}
107112
className="flex items-center gap-1.5 text-[10px] text-zinc-500 hover:text-indigo-400 transition-colors"
113+
aria-label="Copy BibTeX"
108114
>
109-
{copiedBib ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
110-
{copiedBib ? 'Copied!' : 'Copy'}
115+
{copiedFormat === 'bibtex' ? <Check className="w-3 h-3 text-emerald-500" /> : <Copy className="w-3 h-3" />}
116+
{copiedFormat === 'bibtex' ? 'Copied!' : 'Copy'}
111117
</button>
112118
</div>
113-
<pre className="text-[10px] leading-relaxed p-2 bg-zinc-900/50 rounded-lg border border-zinc-800 text-zinc-400 overflow-x-auto text-left">
114-
{bibtex}
119+
<pre className="text-[10px] leading-relaxed p-2 bg-zinc-900/50 rounded-lg border border-zinc-800 text-zinc-400 overflow-x-auto text-left scrollbar-thin scrollbar-thumb-zinc-700 scrollbar-track-transparent">
120+
{formatCitation(citation, 'bibtex')}
115121
</pre>
116122
</div>
117123

@@ -120,15 +126,35 @@ const CitationButton = () => {
120126
<div className="flex items-center justify-between">
121127
<span className="text-xs text-zinc-400">APA</span>
122128
<button
123-
onClick={() => handleCopy(apa, 'apa')}
129+
onClick={() => handleCopy('apa')}
124130
className="flex items-center gap-1.5 text-[10px] text-zinc-500 hover:text-indigo-400 transition-colors"
131+
aria-label="Copy APA"
125132
>
126-
{copiedAPA ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
127-
{copiedAPA ? 'Copied!' : 'Copy'}
133+
{copiedFormat === 'apa' ? <Check className="w-3 h-3 text-emerald-500" /> : <Copy className="w-3 h-3" />}
134+
{copiedFormat === 'apa' ? 'Copied!' : 'Copy'}
128135
</button>
129136
</div>
130137
<div className="text-[10px] leading-relaxed p-2 bg-zinc-900/50 rounded-lg border border-zinc-800 text-zinc-400 text-left">
131-
{apa}
138+
{formatCitation(citation, 'apa')}
139+
</div>
140+
</div>
141+
142+
{/* Download Options */}
143+
<div className="pt-3 border-t border-zinc-800">
144+
<div className="text-[10px] text-zinc-500 font-bold uppercase tracking-wider mb-2">
145+
Download Citation
146+
</div>
147+
<div className="grid grid-cols-2 gap-2">
148+
{(['bibtex', 'ris', 'endnote'] as CitationFormat[]).map(format => (
149+
<button
150+
key={format}
151+
onClick={() => handleDownload(format)}
152+
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-900/50 hover:bg-zinc-800 border border-zinc-800 text-zinc-400 hover:text-emerald-400 transition-colors text-[10px] text-left"
153+
>
154+
<Download className="w-3 h-3" />
155+
{FORMAT_LABELS[format]} (.{getFileExtension(format)})
156+
</button>
157+
))}
132158
</div>
133159
</div>
134160
</div>

components/CitationActions.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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

Comments
 (0)