Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | 2x 41x 10x 8x 7x 14x 2x 2x 36x 36x 30x 30x 19x 30x 36x 41x 10x 8x 7x 14x 2x 36x 36x 36x 41x 41x | // src/features/flyer/ProcessingStatus.tsx
import React, { useState, useEffect } from 'react';
import { LoadingSpinner } from '../../components/LoadingSpinner';
import { CheckCircleIcon } from '../../components/icons/CheckCircleIcon';
import { ExclamationTriangleIcon } from '../../components/icons/ExclamationTriangleIcon';
import { StageStatus, ProcessingStage } from '../../types';
interface ProcessingStatusProps {
stages: ProcessingStage[];
estimatedTime: number;
currentFile?: string | null;
pageProgress?: { current: number; total: number } | null;
bulkProgress?: number;
bulkFileCount?: { current: number; total: number } | null;
}
interface StageIconProps {
status: StageStatus;
isCritical: boolean;
}
const StageIcon: React.FC<StageIconProps> = ({ status, isCritical }) => {
switch (status) {
case 'in-progress':
return (
<div className="w-5 h-5 text-brand-primary">
<LoadingSpinner />
</div>
);
case 'completed':
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
case 'pending':
return (
<div className="w-5 h-5 rounded-full border-2 border-gray-400 dark:border-gray-600"></div>
);
case 'error':
return isCritical ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5 text-red-500"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
) : (
<ExclamationTriangleIcon className="w-5 h-5 text-yellow-500" />
);
default:
return null;
}
};
export const ProcessingStatus: React.FC<ProcessingStatusProps> = ({
stages,
estimatedTime,
currentFile,
pageProgress,
bulkProgress,
bulkFileCount,
}) => {
const [timeRemaining, setTimeRemaining] = useState(estimatedTime);
useEffect(() => {
setTimeRemaining(estimatedTime); // Reset when component gets new props
const timer = setInterval(() => {
setTimeRemaining((prevTime) => (prevTime > 0 ? prevTime - 1 : 0));
}, 1000);
return () => clearInterval(timer);
}, [estimatedTime]);
const getStatusTextColor = (status: StageStatus, isCritical: boolean) => {
switch (status) {
case 'in-progress':
return 'text-brand-primary font-semibold';
case 'completed':
return 'text-gray-700 dark:text-gray-300';
case 'pending':
return 'text-gray-400 dark:text-gray-500';
case 'error':
return isCritical ? 'text-red-500 font-semibold' : 'text-yellow-600 dark:text-yellow-400';
default:
return '';
}
};
const title = currentFile ? `Processing: ${currentFile}` : 'Processing Your Flyer...';
const subTitle = `Estimated time remaining: ${Math.floor(timeRemaining / 60)}m ${timeRemaining % 60}s`;
return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 min-h-[400px] flex flex-col justify-center items-center">
<h2 className="text-xl font-bold mb-2 text-gray-800 dark:text-white">{title}</h2>
<p className="text-gray-500 dark:text-gray-400 mb-6 font-semibold text-brand-primary truncate max-w-full px-4">
{subTitle}
</p>
{pageProgress && pageProgress.total > 1 && (
<div className="w-full max-w-sm mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1 text-left">
Converting PDF: Page {pageProgress.current} of {pageProgress.total}
</p>
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div
className="bg-blue-500 h-2 rounded-full"
style={{
width: `${(pageProgress.current / pageProgress.total) * 100}%`,
transition: 'width 0.2s ease-in-out',
}}
></div>
</div>
</div>
)}
{/* Overall Bulk Progress */}
{bulkFileCount && (
<div className="w-full max-w-sm mb-6">
<p className="text-sm text-center text-gray-500 dark:text-gray-400 mb-1">
Overall Progress: File {bulkFileCount.current} of {bulkFileCount.total}
</p>
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div
className="bg-brand-primary h-2.5 rounded-full"
style={{ width: `${bulkProgress || 0}%`, transition: 'width 0.5s ease-in-out' }}
></div>
</div>
</div>
)}
<div className="w-full max-w-sm text-left">
<ul className="space-y-3">
{stages.map((stage, index) => {
const isCritical = stage.critical ?? true;
return (
<li key={index} data-testid={`stage-item-${index}`}>
<div className="flex items-center space-x-3">
<div className="shrink-0" data-testid={`stage-icon-${index}`}>
<StageIcon status={stage.status} isCritical={isCritical} />
</div>
<span
className={`text-sm ${getStatusTextColor(stage.status, isCritical)}`}
data-testid={`stage-text-${index}`}
>
{stage.name}
{!isCritical && (
<span className="text-gray-400 dark:text-gray-500 text-xs italic">
{' '}
(optional)
</span>
)}
<span className="text-gray-400 dark:text-gray-500 ml-1">{stage.detail}</span>
</span>
</div>
{stage.progress && stage.status === 'in-progress' && stage.progress.total > 1 && (
<div className="w-full mt-2 pl-8">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
Analyzing page {stage.progress.current} of {stage.progress.total}
</p>
<div className="w-full bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
<div
className="bg-purple-500 h-1.5 rounded-full"
style={{
width: `${(stage.progress.current / stage.progress.total) * 100}%`,
transition: 'width 0.5s ease-out',
}}
></div>
</div>
</div>
)}
</li>
);
})}
</ul>
</div>
</div>
);
};
|