-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathApp.tsx
More file actions
269 lines (249 loc) · 12.3 KB
/
App.tsx
File metadata and controls
269 lines (249 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import React, { Suspense, lazy, useEffect, useState } from 'react';
import { AppView, HubMapping } from './types';
import Landing from './components/Landing';
import ExplorerView from './components/ExplorerView';
import { exportExplorerAsTsv } from './services/export/explorerExport';
import { PathwayData } from './services/pathwayLoader';
import { useDatasetStats } from './hooks/useDatasetStats';
import { useExplorerData } from './hooks/useExplorerData';
const NetworkVisualization = lazy(() => import('./components/NetworkVisualization'));
const EnrichmentPanel = lazy(() => import('./components/EnrichmentPanel'));
function useDebouncedValue<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = window.setTimeout(() => setDebounced(value), delayMs);
return () => window.clearTimeout(timer);
}, [value, delayMs]);
return debounced;
}
function ViewFallback({ label }: { label: string }) {
return (
<div className="print-panel rounded-3xl p-8 text-sm text-[var(--print-fog)]">
Loading {label}...
</div>
);
}
const App: React.FC = () => {
const [activeView, setActiveView] = useState<AppView>('explorer');
const [showLanding, setShowLanding] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebouncedValue(searchTerm, 350);
const [priorityTfFilter, setPriorityTfFilter] = useState<string | null>(null);
const [exportFormat, setExportFormat] = useState<'symbol' | 'geneId'>('geneId');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [showExplorerSummary, setShowExplorerSummary] = useState(true);
const [hubMapping] = useState<HubMapping>({});
const [explorerPage, setExplorerPage] = useState(1);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [exportingTSV, setExportingTSV] = useState(false);
const [exportProgress, setExportProgress] = useState<string | null>(null);
const [pathwayData, setPathwayData] = useState<PathwayData | null>(null);
const [selectedSources] = useState<string[]>(['TARGET', 'DAP', 'CHIP']);
const minConfidence = 1;
const { stats: datasetStats, error: statsError } = useDatasetStats(!showLanding);
const explorer = useExplorerData({
enabled: !showLanding && activeView === 'explorer',
filters: {
minConfidence,
priorityTfFilter,
searchTerm: debouncedSearchTerm,
selectedSources
},
page: explorerPage
});
useEffect(() => {
setExplorerPage(1);
}, [debouncedSearchTerm, priorityTfFilter]);
useEffect(() => {
if (explorer.error) {
setErrorMessage(explorer.error);
} else if (statsError) {
setErrorMessage(statsError);
}
}, [explorer.error, statsError]);
const handleDownloadTSV = () => {
const run = async () => {
setExportingTSV(true);
setExportProgress('Preparing export...');
try {
await exportExplorerAsTsv({
exportFormat,
filters: {
minConfidence,
priorityTfFilter,
searchTerm: debouncedSearchTerm,
selectedSources
},
onProgress: setExportProgress
});
} catch (downloadError) {
console.error(downloadError);
setErrorMessage('Error exporting filtered TSV from Supabase.');
} finally {
setExportingTSV(false);
setExportProgress(null);
}
};
void run();
};
if (showLanding) {
return (
<Landing
onEnter={() => {
setShowLanding(false);
setActiveView('explorer');
}}
/>
);
}
return (
<div className="print-shell print-grid flex h-screen overflow-hidden">
<div className={`fixed inset-0 bg-slate-950/70 z-20 transition-opacity md:hidden ${isSidebarOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`} onClick={() => setIsSidebarOpen(false)}></div>
<aside className={`fixed md:static z-30 w-72 h-full print-panel border-r border-[var(--print-line)] text-slate-300 flex flex-col shrink-0 transition-transform md:translate-x-0 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
<div className="p-6 border-b border-[var(--print-line)] flex items-center gap-3 justify-between">
<div className="flex items-center gap-3">
<div className="print-logo-frame w-16 h-16 rounded-2xl flex items-center justify-center p-2">
<img src="/logos/sinfondoprint.png" alt="PRINT logo" className="w-full h-full object-contain" />
</div>
<div>
<h1 className="text-lg font-bold text-white tracking-tight leading-tight">PRINT</h1>
<p className="text-[10px] text-[var(--print-fog)] -mt-0.5">Plant Regulatory Information Network Tool</p>
</div>
</div>
<button onClick={() => setIsSidebarOpen(false)} className="md:hidden text-slate-400 hover:text-white">✕</button>
</div>
<nav className="print-scrollbar flex-1 p-4 space-y-2 overflow-y-auto">
<div className="mb-6 px-3 py-3 print-panel-soft rounded-xl">
<div className="text-[10px] font-bold text-[var(--print-mint)] uppercase tracking-widest mb-2">Database</div>
<div className="text-xs font-medium text-slate-300 flex justify-between">
<span>Interactions:</span>
<span className="text-[var(--print-mint)] font-bold">{datasetStats?.totalInteractions ?? '...'}</span>
</div>
<div className="text-xs font-medium text-slate-300 flex justify-between mt-1">
<span>Unique targets:</span>
<span className="text-[var(--print-mint)] font-bold">{datasetStats?.uniqueTargets ?? '...'}</span>
</div>
</div>
<button onClick={() => setActiveView('explorer')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeView === 'explorer' ? 'print-button' : 'hover:bg-white/5 text-slate-400'}`}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Explore Data
</button>
<button onClick={() => setActiveView('network')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeView === 'network' ? 'print-button' : 'hover:bg-white/5 text-slate-400'}`}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
Network View
</button>
<button onClick={() => setActiveView('enrichment')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeView === 'enrichment' ? 'print-button' : 'hover:bg-white/5 text-slate-400'}`}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3v18h18M7 13l3-3 4 4 5-6" />
</svg>
Enrichment
</button>
</nav>
<div className="p-4 border-t border-[var(--print-line)]">
<div className="grid grid-cols-2 gap-3">
<a
href="https://pgrlab.cl"
target="_blank"
rel="noreferrer"
className="print-partner-card rounded-2xl px-3 py-2.5 flex items-center justify-center overflow-hidden transition-all hover:border-[var(--print-line-strong)] hover:bg-white/5"
>
<div className="h-11 w-28 flex items-center justify-center shrink-0">
<img
src="/logos/Logo Lab (transparent bg).png"
alt="Plant Genome Regulation Lab"
className="print-partner-logo max-h-full max-w-full object-contain opacity-100"
/>
</div>
</a>
<a
href="https://phytolearning.cl"
target="_blank"
rel="noreferrer"
className="rounded-2xl px-3 py-2 flex items-center justify-center overflow-hidden transition-all hover:border-[var(--print-line-strong)] hover:bg-white/5 border border-[rgba(119,167,159,0.28)] bg-[linear-gradient(180deg,rgba(78,112,121,0.9),rgba(55,81,89,0.9))]"
>
<div className="h-10 w-24 flex items-center justify-center shrink-0">
<img
src="/logos/2025 - Logo PhytoLearning sin fondo (1).png"
alt="PhytoLearning"
className="max-h-full max-w-full object-contain opacity-95"
/>
</div>
</a>
</div>
</div>
</aside>
<main className="flex-1 flex flex-col min-w-0 bg-black/10">
<header className="h-20 print-panel-soft border-b border-[var(--print-line)] flex items-center justify-between px-4 md:px-8 shrink-0 z-10">
<div className="flex items-center gap-3">
<button onClick={() => setIsSidebarOpen(true)} className="md:hidden px-3 py-2 rounded-lg bg-black/10 border border-[var(--print-line)] text-slate-200">☰</button>
<div className="flex flex-col gap-1 items-start">
<div className="flex items-center gap-4">
<h2 className="text-lg font-bold text-[var(--print-mint)]">Regulatory Dashboard</h2>
</div>
</div>
</div>
</header>
<div className="flex-1 overflow-y-auto p-4 md:p-8">
{activeView === 'explorer' ? (
<ExplorerView
data={explorer.displayRows}
datasetStats={datasetStats}
errorMessage={errorMessage}
explorerLoading={explorer.loading}
exportFormat={exportFormat}
exportProgress={exportProgress}
exportingTSV={exportingTSV}
explorerDisplayRows={explorer.displayRows}
explorerDisplayTotal={explorer.displayTotal}
explorerPage={explorerPage}
explorerTotalPages={explorer.totalPages}
hubMapping={hubMapping}
onClearError={() => setErrorMessage(null)}
onDownloadTSV={handleDownloadTSV}
onNextPage={() => setExplorerPage((page) => Math.min(explorer.totalPages, page + 1))}
onPrevPage={() => setExplorerPage((page) => Math.max(1, page - 1))}
onSearchTermChange={(value) => {
setSearchTerm(value);
setPriorityTfFilter(null);
}}
onSetExportFormat={setExportFormat}
onTogglePriorityTf={(gene) => {
const isActive = priorityTfFilter === gene;
setPriorityTfFilter(isActive ? null : gene);
setSearchTerm(isActive ? '' : gene);
}}
onToggleSummary={() => setShowExplorerSummary((value) => !value)}
priorityTfFilter={priorityTfFilter}
searchTerm={searchTerm}
showExplorerSummary={showExplorerSummary}
totalInteractions={datasetStats?.totalInteractions || explorer.displayTotal}
/>
) : activeView === 'network' ? (
<Suspense fallback={<ViewFallback label="network view" />}>
<NetworkVisualization
pathwayData={pathwayData}
onPathwayChange={setPathwayData}
/>
</Suspense>
) : activeView === 'enrichment' ? (
<Suspense fallback={<ViewFallback label="enrichment view" />}>
<EnrichmentPanel
selectedSources={selectedSources}
minConfidence={minConfidence}
/>
</Suspense>
) : (
<div className="print-panel rounded-3xl p-8 text-sm text-[var(--print-fog)]">
Unknown view.
</div>
)}
</div>
</main>
</div>
);
};
export default App;