MuSProt / frontend /src /components /datasets /protein /SequencePanel.tsx
WinslowFan's picture
Mobile chain-detail layout: stack in portrait, wrap long content
5ec3025
Raw
History Blame Contribute Delete
4.99 kB
/**
* Sequence display panel for a selected chain
* Shows sequence length and sequence with copy/download features
*/
import React, { useState } from 'react';
import { ChainMetadata } from '../../../types/protein';
import { formatText, formatNumber } from '../../../utils/format';
interface SequencePanelProps {
metadata: ChainMetadata | null;
isLoading?: boolean;
}
const SequencePanel: React.FC<SequencePanelProps> = ({ metadata, isLoading }) => {
const [copied, setCopied] = useState(false);
const [showAll, setShowAll] = useState(false);
const sequence = metadata?.polymer_entity?.seq_can;
const sequenceLength = metadata?.polymer_entity?.sequence_length;
// Format sequence with line breaks every 60 chars
const formatSequence = (seq: string): string[] => {
if (!seq) return [];
const lines = [];
for (let i = 0; i < seq.length; i += 60) {
lines.push(seq.substring(i, i + 60));
}
return lines;
};
const handleCopy = async () => {
if (sequence) {
try {
await navigator.clipboard.writeText(sequence);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
const handleDownload = () => {
if (!sequence || !metadata) return;
const pdbId = formatText(metadata.pdb_id || metadata.entry?.rcsb_id, 'unknown');
const chainId = formatText(metadata.chain_id || metadata.polymer_entity_instance?.auth_asym_id, 'X');
const title = formatText(metadata.entry?.title, 'Unknown protein');
const fasta = `>${pdbId}:${chainId} ${title}\n${sequence}`;
const blob = new Blob([fasta], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${pdbId}_${chainId}.fasta`;
a.click();
URL.revokeObjectURL(url);
};
if (isLoading) {
return (
<section className="space-y-4">
<h3 className="text-xl font-semibold text-slate-900">Amino Acid Sequence</h3>
<div className="text-center py-10 text-slate-600">Loading sequence...</div>
</section>
);
}
if (!sequence) {
return (
<section className="space-y-4">
<h3 className="text-xl font-semibold text-slate-900">Amino Acid Sequence</h3>
<div className="text-center py-10 text-slate-600">No sequence data available</div>
</section>
);
}
const lines = formatSequence(sequence);
const displayLines = showAll ? lines : lines.slice(0, 10);
const hasMore = lines.length > 10;
const formattedLength = formatNumber(sequenceLength, { decimals: 0 });
return (
<section className="space-y-4">
{/* Header + Actions */}
<div className="flex justify-between items-start">
<div>
<h3 className="text-xl font-semibold text-slate-900 mb-1">Amino Acid Sequence</h3>
{formattedLength !== '—' && (
<div className="text-sm text-slate-600">
<span className="font-medium">{formattedLength}</span> residues
</div>
)}
</div>
<div className="flex gap-2">
<button
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-gray-300 text-slate-900 font-medium rounded-lg hover:bg-slate-50 transition-colors text-xs"
onClick={handleCopy}
title="Copy to clipboard"
>
{copied ? '✓' : '📋'}
</button>
<button
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-900 text-white font-medium rounded-lg hover:bg-slate-800 transition-colors text-xs"
onClick={handleDownload}
title="Download FASTA"
>
⬇ FASTA
</button>
</div>
</div>
{/* Sequence Display */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 font-mono text-xs max-h-80 overflow-y-auto overflow-x-hidden">
{displayLines.map((line, idx) => (
<div key={idx} className="flex mb-1 leading-relaxed">
<span className="text-gray-400 mr-4 min-w-[50px] shrink-0 text-right select-none">{idx * 60 + 1}</span>
<span className="text-gray-800 tracking-wide wrap-anywhere">{line}</span>
</div>
))}
</div>
{hasMore && (
<div className="text-center">
<button
className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-slate-900 font-medium rounded-lg hover:bg-slate-50 transition-colors text-sm"
onClick={() => setShowAll(!showAll)}
>
{showAll ? '▲ Show less' : `▼ Show all (${lines.length - 10} more lines)`}
</button>
</div>
)}
</section>
);
};
export default SequencePanel;