nusaibah0110's picture
Update application with new features and components
1c68fe6
import { useState, useRef } from 'react';
import { ArrowLeft, X, ZoomIn, ZoomOut, Download, RotateCcw } from 'lucide-react';
import { sessionStore } from '../store/sessionStore';
type CapturedImage = {
id: string;
src: string;
stepId: string;
type: 'image' | 'video';
};
type CompareImage = {
id: string;
src: string;
stepId: string;
label: string;
};
type Props = {
onBack: () => void;
onNext: () => void;
capturedImages: CapturedImage[];
};
const stepLabels: Record<string, string> = {
native: 'Native',
acetowhite: 'Acetic Acid',
greenFilter: 'Green Filter',
lugol: 'Lugol'
};
const stepColors: Record<string, string> = {
native: 'from-blue-500 to-blue-600',
acetowhite: 'from-purple-500 to-purple-600',
greenFilter: 'from-green-500 to-green-600',
lugol: 'from-amber-500 to-amber-600'
};
export function Compare({ onBack, onNext, capturedImages }: Props) {
const [leftImage, setLeftImage] = useState<CompareImage | null>(null);
const [rightImage, setRightImage] = useState<CompareImage | null>(null);
const [additionalNotes, setAdditionalNotes] = useState('');
const [zoomLevel, setZoomLevel] = useState(1);
const [draggedImageData, setDraggedImageData] = useState<string | null>(null);
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
const [isPanning, setIsPanning] = useState(false);
const [panStart, setPanStart] = useState({ x: 0, y: 0 });
const leftImageRef = useRef<HTMLDivElement>(null);
const rightImageRef = useRef<HTMLDivElement>(null);
const patientId = sessionStore.get().patientInfo?.id;
// Group images by step
const imagesByStep = capturedImages.reduce((acc, img) => {
if (img.type === 'image') {
if (!acc[img.stepId]) acc[img.stepId] = [];
acc[img.stepId].push(img);
}
return acc;
}, {} as Record<string, CapturedImage[]>);
const handleImageDragStart = (e: React.DragEvent, image: CapturedImage, side: 'left' | 'right') => {
const data = JSON.stringify({ ...image, side });
e.dataTransfer.setData('application/compare-image', data);
e.dataTransfer.effectAllowed = 'copy';
setDraggedImageData(data);
};
const handleImageDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
};
const handleImageDrop = (e: React.DragEvent, side: 'left' | 'right') => {
e.preventDefault();
const data = e.dataTransfer.getData('application/compare-image');
if (data) {
try {
const image = JSON.parse(data);
const compareImage: CompareImage = {
id: image.id,
src: image.src,
stepId: image.stepId,
label: stepLabels[image.stepId] || image.stepId
};
if (side === 'left') {
setLeftImage(compareImage);
} else {
setRightImage(compareImage);
}
} catch {
// ignore invalid drag data
}
}
setDraggedImageData(null);
};
const handleDownloadComparison = () => {
if (!leftImage || !rightImage) return;
// Create a canvas to combine both images
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
canvas.width = 1600;
canvas.height = 800;
// Load and draw images
const leftImg = new Image();
const rightImg = new Image();
let imagesLoaded = 0;
const drawComparison = () => {
// Draw left image
ctx.drawImage(leftImg, 0, 0, 800, 800);
// Draw right image
ctx.drawImage(rightImg, 800, 0, 800, 800);
// Add labels
ctx.fillStyle = 'white';
ctx.font = 'bold 28px Arial';
ctx.fillText(leftImage.label, 20, 750);
ctx.fillText(rightImage.label, 820, 750);
// Download
canvas.toBlob(blob => {
if (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `comparison_${Date.now()}.png`;
a.click();
URL.revokeObjectURL(url);
}
});
};
leftImg.onload = () => {
imagesLoaded++;
if (imagesLoaded === 2) drawComparison();
};
rightImg.onload = () => {
imagesLoaded++;
if (imagesLoaded === 2) drawComparison();
};
leftImg.src = leftImage.src;
rightImg.src = rightImage.src;
};
const handleMouseDown = (e: React.MouseEvent) => {
if (zoomLevel > 1) {
setIsPanning(true);
setPanStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y });
}
};
const handleMouseMove = (e: React.MouseEvent) => {
if (isPanning && zoomLevel > 1) {
setPanOffset({
x: e.clientX - panStart.x,
y: e.clientY - panStart.y
});
}
};
const handleMouseUp = () => {
setIsPanning(false);
};
const handleWheel = (e: React.WheelEvent) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
setZoomLevel(prev => Math.max(0.5, Math.min(3, prev + delta)));
};
const handleReset = () => {
setZoomLevel(1);
setPanOffset({ x: 0, y: 0 });
};
return (
<div className="w-full bg-white">
<div className="py-4 md:py-6">
<div className="w-full max-w-7xl mx-auto px-4 md:px-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<button
onClick={onBack}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="w-5 h-5 text-gray-700" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Image Comparison</h1>
{patientId && (
<div className="flex items-center gap-2 px-4 py-1.5 bg-purple-50 border border-purple-200 rounded-full text-sm font-mono font-semibold text-[#0A2540]">
<span>ID:</span>
<span>{patientId}</span>
</div>
)}
</div>
<div className="flex items-center gap-3">
<button
onClick={handleDownloadComparison}
disabled={!leftImage || !rightImage}
className="flex items-center gap-2 px-6 py-3 bg-[#05998c] text-white rounded-lg font-semibold hover:bg-[#047569] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Download className="w-4 h-4" />
Download Comparison
</button>
<button
onClick={onNext}
className="flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg font-semibold hover:bg-slate-700 transition-colors"
>
Next
<ArrowLeft className="w-4 h-4 transform rotate-180" />
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Comparison View */}
<div className="lg:col-span-2">
<div className="grid grid-cols-2 gap-6">
{/* Left Side */}
<div
onDragOver={handleImageDragOver}
onDrop={(e) => handleImageDrop(e, 'left')}
className={`rounded-lg border-4 border-dashed transition-all ${
draggedImageData ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50'
}`}
>
{leftImage ? (
<div
ref={leftImageRef}
className="relative h-full flex items-center justify-center bg-black rounded-lg overflow-hidden"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
style={{ cursor: zoomLevel > 1 ? (isPanning ? 'grabbing' : 'grab') : 'default' }}
>
<img
src={leftImage.src}
alt={leftImage.label}
style={{
transform: `scale(${zoomLevel}) translate(${panOffset.x / zoomLevel}px, ${panOffset.y / zoomLevel}px)`,
transformOrigin: 'center center'
}}
className="w-full h-96 object-contain transition-transform select-none"
draggable={false}
/>
<button
onClick={() => setLeftImage(null)}
className="absolute top-3 right-3 p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors z-10"
>
<X className="w-4 h-4" />
</button>
<div className="absolute bottom-3 left-3 bg-black/70 text-white px-3 py-2 rounded-lg z-10">
<p className="text-sm font-semibold">{leftImage.label}</p>
</div>
</div>
) : (
<div className="h-96 flex flex-col items-center justify-center text-gray-500">
<div className="text-center">
<p className="text-lg font-semibold mb-2">Left Image</p>
<p className="text-sm">Drag and drop an image here</p>
</div>
</div>
)}
</div>
{/* Right Side */}
<div
onDragOver={handleImageDragOver}
onDrop={(e) => handleImageDrop(e, 'right')}
className={`rounded-lg border-4 border-dashed transition-all ${
draggedImageData ? 'border-green-500 bg-green-50' : 'border-gray-300 bg-gray-50'
}`}
>
{rightImage ? (
<div
ref={rightImageRef}
className="relative h-full flex items-center justify-center bg-black rounded-lg overflow-hidden"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
style={{ cursor: zoomLevel > 1 ? (isPanning ? 'grabbing' : 'grab') : 'default' }}
>
<img
src={rightImage.src}
alt={rightImage.label}
style={{
transform: `scale(${zoomLevel}) translate(${panOffset.x / zoomLevel}px, ${panOffset.y / zoomLevel}px)`,
transformOrigin: 'center center'
}}
className="w-full h-96 object-contain transition-transform select-none"
draggable={false}
/>
<button
onClick={() => setRightImage(null)}
className="absolute top-3 right-3 p-2 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors z-10"
>
<X className="w-4 h-4" />
</button>
<div className="absolute bottom-3 left-3 bg-black/70 text-white px-3 py-2 rounded-lg z-10">
<p className="text-sm font-semibold">{rightImage.label}</p>
</div>
</div>
) : (
<div className="h-96 flex flex-col items-center justify-center text-gray-500">
<div className="text-center">
<p className="text-lg font-semibold mb-2">Right Image</p>
<p className="text-sm">Drag and drop an image here</p>
</div>
</div>
)}
</div>
</div>
{/* Zoom Controls */}
{(leftImage || rightImage) && (
<div className="mt-6 flex items-center justify-center gap-4 bg-gray-50 p-4 rounded-lg">
<button
onClick={() => setZoomLevel(prev => Math.max(0.5, prev - 0.1))}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Zoom Out"
>
<ZoomOut className="w-5 h-5 text-gray-700" />
</button>
<span className="text-sm font-semibold text-gray-700 w-16 text-center">
{Math.round(zoomLevel * 100)}%
</span>
<button
onClick={() => setZoomLevel(prev => Math.min(3, prev + 0.1))}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Zoom In"
>
<ZoomIn className="w-5 h-5 text-gray-700" />
</button>
<div className="flex-1 ml-4">
<input
type="range"
min="0.5"
max="3"
step="0.1"
value={zoomLevel}
onChange={(e) => setZoomLevel(parseFloat(e.target.value))}
className="w-full"
/>
</div>
<button
onClick={handleReset}
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
title="Reset Zoom & Pan"
>
<RotateCcw className="w-5 h-5 text-gray-700" />
</button>
</div>
)}
{/* Instructions */}
{(leftImage || rightImage) && zoomLevel > 1 && (
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs text-blue-700">
🖱️ <strong>Click and drag</strong> to pan the image | <strong>Scroll</strong> to zoom | Use controls to fine-tune
</p>
</div>
)}
</div>
{/* Image Library Sidebar */}
<div className="lg:col-span-1">
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-4 sticky top-20">
<h2 className="font-bold text-gray-900 mb-4 pb-2 border-b border-gray-300">
Image Library
</h2>
{Object.entries(imagesByStep).length === 0 ? (
<div className="text-center py-8 text-gray-400">
<p className="text-sm font-medium">No images available</p>
<p className="text-xs mt-1">Capture images to compare</p>
</div>
) : (
<div className="space-y-4 max-h-[600px] overflow-y-auto">
{Object.entries(imagesByStep).map(([stepId, images]) => (
<div key={stepId} className={`bg-gradient-to-br ${stepColors[stepId] || 'from-gray-500 to-gray-600'} rounded-lg overflow-hidden shadow-sm`}>
<div className="bg-black/30 px-3 py-2">
<p className="text-white font-bold text-sm">{stepLabels[stepId] || stepId}</p>
</div>
<div className="p-3 space-y-2 bg-white">
{images.map((image, idx) => (
<div
key={image.id}
draggable
onDragStart={(e) => handleImageDragStart(e, image, 'left')}
className="relative group cursor-grab hover:cursor-grabbing"
>
<div className="bg-gray-100 rounded-lg overflow-hidden border-2 border-gray-200 hover:border-[#05998c] transition-colors">
<img
src={image.src}
alt={`${stepLabels[stepId]} ${idx + 1}`}
className="w-full h-20 object-cover group-hover:opacity-80 transition-opacity"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
<p className="text-white text-xs font-semibold">Drag to compare</p>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
<div className="mt-4 pt-4 border-t border-gray-200">
<label className="block text-sm font-semibold text-gray-700 mb-2">
Additional Notes
</label>
<textarea
value={additionalNotes}
onChange={(e) => setAdditionalNotes(e.target.value)}
rows={4}
placeholder="Add comparison notes..."
className="w-full px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-[#05998c] focus:border-transparent outline-none transition-all resize-none"
/>
</div>
{/* Info Box */}
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs text-blue-700">
💡 Drag images from the library to left or right side to compare them side by side.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}