prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

View 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>
);
}