MuSProt / frontend /src /components /Layout.tsx
WinslowFan's picture
Claude Opus 5.5
Redesign the MuSProt website for the new database release
dd079af
Raw History Blame Contribute Delete
4.73 kB
import { useEffect, useState } from 'react';
import { Link, NavLink, useLocation } from 'react-router-dom';
import { Download, Menu, X } from 'lucide-react';
import { api } from '../lib/api';
import { SearchBox } from './SearchBox';
export function Logo({ size = 28 }: { size?: number }) {
// Three conformational "states" joined by transitions.
return (
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden className="logo-mark">
<rect width="32" height="32" rx="8" className="logo-mark__bg" />
<path d="M9 21.5 L16 10 L23 21.5" className="logo-mark__edge" />
<path d="M9 21.5 H23" className="logo-mark__edge logo-mark__edge--soft" />
<circle cx="16" cy="10" r="3.4" className="logo-mark__n1" />
<circle cx="9" cy="21.5" r="3.4" className="logo-mark__n2" />
<circle cx="23" cy="21.5" r="3.4" className="logo-mark__n3" />
</svg>
);
}
const NAV = [
{ to: '/browse', label: 'Browse' },
{ to: '/statistics', label: 'Statistics' },
{ to: '/docs', label: 'Docs' },
];
export function Header() {
const location = useLocation();
const [open, setOpen] = useState(false);
const isHome = location.pathname === '/';
useEffect(() => setOpen(false), [location.pathname, location.search]);
return (
<header className={`site-header ${isHome ? 'site-header--home' : ''}`}>
<div className="site-header__inner">
<Link to="/" className="brand" aria-label="MuSProt home">
<Logo />
<span className="brand__name">MuSProt</span>
</Link>
{!isHome && (
<div className="site-header__search">
<SearchBox placeholder="Search chain, entry, UniProt, function…" />
</div>
)}
<nav className="site-nav" aria-label="Main">
{NAV.map((n) => (
<NavLink key={n.to} to={n.to} className={({ isActive }) => `site-nav__link ${isActive ? 'is-active' : ''}`}>
{n.label}
</NavLink>
))}
<a href={api.downloadUrl} className="btn btn--primary btn--sm">
<Download aria-hidden /> Download
</a>
</nav>
<button
type="button"
className="menu-toggle"
aria-label="Menu"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
{open ? <X aria-hidden /> : <Menu aria-hidden />}
</button>
</div>
{open && (
<div className="mobile-nav">
{!isHome && <SearchBox placeholder="Search MuSProt…" />}
{NAV.map((n) => (
<NavLink key={n.to} to={n.to} className="mobile-nav__link">
{n.label}
</NavLink>
))}
<a href={api.downloadUrl} className="btn btn--primary">
<Download aria-hidden /> Download MuSProt.db
</a>
</div>
)}
</header>
);
}
export function Footer() {
return (
<footer className="site-footer">
<div className="site-footer__inner">
<div className="site-footer__brand">
<Link to="/" className="brand">
<Logo size={24} />
<span className="brand__name">MuSProt</span>
</Link>
<p>
A million-scale database of experimentally observed conformational states and state transitions of proteins,
built from the Protein Data Bank.
</p>
</div>
<div className="site-footer__cols">
<div>
<h3>Explore</h3>
<Link to="/browse">Browse sequences</Link>
<Link to="/browse?min_states=5">Highly multistate</Link>
<Link to="/chain/3vgk/A">Example chain</Link>
</div>
<div>
<h3>Data</h3>
<Link to="/docs">Documentation</Link>
<a href={api.downloadUrl}>Download SQLite</a>
<a href="/api/docs" target="_blank" rel="noopener noreferrer">
REST API
</a>
</div>
<div>
<h3>Sources</h3>
<a href="https://www.rcsb.org" target="_blank" rel="noopener noreferrer">
RCSB PDB
</a>
<a href="https://www.uniprot.org" target="_blank" rel="noopener noreferrer">
UniProt
</a>
<a href="https://www.cathdb.info" target="_blank" rel="noopener noreferrer">
CATH
</a>
</div>
</div>
</div>
<div className="site-footer__base">
<span>MuSProt · Multistate Protein Database</span>
<a href="https://huggingface.co/spaces/omaib/MuSProt" target="_blank" rel="noopener noreferrer">
Hosted on Hugging Face
</a>
</div>
</footer>
);
}