// ===== src/data.jsx ===== // Product, program, faculty, etc. data for SBDA Research const PRODUCTS = [ { id: 'virdb', name: 'VIRdb', version: 'v1.0', category: 'Database', subtitle: 'Vitiligo Interactive Resource', color: 'var(--c-teal)', glyph: 'V', short: 'A comprehensive database for interactive analysis of genes and proteins involved in the pathogenesis of vitiligo.', long: 'VIRdb is a curated and queryable resource that consolidates over a decade of vitiligo-related molecular research. It enables researchers to interactively explore gene and protein networks, biomarkers, and pathways implicated in vitiligo pathogenesis — accelerating hypothesis generation and translational discovery.', tags: ['Genomics', 'Proteomics', 'Vitiligo', 'Pathways'], stats: [ { v: '4,800+', l: 'Curated entries' }, { v: '120+', l: 'Pathways' }, { v: 'Open', l: 'Access' }], features: [ 'Searchable catalogue of vitiligo-related genes and proteins', 'Visualisation of interaction networks and signaling pathways', 'Cross-references to UniProt, KEGG and PubMed', 'Downloadable datasets for reproducible research'] }, { id: 'virdb2', name: 'VIRdb 2.0', version: 'v2.0', category: 'Database', subtitle: 'Comorbidity Network Resource', color: 'var(--c-pink)', glyph: 'V²', short: 'Interactive analysis of comorbidity conditions associated with vitiligo pathogenesis using a co-expression network-based approach.', long: 'The next-generation expansion of VIRdb, extending the resource into the realm of comorbidities. VIRdb 2.0 integrates co-expression networks to model how vitiligo shares molecular signatures with associated disorders — from autoimmune conditions to metabolic syndromes — empowering systems-level inquiry.', tags: ['Co-expression', 'Networks', 'Comorbidity', 'Systems Biology'], stats: [ { v: '11,200+', l: 'Network edges' }, { v: '34', l: 'Comorbidities' }, { v: '2024', l: 'Latest release' }], features: [ 'Co-expression network exploration across comorbid conditions', 'Cross-disease module detection and visualization', 'Interactive heatmaps, force-directed graphs, and Sankey views', 'Programmatic API access for batch queries'] }, { id: 'net2align', name: 'Net2Align', version: 'Tool', category: 'Algorithm', subtitle: 'Pairwise Network Alignment', color: 'var(--c-blue)', glyph: 'N', short: 'An algorithm for pairwise global alignment of biological networks.', long: 'Net2Align is an alignment algorithm designed for the global comparison of two biological networks. It identifies conserved structural and functional correspondences across species or conditions, helping researchers transfer knowledge between model organisms and understand evolutionary conservation of molecular machinery.', tags: ['Alignment', 'Graph theory', 'Cross-species', 'CLI'], stats: [ { v: '8x', l: 'Faster than baseline' }, { v: 'CLI', l: 'Reproducible runs' }, { v: 'MIT', l: 'License' }], features: [ 'Global pairwise network alignment with tunable scoring', 'Supports weighted, directed, and multigraph inputs', 'Outputs alignment matrices and conserved subgraphs', 'Python bindings + command-line interface'] }, { id: 'plurimet', name: 'PluriMetNet', version: 'Model', category: 'Model', subtitle: 'hESC Metabolic Dynamics', color: 'var(--c-violet)', glyph: 'P', short: 'A dynamic electronic model decrypting the metabolic variations in human embryonic stem cells at fluctuating oxygen concentrations.', long: 'PluriMetNet is a dynamic computational model that simulates the metabolic rewiring of human embryonic stem cells (hESCs) under varying oxygen tensions. By linking glycolysis, oxidative phosphorylation, and pluripotency markers, it offers a quantitative lens on how environment shapes stem-cell identity.', tags: ['Metabolism', 'hESC', 'Hypoxia', 'Dynamic model'], stats: [ { v: '180+', l: 'Reactions modelled' }, { v: '24h', l: 'Simulated window' }, { v: 'Open', l: 'Source code' }], features: [ 'Time-resolved metabolic flux predictions', 'Oxygen-tension parameter sweeps with phase plots', 'Coupled to pluripotency gene expression signals', 'Reproducible Jupyter notebooks for every figure'] }, { id: 'tfis', name: 'TFIS', version: 'Tool', category: 'Tool', subtitle: 'Transcription Factor Information System', color: 'var(--c-yellow)', glyph: 'T', short: 'Transcription Factor Information System — a tool for detection of transcription factor binding sites.', long: 'TFIS is a sequence-analysis tool for the identification of transcription factor binding sites (TFBS) across promoter regions. With customizable PWM libraries and statistical scoring, it streamlines the discovery of regulatory motifs that orchestrate gene expression programs.', tags: ['TFBS', 'Regulation', 'PWM', 'Motifs'], stats: [ { v: '650+', l: 'PWM library' }, { v: 'p<10⁻⁵', l: 'Default cutoff' }, { v: 'Web + CLI', l: 'Interfaces' }], features: [ 'PWM-based scanning with thresholded scoring', 'Cross-species comparisons via aligned promoters', 'Batch upload and downloadable BED/CSV exports', 'Interactive genome-browser style visualisation'] }, { id: 'hepnet', name: 'HEPNet', version: 'Model', category: 'Model', subtitle: 'Human Energy Pool Network', color: 'var(--c-lime)', glyph: 'H', short: 'A knowledge-base model of the Human Energy Pool Network for predicting the energy availability status of an individual.', long: 'HEPNet is a knowledge-base model that integrates physiological, biochemical, and behavioral signals into a unified network describing human energy flux. By predicting energy availability status, it lays groundwork for personalised diagnostics in metabolic and lifestyle disorders.', tags: ['Energy metabolism', 'Knowledge base', 'Predictive', 'Personalised'], stats: [ { v: '2,400+', l: 'Curated relations' }, { v: '6', l: 'Organ systems' }, { v: 'β', l: 'Release stage' }], features: [ 'Knowledge graph linking nutrients, hormones, and organ systems', 'Predicts individualised energy availability status', 'API integration with wearable and clinical datasets', 'Interpretable, evidence-linked predictions'] }]; const PROGRAMS = [ { id: 'p1', cat: 'Internship', title: 'Systems Biology Internship', duration: '8 weeks', desc: 'Hands-on mentorship in network analysis, omics data wrangling, and reproducible research practice. Cohort-based with weekly journal clubs.', price: 'Stipend-based', seats: '12 seats' }, { id: 'p2', cat: 'Workshop', title: 'Bioinformatics Bootcamp', duration: '4 weekends', desc: 'From Linux fundamentals to RNA-seq pipelines. Live coding, real datasets, certificate of completion.', price: '₹14,500', seats: '24 seats' }, { id: 'p3', cat: 'Course', title: 'Network Biology Masterclass', duration: '12 weeks', desc: 'Advanced graph algorithms applied to biological networks — pathway enrichment, alignment, and dynamics.', price: '₹32,000', seats: '30 seats' }, { id: 'p4', cat: 'Fellowship', title: 'Research Fellowship', duration: '6–12 months', desc: 'Funded research positions for graduates interested in pursuing publication-grade work in systems biology.', price: 'Funded', seats: '4 seats' }, { id: 'p5', cat: 'Course', title: 'ML for Life Sciences', duration: '10 weeks', desc: 'Practical machine learning curriculum tailored for biologists — feature engineering, model interpretability, ethics.', price: '₹28,000', seats: '20 seats' }, { id: 'p6', cat: 'Workshop', title: 'Scientific Writing Lab', duration: '2 weekends', desc: 'Crafting a publishable manuscript: structure, figures, and the art of the persuasive abstract.', price: '₹6,500', seats: '40 seats' }]; const FACULTY = [ { id: 'f1', name: 'Dr. A. Mehrotra', role: 'Principal Investigator', tags: ['Systems Biology', 'Vitiligo'], glyph: 'AM' }, { id: 'f2', name: 'Dr. R. Kapoor', role: 'Senior Researcher', tags: ['Networks', 'Algorithms'], glyph: 'RK' }, { id: 'f3', name: 'Dr. S. Iyer', role: 'Computational Biologist', tags: ['Metabolism', 'hESC'], glyph: 'SI' }, { id: 'f4', name: 'Dr. M. Banerjee', role: 'Bioinformatics Lead', tags: ['TFBS', 'Regulation'], glyph: 'MB' }]; const TESTIMONIALS = [ { quote: 'My internship at SBDA changed the way I think about biology. I went in writing scripts; I came out designing studies.', name: 'Anika Verma', role: 'MSc → PhD candidate, IISc', av: 'AV' }, { quote: 'VIRdb 2.0 has become a regular stop in our comorbidity analyses. The network views are exactly the lens we needed.', name: 'Dr. P. Krishnan', role: 'Clinical Researcher, AIIMS', av: 'PK' }, { quote: 'The Net2Align workshop replaced two weeks of trial and error with two days of clarity. Thoughtful, rigorous teaching.', name: 'Rishi Datta', role: 'Senior Engineer, Strand Life Sci.', av: 'RD' }, { quote: 'A rare lab that takes mentorship seriously. I left with a publication and, more importantly, a research compass.', name: 'Tanvi Rao', role: 'Fellow, 2024 cohort', av: 'TR' }]; const POSTS = [ { id: 'b1', cat: 'Research', date: 'May 02, 2026', title: 'Comorbidity networks: when shared genes mean shared destinies', excerpt: 'A look at how VIRdb 2.0 surfaces molecular crosstalk between vitiligo and adjacent autoimmune conditions — and what it implies for therapy.', color: 'var(--c-pink)', featured: true }, { id: 'b2', cat: 'Tutorial', date: 'Apr 18, 2026', title: 'Five rules for aligning biological networks well', excerpt: 'Pragmatic guidance from the Net2Align team on scoring, normalisation, and validation.', color: 'var(--c-blue)' }, { id: 'b3', cat: 'Lab Notes', date: 'Apr 04, 2026', title: 'Hypoxia, glycolysis, and pluripotency — modelling the trio', excerpt: 'A walkthrough of PluriMetNet’s parameter sweep methodology and why oxygen still matters.', color: 'var(--c-violet)' }]; const STATS = [ { v: 142, suffix: '+', l: 'Peer-reviewed papers' }, { v: 6, suffix: '', l: 'Hero products shipped' }, { v: 2400, suffix: '+', l: 'Researchers served' }, { v: 18, suffix: '', l: 'Years of inquiry' }]; Object.assign(window, { PRODUCTS, PROGRAMS, FACULTY, TESTIMONIALS, POSTS, STATS }); // ===== src/atoms.jsx ===== // Small atoms: icons, scroll spy, counter, etc. const { useState, useEffect, useRef, useCallback } = React; // Minimal stroke icons — original line set function Icon({ name, size = 16, stroke = 1.6 }) { const s = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'arrow-right':return ; case 'arrow-up-right':return ; case 'arrow-left':return ; case 'close':return ; case 'sun':return ; case 'moon':return ; case 'user':return ; case 'mail':return ; case 'phone':return ; case 'pin':return ; case 'check':return ; case 'home':return ; case 'grid':return ; case 'book':return ; case 'spark':return ; case 'logout':return ; default:return null; } } // Animated counter (intersection observer) function Counter({ to, suffix = '', duration = 1400 }) { const ref = useRef(null); const [val, setVal] = useState(0); const fired = useRef(false); useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !fired.current) { fired.current = true; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(Math.round(eased * to)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.25 }); if (ref.current) io.observe(ref.current); return () => io.disconnect(); }, [to, duration]); return {val.toLocaleString()}{suffix}; } // Scroll lock for modal function useBodyLock(active) { useEffect(() => { if (!active) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => {document.body.style.overflow = prev;}; }, [active]); } // Theme controller function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } Object.assign(window, { Icon, Counter, useBodyLock, applyTheme }); // ===== src/nav-hero.jsx ===== // Nav + Hero const { useState: useStateNH, useEffect: useEffectNH } = React; function Nav({ onLogin, onDashboard, isAuthed, theme, onToggleTheme, onNavigate, view }) { const [scrolled, setScrolled] = useStateNH(false); const [menuOpen, setMenuOpen] = useStateNH(false); useEffectNH(() => { const onScroll = () => setScrolled(window.scrollY > 16); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); const links = [ ['Products', '#products'], ['About', '#about'], ['Programs', '#programs'], ['Faculty', '#faculty'], ['Resources', '#blog'], ['Contact', '#contact']]; const closeMenu = () => setMenuOpen(false); return (
{e.preventDefault();onNavigate('home');closeMenu();}}> S SBDAResearch {view === 'home' && }
{view === 'home' && } {isAuthed ? : } Get in touch
{menuOpen && view === 'home' && (
{links.map(([label, href]) => {label} )}
)} {view === 'home' && ( )}
); } function HeroViz() { // Render a network of dots + lines + 3 floating tags const nodes = [ { x: 50, y: 50, r: 26, c: 'var(--accent)' }, { x: 18, y: 22, r: 9, c: 'var(--c-teal)' }, { x: 82, y: 18, r: 7, c: 'var(--c-yellow)' }, { x: 90, y: 60, r: 11, c: 'var(--c-pink)' }, { x: 70, y: 88, r: 8, c: 'var(--c-blue)' }, { x: 22, y: 82, r: 10, c: 'var(--c-lime)' }, { x: 8, y: 55, r: 6, c: 'var(--c-violet)' }, { x: 38, y: 12, r: 5, c: 'var(--ink-2)' }, { x: 62, y: 38, r: 5, c: 'var(--ink-2)' }, { x: 40, y: 70, r: 5, c: 'var(--ink-2)' }, { x: 78, y: 78, r: 4, c: 'var(--ink-2)' }, { x: 30, y: 40, r: 4, c: 'var(--ink-2)' }]; const edges = [ [0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [1, 7], [2, 7], [2, 8], [3, 8], [4, 9], [5, 9], [5, 10], [4, 10], [6, 11], [1, 11], [0, 7], [0, 8], [0, 9], [0, 10], [0, 11]]; return (
{edges.map(([a, b], i) => { const A = nodes[a],B = nodes[b]; return ; })} {nodes.map((n, i) => {i === 0 && } {i > 0 && i < 7 && } )} SBDA CORE
NETWORK · 11,200 edges
VIRdb · 4,800 genes
HEPNet · live
); } function Hero() { return (
Systems Biology · Data Analytics

Where biology
meets data.

SBDA Research builds open scientific instruments — databases, algorithms, and models — that let researchers see disease, metabolism, and regulation as the connected systems they truly are.

Explore products Join a program
Hero products
Publications
Researchers served
); } function Strip() { const items = ['VIRdb', 'Net2Align', 'PluriMetNet', 'TFIS', 'HEPNet', 'VIRdb 2.0', 'Open Science', 'Reproducible by default']; const doubled = [...items, ...items]; return (
{doubled.map((t, i) => {t})}
); } Object.assign(window, { Nav, Hero, Strip }); // ===== src/products.jsx ===== // Products carousel + modal const { useState: useStateP, useRef: useRefP, useEffect: useEffectP } = React; function ProductCard({ p, onOpen }) { return (
onOpen(p)}>
{p.glyph}
{p.version}
{p.subtitle}

{p.name}

{p.tags.slice(0, 3).map((t) => {t})}

{p.short}

{p.stats[0].v} · {p.stats[0].l}
); } function ProductModal({ p, onClose, onEnquire }) { useBodyLock(!!p); useEffectP(() => { if (!p) return; const onKey = (e) => {if (e.key === 'Escape') onClose();}; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [p, onClose]); if (!p) return null; return (
e.stopPropagation()}>
{p.glyph}
{p.subtitle} · {p.version}

{p.name}

{p.tags.map((t) => {t})}

{p.long}

{p.stats.map((s, i) =>
{s.v}
{s.l}
)}
What's inside
    {p.features.map((f, i) =>
  • {f}
  • )}
e.preventDefault()}> Launch tool
); } function Products({ onOpen }) { const [tab, setTab] = useStateP('all'); const trackRef = useRefP(null); const filtered = tab === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category.toLowerCase() === tab); const tabs = [ ['all', 'All'], ['database', 'Databases'], ['algorithm', 'Algorithms'], ['tool', 'Tools'], ['model', 'Models']]; const scroll = (dir) => { const el = trackRef.current; if (!el) return; const card = el.querySelector('.product-card'); const step = card ? card.offsetWidth + 24 : 360; const half = el.scrollWidth / 2; // If going backwards from the start, jump silently to the end of the first set if (dir < 0 && el.scrollLeft <= 1) { el.scrollTo({ left: half, behavior: 'auto' }); // next frame, smooth-step back requestAnimationFrame(() => el.scrollBy({ left: -step, behavior: 'smooth' })); return; } el.scrollBy({ left: dir * step, behavior: 'smooth' }); }; // Continuous infinite scroll — duplicate set + RAF translates the track // smoothly left→right. When scrollLeft crosses the duplicate boundary we // silently rewind, so the loop is seamless. const [paused, setPaused] = React.useState(false); React.useEffect(() => { const el = trackRef.current; if (!el) return; let raf = 0; let last = performance.now(); const SPEED = 70; // pixels per second const tick = (now) => { const dt = (now - last) / 1000; last = now; if (!paused) { el.scrollLeft += SPEED * dt; const half = el.scrollWidth / 2; if (half && el.scrollLeft >= half) el.scrollLeft -= half; } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [paused, tab]); return (
Hero products

Six instruments
for systems-level inquiry.

{tabs.map(([id, label]) => )}
setPaused(true)} onMouseLeave={() => setPaused(false)}>
{filtered.map((p) => )} {filtered.map((p) => )}
); } Object.assign(window, { Products, ProductModal }); // ===== src/about-results.jsx ===== // About + Results (stats) function About() { const pillars = [ { ico: 'spark', color: 'var(--c-teal)', title: 'Curate', desc: 'We hand-curate research-grade datasets and knowledge bases for vitiligo, metabolism, and regulation.' }, { ico: 'grid', color: 'var(--c-yellow)', title: 'Compute', desc: 'We design algorithms — alignment, network analysis, dynamic models — that move the field forward.' }, { ico: 'book', color: 'var(--c-pink)', title: 'Publish', desc: 'Open access, reproducible methods, and shared code. Science worth standing on.' }, { ico: 'user', color: 'var(--c-blue)', title: 'Mentor', desc: 'Interns and fellows ship real research. We optimise for graduates with research compass.' }]; return (
About the lab

Our commitment lies in advancing the languages biology speaks.

SBDA — Systems Biology and Data Analytics — was built on the conviction that biology is best understood as a connected system. We make the instruments researchers need to ask better questions, faster.

From curating disease-specific knowledge bases to algorithmic breakthroughs in network alignment, our work threads through publications, products, and the next generation of computational biologists trained here.

Work with us Read the journal
{pillars.map((p) =>

{p.title}

{p.desc}

)}
); } function Results() { const palette = ['var(--accent)', 'var(--c-teal)', 'var(--c-pink)', 'var(--c-yellow)']; return (
Results · impact

Numbers that earn the work.

A snapshot of what eighteen years of disciplined inquiry, mentorship, and open tooling adds up to.

{STATS.map((s, i) =>
{s.l}
)}
); } Object.assign(window, { About, Results }); // ===== src/programs-faculty.jsx ===== // Programs + Faculty function Programs({ onEnquire, user, onOpenDashboard }) { const dotColor = { Internship: 'var(--c-teal)', Workshop: 'var(--c-yellow)', Course: 'var(--c-blue)', Fellowship: 'var(--c-pink)' }; const [grantedIds, setGrantedIds] = React.useState(new Set()); const [moduleCounts, setModuleCounts] = React.useState({}); React.useEffect(() => { const client = getSupa && getSupa(); if (!client || !user) { setGrantedIds(new Set()); setModuleCounts({}); return; } (async () => { const { data: gs } = await client.from('access_grants') .select('item_id, item_kind').eq('status', 'active').eq('item_kind', 'course'); const ids = new Set((gs || []).map((g) => g.item_id)); setGrantedIds(ids); if (ids.size === 0) return; const { data: rows } = await client.from('course_content') .select('item_id').eq('item_kind', 'course').in('item_id', Array.from(ids)); const counts = {}; (rows || []).forEach((r) => { counts[r.item_id] = (counts[r.item_id] || 0) + 1; }); setModuleCounts(counts); })(); }, [user]); return (
Programs · for researchers & interns

Learn the craft of
computational biology.

View full catalogue
{PROGRAMS.map((p) => { const granted = grantedIds.has(p.id); const count = moduleCounts[p.id] || 0; return (
{p.cat} {p.duration}

{p.title}

{p.desc}

{granted && ( {count > 0 ? `${count} module${count === 1 ? '' : 's'} available` : 'Enrolled · content coming'} )}
{p.price}· {p.seats} {granted ? ( ) : ( )}
); })}
); } function Faculty() { const tagColors = ['var(--c-teal)', 'var(--c-pink)', 'var(--c-yellow)', 'var(--c-blue)', 'var(--c-lime)', 'var(--c-violet)']; return (
Faculty · mentors

The people behind the instruments.

A small team. Published, opinionated, generous with time. Each card opens a research profile.

{FACULTY.map((f, i) =>
{f.glyph}

{f.name}

{f.role}

{f.tags.map((t, j) => {t} )}
)}
); } Object.assign(window, { Programs, Faculty }); // ===== src/testimonials-blog.jsx ===== // Testimonials + Blog function Testimonials() { return (
Testimonials

Words from the people who used the instruments.

{TESTIMONIALS.map((t, i) =>
{t.quote}
{t.av}
{t.name}
{t.role}
)}
); } function Blog() { return (
Journal · resources

From the lab notebook.

e.preventDefault()}>All posts
{POSTS.map((p) =>
{p.cat}
{p.cat} {p.date}

{p.title}

{p.excerpt}

)}
); } Object.assign(window, { Testimonials, Blog }); // ===== src/contact-footer.jsx ===== // Contact form (with validation) + Footer // Submits directly to Supabase using the anon key (no Pages Function needed). const { useState: useStateCF, useEffect: useEffectCF } = React; function getSupaAnon() { const cfg = window.__SBDA_CONFIG; if (!cfg || !cfg.supabase_url || !cfg.supabase_anon_key) return null; return { url: cfg.supabase_url, key: cfg.supabase_anon_key }; } const INTEREST_OPTIONS = [ 'Internship', 'Fellowship', 'Workshop / Bootcamp', 'Collaboration', 'Product enquiry — VIRdb', 'Product enquiry — VIRdb 2.0', 'Product enquiry — Net2Align', 'Product enquiry — PluriMetNet', 'Product enquiry — TFIS', 'Product enquiry — HEPNet', 'Systems Biology Internship', 'Bioinformatics Bootcamp', 'Network Biology Masterclass', 'Research Fellowship', 'ML for Life Sciences', 'Scientific Writing Lab', 'Press / Media', 'Other', ]; function Contact({ initialInterest }) { const [form, setForm] = useStateCF({ name: '', email: '', interest: initialInterest || 'Internship', message: '' }); useEffectCF(() => { if (initialInterest) setForm((f) => ({ ...f, interest: initialInterest })); }, [initialInterest]); const [errors, setErrors] = useStateCF({}); const [sent, setSent] = useStateCF(false); const [busy, setBusy] = useStateCF(false); const validate = () => { const e = {}; if (!form.name.trim()) e.name = 'Please enter your name'; if (!form.email.trim()) e.email = 'We need an email to reply'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) e.email = 'Hmm, that email looks off'; if (!form.message.trim() || form.message.trim().length < 12) e.message = 'A sentence or two would help'; return e; }; const onSubmit = async (ev) => { ev.preventDefault(); const e = validate(); setErrors(e); if (Object.keys(e).length > 0) return; setBusy(true); try { const cfg = getSupaAnon(); if (!cfg) throw new Error('Service not ready — please try again in a moment.'); const payload = { name: form.name.trim(), email: form.email.trim().toLowerCase(), interest: form.interest, message: form.message.trim(), source: 'website', }; const res = await fetch(`${cfg.url}/rest/v1/enquiries`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': cfg.key, 'Authorization': `Bearer ${cfg.key}`, 'Prefer': 'return=minimal', }, body: JSON.stringify(payload), }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.message || detail.error || 'Could not save enquiry.'); } setSent(true); setTimeout(() => { setSent(false); setForm({ name: '', email: '', interest: 'Internship', message: '' }); }, 4000); } catch (err) { setErrors({ message: err.message || 'Network error — please try again.' }); } finally { setBusy(false); } }; const update = (k) => (ev) => setForm({ ...form, [k]: ev.target.value }); return (
Contact · enquiry

Reach out.
We reply within 48 hours.

Whether you're a prospective intern, a clinician with a dataset, or a collaborator with a question — we'd be glad to hear what you're working on.

Email
research@sbdaresearch.in
Phone
+91 11 4000 0000
Lab address
Block C, Research Park, New Delhi 110016, India
{sent &&
Thanks — your enquiry is on its way. We'll reply within 48 hours.
}
{errors.name && {errors.name}}
{errors.email && {errors.email}}
{errors.message && {errors.message}}
); } function Footer() { return ( ); } Object.assign(window, { Contact, Footer }); // ===== src/login-dashboard.jsx ===== // Login modal + Dashboard view — wired to Supabase auth const { useState: useStateLD, useEffect: useEffectLD } = React; let _supa = null; function getSupa() { if (_supa) return _supa; const cfg = window.__SBDA_CONFIG; if (!cfg || !cfg.supabase_url || !cfg.supabase_anon_key) return null; _supa = window.supabase.createClient(cfg.supabase_url, cfg.supabase_anon_key, { auth: { persistSession: true, autoRefreshToken: true }, }); return _supa; } function LoginModal({ open, onClose, onAuth }) { const [mode, setMode] = useStateLD('login'); const [form, setForm] = useStateLD({ email: '', password: '', name: '' }); const [error, setError] = useStateLD(''); const [busy, setBusy] = useStateLD(false); useBodyLock(open); if (!open) return null; const submit = async (e) => { e.preventDefault(); setError(''); if (!form.email.includes('@')) { setError('Please enter a valid email'); return; } if (form.password.length < 6) { setError('Password must be at least 6 characters'); return; } if (mode === 'signup' && !form.name.trim()) { setError('Please enter your name'); return; } const client = getSupa(); if (!client) { setError('Auth service is not configured yet.'); return; } setBusy(true); try { if (mode === 'login') { const { data, error: err } = await client.auth.signInWithPassword({ email: form.email, password: form.password }); if (err) throw err; const { data: profile } = await client.from('profiles').select('full_name, role').eq('id', data.user.id).single(); onAuth({ id: data.user.id, name: profile?.full_name || form.email.split('@')[0], email: data.user.email, role: profile?.role || 'researcher' }); } else { const { data, error: err } = await client.auth.signUp({ email: form.email, password: form.password, options: { data: { full_name: form.name.trim() } }, }); if (err) throw err; if (data.session) { onAuth({ id: data.user.id, name: form.name.trim(), email: data.user.email, role: 'researcher' }); } else { setError('Account created — please check your email to confirm, then log in.'); setMode('login'); } } } catch (err) { setError(err.message || 'Something went wrong. Please try again.'); } finally { setBusy(false); } }; return (
e.stopPropagation()}>
S

{mode === 'login' ? 'Welcome back.' : 'Create an account.'}

{mode === 'login' ? 'Access your dashboard, grants, and program enrolments.' : 'Researchers and interns — set up your SBDA profile in under a minute.'}

{mode === 'signup' &&
setForm({ ...form, name: e.target.value })} placeholder="Your name" />
}
setForm({ ...form, email: e.target.value })} placeholder="you@institution.edu" />
setForm({ ...form, password: e.target.value })} placeholder="••••••••" />
{error && {error}}
{mode === 'login' ? <>New to SBDA? { e.preventDefault(); setMode('signup'); setError(''); }} style={{ color: 'var(--accent)' }}>Create an account : <>Already have an account? { e.preventDefault(); setMode('login'); setError(''); }} style={{ color: 'var(--accent)' }}>Log in }
); } const PRODUCT_COLORS_D = { virdb: 'var(--c-teal)', virdb2: 'var(--c-pink)', net2align: 'var(--c-blue)', plurimet: 'var(--c-violet)', tfis: 'var(--c-yellow)', hepnet: 'var(--c-lime)' }; function MaterialItem({ c }) { const kind = c.file_url ? 'file' : c.link_url ? 'link' : 'text'; const icon = kind === 'file' ? : kind === 'link' ? : ; return (
{icon}
{c.title} {kind === 'file' ? 'File' : kind === 'link' ? 'Link' : 'Note'}
{c.body &&

{c.body}

} {c.file_url && ( Download {c.file_name ? `· ${c.file_name}` : 'file'} )} {c.link_url && ( Open link )}
); } function GrantCard({ g, defaultOpen }) { const [open, setOpen] = useStateLD(!!defaultOpen); const [contents, setContents] = useStateLD(null); const [loading, setLoading] = useStateLD(false); const load = async () => { setLoading(true); const client = getSupa(); if (client) { const { data } = await client.from('course_content').select('*') .eq('item_kind', g.item_kind).eq('item_id', g.item_id) .order('sort_order').order('created_at'); setContents(data || []); } setLoading(false); }; useEffectLD(() => { if (open && contents === null) load(); }, [open]); const toggle = () => setOpen((v) => !v); const glyphBg = PRODUCT_COLORS_D[g.item_id] || 'var(--accent)'; return (
{g.item_label[0]}
{g.item_label}
{g.item_kind} Granted {new Date(g.granted_at).toLocaleDateString()}
{contents !== null && ( {contents.length} {contents.length === 1 ? 'item' : 'items'} )}
{open && (
{loading &&

Loading content…

} {!loading && contents && contents.length === 0 && (

No materials published yet — the admin will upload soon.

)} {!loading && contents && contents.map((c) => )}
)}
); } function AccountEditor({ user, onUpdated }) { const [fullName, setFullName] = useStateLD(user?.name || ''); const [newsletter, setNewsletter] = useStateLD(false); const [busy, setBusy] = useStateLD(false); const [savedAt, setSavedAt] = useStateLD(null); const [err, setErr] = useStateLD(''); useEffectLD(() => { const client = getSupa(); if (!client || !user) return; client.from('profiles').select('newsletter_optin').eq('id', user.id).single() .then(({ data }) => { if (data) setNewsletter(!!data.newsletter_optin); }); }, [user]); const save = async (e) => { e.preventDefault(); const client = getSupa(); if (!client || !user) return; setBusy(true); setErr(''); const { error } = await client.from('profiles') .update({ full_name: fullName.trim(), newsletter_optin: newsletter }) .eq('id', user.id); setBusy(false); if (error) { setErr(error.message); return; } setSavedAt(Date.now()); onUpdated && onUpdated({ ...user, name: fullName.trim() }); setTimeout(() => setSavedAt(null), 2500); }; return (
setFullName(e.target.value)} placeholder="Your name" />
{err &&
{err}
}
{savedAt && ✓ Saved}
); } function Dashboard({ user, onBack, onLogout, onUserUpdate }) { const [tab, setTab] = useStateLD('overview'); const [grants, setGrants] = useStateLD([]); const [loadingGrants, setLoadingGrants] = useStateLD(true); const [materialCount, setMaterialCount] = useStateLD(0); const initial = user?.name ? user.name[0].toUpperCase() : 'R'; useEffectLD(() => { const client = getSupa(); if (!client || !user) { setLoadingGrants(false); return; } client.from('access_grants').select('*').eq('status', 'active').order('granted_at', { ascending: false }) .then(({ data }) => { setGrants(data || []); setLoadingGrants(false); }); client.from('course_content').select('id', { count: 'exact', head: true }) .then(({ count }) => { setMaterialCount(count || 0); }); }, [user]); const handleLogout = async () => { const client = getSupa(); if (client) await client.auth.signOut(); onLogout(); }; const productGrants = grants.filter((g) => g.item_kind === 'product'); const courseGrants = grants.filter((g) => g.item_kind === 'course'); const visibleGrants = tab === 'products' ? productGrants : tab === 'courses' ? courseGrants : grants; const lastGranted = grants[0]?.granted_at ? new Date(grants[0].granted_at).toLocaleDateString() : '—'; const grantsBlock = loadingGrants ?

Loading…

: visibleGrants.length === 0 ?

No active grants yet

Reach out via the contact form to request access to a product or enrol in a program.

:
{visibleGrants.map((g, i) => )}
; return (
{initial}
{user?.role || 'researcher'}
Welcome back, {user?.name?.split(' ')[0] || 'Researcher'}.
{user?.email || ''}
{loadingGrants ? '—' : grants.length}
Active grants
{loadingGrants ? '—' : productGrants.length}
Products
{loadingGrants ? '—' : courseGrants.length}
Courses
{lastGranted}
Last granted
{tab}

{tab === 'overview' && 'Your library'} {tab === 'products' && 'Your products'} {tab === 'courses' && 'Your courses'} {tab === 'account' && 'Account settings'}

{tab === 'overview' && 'All your granted products and courses. Expand any card to read notes, download files, or open external links uploaded by the admin.'} {tab === 'products' && 'Tools and databases you have access to.'} {tab === 'courses' && 'Programs and courses you are enrolled in.'} {tab === 'account' && 'Update your name and email preferences.'}

{(tab === 'overview' || tab === 'products' || tab === 'courses') && grantsBlock} {tab === 'account' && }
); } Object.assign(window, { LoginModal, Dashboard }); // ===== src/app.jsx ===== // Root App — composes everything, manages global state, wires Tweaks const { useState: useStateApp, useEffect: useEffectApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "dark", "accent": "#ff6b35", "showStrip": true, "density": "comfortable" } /*EDITMODE-END*/; function App() { const [view, setView] = useStateApp('home'); // 'home' | 'dashboard' const [user, setUser] = useStateApp(null); const [loginOpen, setLoginOpen] = useStateApp(false); const [modalProduct, setModalProduct] = useStateApp(null); const [enquiryInterest, setEnquiryInterest] = useStateApp(null); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const handleEnquire = (interest) => { setEnquiryInterest(interest); setView('home'); setTimeout(() => { const el = document.getElementById('contact'); if (el) el.scrollIntoView({ behavior: 'smooth' }); }, 50); }; // Restore Supabase session on mount (after config loads) useEffectApp(() => { const restore = async () => { const client = getSupa(); if (!client) return; const { data } = await client.auth.getSession(); if (data?.session?.user) { const u = data.session.user; const { data: profile } = await client.from('profiles').select('full_name, role').eq('id', u.id).single(); setUser({ id: u.id, name: profile?.full_name || u.email.split('@')[0], email: u.email, role: profile?.role || 'researcher' }); } }; if (window.__SBDA_CONFIG) { restore(); return; } const fn = () => restore(); window.addEventListener('sbda-config', fn); return () => window.removeEventListener('sbda-config', fn); }, []); // Apply theme + accent useEffectApp(() => { applyTheme(t.theme); document.documentElement.style.setProperty('--accent', t.accent); document.documentElement.style.setProperty('--accent-2', shade(t.accent, 12)); }, [t.theme, t.accent]); // Density tweak — adjusts section padding useEffectApp(() => { document.documentElement.style.setProperty('--section-pad', t.density === 'compact' ? '80px' : '120px'); }, [t.density]); const handleAuth = (u) => { setUser(u); setLoginOpen(false); setView('dashboard'); }; const handleLogout = () => { setUser(null); setView('home'); }; return ( <>