Skip to content

Commit 62faba9

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: added interactive structure links to PDF report and deep-link support to viewer
1 parent 3802436 commit 62faba9

3 files changed

Lines changed: 108 additions & 7 deletions

File tree

src/ViewerApp.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
} from 'lucide-react';
4949
import { startOnboardingTour } from './components/TourGuide';
5050
import { ViewportSelector } from './components/ViewportSelector';
51+
import { supabase } from './lib/supabase';
52+
import { getDownloadUrl } from './lib/structuresService';
5153
import { SnapshotModal } from './components/SnapshotModal';
5254
import { ToastContainer } from './components/Toast';
5355
import { useToast } from './hooks/useToast';
@@ -230,6 +232,39 @@ function App() {
230232
window.history.replaceState({}, '', url);
231233
}, 1000);
232234
}
235+
236+
// Direct Structure Load from URL
237+
const structId = params.get('struct');
238+
if (structId) {
239+
const fetchAndLoad = async () => {
240+
try {
241+
const { data, error } = await supabase
242+
.from('structures')
243+
.select('*')
244+
.eq('id', structId)
245+
.single();
246+
247+
if (error) throw error;
248+
const url = await getDownloadUrl(data.file_path);
249+
250+
const res = await fetch(url);
251+
if (!res.ok) throw new Error('Failed to fetch structure file');
252+
const blob = await res.blob();
253+
254+
const file = new File([blob], `${data.name}.${data.file_type.toLowerCase()}`, { type: 'application/octet-stream' });
255+
controllers[0].handleUpload(file, data.file_type.toLowerCase() === 'cif' || data.file_type.toLowerCase() === 'mmcif');
256+
setShowLanding(false);
257+
258+
// Clean URL
259+
const newUrl = new URL(window.location.href);
260+
newUrl.searchParams.delete('struct');
261+
window.history.replaceState({}, '', newUrl);
262+
} catch (err) {
263+
console.error('Failed to load structure from URL:', err);
264+
}
265+
};
266+
fetchAndLoad();
267+
}
233268
}, []);
234269

235270
// Parse Global URL State Once

src/components/dashboard/LabNotebook.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,14 @@ export const LabNotebook: React.FC<{ isDrawer?: boolean }> = ({ isDrawer = false
261261
scale: 2,
262262
useCORS: true,
263263
backgroundColor: '#ffffff',
264-
width: 794, // 8.27in at 96dpi
265-
height: 1122, // 11.69in at 96dpi
264+
width: 794,
265+
height: 1122,
266266
});
267267
const imgData1 = canvas1.toDataURL('image/png');
268268
pdf.addImage(imgData1, 'PNG', 0, 0, 210, 297);
269+
270+
// Add links for Page 1 (usually empty of structure links but good for consistency)
271+
addLinksToPdf(page1, pdf, 0);
269272
}
270273

271274
// Add Page 2: Content
@@ -281,10 +284,13 @@ export const LabNotebook: React.FC<{ isDrawer?: boolean }> = ({ isDrawer = false
281284
});
282285
const imgData2 = canvas2.toDataURL('image/png');
283286
pdf.addImage(imgData2, 'PNG', 0, 0, 210, 297);
287+
288+
// Add interactive links for Page 2
289+
addLinksToPdf(page2, pdf, 0);
284290
}
285291

286292
pdf.save(`LabReport_${activeNotebook.title.replace(/\s+/g, '_') || 'Untitled'}.pdf`);
287-
console.log('Multi-page PDF saved successfully.');
293+
console.log('Multi-page PDF with links saved successfully.');
288294
}
289295
} catch (err) {
290296
console.error('Failed to export PDF:', err);
@@ -295,6 +301,27 @@ export const LabNotebook: React.FC<{ isDrawer?: boolean }> = ({ isDrawer = false
295301
}
296302
};
297303

304+
// Helper to add interactive links to the PDF
305+
const addLinksToPdf = (pageElement: HTMLElement, pdf: jsPDF, yOffset: number) => {
306+
const links = pageElement.querySelectorAll('.pdf-structure-link');
307+
const pageRect = pageElement.getBoundingClientRect();
308+
309+
// PDF is 210mm x 297mm. Input canvas/element is 794px x 1122px (at 96dpi)
310+
// Ratio: 210 / 794 = 0.2645
311+
const pxToMm = 210 / 794;
312+
313+
links.forEach(link => {
314+
const rect = link.getBoundingClientRect();
315+
const relX = (rect.left - pageRect.left) * pxToMm;
316+
const relY = (rect.top - pageRect.top) * pxToMm + yOffset;
317+
const relW = rect.width * pxToMm;
318+
const relH = rect.height * pxToMm;
319+
320+
const url = (link as HTMLAnchorElement).href;
321+
pdf.link(relX, relY, relW, relH, { url });
322+
});
323+
};
324+
298325
if (!user) return null;
299326

300327
return (
@@ -557,6 +584,7 @@ export const LabNotebook: React.FC<{ isDrawer?: boolean }> = ({ isDrawer = false
557584
date={activeNotebook.created_at}
558585
author={user?.email || 'Quercus User'}
559586
id={activeNotebook.id.slice(0, 8)}
587+
allStructures={allStructures}
560588
/>
561589
)}
562590
</div>

src/components/dashboard/LabReportTemplate.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import React from 'react';
22
import ReactMarkdown from 'react-markdown';
33
import remarkGfm from 'remark-gfm';
44
import { Microscope } from 'lucide-react';
5+
import type { Structure } from '../../lib/structuresService';
56

67
interface LabReportTemplateProps {
78
title: string;
89
content: string;
910
date: string;
1011
author: string;
1112
id: string;
13+
allStructures?: Structure[];
1214
}
1315

1416
/**
1517
* LabReportTemplate component for multi-page PDF generation.
1618
* Separated into Cover Page and Content Page.
1719
*/
1820
export const LabReportTemplate = React.forwardRef<HTMLDivElement, LabReportTemplateProps>(
19-
({ title, content, date, author, id }, ref) => {
21+
({ title, content, date, author, id, allStructures = [] }, ref) => {
2022
return (
2123
<div
2224
ref={ref}
@@ -42,15 +44,24 @@ export const LabReportTemplate = React.forwardRef<HTMLDivElement, LabReportTempl
4244
.pdf-page * {
4345
box-sizing: border-box !important;
4446
}
47+
.pdf-structure-link {
48+
color: #2563eb !important;
49+
text-decoration: underline !important;
50+
font-weight: 600 !important;
51+
cursor: pointer !important;
52+
}
4553
`}} />
4654

55+
{/* ... (rest of the component) ... */}
56+
4757
{/* PAGE 1: COVER PAGE */}
4858
<div id="report-page-1" className="pdf-page" style={{
4959
display: 'flex',
5060
flexDirection: 'column',
5161
justifyContent: 'center',
5262
textAlign: 'center'
5363
}}>
64+
{/* ... (Cover Page content) ... */}
5465
<div style={{ marginBottom: '4rem', display: 'flex', justifyContent: 'center' }}>
5566
<div style={{ backgroundColor: '#0f172a', padding: '1.5rem', borderRadius: '1rem', display: 'inline-flex' }}>
5667
<Microscope size={64} color="#ffffff" />
@@ -114,7 +125,6 @@ export const LabReportTemplate = React.forwardRef<HTMLDivElement, LabReportTempl
114125

115126
{/* PAGE 2: CONTENT PAGE */}
116127
<div id="report-page-2" className="pdf-page">
117-
{/* Header on page 2 */}
118128
<div style={{
119129
display: 'flex',
120130
justifyContent: 'space-between',
@@ -139,7 +149,36 @@ export const LabReportTemplate = React.forwardRef<HTMLDivElement, LabReportTempl
139149
h1: ({node, ...props}) => <h1 style={{fontSize: '1.875rem', fontWeight: 'bold', color: '#0f172a', marginBottom: '1rem', marginTop: '1rem'}} {...props} />,
140150
h2: ({node, ...props}) => <h2 style={{fontSize: '1.5rem', fontWeight: 'bold', color: '#0f172a', marginTop: '2rem', marginBottom: '0.75rem'}} {...props} />,
141151
h3: ({node, ...props}) => <h3 style={{fontSize: '1.25rem', fontWeight: 'bold', color: '#0f172a', marginTop: '1.5rem', marginBottom: '0.5rem'}} {...props} />,
142-
p: ({node, ...props}) => <p style={{marginBottom: '1rem', lineHeight: '1.6'}} {...props} />,
152+
p: ({children}) => {
153+
const processed = React.Children.map(children, child => {
154+
if (typeof child === 'string') {
155+
const parts = child.split(/(\[\[structure:[a-f0-9-]{36}\]\])/g);
156+
return parts.map((part, i) => {
157+
const match = part.match(/\[\[structure:([a-f0-9-]{36})\]\]/);
158+
if (match) {
159+
const sid = match[1];
160+
const s = allStructures.find(st => st.id === sid);
161+
const name = s?.name || sid.substring(0, 8);
162+
const url = `${window.location.origin}/?struct=${sid}`;
163+
return (
164+
<a
165+
key={i}
166+
href={url}
167+
className="pdf-structure-link"
168+
data-structure-id={sid}
169+
style={{ color: '#2563eb', textDecoration: 'underline', fontWeight: 'bold' }}
170+
>
171+
@{name}
172+
</a>
173+
);
174+
}
175+
return part;
176+
});
177+
}
178+
return child;
179+
});
180+
return <p style={{marginBottom: '1rem', lineHeight: '1.6'}}>{processed}</p>;
181+
},
143182
ul: ({node, ...props}) => <ul style={{listStyleType: 'disc', paddingLeft: '1.5rem', marginBottom: '1rem'}} {...props} />,
144183
ol: ({node, ...props}) => <ol style={{listStyleType: 'decimal', paddingLeft: '1.5rem', marginBottom: '1rem'}} {...props} />,
145184
li: ({node, ...props}) => <li style={{marginBottom: '0.5rem'}} {...props} />,
@@ -158,7 +197,6 @@ export const LabReportTemplate = React.forwardRef<HTMLDivElement, LabReportTempl
158197
</ReactMarkdown>
159198
</div>
160199

161-
{/* Footer on page 2 */}
162200
<div
163201
style={{
164202
position: 'absolute',

0 commit comments

Comments
 (0)