138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||
|
|
|
||
|
|
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) */}
|
||
|
|
<img
|
||
|
|
src={afterSrc}
|
||
|
|
alt={afterLabel}
|
||
|
|
className="absolute inset-0 w-full h-full object-cover"
|
||
|
|
data-testid="after-image"
|
||
|
|
draggable={false}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Before image (clipped to left of slider) */}
|
||
|
|
<div
|
||
|
|
className="absolute inset-0 overflow-hidden"
|
||
|
|
style={{ width: `${sliderPos}%` }}
|
||
|
|
>
|
||
|
|
<img
|
||
|
|
src={beforeSrc}
|
||
|
|
alt={beforeLabel}
|
||
|
|
className="absolute inset-0 h-full object-cover"
|
||
|
|
style={{ width: `${containerRef.current?.clientWidth || 100}%` }}
|
||
|
|
data-testid="before-image"
|
||
|
|
draggable={false}
|
||
|
|
/>
|
||
|
|
</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>
|
||
|
|
);
|
||
|
|
}
|