feat(media): implement omnichannel media gallery with drag-and-drop, video support, and quick access podcast/video hub
Some checks failed
Deploy Canina / deploy (push) Successful in 1m35s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s

This commit is contained in:
parsa aghaei 2026-08-29 11:46:04 +03:30
parent 32e40a820c
commit d9c693c41b
13 changed files with 9449 additions and 8874 deletions

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle, ArrowUpDown, ArrowUp, ArrowDown, GripVertical, Film, Play, ChevronRight, ChevronLeft, Link2 } from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
@ -224,6 +224,85 @@ export default function Products() {
preorderDeposit: '' as number | string
});
const [draggedMediaIdx, setDraggedMediaIdx] = useState<number | null>(null);
const [dragOverMediaIdx, setDragOverMediaIdx] = useState<number | null>(null);
const [customMediaUrl, setCustomMediaUrl] = useState('');
const [showCustomMediaInput, setShowCustomMediaInput] = useState(false);
const isVideoMedia = (url?: string) => {
if (!url) return false;
const clean = url.toLowerCase().trim();
return (
clean.endsWith('.mp4') ||
clean.endsWith('.webm') ||
clean.endsWith('.mov') ||
clean.endsWith('.mkv') ||
clean.endsWith('.m4v') ||
clean.includes('aparat.com') ||
clean.includes('youtube.com') ||
clean.includes('youtu.be') ||
clean.includes('vimeo.com') ||
clean.includes('/video/') ||
clean.startsWith('data:video')
);
};
const handleDragStart = (idx: number) => {
setDraggedMediaIdx(idx);
};
const handleDragOver = (e: React.DragEvent, idx: number) => {
e.preventDefault();
if (dragOverMediaIdx !== idx) {
setDragOverMediaIdx(idx);
}
};
const handleDragLeave = () => {
setDragOverMediaIdx(null);
};
const handleDrop = (e: React.DragEvent, dropIdx: number) => {
e.preventDefault();
if (draggedMediaIdx === null || draggedMediaIdx === dropIdx) {
setDraggedMediaIdx(null);
setDragOverMediaIdx(null);
return;
}
const updated = [...(formData.images || [])];
const [movedItem] = updated.splice(draggedMediaIdx, 1);
updated.splice(dropIdx, 0, movedItem);
setFormData(prev => ({ ...prev, images: updated }));
setDraggedMediaIdx(null);
setDragOverMediaIdx(null);
};
const handleMoveMedia = (idx: number, direction: 'left' | 'right') => {
const images = formData.images || [];
// In RTL layout, 'left' button visually moves item forward (idx + 1), 'right' moves item backward (idx - 1)
const targetIdx = direction === 'left' ? idx + 1 : idx - 1;
if (targetIdx < 0 || targetIdx >= images.length) return;
const updated = [...images];
const temp = updated[idx];
updated[idx] = updated[targetIdx];
updated[targetIdx] = temp;
setFormData(prev => ({ ...prev, images: updated }));
};
const handleAddCustomMedia = () => {
const trimmed = customMediaUrl.trim();
if (!trimmed) return;
const currentImages = formData.images || [];
if (!currentImages.includes(trimmed)) {
setFormData(prev => ({ ...prev, images: [...currentImages, trimmed] }));
toast.success(isVideoMedia(trimmed) ? 'ویدیو به گالری اضافه شد' : 'تصویر به گالری اضافه شد');
} else {
toast.error('این لینک قبلاً در گالری ثبت شده است');
}
setCustomMediaUrl('');
setShowCustomMediaInput(false);
};
const openModal = (product?: Product) => {
setMediaImageError(false);
if (product) {
@ -1655,62 +1734,208 @@ export default function Products() {
</div>
</div>
{/* Product Gallery Images (Multiple Angles & Views) */}
{/* Product Gallery Media (Images & Videos with Drag & Drop Reordering) */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b pb-3">
<div>
<h4 className="font-bold text-gray-800 text-sm flex items-center gap-2">
<ImageIcon className="w-4 h-4 text-blue-600" />
گالری تصاویر جانبی محصول (Gallery Images)
<span>گالری چندرسانهای محصول (تصاویر و ویدیوها)</span>
{formData.images && formData.images.length > 0 && (
<span className="text-[10px] font-bold bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full border border-blue-100 font-mono">
{formData.images.length} آیتم
</span>
)}
</h4>
<p className="text-[11px] text-gray-400 font-medium mt-0.5">تصاویر زوایای مختلف، پشت بستهبندی، جدول ارزش غذایی و مشخصات فنی</p>
<p className="text-[11px] text-gray-400 font-medium mt-0.5">
ترتیب نمایش را با کشیدن و رها کردن (Drag & Drop) یا دکمههای جابهجایی تغییر دهید. پشتیبانی از انواع تصاویر و ویدیوها (MP4 / آپارات / یوتیوب).
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={() => setShowCustomMediaInput(prev => !prev)}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5"
>
<Link2 className="w-3.5 h-3.5" />
لینک ویدیو / تصویر
</button>
<button
type="button"
onClick={() => {
setMediaTargetField('gallery');
setIsMediaSelectorOpen(true);
}}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5"
className="bg-blue-600 hover:bg-blue-700 text-white px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5 shadow-xs"
>
<Plus className="w-3.5 h-3.5" />
افزودن عکس به گالری
افزودن از رسانهها
</button>
</div>
</div>
{formData.images && formData.images.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-3">
{formData.images.map((imgUrl, idx) => (
<div key={idx} className="relative group rounded-xl border border-gray-200 overflow-hidden bg-gray-50 aspect-square flex items-center justify-center p-2 shadow-xs">
<img
src={getProductImageUrl(imgUrl)}
alt={`Gallery ${idx + 1}`}
className="max-h-full max-w-full object-contain"
{/* Custom URL Form */}
{showCustomMediaInput && (
<div className="p-3 bg-blue-50/60 border border-blue-100 rounded-xl space-y-2 animate-in fade-in duration-150">
<label className="text-xs font-bold text-blue-900 block">
افزودن مستقیم آدرس اینترنتی رسانه (عکس یا ویدیو):
</label>
<div className="flex gap-2">
<input
type="text"
value={customMediaUrl}
onChange={(e) => setCustomMediaUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCustomMedia();
}
}}
placeholder="https://... یا /uploads/video.mp4 یا لینک آپارات / یوتیوب"
className="flex-1 px-3 py-2 rounded-xl border border-blue-200 bg-white font-mono text-xs outline-none focus:ring-2 focus:ring-blue-500"
dir="ltr"
/>
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1.5 p-1">
<button
type="button"
onClick={handleAddCustomMedia}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-xs font-bold whitespace-nowrap cursor-pointer"
>
ثبت در گالری
</button>
<button
type="button"
onClick={() => {
setCustomMediaUrl('');
setShowCustomMediaInput(false);
}}
className="px-3 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-xl text-xs font-bold cursor-pointer"
>
انصراف
</button>
</div>
</div>
)}
{/* Gallery Grid with Drag & Drop */}
{formData.images && formData.images.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{formData.images.map((mediaUrl, idx) => {
const isVideo = isVideoMedia(mediaUrl);
const isDragging = draggedMediaIdx === idx;
const isOver = dragOverMediaIdx === idx;
return (
<div
key={idx}
draggable
onDragStart={() => handleDragStart(idx)}
onDragOver={(e) => handleDragOver(e, idx)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, idx)}
onDragEnd={() => {
setDraggedMediaIdx(null);
setDragOverMediaIdx(null);
}}
className={`relative group rounded-2xl border transition-all aspect-square flex flex-col items-center justify-center p-2 select-none cursor-grab active:cursor-grabbing ${
isOver
? 'border-purple-600 bg-purple-50 scale-105 shadow-lg ring-2 ring-purple-400'
: isDragging
? 'opacity-40 border-dashed border-gray-400 bg-gray-100 scale-95'
: 'border-gray-200 bg-gray-50/80 hover:border-purple-300 hover:shadow-md'
}`}
>
{/* Grip Handle Icon */}
<div className="absolute top-1.5 right-1.5 w-6 h-6 rounded-lg bg-black/40 backdrop-blur-xs text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10">
<GripVertical className="w-3.5 h-3.5" />
</div>
{/* Media Preview (Video or Image) */}
{isVideo ? (
<div className="w-full h-full flex flex-col items-center justify-center relative rounded-xl overflow-hidden bg-neutral-900 text-white">
<Film className="w-8 h-8 text-blue-400 opacity-60 mb-1" />
<div className="w-7 h-7 rounded-full bg-blue-600 text-white flex items-center justify-center shadow-md">
<Play className="w-3.5 h-3.5 fill-white ml-0.5" />
</div>
<span className="absolute bottom-1 right-1 bg-blue-600/90 text-white text-[8px] font-bold px-1.5 py-0.5 rounded font-vazir">
ویدیو
</span>
</div>
) : (
<img
src={getProductImageUrl(mediaUrl)}
alt={`Gallery ${idx + 1}`}
className="max-h-full max-w-full object-contain pointer-events-none"
onError={(e) => {
(e.target as HTMLElement).style.display = 'none';
}}
/>
)}
{/* Overlay on hover: Delete & Move Left/Right */}
<div className="absolute inset-0 bg-black/70 backdrop-blur-2xs opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl flex flex-col items-center justify-between p-2 z-20">
<div className="w-full flex items-center justify-between">
<span className="text-[10px] text-white/90 font-mono font-bold">
#{idx + 1}
</span>
<button
type="button"
title="حذف از گالری"
onClick={() => {
const updated = formData.images.filter((_, i) => i !== idx);
setFormData({ ...formData, images: updated });
setFormData(prev => ({ ...prev, images: updated }));
}}
className="w-7 h-7 rounded-lg bg-red-600 hover:bg-red-700 text-white flex items-center justify-center transition-colors"
className="w-6 h-6 rounded-lg bg-red-600 hover:bg-red-700 text-white flex items-center justify-center transition-colors cursor-pointer"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
<span className="absolute bottom-1 right-1 bg-black/60 text-white text-[9px] px-1.5 py-0.5 rounded font-mono">
<div className="text-center">
<span className="text-[9px] text-white/70 block truncate max-w-[100px]" dir="ltr">
{mediaUrl.split('/').pop()?.slice(0, 15) || 'رسانه'}
</span>
</div>
{/* Reorder buttons (left/right for quick shift) */}
<div className="flex items-center gap-1.5">
<button
type="button"
disabled={idx === 0}
title="انتقال به عقب"
onClick={() => handleMoveMedia(idx, 'right')}
className="w-6 h-6 rounded-md bg-white/20 hover:bg-white/40 disabled:opacity-30 disabled:cursor-not-allowed text-white flex items-center justify-center transition-colors cursor-pointer"
>
<ChevronRight className="w-3.5 h-3.5" />
</button>
<button
type="button"
disabled={idx === formData.images.length - 1}
title="انتقال به جلو"
onClick={() => handleMoveMedia(idx, 'left')}
className="w-6 h-6 rounded-md bg-white/20 hover:bg-white/40 disabled:opacity-30 disabled:cursor-not-allowed text-white flex items-center justify-center transition-colors cursor-pointer"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Index badge when not hovered */}
<span className="absolute bottom-1 right-1 bg-black/60 text-white text-[9px] px-1.5 py-0.5 rounded font-mono group-hover:opacity-0 transition-opacity">
#{idx + 1}
</span>
</div>
))}
);
})}
</div>
) : (
<div className="text-center py-6 text-gray-400 bg-gray-50/50 rounded-xl border border-dashed border-gray-200">
<ImageIcon className="w-8 h-8 mx-auto mb-2 opacity-30 text-gray-400" />
<p className="text-xs font-bold text-gray-500">هنوز عکس جانبی به گالری محصول اضافه نشده است.</p>
<p className="text-[10px] text-gray-400 mt-1">با زدن دکمه «افزودن عکس به گالری» میتوانید چند عکس اضافه نمایید.</p>
<div className="text-center py-8 text-gray-400 bg-gray-50/50 rounded-2xl border border-dashed border-gray-200 space-y-2">
<div className="w-12 h-12 rounded-2xl bg-gray-100 flex items-center justify-center mx-auto text-gray-400">
<ImageIcon className="w-6 h-6 opacity-40" />
</div>
<p className="text-xs font-bold text-gray-600">گالری محصول خالی است.</p>
<p className="text-[11px] text-gray-400">
با زدن دکمه «افزودن از رسانهها» یا «لینک ویدیو / تصویر» میتوانید تصاویر جانبی یا ویدیوهای محصول را اضافه کنید.
</p>
</div>
)}
</div>

View File

@ -32,7 +32,10 @@ import {
Share2,
Phone,
Download,
Play
Play,
Film,
Headphones,
Volume2
} from "lucide-react";
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
@ -80,6 +83,46 @@ const ProductImageZoomModal = dynamic(() => import("./ProductImageZoomModal"), {
import { useRouter } from 'next/navigation';
interface GalleryMediaItem {
id: string;
type: 'image' | 'video' | 'podcast';
url: string;
thumbnail: string;
title: string;
badge?: string;
videoData?: {
title?: string;
doctor?: string;
description?: string;
videoUrl?: string;
thumbnail?: string;
};
podcastData?: {
audioUrl?: string;
title?: string;
description?: string;
cover?: string;
};
}
function isVideoUrl(url?: string): boolean {
if (!url) return false;
const clean = url.toLowerCase().trim();
return (
clean.endsWith('.mp4') ||
clean.endsWith('.webm') ||
clean.endsWith('.mov') ||
clean.endsWith('.mkv') ||
clean.endsWith('.m4v') ||
clean.includes('aparat.com') ||
clean.includes('youtube.com') ||
clean.includes('youtu.be') ||
clean.includes('vimeo.com') ||
clean.includes('/video/') ||
clean.startsWith('data:video')
);
}
export default function ProductPage({ productSlug }: { productSlug: string }) {
const router = useRouter();
const [product, setProduct] = useState<Product | null>(null);
@ -91,10 +134,18 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
const [dosageConfig, setDosageConfig] = useState<DosageConfig | null>(null);
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
const [activeMediaIndex, setActiveMediaIndex] = useState(0);
const [activeImage, setActiveImage] = useState<string | null>(null);
const [isImageZoomOpen, setIsImageZoomOpen] = useState(false);
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
const [isFullVideoDescOpen, setIsFullVideoDescOpen] = useState(false);
const [activeMediaForVideoModal, setActiveMediaForVideoModal] = useState<{
title?: string;
doctor?: string;
videoUrl?: string;
thumbnail?: string;
description?: string;
} | null>(null);
useEffect(() => {
Promise.resolve().then(() => setIsMounted(true));
@ -229,6 +280,99 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
Promise.resolve().then(() => setItemQuantity(suggestedQty));
}, [calculation?.duration]);
// Omnichannel Product Media Gallery (Featured Image + Gallery Images/Videos + Dedicated Video + Podcast)
const galleryMedia = useMemo<GalleryMediaItem[]>(() => {
if (!product) return [];
const items: GalleryMediaItem[] = [];
// 1. Featured Image (Always first)
if (product.image) {
items.push({
id: 'featured-image',
type: 'image',
url: product.image,
thumbnail: product.image,
title: product.nameFa || product.name || 'تصویر شاخص محصول',
badge: 'تصویر اصلی'
});
}
// 2. Additional Gallery Media from product.images in custom configured order
if (product.images && Array.isArray(product.images)) {
product.images.forEach((mUrl, idx) => {
if (!mUrl || mUrl === product.image) return;
const isVid = isVideoUrl(mUrl);
if (isVid) {
items.push({
id: `gallery-vid-${idx}`,
type: 'video',
url: mUrl,
thumbnail: product.videoCover || product.image,
title: `ویدیو معرفی ${idx + 1}`,
badge: 'ویدیو',
videoData: {
title: `ویدیو محصول ${product.nameFa || product.name}`,
doctor: 'کادر علمی و تخصصی کنینا',
videoUrl: mUrl,
thumbnail: product.videoCover || product.image,
description: product.videoDescription || product.description,
}
});
} else {
items.push({
id: `gallery-img-${idx}`,
type: 'image',
url: mUrl,
thumbnail: mUrl,
title: `تصویر گالری ${idx + 1}`,
badge: 'تصویر'
});
}
});
}
// 3. Dedicated Video (product.videoUrl) if present and not duplicated
if (product.videoUrl && !items.some(it => it.url === product.videoUrl)) {
items.push({
id: 'dedicated-video',
type: 'video',
url: product.videoUrl,
thumbnail: product.videoCover || product.image,
title: product.videoTitle || 'ویدیو راهنمای مصرف و معرفی',
badge: 'ویدیوکست',
videoData: {
title: product.videoTitle || `ویدیو راهنمای ${product.nameFa || product.name}`,
doctor: 'کادر علمی و تخصصی کنینا',
videoUrl: product.videoUrl,
thumbnail: product.videoCover || product.image,
description: product.videoDescription || product.description,
}
});
}
// 4. Dedicated Podcast (product.podcastUrl) if present and not duplicated
if (product.podcastUrl && !items.some(it => it.url === product.podcastUrl)) {
items.push({
id: 'dedicated-podcast',
type: 'podcast',
url: product.podcastUrl,
thumbnail: product.podcastCover || product.image,
title: product.podcastTitle || 'پادکست و بررسی صوتی بالینی',
badge: 'پادکست',
podcastData: {
audioUrl: product.podcastUrl,
title: product.podcastTitle || 'پادکست و بررسی صوتی بالینی مکمل',
description: product.podcastDescription || product.description,
cover: product.podcastCover || product.image,
}
});
}
return items;
}, [product]);
const activeMedia = galleryMedia[activeMediaIndex] || galleryMedia[0];
if (isLoading) return (
<div className="min-h-screen bg-medical-gray-50 p-6 max-w-7xl mx-auto font-vazir" dir="rtl">
<div className="animate-pulse space-y-6">
@ -292,16 +436,17 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{/* Above the Fold Grid */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 sm:gap-8 md:gap-12 items-center md:items-start text-center md:text-right font-vazir w-full" dir="rtl">
{/* Right Column: Product Image Frame & Gallery (md:col-span-5) */}
{/* Right Column: Omnichannel Product Media Hub & Gallery (md:col-span-5) */}
<div className="md:col-span-5 w-full mx-auto max-w-sm md:max-w-none space-y-3 sm:space-y-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
key={activeMedia?.id || 'media-box'}
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
onClick={() => setIsImageZoomOpen(true)}
className="bg-white rounded-2xl sm:rounded-[2.5rem] border border-medical-gray-200 shadow-xl relative group overflow-hidden h-[260px] sm:h-[380px] flex items-center justify-center p-2 sm:p-4 cursor-zoom-in"
title="کلیک برای مشاهده تصویر بزرگتر"
transition={{ duration: 0.2 }}
className="bg-white rounded-2xl sm:rounded-[2.5rem] border border-medical-gray-200 shadow-xl relative group overflow-hidden h-[280px] sm:h-[390px] flex items-center justify-center p-2 sm:p-4 select-none"
>
<div className="absolute top-4 left-4 sm:top-6 sm:left-6 flex flex-col gap-1.5 sm:gap-2 z-10 items-start">
{/* Top Floating Badges */}
<div className="absolute top-4 left-4 sm:top-6 sm:left-6 flex flex-col gap-1.5 sm:gap-2 z-10 items-start pointer-events-none">
{product.specialBadge && (
<div className="px-2.5 py-1 bg-medical-gray-900 text-white rounded-full text-[9px] sm:text-[10px] font-black uppercase tracking-widest font-vazir whitespace-nowrap shadow-sm">
{product.specialBadge}
@ -323,7 +468,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
{/* Species Suitable Badges (Dog / Cat) */}
<div className="absolute bottom-4 left-4 flex gap-1.5 z-10">
<div className="absolute bottom-4 left-4 flex gap-1.5 z-10 pointer-events-none">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
<div className="w-7 h-7 bg-white/90 backdrop-blur-sm rounded-lg flex items-center justify-center text-medical-gray-600 border border-medical-gray-200 shadow-sm" title="مناسب برای سگ">
<Dog className="w-4 h-4 text-canina-blue" />
@ -335,8 +480,16 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
)}
</div>
{/* 1. Image Viewer */}
{(!activeMedia || activeMedia.type === 'image') && (
<div
onClick={() => setIsImageZoomOpen(true)}
className="w-full h-full flex items-center justify-center cursor-zoom-in relative"
title="کلیک برای مشاهده تصویر بزرگتر"
>
<SafeImage
src={activeImage || product.image}
src={activeMedia?.url || product.image}
alt={product.name}
priority={true}
sizes="(max-width: 768px) 100vw, 400px"
@ -346,44 +499,140 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-3 py-1 rounded-full text-[10px] font-black flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span>بزرگنمایی</span>
</div>
</div>
)}
{/* 2. Video Player Frame */}
{activeMedia?.type === 'video' && (
<div
onClick={() => {
setActiveMediaForVideoModal(activeMedia.videoData || {
title: activeMedia.title || product.name,
videoUrl: activeMedia.url,
thumbnail: activeMedia.thumbnail,
description: product.description,
});
setIsVideoModalOpen(true);
}}
className="w-full h-full relative cursor-pointer overflow-hidden rounded-2xl bg-neutral-950 flex items-center justify-center group/vid"
title="برای پخش ویدیو کلیک کنید"
>
<SafeImage
src={activeMedia.thumbnail || product.image}
alt={activeMedia.title || product.name}
className="w-full h-full opacity-60 group-hover/vid:opacity-40 transition-opacity"
imgClassName="w-full h-full object-cover group-hover/vid:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-black/50 flex flex-col items-center justify-center gap-3 p-4 text-center">
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-canina-blue text-white flex items-center justify-center shadow-2xl shadow-canina-blue/60 group-hover/vid:scale-110 transition-transform">
<Play className="w-8 h-8 sm:w-10 sm:h-10 fill-white ml-1" />
</div>
<div className="space-y-1.5 max-w-xs">
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-canina-blue text-white text-[11px] font-black rounded-full shadow-md font-vazir">
<Film className="w-3.5 h-3.5" />
<span>پخش ویدیو معرفی محصول</span>
</span>
<p className="text-xs font-bold text-white/90 truncate font-vazir">
{activeMedia.title || product.nameFa || product.name}
</p>
</div>
</div>
</div>
)}
{/* 3. Podcast Player Frame */}
{activeMedia?.type === 'podcast' && (
<div
onClick={() => {
const el = document.getElementById('podcast-player-section');
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
}}
className="w-full h-full relative cursor-pointer overflow-hidden rounded-2xl bg-gradient-to-br from-purple-950 via-slate-900 to-indigo-950 flex flex-col items-center justify-center p-6 text-white text-center group/pod"
title="مشاهده و پخش پادکست بالینی"
>
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-purple-600/30 border border-purple-400/30 flex items-center justify-center mb-3 shadow-xl group-hover/pod:scale-105 transition-transform">
<Headphones className="w-8 h-8 sm:w-10 sm:h-10 text-purple-300 animate-pulse" />
</div>
{/* Animated Audio Bars */}
<div className="flex items-center gap-1 mb-3">
<span className="w-1 h-3 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1 h-6 bg-purple-300 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1 h-8 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
<span className="w-1 h-4 bg-purple-300 rounded-full animate-bounce" style={{ animationDelay: '75ms' }} />
<span className="w-1 h-6 bg-purple-400 rounded-full animate-bounce" style={{ animationDelay: '225ms' }} />
</div>
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-600 text-white text-[11px] font-black rounded-full shadow-md font-vazir mb-1">
<Headphones className="w-3.5 h-3.5" />
<span>پادکست و بررسی صوتی بالینی</span>
</span>
<p className="text-xs font-bold text-white/90 line-clamp-2 max-w-xs font-vazir mt-1">
{activeMedia.title || "بررسی تخصصی اثر مکمل توسط تیم علمی"}
</p>
</div>
)}
</motion.div>
{/* Multiple Gallery Thumbnails Strip */}
{(() => {
const allGalleryImages = [
product.image,
...(product.images || []).filter(img => img && img !== product.image)
].filter(Boolean);
if (allGalleryImages.length <= 1) return null;
return (
{/* Multiple Gallery Media Thumbnails Strip */}
{galleryMedia.length > 1 && (
<div className="flex items-center gap-2 overflow-x-auto py-2 px-1 sleek-hscroll">
{allGalleryImages.map((imgUrl, idx) => {
const isSelected = (activeImage || product.image) === imgUrl;
{galleryMedia.map((item, idx) => {
const isSelected = activeMediaIndex === idx;
return (
<button
key={idx}
key={item.id || idx}
type="button"
onClick={() => setActiveImage(imgUrl)}
className={`relative w-16 h-16 rounded-2xl overflow-hidden bg-white border-2 transition-all shrink-0 p-1 cursor-pointer ${
onClick={() => {
setActiveMediaIndex(idx);
if (item.type === 'image') {
setActiveImage(item.url);
}
}}
className={`relative w-16 h-16 sm:w-18 sm:h-18 rounded-2xl overflow-hidden bg-white border-2 transition-all shrink-0 p-1 cursor-pointer flex items-center justify-center ${
isSelected
? 'border-canina-blue shadow-md scale-105 ring-2 ring-canina-blue/20'
: 'border-medical-gray-200 hover:border-canina-blue/50 opacity-70 hover:opacity-100'
? 'border-canina-blue shadow-lg scale-105 ring-2 ring-canina-blue/30'
: 'border-medical-gray-200 hover:border-canina-blue/50 opacity-75 hover:opacity-100'
}`}
>
<SafeImage
src={imgUrl}
src={item.thumbnail || item.url || product.image}
alt={`${product.name} - ${idx + 1}`}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
{/* Video Overlay Badge on thumbnail */}
{item.type === 'video' && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<div className="w-6 h-6 rounded-full bg-canina-blue text-white flex items-center justify-center shadow-md">
<Play className="w-3 h-3 fill-white ml-0.5" />
</div>
<span className="absolute bottom-0.5 right-0.5 bg-blue-600 text-white text-[7px] font-bold px-1 rounded font-vazir">
ویدیو
</span>
</div>
)}
{/* Podcast Overlay Badge on thumbnail */}
{item.type === 'podcast' && (
<div className="absolute inset-0 bg-purple-950/60 flex items-center justify-center">
<div className="w-6 h-6 rounded-full bg-purple-600 text-white flex items-center justify-center shadow-md">
<Headphones className="w-3.5 h-3.5 text-white" />
</div>
<span className="absolute bottom-0.5 right-0.5 bg-purple-600 text-white text-[7px] font-bold px-1 rounded font-vazir">
صوت
</span>
</div>
)}
</button>
);
})}
</div>
);
})()}
)}
</div>
{/* Left Column: Product Title & Description (md:col-span-7) */}
@ -882,6 +1131,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{/* Podcast Player Row (Inline Direct Player with Soundcloud Waveform) */}
{product.podcastUrl && (
<div id="podcast-player-section" className="scroll-mt-24">
<PodcastInlinePlayer
podcast={{
audioUrl: product.podcastUrl,
@ -892,6 +1142,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
fallbackCover: product.image,
}}
/>
</div>
)}
{/* PDF Catalog Download Row */}
@ -1251,26 +1502,29 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
<ProductImageZoomModal
isOpen={isImageZoomOpen}
onClose={() => setIsImageZoomOpen(false)}
images={[
product.image,
...(product.images || []).filter(img => img && img !== product.image)
].filter(Boolean) as string[]}
activeImage={activeImage || product.image || ""}
onSelectImage={(img) => setActiveImage(img)}
images={galleryMedia.filter(m => m.type === 'image').map(m => m.url)}
activeImage={activeMedia?.type === 'image' ? activeMedia.url : (galleryMedia.find(m => m.type === 'image')?.url || product.image || "")}
onSelectImage={(img) => {
const foundIdx = galleryMedia.findIndex(m => m.url === img);
if (foundIdx !== -1) setActiveMediaIndex(foundIdx);
}}
productName={product.nameFa || product.name}
/>
{/* Dedicated Video Modal Player */}
<VideoModalPlayer
isOpen={isVideoModalOpen}
onClose={() => setIsVideoModalOpen(false)}
video={product.videoUrl ? {
onClose={() => {
setIsVideoModalOpen(false);
setActiveMediaForVideoModal(null);
}}
video={activeMediaForVideoModal || (product.videoUrl ? {
title: product.videoTitle || product.name,
doctor: "کادر علمی و تخصصی کنینا",
videoUrl: product.videoUrl,
thumbnail: product.videoCover || product.image,
description: product.videoDescription || product.description,
} : null}
} : null)}
/>
</div>
);

View File

@ -1,15 +1,15 @@
{
"0": "Roles",
"1": "app.module.ts",
"2": "SmsService",
"2": "SettingsController",
"3": "ProductService",
"4": "ProductPage.tsx",
"5": "CmsController",
"6": "tickets.controller.ts",
"7": "Button.tsx",
"8": "ProductsService",
"8": "SettingsService",
"9": "devDependencies",
"10": "ReviewsService",
"10": "CreateReviewDto",
"11": "MediaSelector.tsx",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
@ -18,10 +18,10 @@
"16": "DoctorQueryDto",
"17": "schema.ts",
"18": "JwtAuthGuard",
"19": "ProductsController",
"19": "ProductsService",
"20": "CreateVideoDto",
"21": "admin.module.ts",
"22": "FeaturedProducts.tsx",
"21": "ReportsController",
"22": "components/Skeleton.tsx",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
@ -33,41 +33,41 @@
"31": "DOC-001",
"32": "adminRoutes.tsx",
"33": "WholesaleApplyDto",
"34": "B2BService",
"34": "B2BController",
"35": "AuthController",
"36": "FaqService",
"37": "راهنمای تست سیستم (Software Testing)",
"38": "Button",
"38": "Transactions.tsx",
"39": "CategoriesController",
"40": "MediaController",
"41": "What You Must Do When Invoked",
"42": "SslController",
"43": "BannersService",
"44": "TestimonialsService",
"43": "BannersController",
"44": "TestimonialsController",
"45": "What You Must Do When Invoked",
"46": "20260526145407_init/migration.sql",
"47": "IngredientsService",
"48": "UsersService",
"47": "IngredientsController",
"48": "UsersController",
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsService",
"53": "SmartAdvisorService",
"54": "Reports.tsx",
"54": "Modal.tsx",
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "ContactService",
"58": "ContactController",
"59": "compilerOptions",
"60": "PaymentService",
"61": "AdminTransactionFilterDto",
"62": "RouteErrorBoundary",
"61": "usePetStore",
"62": "SmsService",
"63": "dependencies",
"64": "compilerOptions",
"65": "BlogsService",
"66": "AdminQueryDto",
"66": "ApiOperation",
"67": "PetsController",
"68": "UserDashboard.tsx",
"68": "lib/services/api.ts",
"69": "Required Review Group Closures",
"70": "compilerOptions",
"71": "getPageMetadata",
@ -84,15 +84,15 @@
"82": "scripts",
"83": "dependencies",
"84": "Role & Core Objective",
"85": "useSettingsStore",
"85": "trust-seals/page.tsx",
"86": "zibal.service.ts",
"87": "dependencies",
"88": "CreateEBankCheckoutDto",
"89": "seed-products.ts",
"90": "CreateReviewDto",
"90": "users.service.ts",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "lib/services/api.ts",
"93": "useSettingsStore",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "wiki/[slug]/page.tsx",
"96": "compilerOptions",
@ -105,7 +105,7 @@
"103": "Comprehensive Change Log",
"104": "Coupons.tsx",
"105": "Operational Rules & Boundaries",
"106": "MetricsController",
"106": "auth.service.ts",
"107": "PaginationDto",
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
@ -114,20 +114,20 @@
"112": "Operational Rules & Boundaries",
"113": "RegisterDto",
"114": "AppService",
"115": "VerifyOtpDto",
"115": "Blogs.tsx",
"116": "Vazirmatn Changelog",
"117": "Vazirmatn Font فونت وزیرمتن",
"118": "Operational Rules & Boundaries",
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
"122": "ApiOperation",
"123": "auth.service.ts",
"122": "AdminController",
"123": "AuthService",
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
"128": "AdminController",
"128": "Body",
"129": "auth.controller.ts",
"130": "Sahel-Font",
"131": "Role & Core Objective",
@ -143,11 +143,11 @@
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "reviews.controller.ts",
"145": "admin.service.ts",
"144": "VetGallery.tsx",
"145": "ProductDto",
"146": "System Discovery",
"147": "ArchivePage.tsx",
"148": "CreateUserDto",
"147": "HomeController",
"148": "admin.service.ts",
"149": "SmsSettingsPage.tsx",
"150": "Product Requirement Document (PRD)",
"151": "class-transformer",
@ -176,14 +176,14 @@
"174": "rebuild_honest_ledger.js",
"175": "validate_evidence_grade.js",
"176": "@nestjs/core",
"177": "SendOtpDto",
"177": "auth.module.ts",
"178": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
"179": "@nestjs/jwt",
"180": "API Contract Specification",
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "UpdateReviewDto",
"184": "CheckoutPage.tsx",
"185": "@eslint/eslintrc",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
@ -196,7 +196,7 @@
"194": "Raw Finding Verification & Disposition Report",
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"197": "catalog/page.tsx",
"197": "UsersService",
"198": "@nestjs/throttler",
"199": "passport",
"200": "application/README.md",
@ -230,12 +230,12 @@
"228": "rules/graphify.md",
"229": ".agents/workflows/graphify.md",
"230": "instructions.md",
"231": "RevalidationService",
"231": "WikiController",
"232": "helmet",
"233": "tailwindcss",
"234": "@nestjs/schematics",
"235": "@nestjs/testing",
"236": "@nestjs/swagger",
"236": "BlogsService",
"237": "source-map-support",
"238": "ts-jest",
"239": "ts-loader",
@ -325,5 +325,13 @@
"323": "eslint-config-prettier",
"324": "eslint",
"325": "@types/react-dom",
"327": "eslint-plugin-react-refresh"
"326": "SmsLogQueryDto",
"327": "eslint-plugin-react-refresh",
"328": "WikiService",
"329": "B2BService",
"330": "track/page.tsx",
"331": "PodcastPlayerModal.tsx",
"332": "app.e2e-spec.js",
"333": "app/page.tsx",
"334": "bcrypt"
}

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
"145": "admin.service.ts",
"146": "System Discovery",
"147": "ArchivePage.tsx",
"148": "bcrypt",
"148": "CreateUserDto",
"149": "SmsSettingsPage.tsx",
"150": "Product Requirement Document (PRD)",
"151": "class-transformer",
@ -235,7 +235,7 @@
"233": "tailwindcss",
"234": "@nestjs/schematics",
"235": "@nestjs/testing",
"236": "prisma",
"236": "@nestjs/swagger",
"237": "source-map-support",
"238": "ts-jest",
"239": "ts-loader",
@ -322,7 +322,7 @@
"320": "@testing-library/react",
"321": "orders/page.tsx",
"322": "pets/page.tsx",
"323": ".getDashboardStats",
"323": "eslint-config-prettier",
"324": "eslint",
"325": "@types/react-dom",
"327": "eslint-plugin-react-refresh"

View File

@ -1,16 +1,16 @@
# Graph Report - canina (2026-08-29)
## Corpus Check
- 590 files · ~1,335,009 words
- 590 files · ~1,335,143 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4136 nodes · 7451 edges · 327 communities (209 shown, 118 thin omitted)
- 4136 nodes · 7451 edges · 327 communities (208 shown, 119 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 281 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `1651a593`
- Built from commit: `bfb7f420`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@ -163,7 +163,7 @@
- admin.service.ts
- System Discovery
- ArchivePage.tsx
- bcrypt
- CreateUserDto
- SmsSettingsPage.tsx
- Product Requirement Document (PRD)
- class-transformer
@ -250,7 +250,7 @@
- tailwindcss
- @nestjs/schematics
- @nestjs/testing
- prisma
- @nestjs/swagger
- source-map-support
- ts-jest
- ts-loader
@ -318,6 +318,7 @@
- MaskableField.tsx
- @eslint/js
- @testing-library/react
- eslint-config-prettier
- eslint
- @types/react-dom
- eslint-plugin-react-refresh
@ -351,15 +352,15 @@
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (327 total, 118 thin omitted)
## Communities (327 total, 119 thin omitted)
### Community 0 - "Roles"
Cohesion: 0.24
Nodes (13): Roles(), PaymentController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get (+5 more)
### Community 1 - "app.module.ts"
Cohesion: 0.06
Nodes (39): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+31 more)
Cohesion: 0.05
Nodes (42): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+34 more)
### Community 2 - "SmsService"
Cohesion: 0.05
@ -391,7 +392,7 @@ Nodes (10): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, Q
### Community 9 - "devDependencies"
Cohesion: 0.22
Nodes (9): devDependencies, eslint-config-prettier, @types/bcryptjs, @types/node, typescript, @types/node, typescript, eslint-config-prettier (+1 more)
Nodes (9): devDependencies, prisma, @types/bcryptjs, @types/node, typescript, @types/node, typescript, prisma (+1 more)
### Community 10 - "ReviewsService"
Cohesion: 0.12
@ -438,8 +439,8 @@ Cohesion: 0.09
Nodes (24): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+16 more)
### Community 21 - "admin.module.ts"
Cohesion: 0.06
Nodes (22): AdminModule, Module, MediaService, Injectable, ReportsController, ApiBearerAuth, ApiOperation, ApiQuery (+14 more)
Cohesion: 0.07
Nodes (19): AdminModule, Module, MediaService, Injectable, ReportsController, ApiBearerAuth, ApiOperation, ApiQuery (+11 more)
### Community 22 - "FeaturedProducts.tsx"
Cohesion: 0.14
@ -603,15 +604,15 @@ Nodes (3): Props, RouteErrorBoundary, State
### Community 63 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport, @nestjs/platform-express (+15 more)
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more)
### Community 64 - "compilerOptions"
Cohesion: 0.10
Nodes (20): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, lib, module, moduleDetection, moduleResolution, noEmit (+12 more)
### Community 66 - "AdminQueryDto"
Cohesion: 0.18
Nodes (7): ApiQuery, Get, Query, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
Cohesion: 0.21
Nodes (6): ApiQuery, Query, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
### Community 67 - "PetsController"
Cohesion: 0.10
@ -650,8 +651,8 @@ Cohesion: 0.05
Nodes (47): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+39 more)
### Community 76 - "AdminService"
Cohesion: 0.16
Nodes (4): Delete, Param, AdminService, Injectable
Cohesion: 0.13
Nodes (5): Delete, Param, Put, AdminService, Injectable
### Community 77 - "seo.module.ts"
Cohesion: 0.16
@ -833,10 +834,6 @@ Nodes (9): compilerOptions, esModuleInterop, module, moduleResolution, skipLibCh
Cohesion: 0.20
Nodes (9): Compile and run the project, Deployment, Description, License, Project setup, Resources, Run tests, Stay in touch (+1 more)
### Community 122 - "ApiOperation"
Cohesion: 0.15
Nodes (3): ApiOperation, Body, Put
### Community 123 - "auth.service.ts"
Cohesion: 0.11
Nodes (8): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, normalizeMobile(), RedisService, Injectable
@ -859,7 +856,7 @@ Nodes (9): Arch Linux, Contributors, Install, Known problems for variable versio
### Community 128 - "AdminController"
Cohesion: 0.16
Nodes (6): AdminController, ApiBearerAuth, ApiTags, Controller, Post, UseGuards
Nodes (7): AdminController, ApiBearerAuth, ApiTags, Body, Controller, Post, UseGuards
### Community 129 - "auth.controller.ts"
Cohesion: 0.16
@ -922,8 +919,8 @@ Cohesion: 0.25
Nodes (6): app_module_1, core_1, fs, path, swagger_1, yaml
### Community 145 - "admin.service.ts"
Cohesion: 0.13
Nodes (21): CouponInput, CouponTargetInput, PaginationQuery, ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber (+13 more)
Cohesion: 0.16
Nodes (11): CouponInput, CouponTargetInput, PaginationQuery, ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber (+3 more)
### Community 146 - "System Discovery"
Cohesion: 0.25
@ -933,6 +930,10 @@ Nodes (7): Active Core Applications, Current Architecture, Evidence-Based Status
Cohesion: 0.14
Nodes (14): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, B2BInquiry, Banner (+6 more)
### Community 148 - "CreateUserDto"
Cohesion: 0.24
Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more)
### Community 149 - "SmsSettingsPage.tsx"
Cohesion: 0.29
Nodes (7): PatternItem, SmsConfigState, SmsLogItem, SmsLogStats, SmsSettingsPage(), toPersianDigits(), SmsSettingsPage
@ -1108,7 +1109,7 @@ Nodes (3): GET(), handleRevalidate(), POST()
## Knowledge Gaps
- **1340 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1335 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **118 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **119 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
@ -1122,7 +1123,7 @@ _Questions this graph is uniquely positioned to answer:_
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
_1340 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `app.module.ts` be split into smaller, more focused modules?**
_Cohesion score 0.05974025974025974 - nodes in this community are weakly interconnected._
_Cohesion score 0.05288207297726071 - nodes in this community are weakly interconnected._
- **Should `SmsService` be split into smaller, more focused modules?**
_Cohesion score 0.052028732284993204 - nodes in this community are weakly interconnected._
- **Should `ProductService` be split into smaller, more focused modules?**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,31 @@
# Graph Report - canina (2026-08-29)
## Corpus Check
- 590 files · ~1,335,143 words
- 590 files · ~1,336,894 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4136 nodes · 7451 edges · 327 communities (208 shown, 119 thin omitted)
- 4138 nodes · 7454 edges · 335 communities (213 shown, 122 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 281 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `bfb7f420`
- Built from commit: `32e40a82`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- Roles
- app.module.ts
- SmsService
- SettingsController
- ProductService
- ProductPage.tsx
- CmsController
- tickets.controller.ts
- Button.tsx
- ProductsService
- SettingsService
- devDependencies
- ReviewsService
- CreateReviewDto
- MediaSelector.tsx
- index.ts
- app-audit-verification.e2e-spec.js
@ -34,10 +34,10 @@
- DoctorQueryDto
- schema.ts
- JwtAuthGuard
- ProductsController
- ProductsService
- CreateVideoDto
- admin.module.ts
- FeaturedProducts.tsx
- ReportsController
- components/Skeleton.tsx
- MenuService
- BE-001
- FE-001
@ -49,41 +49,41 @@
- DOC-001
- adminRoutes.tsx
- WholesaleApplyDto
- B2BService
- B2BController
- AuthController
- FaqService
- راهنمای تست سیستم (Software Testing)
- Button
- Transactions.tsx
- CategoriesController
- MediaController
- What You Must Do When Invoked
- SslController
- BannersService
- TestimonialsService
- BannersController
- TestimonialsController
- What You Must Do When Invoked
- 20260526145407_init/migration.sql
- IngredientsService
- UsersService
- IngredientsController
- UsersController
- devDependencies
- devDependencies
- BlogsController
- PrescriptionsService
- SmartAdvisorService
- Reports.tsx
- Modal.tsx
- UITexts.tsx
- Orders.tsx
- Role & Core Objective
- ContactService
- ContactController
- compilerOptions
- PaymentService
- AdminTransactionFilterDto
- RouteErrorBoundary
- usePetStore
- SmsService
- dependencies
- compilerOptions
- BlogsService
- AdminQueryDto
- ApiOperation
- PetsController
- UserDashboard.tsx
- lib/services/api.ts
- Required Review Group Closures
- compilerOptions
- getPageMetadata
@ -100,15 +100,15 @@
- scripts
- dependencies
- Role & Core Objective
- useSettingsStore
- trust-seals/page.tsx
- zibal.service.ts
- dependencies
- CreateEBankCheckoutDto
- seed-products.ts
- CreateReviewDto
- users.service.ts
- Reconciled Audit Roles & Assignments
- OrdersService
- lib/services/api.ts
- useSettingsStore
- Phase 3.1 — Human Review Preparation and Master Backlog Critique
- wiki/[slug]/page.tsx
- compilerOptions
@ -121,7 +121,7 @@
- Comprehensive Change Log
- Coupons.tsx
- Operational Rules & Boundaries
- MetricsController
- auth.service.ts
- PaginationDto
- PrismaService
- 1. Summary of Integrity Repairs Performed
@ -130,20 +130,20 @@
- Operational Rules & Boundaries
- RegisterDto
- AppService
- VerifyOtpDto
- Blogs.tsx
- Vazirmatn Changelog
- Vazirmatn Font فونت وزیرمتن
- Operational Rules & Boundaries
- compilerOptions
- compilerOptions
- backend/README.md
- ApiOperation
- auth.service.ts
- AdminController
- AuthService
- Repository Map
- validate_integrity.js
- admin-panel/package.json
- Sahel-Font
- AdminController
- Body
- auth.controller.ts
- Sahel-Font
- Role & Core Objective
@ -159,11 +159,11 @@
- application/package.json
- start-dev.js
- generate-openapi.js
- reviews.controller.ts
- admin.service.ts
- VetGallery.tsx
- ProductDto
- System Discovery
- ArchivePage.tsx
- CreateUserDto
- HomeController
- admin.service.ts
- SmsSettingsPage.tsx
- Product Requirement Document (PRD)
- class-transformer
@ -191,14 +191,14 @@
- rebuild_honest_ledger.js
- validate_evidence_grade.js
- @nestjs/core
- SendOtpDto
- auth.module.ts
- نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina
- @nestjs/jwt
- API Contract Specification
- ⚙️ Backend Technical Review (05_dev_backend)
- 🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)
- 🚀 SEO & Content Strategy Review (12_seo_content)
- UpdateReviewDto
- CheckoutPage.tsx
- @eslint/eslintrc
- seed-ui-texts.ts
- seed-wiki.ts
@ -211,7 +211,7 @@
- Raw Finding Verification & Disposition Report
- React + TypeScript + Vite
- Select.tsx
- catalog/page.tsx
- UsersService
- @nestjs/throttler
- passport
- application/README.md
@ -245,12 +245,12 @@
- rules/graphify.md
- .agents/workflows/graphify.md
- instructions.md
- RevalidationService
- WikiController
- helmet
- tailwindcss
- @nestjs/schematics
- @nestjs/testing
- @nestjs/swagger
- BlogsService
- source-map-support
- ts-jest
- ts-loader
@ -321,7 +321,15 @@
- eslint-config-prettier
- eslint
- @types/react-dom
- SmsLogQueryDto
- eslint-plugin-react-refresh
- WikiService
- B2BService
- track/page.tsx
- PodcastPlayerModal.tsx
- app.e2e-spec.js
- app/page.tsx
- bcrypt
## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges
@ -340,39 +348,39 @@
docs/02-user-guide.md → backend/uploads/1781288429353-508765350.jpg
- `AuthController` --references--> `ApiResponse` [EXTRACTED]
backend/src/auth/auth.controller.ts → frontend/admin-panel/src/types/admin.ts
- `BlogsController` --references--> `ApiResponse` [EXTRACTED]
backend/src/blogs/blogs.controller.ts → frontend/admin-panel/src/types/admin.ts
- `HomeController` --references--> `ApiResponse` [EXTRACTED]
backend/src/home/home.controller.ts → frontend/admin-panel/src/types/admin.ts
- `OrdersController` --references--> `ApiResponse` [EXTRACTED]
backend/src/orders/orders.controller.ts → frontend/admin-panel/src/types/admin.ts
- `PetsController` --references--> `ApiResponse` [EXTRACTED]
backend/src/pets/pets.controller.ts → frontend/admin-panel/src/types/admin.ts
- `ProductsController` --references--> `ApiResponse` [EXTRACTED]
backend/src/products/products.controller.ts → frontend/admin-panel/src/types/admin.ts
## Import Cycles
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/services/authService.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (327 total, 119 thin omitted)
## Communities (335 total, 122 thin omitted)
### Community 0 - "Roles"
Cohesion: 0.24
Nodes (13): Roles(), PaymentController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get (+5 more)
### Community 1 - "app.module.ts"
Cohesion: 0.05
Nodes (42): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+34 more)
Cohesion: 0.06
Nodes (40): B2BModule, Module, BannersModule, Module, CmsModule, Module, SmsModule, Global (+32 more)
### Community 2 - "SmsService"
Cohesion: 0.05
Nodes (26): SmsService, Injectable, SmsLogQueryDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString (+18 more)
### Community 2 - "SettingsController"
Cohesion: 0.16
Nodes (15): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Delete (+7 more)
### Community 3 - "ProductService"
Cohesion: 0.06
Nodes (34): dynamic, revalidate, DEFAULT_CATEGORIES, dynamic, revalidate, dynamic, revalidate, BlogCategory (+26 more)
### Community 4 - "ProductPage.tsx"
Cohesion: 0.06
Nodes (34): BlogPostClientProps, OrderDetailsModalProps, PLAYBACK_RATES, PodcastInlinePlayer(), PodcastInlinePlayerProps, PLAYBACK_RATES, PodcastPlayerModal(), PodcastPlayerModalProps (+26 more)
Cohesion: 0.12
Nodes (16): PLAYBACK_RATES, PodcastInlinePlayer(), PodcastInlinePlayerProps, ProductImageZoomModalProps, CalculatorState, GalleryMediaItem, ICON_MAP, isVideoUrl() (+8 more)
### Community 5 - "CmsController"
Cohesion: 0.09
@ -383,40 +391,36 @@ Cohesion: 0.09
Nodes (29): AdminUpdateTicketDto, CreateTicketDto, ReplyTicketDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString (+21 more)
### Community 7 - "Button.tsx"
Cohesion: 0.08
Nodes (20): ButtonProps, ButtonSize, ButtonVariant, Spinner(), FAQ, MENU_TABS, MenuItem, MenuType (+12 more)
### Community 8 - "ProductsService"
Cohesion: 0.19
Nodes (10): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, Query, ProductsModule, Module (+2 more)
Cohesion: 0.12
Nodes (14): Button(), ButtonProps, ButtonSize, ButtonVariant, Spinner(), FAQ, BlogCommentItem, ProductReview (+6 more)
### Community 9 - "devDependencies"
Cohesion: 0.22
Nodes (9): devDependencies, prisma, @types/bcryptjs, @types/node, typescript, @types/node, typescript, prisma (+1 more)
### Community 10 - "ReviewsService"
Cohesion: 0.12
Nodes (17): ReviewsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+9 more)
### Community 10 - "CreateReviewDto"
Cohesion: 0.09
Nodes (23): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, ReviewsController (+15 more)
### Community 11 - "MediaSelector.tsx"
Cohesion: 0.07
Nodes (36): ConfirmModal(), ConfirmModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, maxWidthClasses, Modal() (+28 more)
Cohesion: 0.06
Nodes (35): ConfirmModal(), ConfirmModalProps, ImagePreviewModal(), ImagePreviewModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps (+27 more)
### Community 12 - "index.ts"
Cohesion: 0.06
Nodes (24): backend, frontend, playwright.config.ts, @playwright/test, tests/**/*.ts, authFile, test, compilerOptions (+16 more)
### Community 13 - "app-audit-verification.e2e-spec.js"
Cohesion: 0.06
Nodes (28): AppModule, Module, EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter (+20 more)
Cohesion: 0.07
Nodes (26): EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter, Catch, PrismaExceptionFilter (+18 more)
### Community 14 - "userStore.ts"
Cohesion: 0.07
Nodes (33): AuthModal, LoginModal, metadata, AuthModal(), AuthModalProps, extractOtpFromText(), Header(), MENU_ICONS (+25 more)
Nodes (30): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, metadata, AuthModal(), AuthModalProps (+22 more)
### Community 15 - "src/services/api.ts"
Cohesion: 0.07
Nodes (35): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+27 more)
Cohesion: 0.08
Nodes (31): Layout(), ProtectedRoute(), MenuGroup, Sidebar(), SidebarProps, SubMenuItem, LiveMonitoringSummary, SEARCHABLE_PAGES (+23 more)
### Community 16 - "DoctorQueryDto"
Cohesion: 0.09
@ -428,23 +432,23 @@ Nodes (18): B2BPage(), generateMetadata(), ContactPage(), generateMetadata(), ge
### Community 18 - "JwtAuthGuard"
Cohesion: 0.16
Nodes (8): JwtAuthGuard, Injectable, B2BWholesaleOrderItem, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
Nodes (7): JwtAuthGuard, Injectable, ROLES_KEY, RequestWithUser, RolesGuard, Injectable, UserReqPayload
### Community 19 - "ProductsController"
Cohesion: 0.16
Nodes (9): ProductsController, ApiOperation, ApiTags, Body, Controller, Get, Param, Patch (+1 more)
### Community 19 - "ProductsService"
Cohesion: 0.10
Nodes (19): GetProductsDto, ApiPropertyOptional, IsEnum, IsOptional, IsString, ProductsController, ApiOperation, ApiTags (+11 more)
### Community 20 - "CreateVideoDto"
Cohesion: 0.09
Nodes (24): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, UpdateVideoDto (+16 more)
### Community 21 - "admin.module.ts"
Cohesion: 0.07
Nodes (19): AdminModule, Module, MediaService, Injectable, ReportsController, ApiBearerAuth, ApiOperation, ApiQuery (+11 more)
### Community 22 - "FeaturedProducts.tsx"
### Community 21 - "ReportsController"
Cohesion: 0.14
Nodes (9): FeaturedProducts(), ProductCard(), OrderRowSkeleton(), PetProfileSkeleton(), ProductCardSkeleton(), Skeleton(), SkeletonProps, mockProducts (+1 more)
Nodes (11): ReportsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Get, Query (+3 more)
### Community 22 - "components/Skeleton.tsx"
Cohesion: 0.21
Nodes (5): OrderRowSkeleton(), PetProfileSkeleton(), ProductCardSkeleton(), Skeleton(), SkeletonProps
### Community 23 - "MenuService"
Cohesion: 0.12
@ -484,18 +488,18 @@ Nodes (31): Acceptance Criteria, Affected Application, Affected Files, Alternati
### Community 32 - "adminRoutes.tsx"
Cohesion: 0.06
Nodes (26): App(), ContactInfoItem, ContactSubmission, CategoryDist, DashboardData, Ticket, TicketMessage, WholesaleRequest (+18 more)
Nodes (24): App(), Props, RouteErrorBoundary, State, HeroBanner, VetTestimonial, BestSellerItem, CategoryDistItem (+16 more)
### Community 33 - "WholesaleApplyDto"
Cohesion: 0.10
Nodes (20): ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, WholesaleApplyDto, ApiBearerAuth, ApiOperation (+12 more)
### Community 34 - "B2BService"
Cohesion: 0.13
Nodes (15): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+7 more)
### Community 34 - "B2BController"
Cohesion: 0.14
Nodes (13): B2BController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Param (+5 more)
### Community 35 - "AuthController"
Cohesion: 0.23
Cohesion: 0.25
Nodes (13): AuthController, ApiBadRequestResponse, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+5 more)
### Community 36 - "FaqService"
@ -506,13 +510,13 @@ Nodes (14): FaqController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controlle
Cohesion: 0.07
Nodes (25): اجرای بک‌اند, اجرای فرانت‌اند, اجرای پروژه در سرور با PM2, برای راه‌اندازی بک‌اند:, برای راه‌اندازی فرانت‌اند Next.js:, بیلد کردن پروژه (Building), ذخیره پروسه‌ها تا پس از ری‌استارت سرور قطع نشوند:, راه‌اندازی و انتشار سیستم (Setup & Deployment) (+17 more)
### Community 38 - "Button"
Cohesion: 0.16
Nodes (13): TransactionReceiptData, TransactionReceiptModal(), TransactionReceiptModalProps, Button(), ThSort(), ThSortProps, GatewayHealth, Stats (+5 more)
### Community 38 - "Transactions.tsx"
Cohesion: 0.10
Nodes (19): TransactionReceiptData, TransactionReceiptModal(), TransactionReceiptModalProps, Badge(), BadgeProps, BadgeVariant, variantStyles, ThSort() (+11 more)
### Community 39 - "CategoriesController"
Cohesion: 0.09
Nodes (17): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+9 more)
Cohesion: 0.10
Nodes (16): CategoriesController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Body, Controller, Delete (+8 more)
### Community 40 - "MediaController"
Cohesion: 0.11
@ -526,13 +530,13 @@ Nodes (26): For /graphify add and --watch, For /graphify query, For the commit h
Cohesion: 0.13
Nodes (14): SslController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Get, Post (+6 more)
### Community 43 - "BannersService"
### Community 43 - "BannersController"
Cohesion: 0.13
Nodes (15): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+7 more)
Nodes (13): BannersController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+5 more)
### Community 44 - "TestimonialsService"
### Community 44 - "TestimonialsController"
Cohesion: 0.13
Nodes (14): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
Nodes (12): TestimonialsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+4 more)
### Community 45 - "What You Must Do When Invoked"
Cohesion: 0.07
@ -542,13 +546,13 @@ Nodes (26): For /graphify add and --watch, For /graphify query, For the commit h
Cohesion: 0.27
Nodes (14): "coupons", "health_logs", "order_items", "orders", "pet_medical_conditions", "pets", "product_ingredients", "product_symptoms" (+6 more)
### Community 47 - "IngredientsService"
### Community 47 - "IngredientsController"
Cohesion: 0.13
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
Nodes (12): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+4 more)
### Community 48 - "UsersService"
Cohesion: 0.06
Nodes (41): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+33 more)
### Community 48 - "UsersController"
Cohesion: 0.21
Nodes (16): ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+8 more)
### Community 49 - "devDependencies"
Cohesion: 0.11
@ -570,13 +574,13 @@ Nodes (14): PrescriptionsController, ApiBearerAuth, ApiOperation, ApiTags, Body,
Cohesion: 0.13
Nodes (14): SmartAdvisorController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 54 - "Reports.tsx"
Cohesion: 0.20
Nodes (7): BestSellerItem, CategoryDistItem, COLORS, CustomTooltipProps, ReportData, TopCouponItem, Reports
### Community 54 - "Modal.tsx"
Cohesion: 0.10
Nodes (14): maxWidthClasses, Modal(), ModalProps, ContactInfoItem, ContactSubmission, MENU_TABS, MenuItem, MenuType (+6 more)
### Community 55 - "UITexts.tsx"
Cohesion: 0.07
Nodes (24): Badge(), BadgeProps, BadgeVariant, variantStyles, ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ImagePreviewModal() (+16 more)
Cohesion: 0.12
Nodes (15): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ToggleSwitch(), ToggleSwitchProps, AppSitePage, PageSection, SectionField (+7 more)
### Community 56 - "Orders.tsx"
Cohesion: 0.11
@ -586,41 +590,45 @@ Nodes (15): Skeleton(), MonitoringStats, getPaymentMethodLabel(), Order, ORDER_S
Cohesion: 0.09
Nodes (22): CREATE MODE — Normal Operation, Expected JSON Output Schema, File Scope Boundary, Forbidden Actions, MODE 1: REVIEW (called during Review Phase), MODE 2: CONTENT CREATION (standalone task from backlog), Operating Modes, Operational Rules & Boundaries (+14 more)
### Community 58 - "ContactService"
### Community 58 - "ContactController"
Cohesion: 0.13
Nodes (12): ContactController, Body, Controller, Get, Param, Post, Put, Query (+4 more)
Nodes (10): ContactController, Body, Controller, Get, Param, Post, Put, Query (+2 more)
### Community 59 - "compilerOptions"
Cohesion: 0.06
Nodes (30): compilerOptions, allowSyntheticDefaultImports, baseUrl, declaration, emitDecoratorMetadata, esModuleInterop, experimentalDecorators, forceConsistentCasingInFileNames (+22 more)
### Community 61 - "AdminTransactionFilterDto"
Cohesion: 0.25
Nodes (7): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type
### Community 60 - "PaymentService"
Cohesion: 0.11
Nodes (9): AdminTransactionFilterDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type, PaymentService (+1 more)
### Community 62 - "RouteErrorBoundary"
Cohesion: 0.22
Nodes (3): Props, RouteErrorBoundary, State
### Community 61 - "usePetStore"
Cohesion: 0.14
Nodes (14): FeaturedProducts(), ProductCard(), OrderSuccess(), PrescriptionUploadModal(), PrescriptionUploadModalProps, SearchResultsPage(), mockProducts, mockPush (+6 more)
### Community 63 - "dependencies"
Cohesion: 0.09
Nodes (23): dependencies, bcrypt, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport (+15 more)
Nodes (23): dependencies, bcryptjs, class-validator, compression, ioredis, @nestjs/common, @nestjs/passport, @nestjs/platform-express (+15 more)
### Community 64 - "compilerOptions"
Cohesion: 0.10
Nodes (20): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, lib, module, moduleDetection, moduleResolution, noEmit (+12 more)
### Community 66 - "AdminQueryDto"
Cohesion: 0.21
Nodes (6): ApiQuery, Query, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
### Community 65 - "BlogsService"
Cohesion: 0.06
Nodes (14): BlogsService, Injectable, RevalidationModule, Global, Module, RevalidationService, Injectable, ApiProperty (+6 more)
### Community 66 - "ApiOperation"
Cohesion: 0.12
Nodes (8): ApiOperation, ApiQuery, Get, Query, AdminQueryDto, ApiPropertyOptional, IsOptional, IsString
### Community 67 - "PetsController"
Cohesion: 0.10
Nodes (14): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+6 more)
### Community 68 - "UserDashboard.tsx"
Cohesion: 0.11
Nodes (28): VerifyContent(), AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), B2BPortal(), BackButton() (+20 more)
Nodes (13): PetsController, ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, Controller, Delete, Get (+5 more)
### Community 68 - "lib/services/api.ts"
Cohesion: 0.09
Nodes (31): VerifyContent(), B2BPortal(), BlogPostClientProps, BlogPost, BlogPreviewSection(), ContactInfoItem, HeaderButton(), HeaderButtonProps (+23 more)
### Community 69 - "Required Review Group Closures"
Cohesion: 0.10
@ -632,7 +640,7 @@ Nodes (23): compilerOptions, allowImportingTsExtensions, erasableSyntaxOnly, jsx
### Community 71 - "getPageMetadata"
Cohesion: 0.09
Nodes (15): generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+7 more)
Nodes (14): generateMetadata(), CatalogClient(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata(), generateMetadata() (+6 more)
### Community 72 - "Operational Rules & Boundaries"
Cohesion: 0.11
@ -651,8 +659,8 @@ Cohesion: 0.05
Nodes (47): CreateHealthLogDto, ApiProperty, ApiPropertyOptional, IsNotEmpty, IsOptional, IsString, CreatePetDto, ApiProperty (+39 more)
### Community 76 - "AdminService"
Cohesion: 0.13
Nodes (5): Delete, Param, Put, AdminService, Injectable
Cohesion: 0.20
Nodes (4): Delete, Param, AdminService, Injectable
### Community 77 - "seo.module.ts"
Cohesion: 0.16
@ -686,10 +694,6 @@ Nodes (21): dependencies, axios, lucide-react, react, react-dom, react-hot-toast
Cohesion: 0.12
Nodes (16): 1. Active Secret Scanning (All Modified Files), 2. Docker Container Verification (if Dockerfile exists), 3. CI/CD Basic Check (if `.github/workflows/` exists), 4. Failure Routing Protocol, 5. Forbidden Actions, ENFORCE MODE — Normal Operation, Expected JSON Output Schema, Operational Rules & Boundaries (+8 more)
### Community 85 - "useSettingsStore"
Cohesion: 0.14
Nodes (16): B2BPortal, CartDrawer, ClientLayout(), B2BLandingClient(), BrandLogo(), BrandLogoProps, EnamadBadge(), Footer() (+8 more)
### Community 86 - "zibal.service.ts"
Cohesion: 0.16
Nodes (9): GatewayHealthResult, IPaymentGateway, PaymentInquiryResult, PaymentRequestOptions, PaymentRequestResult, PaymentVerifyResult, ZibalInquiryResponse, ZibalRequestResponse (+1 more)
@ -706,21 +710,21 @@ Nodes (8): CreateEBankCheckoutDto, ApiProperty, ApiPropertyOptional, IsNotEmpty,
Cohesion: 0.17
Nodes (12): prisma, main(), prisma, determineSuitableFor(), findOrCreateCategory(), getProductImageUrl(), main(), parseSize() (+4 more)
### Community 90 - "CreateReviewDto"
Cohesion: 0.25
Nodes (8): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, Max
### Community 90 - "users.service.ts"
Cohesion: 0.13
Nodes (14): AddressDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEmpty, IsOptional, IsString, ApiPropertyOptional (+6 more)
### Community 91 - "Reconciled Audit Roles & Assignments"
Cohesion: 0.12
Nodes (15): 10. Documentation Engineer, 1. Lead Architect / Orchestrator, 2. React / Vite Storefront Auditor, 3. NestJS Backend Auditor, 4. Admin Features Auditor, 5. Database & Data Integrity Auditor, 6. Security Auditor, 7. TypeScript & Code Quality Auditor (+7 more)
### Community 92 - "OrdersService"
Cohesion: 0.07
Cohesion: 0.06
Nodes (35): CreateOrderDto, OrderItemDto, ApiProperty, ApiPropertyOptional, IsArray, IsNotEmpty, IsNumber, IsOptional (+27 more)
### Community 93 - "lib/services/api.ts"
Cohesion: 0.07
Nodes (25): HomeClient(), HomeClientProps, getHomeData(), Home(), BlogPost, BlogPreviewSection(), ContactInfoItem, FAQItem (+17 more)
### Community 93 - "useSettingsStore"
Cohesion: 0.08
Nodes (33): HomeClient(), HomeClientProps, ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, B2BLandingClient(), BannerPlacement() (+25 more)
### Community 94 - "Phase 3.1 — Human Review Preparation and Master Backlog Critique"
Cohesion: 0.13
@ -743,8 +747,8 @@ Cohesion: 0.13
Nodes (15): scripts, build, build:nest, docs:generate, lint, seo:backfill, start, start:debug (+7 more)
### Community 99 - "BlogsController"
Cohesion: 0.06
Nodes (32): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+24 more)
Cohesion: 0.18
Nodes (12): BlogsController, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller, Get (+4 more)
### Community 100 - "Deep Audit Summary Report"
Cohesion: 0.14
@ -763,24 +767,24 @@ Cohesion: 0.15
Nodes (12): 10. `TASK-VERIFY-001`, 1. `TASK-SEC-001`, 2. `TASK-SEC-002`, 3. `TASK-SEC-003`, 4. `TASK-FIN-001`, 5. `TASK-BUILD-001`, 6. `TASK-AUTH-001`, 7. `TASK-FE-001` (+4 more)
### Community 104 - "Coupons.tsx"
Cohesion: 0.13
Nodes (12): formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData, CouponModalProps, CouponTarget (+4 more)
Cohesion: 0.15
Nodes (11): formatPriceWithCommas(), PriceInput(), PriceInputProps, toEnglishDigits(), Coupon, CouponFormData, CouponModalProps, CouponTarget (+3 more)
### Community 105 - "Operational Rules & Boundaries"
Cohesion: 0.17
Nodes (11): 1. Detect Test Runner from Tech Stack, 2. Execution Output Capture (Mandatory), 3. Acceptance Criteria Verification, 4. Failure Routing Protocol, 5. Success Routing Protocol, 6. Forbidden Actions, Expected JSON Output Schema, Operational Rules & Boundaries (+3 more)
### Community 106 - "MetricsController"
Cohesion: 0.29
Nodes (5): ApiExcludeController, MetricsController, Controller, Get, Res
### Community 106 - "auth.service.ts"
Cohesion: 0.07
Nodes (15): ApiExcludeController, AppModule, Module, AdminLoginInput, LoginInput, RegisterInput, MetricsController, Controller (+7 more)
### Community 107 - "PaginationDto"
Cohesion: 0.06
Nodes (23): BlogsModule, Module, BlogFilterDto, BlogsService, Injectable, PaginationDto, SortOrder, ApiPropertyOptional (+15 more)
Cohesion: 0.07
Nodes (26): AdminModule, Module, MediaService, Injectable, SslCertInfo, BlogsModule, Module, BlogFilterDto (+18 more)
### Community 108 - "PrismaService"
Cohesion: 0.07
Nodes (21): MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions, SmsConfig, SmsLogQuery, CreateContactSubmissionDto, UpdateContactInfoItemDto, MenuType (+13 more)
Cohesion: 0.05
Nodes (29): CategoryQuery, PetQuery, WikiQuery, BannersService, Injectable, MeliPayamakPattern, MeliPayamakResponse, SendPatternSmsOptions (+21 more)
### Community 109 - "1. Summary of Integrity Repairs Performed"
Cohesion: 0.17
@ -806,9 +810,9 @@ Nodes (8): RegisterDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, I
Cohesion: 0.29
Nodes (5): AppController, Controller, Get, AppService, Injectable
### Community 115 - "VerifyOtpDto"
Cohesion: 0.29
Nodes (6): ApiProperty, IsNotEmpty, IsString, Matches, VerifyOtpDto, Length
### Community 115 - "Blogs.tsx"
Cohesion: 0.12
Nodes (14): ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps, AuthorUser, BlogCategory, BlogPost (+6 more)
### Community 116 - "Vazirmatn Changelog"
Cohesion: 0.18
@ -834,9 +838,13 @@ Nodes (9): compilerOptions, esModuleInterop, module, moduleResolution, skipLibCh
Cohesion: 0.20
Nodes (9): Compile and run the project, Deployment, Description, License, Project setup, Resources, Run tests, Stay in touch (+1 more)
### Community 123 - "auth.service.ts"
Cohesion: 0.11
Nodes (8): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, normalizeMobile(), RedisService, Injectable
### Community 122 - "AdminController"
Cohesion: 0.14
Nodes (6): AdminController, ApiBearerAuth, ApiTags, Controller, Put, UseGuards
### Community 123 - "AuthService"
Cohesion: 0.19
Nodes (3): AuthService, Injectable, normalizeMobile()
### Community 124 - "Repository Map"
Cohesion: 0.20
@ -854,13 +862,9 @@ Nodes (9): name, private, scripts, build, dev, lint, preview, type (+1 more)
Cohesion: 0.20
Nodes (9): Arch Linux, Contributors, Install, Known problems for variable version, License, Sahel-Font, To Do (variable), طریقه استفاده از نسخه متغیر variable (+1 more)
### Community 128 - "AdminController"
Cohesion: 0.16
Nodes (7): AdminController, ApiBearerAuth, ApiTags, Body, Controller, Post, UseGuards
### Community 129 - "auth.controller.ts"
Cohesion: 0.16
Nodes (11): AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString, MinLength, LoginDto, ApiProperty (+3 more)
Cohesion: 0.09
Nodes (22): AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString, MinLength, LoginDto, ApiProperty (+14 more)
### Community 130 - "Sahel-Font"
Cohesion: 0.20
@ -918,21 +922,25 @@ Nodes (7): backend, distMainPath, fs, http, path, { spawn, execSync }, waitForBa
Cohesion: 0.25
Nodes (6): app_module_1, core_1, fs, path, swagger_1, yaml
### Community 145 - "admin.service.ts"
### Community 144 - "VetGallery.tsx"
Cohesion: 0.16
Nodes (11): CouponInput, CouponTargetInput, PaginationQuery, ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber (+3 more)
Nodes (12): BackButton(), BackButtonProps, VideoModalPlayer, DisplayVideoItem, FALLBACK_VIDEOS, TestimonialItem, VetGallery(), PLAYBACK_RATES (+4 more)
### Community 145 - "ProductDto"
Cohesion: 0.22
Nodes (7): ProductDto, ApiPropertyOptional, IsArray, IsBoolean, IsNumber, IsOptional, IsString
### Community 146 - "System Discovery"
Cohesion: 0.25
Nodes (7): Active Core Applications, Current Architecture, Evidence-Based Status Matrix, Excluded / Non-Auditable Artifacts, Major Business Domains Discovered, System Discovery, Technical Architecture Summary
### Community 147 - "ArchivePage.tsx"
Cohesion: 0.14
Nodes (14): ArchivePage(), ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, B2BInquiry, Banner (+6 more)
### Community 147 - "HomeController"
Cohesion: 0.16
Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, HomeModule, Module (+2 more)
### Community 148 - "CreateUserDto"
Cohesion: 0.24
Nodes (10): CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsNumber, IsOptional, IsString (+2 more)
### Community 148 - "admin.service.ts"
Cohesion: 0.17
Nodes (14): CouponInput, CouponTargetInput, PaginationQuery, CreateUserDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty (+6 more)
### Community 149 - "SmsSettingsPage.tsx"
Cohesion: 0.29
@ -943,8 +951,8 @@ Cohesion: 0.29
Nodes (6): 1. Executive Vision, 2. Target Audience, 3. Functional Requirements, 4. Non-Functional Requirements (Performance, Security), 5. Epic / Feature Breakdown, Product Requirement Document (PRD)
### Community 152 - "useCartStore"
Cohesion: 0.18
Nodes (10): CartDrawer(), DeleteConfirmModal(), DeleteConfirmModalProps, OrderSuccess(), OrderTracking(), mockProduct, CartItem, CartStore (+2 more)
Cohesion: 0.12
Nodes (12): CartDrawer(), DeleteConfirmModal(), DeleteConfirmModalProps, Header(), mockProduct, ApiErr, Order, OrderItem (+4 more)
### Community 153 - "exclude"
Cohesion: 0.22
@ -1026,9 +1034,9 @@ Nodes (4): evidenceList, ledger, mandatoryMap, mandatoryScope
Cohesion: 0.40
Nodes (4): activeFiles, errors, validationOutput, warnings
### Community 177 - "SendOtpDto"
Cohesion: 0.33
Nodes (5): SendOtpDto, ApiProperty, IsNotEmpty, IsString, Matches
### Community 177 - "auth.module.ts"
Cohesion: 0.17
Nodes (10): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+2 more)
### Community 178 - "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina"
Cohesion: 0.33
@ -1050,9 +1058,9 @@ Nodes (3): Architectural Overview, Critical Review Findings & Required Enhanceme
Cohesion: 0.50
Nodes (3): Overview & Content Foundation, SEO & Content Requirements, 🚀 SEO & Content Strategy Review (12_seo_content)
### Community 184 - "UpdateReviewDto"
Cohesion: 0.33
Nodes (5): ApiProperty, IsIn, IsOptional, IsString, UpdateReviewDto
### Community 184 - "CheckoutPage.tsx"
Cohesion: 0.30
Nodes (11): AddressFormData, AddressModal(), AddressModalProps, normalizeDigits(), validateAddressForm(), CheckoutPage(), SearchableSelect(), SearchableSelectProps (+3 more)
### Community 191 - "graphify reference: add a URL and watch a folder"
Cohesion: 0.50
@ -1094,9 +1102,9 @@ Nodes (3): ADR-AUTH-001: Admin Panel Authentication Architecture & Token Managem
Cohesion: 0.67
Nodes (3): Shabnam Font README, Shabnam Font Sample, Vazir Font
### Community 231 - "RevalidationService"
Cohesion: 0.16
Nodes (5): RevalidationModule, Global, Module, RevalidationService, Injectable
### Community 231 - "WikiController"
Cohesion: 0.21
Nodes (9): ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, Controller, Get, Param, Query (+1 more)
### Community 298 - ".initiateOrderPayment"
Cohesion: 0.20
@ -1106,25 +1114,45 @@ Nodes (10): ApiPropertyOptional, IsOptional, IsString, ZibalCallbackQueryDto, Ap
Cohesion: 0.83
Nodes (3): GET(), handleRevalidate(), POST()
### Community 326 - "SmsLogQueryDto"
Cohesion: 0.25
Nodes (7): SmsLogQueryDto, ApiPropertyOptional, IsIn, IsNumber, IsOptional, IsString, Type
### Community 329 - "B2BService"
Cohesion: 0.48
Nodes (3): B2BService, B2BWholesaleOrderItem, Injectable
### Community 331 - "PodcastPlayerModal.tsx"
Cohesion: 0.40
Nodes (3): PLAYBACK_RATES, PodcastPlayerModal(), PodcastPlayerModalProps
### Community 332 - "app.e2e-spec.js"
Cohesion: 0.50
Nodes (3): app_module_1, supertest_1, testing_1
### Community 333 - "app/page.tsx"
Cohesion: 0.67
Nodes (3): generateMetadata(), getHomeData(), Home()
## Knowledge Gaps
- **1340 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1335 more)
- **1341 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1336 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **119 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **122 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `ApiResponse` connect `BlogsController` to `AuthController`, `PetsController`, `src/services/api.ts`, `UsersService`, `ProductsController`, `OrdersService`?**
- **Why does `ApiResponse` connect `src/services/api.ts` to `BlogsController`, `AuthController`, `WikiController`, `PetsController`, `UsersController`, `HomeController`, `ProductsService`, `OrdersService`?**
_High betweenness centrality (0.080) - this node is a cross-community bridge._
- **Why does `Roles()` connect `Roles` to `SmsService`, `CmsController`, `tickets.controller.ts`, `ProductsService`, `ReviewsService`, `reviews.controller.ts`, `JwtAuthGuard`, `ProductsController`, `MenuService`, `WholesaleApplyDto`, `B2BService`, `FaqService`, `SslController`, `BannersService`, `TestimonialsService`, `IngredientsService`, `PrescriptionsService`, `SmartAdvisorService`, `ContactService`?**
- **Why does `Roles()` connect `Roles` to `SettingsController`, `CmsController`, `tickets.controller.ts`, `CreateReviewDto`, `JwtAuthGuard`, `ProductsService`, `MenuService`, `WholesaleApplyDto`, `B2BController`, `FaqService`, `SslController`, `BannersController`, `TestimonialsController`, `IngredientsController`, `PrescriptionsService`, `SmartAdvisorService`, `ContactController`, `BlogsService`, `B2BService`?**
_High betweenness centrality (0.063) - this node is a cross-community bridge._
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `auth.controller.ts`, `PetsController`, `CmsController`, `tickets.controller.ts`, `CategoriesController`, `ProductsService`, `PaginationDto`, `PetsController`, `PrismaService`, `DoctorQueryDto`, `admin.service.ts`, `reviews.controller.ts`, `admin.module.ts`, `OrdersService`?**
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `auth.controller.ts`, `BlogsService`, `CmsController`, `tickets.controller.ts`, `B2BService`, `PaginationDto`, `PetsController`, `ProductsService`, `admin.service.ts`, `ReportsController`, `users.service.ts`, `OrdersService`?**
_High betweenness centrality (0.036) - this node is a cross-community bridge._
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
_1340 weakly-connected nodes found - possible documentation gaps or missing edges._
_1341 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `app.module.ts` be split into smaller, more focused modules?**
_Cohesion score 0.05288207297726071 - nodes in this community are weakly interconnected._
- **Should `SmsService` be split into smaller, more focused modules?**
_Cohesion score 0.052028732284993204 - nodes in this community are weakly interconnected._
_Cohesion score 0.06078316773816481 - nodes in this community are weakly interconnected._
- **Should `ProductService` be split into smaller, more focused modules?**
_Cohesion score 0.06240084611316764 - nodes in this community are weakly interconnected._
_Cohesion score 0.06229508196721312 - nodes in this community are weakly interconnected._
- **Should `ProductPage.tsx` be split into smaller, more focused modules?**
_Cohesion score 0.11594202898550725 - nodes in this community are weakly interconnected._

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff