MuSProt / frontend /src /components /datasets /protein /ExperimentalConditionsPanel.tsx
WinslowFan's picture
Mobile chain-detail layout: stack in portrait, wrap long content
5ec3025
Raw
History Blame Contribute Delete
5.03 kB
/**
* Experimental conditions panel for a selected chain
* Displays method, resolution, pH, temperature, details, and RCSB link
*/
import React, { useState } from 'react';
import { ChainMetadata } from '../../../types/protein';
import { formatText, formatPH, formatTemperature, truncateText } from '../../../utils/format';
interface ExperimentalConditionsPanelProps {
metadata: ChainMetadata | null;
isLoading?: boolean;
}
const ExperimentalConditionsPanel: React.FC<ExperimentalConditionsPanelProps> = ({ metadata, isLoading }) => {
const [showFullDetails, setShowFullDetails] = useState(false);
if (isLoading) {
return (
<section className="space-y-4">
<h3 className="text-xl font-semibold text-slate-900">Experimental Conditions</h3>
<div className="text-center py-10 text-slate-600">Loading conditions...</div>
</section>
);
}
if (!metadata) {
return (
<section className="space-y-4">
<h3 className="text-xl font-semibold text-slate-900">Experimental Conditions</h3>
<div className="text-center py-10 text-slate-600">No experimental data available</div>
</section>
);
}
// Safe extraction with optional chaining and nullish coalescing
const pdbId = metadata.pdb_id || metadata.entry?.rcsb_id;
const website = pdbId ? `https://www.rcsb.org/structure/${String(pdbId).toUpperCase()}` : null;
const exptlMethod = metadata.entry?.exptl_method;
const pH = metadata.entry?.crystal_grow?.pH;
const temp = metadata.entry?.crystal_grow?.temp;
const details = metadata.entry?.crystal_grow?.pdbx_details;
// Safe formatting
const formattedMethod = formatText(exptlMethod);
const formattedPH = formatPH(pH);
const formattedTemp = formatTemperature(temp);
const formattedDetails = formatText(details, '');
const detailsPreview = formattedDetails ? truncateText(formattedDetails, 120) : '';
const hasLongDetails = formattedDetails && formattedDetails.length > 120;
return (
<section className="space-y-4">
{/* Title + Method Chip */}
<div className="flex items-start justify-between">
<h3 className="text-xl font-semibold text-slate-900">Experimental Conditions</h3>
{formattedMethod !== '—' && (
<span className="px-3 py-1.5 bg-green-50 text-green-700 text-xs font-medium rounded-full border border-green-200">
{formattedMethod}
</span>
)}
</div>
{/* Definition List */}
<dl className="space-y-3 pt-2">
{website && (
<div className="flex justify-between items-center py-2 border-b border-slate-100">
<dt className="text-xs font-medium text-slate-500 uppercase tracking-wider">Website</dt>
<dd>
<a href={website} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 font-medium hover:underline transition-colors">
RCSB PDB <span className="text-xs"></span>
</a>
</dd>
</div>
)}
<div className="flex justify-between items-center py-2 border-b border-slate-100">
<dt className="text-xs font-medium text-slate-500 uppercase tracking-wider">Method</dt>
<dd className="text-sm font-semibold text-slate-900">{formattedMethod}</dd>
</div>
<div className="flex justify-between items-center py-2 border-b border-slate-100">
<dt className="text-xs font-medium text-slate-500 uppercase tracking-wider">pH</dt>
<dd className="text-sm font-semibold text-slate-900">{formattedPH}</dd>
</div>
<div className="flex justify-between items-center py-2 border-b border-slate-100">
<dt className="text-xs font-medium text-slate-500 uppercase tracking-wider">Temperature</dt>
<dd className="text-sm font-semibold text-slate-900">{formattedTemp}</dd>
</div>
<div className="py-2 border-b border-slate-100">
{formattedDetails && (
<div className="experimental-item experimental-details min-w-0">
<dt className="text-xs font-medium text-slate-500 uppercase tracking-wider">Details</dt>
<dd className="text-sm font-semibold text-slate-900">
<div className="details-content wrap-anywhere mt-1">
{showFullDetails ? formattedDetails : detailsPreview}
</div>
{hasLongDetails && (
<button
className="details-toggle"
onClick={() => setShowFullDetails(!showFullDetails)}
>
{showFullDetails ? '▲ Show less' : '▼ Show more'}
</button>
)}
</dd>
</div>
)}
</div>
</dl>
</section>
);
};
export default ExperimentalConditionsPanel;