Files
erp-nutzfahrzeuge/frontend/components/retouch/BeforeAfterSlider.tsx
T
Leopoldadmin 341d0c6f38 fix: resolve all ruff and ESLint lint errors
Backend (ruff):
- F821: Add TYPE_CHECKING imports for Vehicle, Contact, File in models
  (file.py, retouch.py, sale.py, vehicle.py)
- E741: Rename ambiguous variable  to /
  (copilot_service.py, price_compare_service.py, openrouter.py)
- F841: Remove unused variable  in sale_service.py

Frontend (ESLint):
- react/no-unescaped-entities: Escape quotes in ContractPreview.tsx
- @next/next/no-img-element: Replace <img> with <Image> from next/image
  (FileGallery.tsx, FileList.tsx, FilePreview.tsx, BeforeAfterSlider.tsx)

Tests: 392 backend passed, 112 frontend passed, next build successful
2026-07-17 21:16:38 +02:00

143 lines
4.2 KiB
TypeScript

'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
import Image from 'next/image';
interface BeforeAfterSliderProps {
beforeSrc: string;
afterSrc: string;
beforeLabel?: string;
afterLabel?: string;
}
export function BeforeAfterSlider({
beforeSrc,
afterSrc,
beforeLabel = 'Original',
afterLabel = 'Retuschiert',
}: BeforeAfterSliderProps) {
const [sliderPos, setSliderPos] = useState(50);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const percent = Math.max(0, Math.min(100, (x / rect.width) * 100));
setSliderPos(percent);
},
[isDragging]
);
const handleTouchMove = useCallback(
(e: TouchEvent) => {
if (!isDragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const percent = Math.max(0, Math.min(100, (x / rect.width) * 100));
setSliderPos(percent);
},
[isDragging]
);
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', () => setIsDragging(false));
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', () => setIsDragging(false));
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('touchmove', handleTouchMove);
};
}
}, [isDragging, handleMouseMove, handleTouchMove]);
const handleMouseDown = useCallback(() => {
setIsDragging(true);
}, []);
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const percent = Math.max(0, Math.min(100, (x / rect.width) * 100));
setSliderPos(percent);
},
[]
);
return (
<div
ref={containerRef}
data-testid="before-after-slider"
className="relative w-full overflow-hidden rounded-lg select-none cursor-ew-resize"
style={{ aspectRatio: '4/3' }}
onClick={handleClick}
>
{/* After image (full, background) */}
<Image
src={afterSrc}
alt={afterLabel}
fill
className="object-cover"
data-testid="after-image"
draggable={false}
unoptimized
/>
{/* Before image (clipped to left of slider) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPos}%` }}
>
<Image
src={beforeSrc}
alt={beforeLabel}
fill
className="object-cover"
style={{ width: `${containerRef.current?.clientWidth || 100}%` }}
data-testid="before-image"
draggable={false}
unoptimized
/>
</div>
{/* Labels */}
<div className="absolute top-2 left-2 px-2 py-1 bg-black/60 text-white text-xs rounded">
{beforeLabel}
</div>
<div className="absolute top-2 right-2 px-2 py-1 bg-black/60 text-white text-xs rounded">
{afterLabel}
</div>
{/* Slider handle */}
<div
className="absolute top-0 bottom-0 w-1 bg-white shadow-lg"
style={{ left: `${sliderPos}%`, transform: 'translateX(-50%)' }}
onMouseDown={handleMouseDown}
data-testid="slider-handle"
>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-white rounded-full shadow-lg flex items-center justify-center">
<svg
className="w-4 h-4 text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7l-5 5 5 5M16 7l5 5-5 5"
/>
</svg>
</div>
</div>
</div>
);
}