diff --git a/src/pages/products/product-detail/ProductDetailPage.tsx b/src/pages/products/product-detail/ProductDetailPage.tsx index 38bebd1..928144b 100644 --- a/src/pages/products/product-detail/ProductDetailPage.tsx +++ b/src/pages/products/product-detail/ProductDetailPage.tsx @@ -9,14 +9,21 @@ import { PRODUCT_TYPE_LABELS } from '../core/_models'; import { PageContainer, PageTitle, SectionTitle } from '../../../components/ui/Typography'; import { API_GATE_WAY, API_ROUTES } from '@/constant/routes'; +type NormalizedMedia = { + id: string; + url: string; + alt: string; + order: number; + type: 'image' | 'video'; +}; + const ProductDetailPage = () => { const navigate = useNavigate(); const { id = "" } = useParams(); const { data: product, isLoading, error } = useProduct(id); - // State for image preview – must be defined before any early returns to keep hook order stable - const [previewImage, setPreviewImage] = useState(null); + const [previewMedia, setPreviewMedia] = useState<{ url: string; type: 'image' | 'video' } | null>(null); if (isLoading) return ; if (error) return
خطا در بارگذاری اطلاعات محصول
; @@ -31,7 +38,7 @@ const ProductDetailPage = () => { }; const resolveFileUrl = (file: any) => { - const rawUrl = file?.url || ''; + const rawUrl = file?.url || file?.file_url || file?.fileUrl || ''; const serveKey = file?.serve_key || file?.serveKey; if (serveKey) { return `${API_GATE_WAY}/${API_ROUTES.DOWNLOAD_FILE(serveKey)}`; @@ -43,33 +50,60 @@ const ProductDetailPage = () => { return ''; }; - const normalizeFiles = (items: any[]) => + const detectMediaType = (file: any, url: string): 'image' | 'video' => { + const candidates = [ + file?.file_type, + file?.type, + file?.mime_type, + file?.mimeType, + file?.file?.type, + file?.file?.mime_type, + ] + .filter(Boolean) + .map((value) => value.toString().toLowerCase()); + if (candidates.some((value) => value.includes('video'))) { + return 'video'; + } + const referenceUrl = url || file?.file_url || ''; + if (/\.(mp4|mov|m4v|webm|avi|mkv)(\?|$)/i.test(referenceUrl)) { + return 'video'; + } + return 'image'; + }; + + const normalizeFiles = (items: any[] = []): NormalizedMedia[] => (items || []).map((file, index) => { if (typeof file === 'number' || typeof file === 'string') { - return { id: file.toString(), url: '', alt: '', order: index }; + return { id: file.toString(), url: '', alt: '', order: index, type: 'image' }; } if (file?.file) { const nested = file.file; + const url = resolveFileUrl(nested); return { id: (nested.id ?? nested.FileID ?? index).toString(), - url: resolveFileUrl(nested), + url, alt: nested.original_name || '', order: index, + type: detectMediaType(nested, url), }; } if (file?.FileID) { + const url = resolveFileUrl(file); return { id: file.FileID.toString(), - url: resolveFileUrl(file), + url, alt: file.original_name || '', order: index, + type: detectMediaType(file, url), }; } + const url = resolveFileUrl(file); return { id: (file.id ?? index).toString(), - url: resolveFileUrl(file), + url, alt: file.original_name || '', order: index, + type: detectMediaType(file, url), }; }); @@ -81,7 +115,7 @@ const ProductDetailPage = () => { (product as any).explorer_files && (product as any).explorer_files.length > 0 ? (product as any).explorer_files : (product as any).explorer_file_ids || []; - const explorerImages = normalizeFiles(rawExplorerFiles).filter((item) => item.url); + const explorerMedia = normalizeFiles(rawExplorerFiles).filter((item) => item.url); // نسخه‌های محصول const variants = (product.variants && product.variants.length > 0) ? product.variants : ((product as any).product_variants || []); @@ -257,23 +291,39 @@ const ProductDetailPage = () => { تصاویر محصول
- {images.map((image: any, index: number) => ( -
- {image.alt setPreviewImage(image.url)} - /> -
- + {images.map((image, index) => { + const isVideo = image.type === 'video'; + return ( +
setPreviewMedia({ url: image.url, type: isVideo ? 'video' : 'image' })} + > + {isVideo ? ( +
-
- ))} + ); + })}
)} - {explorerImages.length > 0 && ( + {explorerMedia.length > 0 && (
@@ -283,19 +333,34 @@ const ProductDetailPage = () => { این تصاویر در بخش Explorer نمایش داده می‌شوند.

- {explorerImages.map((image, index) => ( -
- {image.alt setPreviewImage(image.url)} - /> -
- + {explorerMedia.map((item, index) => { + const isVideo = item.type === 'video'; + return ( +
setPreviewMedia({ url: item.url, type: isVideo ? 'video' : 'image' })} + > + {isVideo ? ( +
-
- ))} + ); + })}
)} @@ -425,7 +490,8 @@ const ProductDetailPage = () => { key={image.id || imgIndex} src={image.url} alt={image.alt || `تصویر نسخه ${imgIndex + 1}`} - className="w-full h-16 object-cover rounded border border-gray-200 dark:border-gray-600 cursor-pointer" onClick={() => setPreviewImage(image.url)} + className="w-full h-16 object-cover rounded border border-gray-200 dark:border-gray-600 cursor-pointer" + onClick={() => setPreviewMedia({ url: image.url, type: 'image' })} /> ))}
@@ -598,9 +664,18 @@ const ProductDetailPage = () => { - {previewImage && ( - setPreviewImage(null)} title="پیش‌نمایش تصویر" size="xl"> - تصویر + {previewMedia && ( + setPreviewMedia(null)} + title={previewMedia.type === 'video' ? 'پیش‌نمایش ویدیو' : 'پیش‌نمایش تصویر'} + size="xl" + > + {previewMedia.type === 'video' ? ( + )} diff --git a/src/pages/products/product-form/ProductFormPage.tsx b/src/pages/products/product-form/ProductFormPage.tsx index b57279c..5c44323 100644 --- a/src/pages/products/product-form/ProductFormPage.tsx +++ b/src/pages/products/product-form/ProductFormPage.tsx @@ -40,8 +40,8 @@ const productSchema = yup.object({ }); const toPublicUrl = (img: any): ProductImage => { - const rawUrl: string = img?.url || ''; - const serveKey: string | undefined = (img && img.serve_key) || undefined; + const rawUrl: string = img?.url || img?.file_url || img?.fileUrl || ''; + const serveKey: string | undefined = img?.serve_key || img?.serveKey; const url = serveKey ? `${API_GATE_WAY}/${API_ROUTES.DOWNLOAD_FILE(serveKey)}` : rawUrl?.startsWith('http') @@ -50,11 +50,13 @@ const toPublicUrl = (img: any): ProductImage => { ? `${API_GATE_WAY}${rawUrl.startsWith('/') ? '' : '/'}${rawUrl}` : ''; return { - id: (img?.id ?? img).toString(), + id: (img?.id ?? img?.file_id ?? img?.FileID ?? img).toString(), url, - alt: img?.alt || '', + alt: img?.alt || img?.original_name || '', order: img?.order ?? 0, - type: img?.mime_type || img?.type, + type: img?.mime_type || img?.type || img?.file_type, + mime_type: img?.mime_type || img?.file_type, + size: img?.size, }; }; @@ -110,29 +112,29 @@ const mapExplorerFiles = (entries: any[]): ProductImage[] => { return { ...media, order: index }; } if (entry?.file_id) { - return { - id: entry.file_id.toString(), - url: entry.url || '', - alt: entry.name || '', - order: index, - type: entry.mime_type, - }; + const normalized = toPublicUrl({ + ...entry, + id: entry.file_id, + url: entry.file_url || entry.url, + file_url: entry.file_url || entry.url, + mime_type: entry.mime_type || entry.file_type, + type: entry.file_type || entry.type, + original_name: entry.name || entry.original_name, + }); + return { ...normalized, order: index }; } if (entry?.FileID) { - return { - id: entry.FileID.toString(), - url: entry.url || '', - alt: '', - order: index, - }; + const normalized = toPublicUrl({ + ...entry, + id: entry.FileID, + url: entry.file_url || entry.url, + file_url: entry.file_url || entry.url, + }); + return { ...normalized, order: index }; } if (typeof entry === 'number' || typeof entry === 'string') { - return { - id: entry.toString(), - url: '', - alt: '', - order: index, - }; + const normalized = toPublicUrl(entry); + return { ...normalized, order: index }; } const normalized = toPublicUrl(entry); return { ...normalized, order: index }; diff --git a/src/pages/tickets/core/_hooks.ts b/src/pages/tickets/core/_hooks.ts index c8ad2db..6e4c5f8 100644 --- a/src/pages/tickets/core/_hooks.ts +++ b/src/pages/tickets/core/_hooks.ts @@ -314,3 +314,4 @@ export const useDeleteTicketSubjectMutation = () => { }); }; + diff --git a/src/pages/tickets/core/_requests.ts b/src/pages/tickets/core/_requests.ts index 1be5f7c..16b5ced 100644 --- a/src/pages/tickets/core/_requests.ts +++ b/src/pages/tickets/core/_requests.ts @@ -206,3 +206,4 @@ export const deleteTicketSubject = async (id: string | number) => { return response.data; }; + diff --git a/src/pages/tickets/tickets-list/TicketsListPage.tsx b/src/pages/tickets/tickets-list/TicketsListPage.tsx index 12e1d7b..37a36d5 100644 --- a/src/pages/tickets/tickets-list/TicketsListPage.tsx +++ b/src/pages/tickets/tickets-list/TicketsListPage.tsx @@ -265,3 +265,4 @@ const TicketsListPage = () => { export default TicketsListPage; +