117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { Button } from './Button';
|
||
import Image from 'next/image';
|
||
import Toast from './Toast';
|
||
|
||
interface ImageZoomModalProps {
|
||
src: string;
|
||
alt?: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export default function ImageZoomModal({ src, alt = 'Bild', onClose }: ImageZoomModalProps) {
|
||
const [zoom, setZoom] = useState(1);
|
||
const [translate, setTranslate] = useState({ x: 0, y: 0 });
|
||
|
||
useEffect(() => {
|
||
const handleKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', handleKey);
|
||
return () => window.removeEventListener('keydown', handleKey);
|
||
}, [onClose]);
|
||
|
||
return (
|
||
<div
|
||
onClick={(e) => {
|
||
if (e.target === e.currentTarget) {
|
||
setZoom(1);
|
||
setTranslate({ x: 0, y: 0 });
|
||
onClose();
|
||
}
|
||
}}
|
||
className="fixed inset-0 z-50 bg-black bg-opacity-80 flex items-center justify-center"
|
||
>
|
||
<div
|
||
onClick={(e) => e.stopPropagation()}
|
||
className="relative w-full h-full flex items-center justify-center overflow-hidden"
|
||
>
|
||
{/* Toast (oben links) – dauerhaft sichtbar */}
|
||
<div className="absolute top-4 left-4 z-50 cursor-default">
|
||
<Toast>
|
||
<div className="grid grid-cols-[1fr_auto] grid-rows-3 gap-x-4 gap-y-2 text-sm text-black dark:text-white">
|
||
<div><kbd className="px-2 py-1 bg-gray-200 text-black rounded">ESC</kbd></div>
|
||
<div>Schließen</div>
|
||
<div><kbd className="px-2 py-1 bg-gray-200 text-black rounded">Mausrad</kbd></div>
|
||
<div>Bild zoomen</div>
|
||
<div><kbd className="px-2 py-1 bg-gray-200 text-black rounded">Linke Maustaste</kbd></div>
|
||
<div>Bild verschieben</div>
|
||
</div>
|
||
</Toast>
|
||
</div>
|
||
|
||
{/* Close Button oben rechts */}
|
||
<div className="absolute top-4 right-4 z-50">
|
||
<Button
|
||
onClick={() => {
|
||
setZoom(1);
|
||
setTranslate({ x: 0, y: 0 });
|
||
onClose();
|
||
}}
|
||
size="default"
|
||
variant="solid"
|
||
color="red"
|
||
>
|
||
✕
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Bild */}
|
||
<div className="relative w-full max-w-4xl aspect-[4/3]">
|
||
<Image
|
||
src={src}
|
||
alt={alt}
|
||
fill
|
||
unoptimized
|
||
className="object-contain select-none cursor-grab active:cursor-grabbing"
|
||
style={{
|
||
transform: `scale(${zoom}) translate(${translate.x}px, ${translate.y}px)`,
|
||
transition: 'transform 0.2s',
|
||
}}
|
||
onWheel={(e) => {
|
||
e.preventDefault();
|
||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
||
setZoom((z) => Math.min(Math.max(z + delta, 1), 5));
|
||
}}
|
||
onMouseDown={(e) => {
|
||
e.preventDefault();
|
||
const startX = e.clientX;
|
||
const startY = e.clientY;
|
||
const startTranslate = { ...translate };
|
||
|
||
const handleMove = (moveEvent: MouseEvent) => {
|
||
const dx = moveEvent.clientX - startX;
|
||
const dy = moveEvent.clientY - startY;
|
||
setTranslate({
|
||
x: startTranslate.x + dx,
|
||
y: startTranslate.y + dy,
|
||
});
|
||
};
|
||
|
||
const handleUp = () => {
|
||
window.removeEventListener('mousemove', handleMove);
|
||
window.removeEventListener('mouseup', handleUp);
|
||
};
|
||
|
||
window.addEventListener('mousemove', handleMove);
|
||
window.addEventListener('mouseup', handleUp);
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|