| |
| |
| |
|
|
| 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;
|
|
|
|
|
| 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;
|
|
|