This commit is contained in:
152
frontend/src/components/dashboard/WorkOrderImageCarousel.jsx
Normal file
152
frontend/src/components/dashboard/WorkOrderImageCarousel.jsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { animated, useTransition } from '@react-spring/web';
|
||||
|
||||
function getBrowserApiBase() {
|
||||
const apiUrl = import.meta.env.PUBLIC_API_URL || '';
|
||||
if (typeof window === 'undefined') {
|
||||
return apiUrl || 'http://localhost:8001/api/';
|
||||
}
|
||||
|
||||
try {
|
||||
if (apiUrl) {
|
||||
const parsed = new URL(apiUrl);
|
||||
const internalHosts = new Set(['backend', 'localhost', '127.0.0.1']);
|
||||
const isInternalDockerHost = parsed.hostname === 'backend';
|
||||
if (!isInternalDockerHost) {
|
||||
return parsed.toString();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fallback is handled below
|
||||
}
|
||||
|
||||
return `${window.location.protocol}//${window.location.hostname}:8001/api/`;
|
||||
}
|
||||
|
||||
export function resolveMediaUrl(pathOrUrl) {
|
||||
if (!pathOrUrl || typeof window === 'undefined') return '';
|
||||
|
||||
let finalSrc = String(pathOrUrl);
|
||||
const apiUrl = getBrowserApiBase();
|
||||
const baseUrl = apiUrl.replace(/api\/?$/, '');
|
||||
|
||||
if (/^https?:\/\//i.test(finalSrc)) {
|
||||
if (finalSrc.includes('backend:8000')) {
|
||||
return finalSrc.replace('http://backend:8000/', baseUrl);
|
||||
}
|
||||
return finalSrc;
|
||||
}
|
||||
|
||||
if (finalSrc.startsWith('/')) {
|
||||
return `${baseUrl.replace(/\/$/, '')}${finalSrc}`;
|
||||
}
|
||||
|
||||
return new URL(finalSrc, apiUrl).toString();
|
||||
}
|
||||
|
||||
export default function WorkOrderImageCarousel({ images = [], editMode = false, emptyLabel = 'Nema priložene tehničke dokumentacije' }) {
|
||||
const normalizedImages = useMemo(() => (
|
||||
Array.isArray(images)
|
||||
? images.filter(Boolean)
|
||||
: (typeof images === 'string' && images ? [images] : [])
|
||||
), [images]);
|
||||
const [currentImgIdx, setCurrentImgIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentImgIdx(0);
|
||||
}, [normalizedImages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode || normalizedImages.length <= 1) return undefined;
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
setCurrentImgIdx((prev) => (prev + 1) % normalizedImages.length);
|
||||
}, 3000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [normalizedImages.length, editMode]);
|
||||
|
||||
if (normalizedImages.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center border-b border-border-hairline bg-canvas-deep font-mono text-[11px] uppercase tracking-wider text-text-muted/40">
|
||||
<svg className="mb-2 h-8 w-8 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
{emptyLabel}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const prevSlide = (event) => {
|
||||
event.preventDefault();
|
||||
setCurrentImgIdx((prev) => (prev - 1 + normalizedImages.length) % normalizedImages.length);
|
||||
};
|
||||
|
||||
const nextSlide = (event) => {
|
||||
event.preventDefault();
|
||||
setCurrentImgIdx((prev) => (prev + 1) % normalizedImages.length);
|
||||
};
|
||||
|
||||
const finalSrc = resolveMediaUrl(normalizedImages[currentImgIdx] || '');
|
||||
const imageTransitions = useTransition(currentImgIdx, {
|
||||
from: { opacity: 0, transform: 'translate3d(8%,0,0) scale(1.01)' },
|
||||
enter: { opacity: 1, transform: 'translate3d(0%,0,0) scale(1)' },
|
||||
leave: { opacity: 0, transform: 'translate3d(-8%,0,0) scale(1.01)' },
|
||||
config: { tension: 220, friction: 26 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="group relative h-72 w-full overflow-hidden border-b border-border-hairline bg-canvas-base sm:h-80">
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{imageTransitions((style, idx) => (
|
||||
<animated.img
|
||||
key={idx}
|
||||
src={resolveMediaUrl(normalizedImages[idx] || finalSrc)}
|
||||
alt={`Dokumentacija s terena ${idx + 1}`}
|
||||
style={style}
|
||||
className="absolute inset-0 h-full w-full object-cover will-change-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-black/40 to-transparent" />
|
||||
|
||||
{normalizedImages.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevSlide}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 rounded-full border border-border-hairline bg-canvas-elevated/80 p-2 text-text-main opacity-0 shadow-lg transition-opacity duration-200 hover:bg-canvas-base group-hover:opacity-100"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextSlide}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 rounded-full border border-border-hairline bg-canvas-elevated/80 p-2 text-text-main opacity-0 shadow-lg transition-opacity duration-200 hover:bg-canvas-base group-hover:opacity-100"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-3 z-10 flex justify-center gap-1.5">
|
||||
{normalizedImages.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setCurrentImgIdx(idx);
|
||||
}}
|
||||
className={`h-1.5 rounded-full transition-all duration-300 ${idx === currentImgIdx ? 'w-4 bg-brand-accent' : 'w-1.5 bg-white/40 hover:bg-white/70'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user