// Main App — no sidebar, tweaks panel function App() { const techniques = [...(window.TECHNIQUES_A || []), ...(window.TECHNIQUES_B || [])]; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accentColor": "#3b82f6", "cardColumns": "auto", "compactCards": false, "showTagline": true, "showFamilyBadge": true, "listPaneWidth": 380 }/*EDITMODE-END*/; const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const proposal = useProposal(); const isMobile = useIsMobile(); const [theme, toggleTheme] = useTheme(); // State const [search, setSearch] = React.useState(''); const [activeId, setActiveId] = React.useState(null); const [filters, setFilters] = React.useState({ family:'', budget:'', stage:'' }); const [showRecommender, setShowRecommender] = React.useState(false); const [showBuilder, setShowBuilder] = React.useState(false); const [filtersOpen, setFiltersOpen] = React.useState(false); // Deep-link React.useEffect(() => { const hash = window.location.hash.replace('#',''); if (hash) setActiveId(hash); // Empty hash means the user navigated back — close the detail view. // Matters most on mobile, where detail is full-screen and back is the natural exit. const onHash = () => { setActiveId(window.location.hash.replace('#','') || null); }; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); React.useEffect(() => { if (activeId) window.location.hash = activeId; else window.history.replaceState(null,'',window.location.pathname); }, [activeId]); const score = (t, q) => { if (!q) return 1; const lq = q.toLowerCase(); let s = 0; if (t.name.toLowerCase().includes(lq)) s += 10; if (t.shortName?.toLowerCase().includes(lq)) s += 8; if ((t.tags.synonyms||[]).some(syn => syn.toLowerCase().includes(lq))) s += 7; if ((t.tags.businessProblems||[]).some(b => b.toLowerCase().includes(lq))) s += 5; if ((t.tags.objectives||[]).some(o => o.toLowerCase().includes(lq))) s += 5; if ((t.tags.useCases||[]).some(u => u.toLowerCase().includes(lq))) s += 4; if ((t.tags.questionTypes||[]).some(qt => qt.toLowerCase().includes(lq))) s += 3; if ((t.tags.outputTypes||[]).some(ot => ot.toLowerCase().includes(lq))) s += 3; if (t.tagline?.toLowerCase().includes(lq)) s += 3; if (t.family?.toLowerCase().includes(lq)) s += 2; return s; }; const filtered = techniques .filter(t => { if (filters.family && t.family !== filters.family) return false; if (filters.budget && t.tags.budgetLevel !== filters.budget) return false; if (filters.stage && !(t.tags.studyStages||[]).includes(filters.stage)) return false; if (search) return score(t, search) > 0; return true; }) .sort((a,b) => search ? score(b,search) - score(a,search) : 0); const activeTech = techniques.find(t => t.id === activeId); const families = [...new Set(techniques.map(t => t.family))]; const stages = [...new Set(techniques.flatMap(t => t.tags.studyStages||[]))]; const hasFilters = filters.family || filters.budget || filters.stage; const clearAll = () => { setSearch(''); setFilters({ family:'', budget:'', stage:'' }); }; const accent = tweaks.accentColor || COLORS.indigo[700]; // Card grid columns const gridCols = tweaks.compactCards ? 'repeat(auto-fill, minmax(200px, 1fr))' : tweaks.cardColumns === 'auto' ? 'repeat(auto-fill, minmax(280px, 1fr))' : tweaks.cardColumns === '2' ? 'repeat(2, 1fr)' : tweaks.cardColumns === '3' ? 'repeat(3, 1fr)' : 'repeat(auto-fill, minmax(280px, 1fr))'; return (
{/* SITE NAV — mirrors the shell in index.php so this page reads as part of DataIngrid. Desktop only: on mobile the ~52px would eat scarce height. */} {!isMobile && } {/* TOP BAR — single row on desktop; on mobile it wraps to two rows (brand + actions, then a full-width search) so nothing gets squeezed out. */}
{/* Brand */}
Technique Library
Healthcare & Pharma Research
{!isMobile &&
} {/* Search — own full-width row on mobile (order:1 puts it after the buttons) */}
setSearch(e.target.value)} style={{ width:'100%', padding:'8px 34px 8px 36px', borderRadius:10, border:`1.5px solid ${search ? accent : COLORS.slate[200]}`, /* 16px on mobile — anything smaller makes iOS Safari zoom on focus */ fontSize: isMobile ? 16 : 13, background: search ? accent+'08' : COLORS.slate[50], color:COLORS.slate[800], boxSizing:'border-box', transition:'all 0.15s', outline:'none' }} /> {search && ( )}
{/* Filters */} {/* Proposal builder */} {/* Theme toggle */} {/* Recommender */}
{/* FILTER BAR */} {filtersOpen && (
FILTER: {[ { key:'family', label:'Family', options:[{v:'',l:'All families'},...families.map(f=>({v:f,l:f}))] }, { key:'budget', label:'Budget', options:[{v:'',l:'All budgets'},{v:'low',l:'Low'},{v:'low-medium',l:'Low–Medium'},{v:'medium',l:'Medium'},{v:'medium-high',l:'Medium–High'},{v:'high',l:'High'}] }, { key:'stage', label:'Stage', options:[{v:'',l:'All stages'},...stages.map(s=>({v:s,l:s}))] }, ].map(({ key, options }) => ( ))} {hasFilters && ( )}
)} {/* BODY */}
{/* CARD LIST PANE — hidden on mobile while a technique is open, so the detail panel gets the full width instead of a ~10px sliver. */} {!(isMobile && activeTech) && (
{/* Header */}
{search || hasFilters ? <>{filtered.length} result{filtered.length!==1?'s':''} : `${techniques.length} techniques`} {!activeTech && ( )}
{filtered.length === 0 && ( setShowRecommender(true)} style={{ marginTop:16, padding:'9px 20px', borderRadius:10, border:'none', background:accent, color:'white', fontSize:13, fontWeight:700, cursor:'pointer' }}> ✦ Use Guided Recommender } /> )} {filtered.length > 0 && (
{filtered.map(t => ( setActiveId(activeId === id ? null : id)} isActive={activeId === t.id} compact={(!!activeTech && !isMobile) || tweaks.compactCards} accentOverride={accent} showTagline={tweaks.showTagline} showFamilyBadge={tweaks.showFamilyBadge} /> ))}
)}
)} {/* DETAIL PANEL — full width on mobile, where it replaces the list */} {activeTech && (
setActiveId(null)} isMobile={isMobile} inCart={proposal.inCart(activeTech.id)} onToggleCart={proposal.toggleCart} ctx={proposal.ctx} />
)}
{/* RECOMMENDER MODAL */} setShowRecommender(false)} title="Guided Method Recommender" width={560}> { setActiveId(id); setShowRecommender(false); }} onClose={() => setShowRecommender(false)} /> {/* PROPOSAL BUILDER MODAL */} setShowBuilder(false)} title="Proposal Builder" width={1000}> { setActiveId(id); setShowBuilder(false); }} /> {/* TWEAKS PANEL */}
); } // Mount const appRoot = ReactDOM.createRoot(document.getElementById('root')); appRoot.render();