gendesign/frontend/src/components/analytics/ObjectPhotoGallery.tsx
2026-04-27 20:26:55 +03:00

246 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import { useObjectPhotos } from "@/lib/analytics-api";
const INITIAL_LIMIT = 24;
export function ObjectPhotoGallery({ objId }: { objId: number | string }) {
const { data, isLoading } = useObjectPhotos(objId, 200);
const [active, setActive] = useState<string | null>(null);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
if (!active) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setActive(null);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [active]);
if (isLoading) {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
gap: 8,
minHeight: 320,
}}
>
{Array.from({ length: 12 }).map((_, i) => (
<div
key={i}
style={{
aspectRatio: "4/3",
background: "#f3f4f6",
borderRadius: 6,
animation: "pulse 1.4s ease-in-out infinite",
}}
/>
))}
<style>{`@keyframes pulse {0%,100%{opacity:1}50%{opacity:.55}}`}</style>
</div>
);
}
const photos = (data ?? []).filter((p) => p.photo_url);
if (photos.length === 0) {
return (
<div style={{ color: "#9ca3af", minHeight: 80 }}>Фото отсутствуют.</div>
);
}
const visible = showAll ? photos : photos.slice(0, INITIAL_LIMIT);
const activeIdx = active
? photos.findIndex((p) => p.obj_file_id === active)
: -1;
const activePhoto = activeIdx >= 0 ? photos[activeIdx] : null;
const move = (delta: number) => {
if (activeIdx < 0) return;
const next = (activeIdx + delta + photos.length) % photos.length;
setActive(photos[next].obj_file_id);
};
return (
<>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
gap: 8,
}}
>
{visible.map((p, i) => (
<button
key={p.obj_file_id}
onClick={() => setActive(p.obj_file_id)}
style={{
padding: 0,
border: "1px solid #e6e8ec",
borderRadius: 6,
overflow: "hidden",
cursor: "zoom-in",
background: "#f3f4f6",
aspectRatio: "4/3",
position: "relative",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={p.thumb_url}
alt={p.photo_name ?? "фото прогресса"}
loading={i < 6 ? "eager" : "lazy"}
decoding="async"
style={{
width: "100%",
height: "100%",
objectFit: "cover",
position: "absolute",
inset: 0,
}}
/>
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
background: "rgba(0,0,0,0.55)",
color: "#fff",
fontSize: 11,
padding: "3px 6px",
pointerEvents: "none",
}}
>
{p.period_dt?.slice(0, 7) ?? "—"}
</div>
</button>
))}
</div>
{photos.length > INITIAL_LIMIT && !showAll ? (
<div style={{ textAlign: "center", marginTop: 12 }}>
<button
onClick={() => setShowAll(true)}
style={{
padding: "6px 14px",
fontSize: 13,
border: "1px solid #d1d5db",
borderRadius: 16,
background: "#fff",
cursor: "pointer",
}}
>
Показать ещё {photos.length - INITIAL_LIMIT}
</button>
</div>
) : null}
{activePhoto ? (
<div
onClick={() => setActive(null)}
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.9)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
cursor: "zoom-out",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={activePhoto.full_url}
alt={activePhoto.photo_name ?? ""}
style={{
maxWidth: "92vw",
maxHeight: "92vh",
objectFit: "contain",
}}
/>
<button
onClick={(e) => {
e.stopPropagation();
move(-1);
}}
aria-label="Предыдущее"
style={navBtnStyle("left")}
>
</button>
<button
onClick={(e) => {
e.stopPropagation();
move(1);
}}
aria-label="Следующее"
style={navBtnStyle("right")}
>
</button>
<button
onClick={(e) => {
e.stopPropagation();
setActive(null);
}}
aria-label="Закрыть"
style={{
position: "absolute",
top: 16,
right: 16,
width: 36,
height: 36,
borderRadius: 18,
border: "none",
background: "rgba(255,255,255,0.15)",
color: "#fff",
fontSize: 20,
cursor: "pointer",
}}
>
×
</button>
<div
style={{
position: "absolute",
bottom: 16,
left: 16,
right: 16,
color: "#fff",
textAlign: "center",
fontSize: 13,
pointerEvents: "none",
}}
>
{activeIdx + 1} / {photos.length} ·{" "}
{activePhoto.period_dt?.slice(0, 10)} · {activePhoto.photo_name}
</div>
</div>
) : null}
</>
);
}
function navBtnStyle(side: "left" | "right"): React.CSSProperties {
return {
position: "absolute",
top: "50%",
[side]: 16,
transform: "translateY(-50%)",
width: 44,
height: 44,
borderRadius: 22,
border: "none",
background: "rgba(255,255,255,0.15)",
color: "#fff",
fontSize: 28,
lineHeight: 1,
cursor: "pointer",
};
}