Web: Artikelbild per Klick gross anzeigen (Lightbox)
Klick aufs Artikelbild oeffnet es bildschirmfuellend; Schliessen per Knopf, Klick auf den Hintergrund oder Escape. Neue Lightbox-Komponente (Portal an <body>), im bestehenden Farbschema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
32
web/src/components/Lightbox.jsx
Normal file
32
web/src/components/Lightbox.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bildschirmfüllende Bildansicht. Schließt per Schließen-Knopf, Klick auf den
|
||||||
|
* Hintergrund oder Escape. Wird per Portal an den <body> gehängt, damit es über
|
||||||
|
* allem liegt und nicht von Containern (overflow) abgeschnitten wird.
|
||||||
|
*/
|
||||||
|
export default function Lightbox({ src, alt, onClose }) {
|
||||||
|
useEffect(() => {
|
||||||
|
function onKey(e) { if (e.key === "Escape") onClose(); }
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
const vorher = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", onKey);
|
||||||
|
document.body.style.overflow = vorher;
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="lightbox" onClick={onClose} role="dialog" aria-modal="true">
|
||||||
|
<button className="lightbox-close" onClick={onClose} title="Schließen" aria-label="Schließen">
|
||||||
|
<Icon name="close" size={20} />
|
||||||
|
</button>
|
||||||
|
{/* Klick aufs Bild soll nicht schließen – nur der Hintergrund. */}
|
||||||
|
<img className="lightbox-img" src={src} alt={alt || ""} onClick={(e) => e.stopPropagation()} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { authorizedObjectUrl } from "../api";
|
import { authorizedObjectUrl } from "../api";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import Lightbox from "./Lightbox";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt das Artikelbild aus der eigenen Datenbank.
|
* Lädt das Artikelbild aus der eigenen Datenbank.
|
||||||
@@ -61,8 +62,15 @@ function useBildUrl(productId, version = 0, onLoaded) {
|
|||||||
*/
|
*/
|
||||||
export default function ProduktBild({ productId, alt, className = "", version = 0, onLoaded }) {
|
export default function ProduktBild({ productId, alt, className = "", version = 0, onLoaded }) {
|
||||||
const url = useBildUrl(productId, version, onLoaded);
|
const url = useBildUrl(productId, version, onLoaded);
|
||||||
|
const [zoom, setZoom] = useState(false);
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
return <img className={`produkt-bild ${className}`.trim()} src={url} alt={alt || ""} />;
|
return (
|
||||||
|
<>
|
||||||
|
<img className={`produkt-bild clickable ${className}`.trim()} src={url} alt={alt || ""}
|
||||||
|
title="Zum Vergrößern klicken" onClick={() => setZoom(true)} />
|
||||||
|
{zoom && <Lightbox src={url} alt={alt} onClose={() => setZoom(false)} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -209,11 +209,35 @@ input::placeholder { color: var(--muted); opacity: 0.7; }
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: var(--sp-2);
|
padding: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
.produkt-bild.clickable { cursor: zoom-in; }
|
||||||
/* Auf schmalen Fenstern haben Bild und Felder nebeneinander keinen Platz mehr. */
|
/* Auf schmalen Fenstern haben Bild und Felder nebeneinander keinen Platz mehr. */
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.produkt-kopf { flex-direction: column-reverse; align-items: center; }
|
.produkt-kopf { flex-direction: column-reverse; align-items: center; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bild-Lightbox: Klick aufs Artikelbild zeigt es gross (Schliessen per Knopf,
|
||||||
|
Klick auf den Hintergrund oder Escape). */
|
||||||
|
.lightbox {
|
||||||
|
position: fixed; inset: 0; z-index: 200;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: var(--sp-5);
|
||||||
|
background: rgba(0, 0, 0, 0.82);
|
||||||
|
}
|
||||||
|
.lightbox-img {
|
||||||
|
max-width: 92vw; max-height: 92vh; object-fit: contain;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 16px 56px rgba(0, 0, 0, 0.5);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.lightbox-close {
|
||||||
|
position: fixed; top: 16px; right: 16px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 40px; height: 40px; border-radius: 50%;
|
||||||
|
background: var(--surface); color: var(--text);
|
||||||
|
border: 1px solid var(--border); cursor: pointer;
|
||||||
|
}
|
||||||
|
.lightbox-close:hover { background: var(--surface-2); }
|
||||||
|
|
||||||
/* Knoepfe neben dem Barcode-Feld: nebeneinander, mit Umbruch auf schmalen
|
/* Knoepfe neben dem Barcode-Feld: nebeneinander, mit Umbruch auf schmalen
|
||||||
Fenstern. Das Eingabefeld daneben fuellt die restliche Breite. */
|
Fenstern. Das Eingabefeld daneben fuellt die restliche Breite. */
|
||||||
.knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 0 auto; }
|
.knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 0 auto; }
|
||||||
|
|||||||
Reference in New Issue
Block a user