respira/src/components/FileUpload.tsx
Jan-Henrik Bruhn 762cb4bc0e Remove duplicate upload success message in FileUpload component
Remove the redundant "Upload Complete!" green banner that appeared at
the bottom of the Pattern File section. The top blue banner "Pattern
uploaded successfully!" already provides this information along with
additional context about the locked state.

This eliminates visual clutter and duplicate messaging while keeping
the more informative message that explains the pattern is locked.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-06 23:26:02 +01:00

240 lines
12 KiB
TypeScript

import { useState, useCallback } from 'react';
import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter';
import { MachineStatus } from '../types/machine';
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
import { PatternInfoSkeleton } from './SkeletonLoader';
import { ArrowUpTrayIcon, CheckCircleIcon } from '@heroicons/react/24/solid';
interface FileUploadProps {
isConnected: boolean;
machineStatus: MachineStatus;
uploadProgress: number;
onPatternLoaded: (pesData: PesPatternData) => void;
onUpload: (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => void;
pyodideReady: boolean;
patternOffset: { x: number; y: number };
patternUploaded: boolean;
resumeAvailable: boolean;
resumeFileName: string | null;
pesData: PesPatternData | null;
isUploading?: boolean;
}
export function FileUpload({
isConnected,
machineStatus,
uploadProgress,
onPatternLoaded,
onUpload,
pyodideReady,
patternOffset,
patternUploaded,
resumeAvailable,
resumeFileName,
pesData: pesDataProp,
isUploading = false,
}: FileUploadProps) {
const [localPesData, setLocalPesData] = useState<PesPatternData | null>(null);
const [fileName, setFileName] = useState<string>('');
// Use prop pesData if available (from cached pattern), otherwise use local state
const pesData = pesDataProp || localPesData;
const displayFileName = resumeFileName || fileName;
const [isLoading, setIsLoading] = useState(false);
const handleFileChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!pyodideReady) {
alert('Python environment is still loading. Please wait...');
return;
}
setIsLoading(true);
try {
const data = await convertPesToPen(file);
setLocalPesData(data);
setFileName(file.name);
onPatternLoaded(data);
} catch (err) {
alert(
`Failed to load PES file: ${
err instanceof Error ? err.message : 'Unknown error'
}`
);
} finally {
setIsLoading(false);
}
},
[onPatternLoaded, pyodideReady]
);
const handleUpload = useCallback(() => {
if (pesData && displayFileName) {
onUpload(pesData.penData, pesData, displayFileName, patternOffset);
}
}, [pesData, displayFileName, onUpload, patternOffset]);
return (
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600 dark:text-white">Pattern File</h2>
<div>
{resumeAvailable && resumeFileName && (
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 rounded mb-4">
<p className="text-sm text-green-800 dark:text-green-200">
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
</p>
</div>
)}
{patternUploaded && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 px-4 py-3 rounded mb-4">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Pattern uploaded successfully!</strong> The pattern is now locked and cannot be changed.
To upload a different pattern, you must first complete or delete the current one.
</p>
</div>
)}
<input
type="file"
accept=".pes"
onChange={handleFileChange}
id="file-input"
className="hidden"
disabled={!pyodideReady || isLoading || patternUploaded}
/>
<label
htmlFor="file-input"
className={`inline-flex items-center gap-2 px-6 py-3 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-sm transition-all ${
!pyodideReady || isLoading || patternUploaded
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
: 'cursor-pointer hover:bg-gray-700 dark:hover:bg-gray-600 hover:shadow-lg active:scale-[0.98]'
}`}
>
{isLoading ? (
<>
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading Pattern...</span>
</>
) : !pyodideReady ? (
<>
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Initializing...</span>
</>
) : patternUploaded ? (
<>
<CheckCircleIcon className="w-5 h-5" />
<span>Pattern Locked</span>
</>
) : (
<span>Choose PES File</span>
)}
</label>
{isLoading && <PatternInfoSkeleton />}
{!isLoading && pesData && (
<div className="mt-4 animate-fadeIn">
<h3 className="text-base font-semibold my-4 dark:text-white">Pattern Information</h3>
<div className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700/50 dark:to-gray-800/50 p-4 rounded-lg space-y-3 border border-gray-200 dark:border-gray-600 shadow-sm">
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700 dark:text-gray-300">File Name:</span>
<span className="font-semibold text-gray-900 dark:text-gray-100 text-right max-w-[200px] truncate" title={displayFileName}>
{displayFileName}
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700 dark:text-gray-300">Pattern Size:</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700 dark:text-gray-300">Thread Colors:</span>
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-900 dark:text-gray-100">{pesData.colorCount}</span>
<div className="flex gap-1">
{pesData.threads.slice(0, 5).map((thread, idx) => (
<div
key={idx}
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600 shadow-sm"
style={{ backgroundColor: thread.hex }}
title={`Thread ${idx + 1}: ${thread.hex}`}
/>
))}
{pesData.colorCount > 5 && (
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-[8px] font-bold text-gray-600 dark:text-gray-300">
+{pesData.colorCount - 5}
</div>
)}
</div>
</div>
</div>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700 dark:text-gray-300">Total Stitches:</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{pesData.stitchCount.toLocaleString()}</span>
</div>
</div>
</div>
)}
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
<button
onClick={handleUpload}
disabled={!isConnected || isUploading}
className="mt-4 inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:shadow-none disabled:active:scale-100"
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
>
{isUploading ? (
<>
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Uploading... {uploadProgress > 0 ? uploadProgress.toFixed(0) + '%' : ''}</span>
</>
) : (
<>
<ArrowUpTrayIcon className="w-5 h-5" />
<span>Upload to Machine</span>
</>
)}
</button>
)}
{pesData && !canUploadPattern(machineStatus) && (
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-4 py-3 rounded-lg border border-yellow-200 dark:border-yellow-800 my-4 font-medium animate-fadeIn">
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
</div>
)}
{isUploading && uploadProgress < 100 && (
<div className="mt-4 animate-fadeIn">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Uploading to Machine</span>
<span className="text-sm font-bold text-blue-600 dark:text-blue-400">{uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'}</span>
</div>
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
<div
className="h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:from-blue-600 dark:via-blue-700 dark:to-blue-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite] rounded-full"
style={{ width: `${uploadProgress}%` }}
/>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-2 text-center">Please wait while your pattern is being transferred...</p>
</div>
)}
</div>
</div>
);
}