canina/frontend/application/components/SafeImage.tsx
parsa aghaei ebdc740bc0
All checks were successful
Deploy Canina / deploy (push) Successful in 3m16s
fix(admin-panel): fix misplaced import in UITexts causing render error and update graphify
2026-08-24 13:08:50 +03:30

89 lines
3.0 KiB
TypeScript

"use client";
import React from "react";
import Image from "next/image";
import { Sparkles } from "lucide-react";
import { cn } from "../lib/utils";
interface SafeImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> {
src?: string;
alt?: string;
className?: string;
imgClassName?: string;
fallbackText?: string;
}
export default function SafeImage({
src,
alt = "تصویر مکمل کنینا",
className,
imgClassName,
fallbackText = "در حال بروزرسانی تصویر..."
}: SafeImageProps) {
const [error, setError] = React.useState(false);
const [loading, setLoading] = React.useState(true);
// Check if external domain is in allowed remote patterns or relative path
const isExternal = typeof src === 'string' && (src.startsWith('http://') || src.startsWith('https://'));
const isBlobOrData = typeof src === 'string' && (src.startsWith('blob:') || src.startsWith('data:'));
const isAllowedHost = React.useMemo(() => {
if (!isExternal || !src) return true;
try {
const url = new URL(src);
const allowedHosts = [
'canina.ir', 'api.canina.ir', 'stage.canina.ir', 'stageapi.canina.ir',
'apicanina.parsaaghayi.ir', 'canina.de', 'www.canina.de',
'images.unsplash.com', 'plus.unsplash.com', 'localhost', '127.0.0.1'
];
return allowedHosts.some(h => url.hostname === h || url.hostname.endsWith(`.${h}`));
} catch {
return false;
}
}, [src, isExternal]);
if (!src || error) {
return (
<div className={cn(
"flex flex-col items-center justify-center bg-medical-gray-50/90 border border-medical-gray-100 p-2 text-center w-full h-full rounded-2xl relative overflow-hidden select-none",
className
)}>
<Image
src="/assets/images/canina-product-placeholder.png"
alt={alt}
width={240}
height={240}
className="max-h-[85%] w-auto object-contain opacity-70"
unoptimized
/>
<span className="text-[9px] font-black text-canina-blue/80 font-vazir leading-tight mt-1 line-clamp-1 bg-white/80 px-2 py-0.5 rounded-md shadow-xs">
{fallbackText}
</span>
</div>
);
}
return (
<div className={cn("relative overflow-hidden group flex items-center justify-center", className)}>
{loading && (
<div className="absolute inset-0 bg-medical-gray-50 animate-pulse flex items-center justify-center z-10">
<Sparkles className="w-6 h-6 text-canina-blue/20" />
</div>
)}
<Image
src={src}
alt={alt}
width={500}
height={500}
loading="lazy"
unoptimized={isBlobOrData || !isAllowedHost}
className={cn(
"transition-all duration-700 w-full h-full",
imgClassName,
loading ? "opacity-0 scale-110" : "opacity-100 scale-100"
)}
onLoad={() => setLoading(false)}
onError={() => setError(true)}
/>
</div>
);
}