feat: complete admin url query sync, full sorting/filters, latin search, product media players, personalized pet name and stage noindex
All checks were successful
Deploy Canina / deploy (push) Successful in 1m57s

This commit is contained in:
parsa aghaei 2026-08-18 11:02:57 +03:30
parent d5c05b7577
commit 27a1517464
23 changed files with 168815 additions and 46100 deletions

View File

@ -20,6 +20,11 @@ export class PaginationQuery {
role?: string;
categoryId?: string;
status?: string;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
suitableFor?: string;
minPrice?: number | string;
maxPrice?: number | string;
}
export class CouponTargetInput {
@ -114,12 +119,17 @@ export class AdminService {
}
}
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['createdAt', 'firstName', 'lastName', 'mobile', 'email', 'role', 'walletBalance'].includes(query.sortBy))
? query.sortBy
: 'createdAt';
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
const [data, total] = await Promise.all([
this.prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
orderBy: { [sortField]: sortDirection },
include: {
addresses: true,
walletTransactions: {
@ -274,22 +284,59 @@ export class AdminService {
const skip = (page - 1) * limit;
const where: Prisma.ProductWhereInput = {};
const andConditions: Prisma.ProductWhereInput[] = [];
if (query.search) {
where.OR = [
{ nameFa: { contains: query.search, mode: 'insensitive' } },
{ artNo: { contains: query.search } },
];
const trimmed = query.search.trim();
andConditions.push({
OR: [
{ nameFa: { contains: trimmed, mode: 'insensitive' } },
{ nameEn: { contains: trimmed, mode: 'insensitive' } },
{ artNo: { contains: trimmed, mode: 'insensitive' } },
{ barcode: { contains: trimmed, mode: 'insensitive' } },
{ slug: { contains: trimmed, mode: 'insensitive' } },
{ description: { contains: trimmed, mode: 'insensitive' } },
{ shortDescription: { contains: trimmed, mode: 'insensitive' } },
{ scientificTagline: { contains: trimmed, mode: 'insensitive' } },
{
symptoms: {
some: {
symptom: { contains: trimmed, mode: 'insensitive' },
},
},
},
{
category: {
name: { contains: trimmed, mode: 'insensitive' },
},
},
],
});
}
if (query.categoryId) {
where.categoryId = query.categoryId;
andConditions.push({ categoryId: query.categoryId });
}
if (query.suitableFor && query.suitableFor !== 'all') {
andConditions.push({ suitableFor: { in: [query.suitableFor, 'سگ و گربه', 'هر دو'] } });
}
if (andConditions.length > 0) {
where.AND = andConditions;
}
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['nameFa', 'artNo', 'priceValue', 'packageSize', 'createdAt', 'updatedAt'].includes(query.sortBy))
? query.sortBy
: 'createdAt';
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
const [data, total] = await Promise.all([
this.prisma.product.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
orderBy: { [sortField]: sortDirection },
include: { category: true, symptoms: true },
}),
this.prisma.product.count({ where }),
@ -441,12 +488,17 @@ export class AdminService {
where.status = query.status;
}
const sortField = (query.sortBy && typeof query.sortBy === 'string' && ['createdAt', 'totalAmount', 'status', 'finalAmount'].includes(query.sortBy))
? query.sortBy
: 'createdAt';
const sortDirection = query.sortOrder === 'asc' ? 'asc' : 'desc';
const [data, total] = await Promise.all([
this.prisma.order.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
orderBy: { [sortField]: sortDirection },
include: {
user: true,
orderItems: {

View File

@ -326,7 +326,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
) : (
<>
<Upload className="w-4 h-4" />
<span>آپلود فایل جدید (همگانی)</span>
<span>آپلود فایل جدید</span>
</>
)}
</button>

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
ShoppingCart,
Eye,
@ -18,7 +19,10 @@ import {
Download,
PackageCheck,
Heart,
CreditCard
CreditCard,
ArrowUpDown,
ArrowUp,
ArrowDown
} from 'lucide-react';
import api from '../services/api';
import Skeleton from '../components/ui/Skeleton';
@ -58,44 +62,90 @@ export interface Order {
id: string;
trackingNumber?: string;
status: string;
createdAt?: string;
date?: string;
charityDonation?: number;
totalAmount?: number;
total?: number;
isRefill?: boolean;
paymentMethod?: string;
address?: string;
shippingAddress?: string;
orderItems?: OrderItem[];
items?: OrderItem[];
paymentTransactions?: PaymentTx[];
totalAmount: number;
finalAmount: number;
discountAmount: number;
shippingCost: number;
createdAt: string;
updatedAt: string;
itemsCount?: number;
user?: {
id?: string;
firstName?: string;
lastName?: string;
mobile?: string;
phone?: string;
email?: string;
};
address?: {
fullAddress?: string;
city?: string;
province?: string;
postalCode?: string;
recipientName?: string;
recipientMobile?: string;
};
orderItems: OrderItem[];
items?: OrderItem[];
paymentTransactions?: PaymentTx[];
notes?: string;
shippingMethod?: string;
paymentMethod?: string;
date?: string;
charityDonation?: number;
total?: number;
isRefill?: boolean;
shippingAddress?: string;
}
const statusStyles: Record<string, { label: string; color: string; icon: React.ElementType }> = {
pending_payment: { label: 'در انتظار پرداخت / ناموفق', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Clock },
export const ORDER_STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType }> = {
pending_payment: { label: 'در انتظار پرداخت', color: 'bg-amber-50 text-amber-700 border-amber-200', icon: Clock },
pending: { label: 'در حال بررسی', color: 'bg-orange-100 text-orange-700 border-orange-200', icon: Clock },
processing: { label: 'در حال پردازش', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Clock },
processing: { label: 'در حال پردازش (جدید)', color: 'bg-blue-50 text-blue-700 border-blue-200', icon: Clock },
packaged: { label: 'بسته‌بندی شده (آماده ارسال)', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
ready_to_ship: { label: 'آماده ارسال', color: 'bg-purple-100 text-purple-800 border-purple-200', icon: PackageCheck },
shipped: { label: 'ارسال شده', color: 'bg-blue-100 text-blue-700 border-blue-200', icon: Truck },
delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-700 border-green-200', icon: CheckCircle2 },
cancelled: { label: 'لغو شده', color: 'bg-red-100 text-red-700 border-red-200', icon: XCircle },
shipped: { label: 'ارسال شده', color: 'bg-indigo-50 text-indigo-700 border-indigo-200', icon: Truck },
delivered: { label: 'تحویل داده شده', color: 'bg-emerald-50 text-emerald-700 border-emerald-200', icon: CheckCircle2 },
cancelled: { label: 'لغو شده', color: 'bg-red-50 text-red-700 border-red-200', icon: XCircle },
};
const getPaymentMethodLabel = (method?: string): { label: string; color: string } => {
switch (method) {
case 'wallet': return { label: 'کیف پول', color: 'bg-purple-50 text-purple-700 border-purple-200' };
export const getPaymentStatusBadge = (status: string) => {
switch (status?.toLowerCase()) {
case 'success':
case 'paid':
case 'completed':
return { label: 'پرداخت موفق', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
case 'pending':
case 'in_progress':
return { label: 'در انتظار پرداخت', color: 'bg-amber-50 text-amber-700 border-amber-200' };
case 'failed':
case 'error':
return { label: 'ناموفق', color: 'bg-red-50 text-red-700 border-red-200' };
default:
return { label: 'نامشخص', color: 'bg-gray-50 text-gray-700 border-gray-200' };
}
};
export const getShippingMethodLabel = (method?: string) => {
switch (method?.toLowerCase()) {
case 'express':
case 'tipax': return { label: 'تیپاکس (اکسپرس)', color: 'bg-purple-50 text-purple-700 border-purple-200' };
case 'post':
case 'pishtaz': return { label: 'پست پیشتاز', color: 'bg-blue-50 text-blue-700 border-blue-200' };
case 'courier':
case 'peyk': return { label: 'پیک اختصاصی فوری', color: 'bg-amber-50 text-amber-700 border-amber-200' };
default: return { label: method || 'پست پیشتاز', color: 'bg-gray-50 text-gray-700 border-gray-200' };
}
};
export const getPaymentMethodLabel = (method?: string) => {
switch (method?.toLowerCase()) {
case 'online':
case 'zibal':
case 'online': return { label: 'درگاه زیبال (آنلاین)', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
case 'card_to_card': return { label: 'کارت به کارت', color: 'bg-blue-50 text-blue-700 border-blue-200' };
case 'gateway': return { label: 'درگاه آنلاین (زیبال)', color: 'bg-emerald-50 text-emerald-700 border-emerald-200' };
case 'wallet': return { label: 'کیف پول کنینا', color: 'bg-blue-50 text-blue-700 border-blue-200' };
case 'card':
case 'cart':
case 'card_to_card': return { label: 'کارت به کارت', color: 'bg-indigo-50 text-indigo-700 border-indigo-200' };
case 'cod': return { label: 'پرداخت در محل', color: 'bg-amber-50 text-amber-700 border-amber-200' };
default: return { label: method || 'آنلاین', color: 'bg-gray-50 text-gray-700 border-gray-200' };
}
@ -111,13 +161,44 @@ export default function Orders() {
const [orders, setOrders] = useState<Order[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Queries
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [status, setStatus] = useState('');
// URL Query Params Management
const [searchParams, setSearchParams] = useSearchParams();
// Queries initialized from URL
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
const [search, setSearch] = useState(() => searchParams.get('search') || '');
const [status, setStatus] = useState(() => searchParams.get('status') || '');
const [sortBy, setSortBy] = useState(() => searchParams.get('sortBy') || 'createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc');
const [totalPages, setTotalPages] = useState(1);
const [processingId, setProcessingId] = useState<string | null>(null);
// Sync state changes to URL search params
const updateUrlParams = useCallback((paramsObj: Record<string, string | number | undefined | null>) => {
const current = Object.fromEntries(searchParams.entries());
const merged = { ...current, ...paramsObj };
const cleaned: Record<string, string> = {};
Object.entries(merged).forEach(([key, val]) => {
if (val !== undefined && val !== null && String(val).trim() !== '' && !(key === 'page' && String(val) === '1') && !(key === 'sortBy' && String(val) === 'createdAt') && !(key === 'sortOrder' && String(val) === 'desc')) {
cleaned[key] = String(val);
}
});
setSearchParams(cleaned, { replace: true });
}, [searchParams, setSearchParams]);
const handleSort = (field: string) => {
let newOrder: 'asc' | 'desc' = 'asc';
if (sortBy === field) {
newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
setSortBy(field);
setSortOrder(newOrder);
setPage(1);
updateUrlParams({ sortBy: field, sortOrder: newOrder, page: 1 });
};
// Modal State
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
const [modalTrackingCode, setModalTrackingCode] = useState('');
@ -131,13 +212,14 @@ export default function Orders() {
params.append('page', page.toString());
if (search) params.append('search', search);
if (status) params.append('status', status);
if (sortBy) params.append('sortBy', sortBy);
if (sortOrder) params.append('sortOrder', sortOrder);
const response = await api.get(`/admin/orders?${params.toString()}`);
if (response.data?.success) {
setOrders(response.data.data);
setTotalPages(response.data.meta?.lastPage || 1);
// Update selectedOrder if open without causing re-fetch loop
setSelectedOrder((currentSelected) => {
if (!currentSelected) return null;
const updated = response.data.data.find((o: Order) => o.id === currentSelected.id);
@ -149,12 +231,12 @@ export default function Orders() {
} finally {
setIsLoading(false);
}
}, [page, search, status]);
}, [page, search, status, sortBy, sortOrder]);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
fetchOrders();
}, 500);
}, 400);
return () => clearTimeout(delayDebounceFn);
}, [fetchOrders]);
@ -370,7 +452,12 @@ export default function Orders() {
<select
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-sm"
value={status}
onChange={(e) => { setStatus(e.target.value); setPage(1); }}
onChange={(e) => {
const val = e.target.value;
setStatus(val);
setPage(1);
updateUrlParams({ status: val, page: 1 });
}}
>
<option value="">همه وضعیتها</option>
<option value="pending_payment">در انتظار پرداخت / ناموفق</option>
@ -388,7 +475,12 @@ export default function Orders() {
className="pl-4 pr-10 py-2.5 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium text-sm"
dir="rtl"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
onChange={(e) => {
const val = e.target.value;
setSearch(val);
setPage(1);
updateUrlParams({ search: val, page: 1 });
}}
/>
</div>
</div>
@ -409,7 +501,11 @@ export default function Orders() {
return (
<button
key={tab.value}
onClick={() => { setStatus(tab.value); setPage(1); }}
onClick={() => {
setStatus(tab.value);
setPage(1);
updateUrlParams({ status: tab.value, page: 1 });
}}
className={`px-4 py-2 rounded-xl font-bold text-xs border transition-all cursor-pointer ${
isSelected
? 'bg-purple-600 text-white border-purple-600 shadow-md shadow-purple-600/20'
@ -426,12 +522,36 @@ export default function Orders() {
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right">
<thead className="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200">
<thead className="bg-gray-50 text-gray-500 text-xs uppercase tracking-wider border-b border-gray-200 select-none">
<tr>
<th className="py-4 px-6 font-bold">شماره / کد پیگیری</th>
<th className="py-4 px-6 font-bold">مشتری</th>
<th className="py-4 px-6 font-bold">مبلغ کل</th>
<th className="py-4 px-6 font-bold">تاریخ و ساعت ثبت</th>
<th
onClick={() => handleSort('totalAmount')}
className="py-4 px-6 font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>مبلغ کل</span>
{sortBy === 'totalAmount' ? (
sortOrder === 'asc' ? <ArrowUp className="w-3.5 h-3.5 text-purple-600" /> : <ArrowDown className="w-3.5 h-3.5 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th
onClick={() => handleSort('createdAt')}
className="py-4 px-6 font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>تاریخ و ساعت ثبت</span>
{sortBy === 'createdAt' ? (
sortOrder === 'asc' ? <ArrowUp className="w-3.5 h-3.5 text-purple-600" /> : <ArrowDown className="w-3.5 h-3.5 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th className="py-4 px-6 font-bold">اقلام</th>
<th className="py-4 px-6 font-bold">وضعیت سفارش</th>
<th className="py-4 px-6 font-bold text-center">عملیات ادمین</th>

View File

@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle } from 'lucide-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 { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
@ -84,10 +85,16 @@ export default function Products() {
.replace(/^-|-$/g, '');
};
// Filters
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('');
const [page, setPage] = useState(1);
// URL Query Params Management
const [searchParams, setSearchParams] = useSearchParams();
// Filters initialized from URL
const [search, setSearch] = useState(() => searchParams.get('search') || '');
const [categoryFilter, setCategoryFilter] = useState(() => searchParams.get('category') || '');
const [suitableForFilter, setSuitableForFilter] = useState(() => searchParams.get('suitableFor') || '');
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
const [sortBy, setSortBy] = useState(() => searchParams.get('sortBy') || 'createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc');
const [totalPages, setTotalPages] = useState(1);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const [mediaImageError, setMediaImageError] = useState(false);
@ -99,6 +106,33 @@ export default function Products() {
const [showSymptomSuggestions, setShowSymptomSuggestions] = useState(false);
const limit = 10;
// Sync state changes to URL search params
const updateUrlParams = useCallback((paramsObj: Record<string, string | number | undefined | null>) => {
const current = Object.fromEntries(searchParams.entries());
const merged = { ...current, ...paramsObj };
// Remove empty/default values to keep URL clean
const cleaned: Record<string, string> = {};
Object.entries(merged).forEach(([key, val]) => {
if (val !== undefined && val !== null && String(val).trim() !== '' && !(key === 'page' && String(val) === '1') && !(key === 'sortBy' && String(val) === 'createdAt') && !(key === 'sortOrder' && String(val) === 'desc')) {
cleaned[key] = String(val);
}
});
setSearchParams(cleaned, { replace: true });
}, [searchParams, setSearchParams]);
const handleSort = (field: string) => {
let newOrder: 'asc' | 'desc' = 'asc';
if (sortBy === field) {
newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
setSortBy(field);
setSortOrder(newOrder);
setPage(1);
updateUrlParams({ sortBy: field, sortOrder: newOrder, page: 1 });
};
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
@ -142,14 +176,25 @@ export default function Products() {
const fetchData = useCallback(async () => {
try {
// Fetch products first
const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } });
setIsLoading(true);
// Fetch products with full query params
const prodRes = await api.get('/admin/products', {
params: {
page,
limit,
search: search || undefined,
categoryId: categoryFilter || undefined,
suitableFor: suitableForFilter || undefined,
sortBy,
sortOrder
}
});
if (prodRes.data?.data) {
setProducts(prodRes.data.data);
setTotalPages(prodRes.data.meta?.lastPage || 1);
}
// Fetch categories separately so it doesn't break products
// Fetch categories separately
try {
const catRes = await api.get('/admin/categories');
if (catRes.data?.data) {
@ -166,12 +211,12 @@ export default function Products() {
} finally {
setIsLoading(false);
}
}, [page, search, categoryFilter]);
}, [page, search, categoryFilter, suitableForFilter, sortBy, sortOrder]);
useEffect(() => {
const timer = setTimeout(() => {
fetchData();
}, 500);
}, 400);
return () => clearTimeout(timer);
}, [fetchData]);
@ -248,15 +293,23 @@ export default function Products() {
};
if (editingProduct) {
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
const res = await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
const updated = res.data?.data || res.data;
if (updated && updated.id) {
setProducts(prev => prev.map(p => p.id === editingProduct.id ? { ...p, ...updated } : p));
}
toast.success('محصول با موفقیت ویرایش شد');
} else {
await api.post('/admin/products', finalPayload);
const res = await api.post('/admin/products', finalPayload);
const created = res.data?.data || res.data;
if (created && created.id) {
setProducts(prev => [created, ...prev]);
}
toast.success('محصول با موفقیت ایجاد شد');
}
setIsModalOpen(false);
setImageErrors({});
fetchData();
await fetchData();
} catch (error) {
console.error('Save failed', error);
toast.error('خطا در ذخیره محصول');
@ -297,40 +350,106 @@ export default function Products() {
</button>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
{/* Filters Bar */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در نام و کد محصول..."
placeholder="جستجو حرفه‌ای در نام فارسی، نام انگلیسی (Latin)، کد محصول، بارکد، ترکیبات و علائم..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all"
onChange={(e) => {
const val = e.target.value;
setSearch(val);
setPage(1);
updateUrlParams({ search: val, page: 1 });
}}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all text-sm"
/>
</div>
<div className="relative w-full sm:w-64">
<div className="flex flex-wrap sm:flex-nowrap gap-3">
<div className="relative w-full sm:w-56">
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<select
value={categoryFilter}
onChange={(e) => { setCategoryFilter(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none"
onChange={(e) => {
const val = e.target.value;
setCategoryFilter(val);
setPage(1);
updateUrlParams({ category: val, page: 1 });
}}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none bg-white text-sm"
>
<option value="">همه دستهبندیها</option>
{categories.map(c => <option key={c.id} value={c.id}>{typeof c === 'string' ? c : (c.nameFa || c.name)}</option>)}
</select>
</div>
<div className="relative w-full sm:w-44">
<select
value={suitableForFilter}
onChange={(e) => {
const val = e.target.value;
setSuitableForFilter(val);
setPage(1);
updateUrlParams({ suitableFor: val, page: 1 });
}}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none bg-white text-sm"
>
<option value="">همه گونهها</option>
<option value="سگ">مخصوص سگ</option>
<option value="گربه">مخصوص گربه</option>
<option value="سگ و گربه">سگ و گربه</option>
</select>
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right">
<thead className="bg-gray-50 border-b border-gray-100">
<thead className="bg-gray-50 border-b border-gray-100 select-none">
<tr>
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">کد (Art No)</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">نام محصول</th>
<th
onClick={() => handleSort('artNo')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>کد (Art No)</span>
{sortBy === 'artNo' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th
onClick={() => handleSort('nameFa')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>نام محصول</span>
{sortBy === 'nameFa' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">دستهبندی</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">موجودی / قیمت</th>
<th
onClick={() => handleSort('priceValue')}
className="py-4 px-6 text-sm font-bold text-gray-700 hover:text-purple-600 cursor-pointer transition-colors"
>
<div className="flex items-center gap-1.5">
<span>موجودی / قیمت</span>
{sortBy === 'priceValue' ? (
sortOrder === 'asc' ? <ArrowUp className="w-4 h-4 text-purple-600" /> : <ArrowDown className="w-4 h-4 text-purple-600" />
) : (
<ArrowUpDown className="w-3.5 h-3.5 text-gray-400 opacity-50" />
)}
</div>
</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
</tr>
</thead>
@ -434,7 +553,7 @@ export default function Products() {
{ id: 'general', label: 'اطلاعات پایه' },
{ id: 'pricing', label: 'موجودی و قیمت' },
{ id: 'seo', label: 'سئو (SEO)' },
{ id: 'media', label: 'تصاویر' }
{ id: 'media', label: 'رسانه' }
].map(tab => (
<button
key={tab.id}

View File

@ -36,6 +36,15 @@ export async function generateMetadata(): Promise<Metadata> {
const ogImg = seoSettings?.ogImageUrl || '/assets/images/regenerated_image_1779109861747.png';
const isStaging =
process.env.NEXT_PUBLIC_APP_ENV === 'staging' ||
process.env.NODE_ENV !== 'production' ||
(typeof process.env.NEXT_PUBLIC_SITE_URL === 'string' && (
process.env.NEXT_PUBLIC_SITE_URL.includes('stage') ||
process.env.NEXT_PUBLIC_SITE_URL.includes('test') ||
process.env.NEXT_PUBLIC_SITE_URL.includes('dev')
));
return {
metadataBase: new URL(siteUrl),
title: {
@ -47,6 +56,26 @@ export async function generateMetadata(): Promise<Metadata> {
authors: [{ name: 'Canina Iran' }],
creator: 'Canina Iran',
publisher: 'Canina Iran',
robots: isStaging ? {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
follow: false,
noimageindex: true,
}
} : {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
}
},
alternates: {
canonical: '/',
},
@ -77,17 +106,6 @@ export async function generateMetadata(): Promise<Metadata> {
description: descriptionDefault,
images: [ogImg],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
}

View File

@ -1,7 +1,22 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canino-iran.com';
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://canina-iran.com';
const isStaging =
process.env.NEXT_PUBLIC_APP_ENV === 'staging' ||
process.env.NODE_ENV !== 'production' ||
baseUrl.includes('stage') ||
baseUrl.includes('test') ||
baseUrl.includes('dev');
if (isStaging) {
return {
rules: {
userAgent: '*',
disallow: '/',
},
};
}
return {
rules: {

View File

@ -27,7 +27,8 @@ import {
Thermometer,
AlertTriangle,
Share2,
Phone
Phone,
Download
} from "lucide-react";
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
@ -454,8 +455,14 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
<Clock className="w-6 h-6" />
</div>
<div>
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">ماشین حساب هوشمند مصرف</h3>
<p className="text-[11px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس مشخصات پت شما</p>
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
{activePet ? `ماشین حساب هوشمند مصرف برای ${activePet.name}` : 'ماشین حساب هوشمند مصرف'}
</h3>
<p className="text-[11px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">
{activePet
? `تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس وزن و سن ${activePet.name}`
: 'تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس مشخصات پت شما'}
</p>
</div>
</div>
@ -463,7 +470,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{/* Manual Slider Inputs */}
<div className="bg-medical-gray-50 rounded-[2rem] p-6 border border-medical-gray-100 space-y-6">
<div className="flex items-center justify-between">
<span className="text-xs font-black text-medical-gray-500 font-vazir">وزن پت (کیلوگرم)</span>
<span className="text-xs font-black text-medical-gray-500 font-vazir">
{activePet ? `وزن ${activePet.name} (کیلوگرم)` : 'وزن پت (کیلوگرم)'}
</span>
<span className="text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
</div>
<input
@ -475,7 +484,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
/>
<div className="space-y-2">
<span className="text-[10px] font-black text-medical-gray-400 font-vazir">سن حیوان</span>
<span className="text-[10px] font-black text-medical-gray-400 font-vazir">
{activePet ? `سن ${activePet.name}` : 'سن حیوان'}
</span>
<div className="flex gap-2">
<button onClick={() => setPetType("young")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "young" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>جوان</button>
<button onClick={() => setPetType("adult")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "adult" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>بالغ</button>
@ -488,7 +499,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
<div className="flex items-center justify-between mb-4 opacity-90 border-b border-white/10 pb-3">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-300" />
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">محاسبهگر دوره درمانی (AI Calculator)</span>
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">
{activePet ? `محاسبه‌گر دوره درمانی برای ${activePet.name}` : 'محاسبه‌گر دوره درمانی (AI Calculator)'}
</span>
</div>
<span className="text-[10px] bg-white/20 px-2.5 py-0.5 rounded-full font-black">
پیشنهاد: {toPersian(itemQuantity)} بسته
@ -514,7 +527,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
<p className="text-[10px] text-white/60 font-bold font-vazir mt-3 leading-relaxed">
* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بستههای پیشنهادی توصیه میشود.
{activePet
? `* دوز دقیق بر اساس وزن ${activePet.name} (${toPersian(weight)} ک‌گ) محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.`
: '* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.'}
</p>
</div>
</div>
@ -659,6 +674,100 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
</section>
{/* Multimedia & Scientific Documentation (Video, Podcast & PDF Catalog) */}
{(product.videoUrl || product.podcastUrl || product.pdfUrl) && (
<section className="bg-white border border-medical-gray-200 rounded-[2.5rem] p-8 md:p-10 shadow-md space-y-8 font-vazir" dir="rtl">
<div className="flex items-center gap-4 border-b border-medical-gray-100 pb-6">
<div className="w-12 h-12 bg-purple-600 text-white rounded-2xl flex items-center justify-center shadow-lg">
<Sparkles className="w-6 h-6" />
</div>
<div>
<h3 className="text-2xl md:text-3xl font-black text-medical-gray-900 italic">رسانه و مستندات تخصصی محصول</h3>
<p className="text-xs text-medical-gray-400 font-bold mt-1">ویدیو آموزشی، پادکست تحلیل علمی و دفترچه کاتالوگ کارخانه Canina</p>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
{/* Video Player Box */}
{product.videoUrl && (
<div className="bg-gray-900 rounded-3xl overflow-hidden shadow-xl border border-gray-800 p-4 space-y-3">
<div className="flex items-center justify-between px-2 text-white">
<span className="text-sm font-bold flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse"></span>
ویدیو معرفی و راهنمای محصول
</span>
<span className="text-[11px] text-gray-400 font-mono">Video Guide</span>
</div>
<div className="relative w-full aspect-video rounded-2xl overflow-hidden bg-black flex items-center justify-center">
{product.videoUrl.includes('<iframe') ? (
<div
className="w-full h-full [&>iframe]:w-full [&>iframe]:h-full"
dangerouslySetInnerHTML={{ __html: product.videoUrl }}
/>
) : (
<video
src={product.videoUrl}
controls
playsInline
className="w-full h-full object-contain"
poster={product.image}
>
مرورگر شما از پخش ویدیو پشتیبانی نمیکند.
</video>
)}
</div>
</div>
)}
{/* Podcast / Audio Player Box */}
{product.podcastUrl && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50/60 border border-blue-200 rounded-3xl p-6 space-y-4 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-canina-blue text-white flex items-center justify-center shadow-md">
🎧
</div>
<div>
<h4 className="text-sm font-black text-gray-900">پادکست و بررسی صوتی بالینی مکمل</h4>
<p className="text-[11px] text-gray-500 font-bold">توضیحات صوتی دامپزشک درباره اثربخشی و دوز مصرفی</p>
</div>
</div>
<span className="text-[10px] bg-canina-blue/10 text-canina-blue px-3 py-1 rounded-full font-bold">پادکست اختصاصی</span>
</div>
<audio controls className="w-full h-10 rounded-xl accent-canina-blue">
<source src={product.podcastUrl} />
مرورگر شما از پخش صوت پشتیبانی نمیکند.
</audio>
</div>
)}
{/* PDF Catalog Download Box */}
{product.pdfUrl && (
<div className="bg-medical-gray-50 border border-medical-gray-200 rounded-3xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4 shadow-xs">
<div className="flex items-center gap-4 text-center sm:text-right">
<div className="w-12 h-12 rounded-2xl bg-red-100 text-red-600 flex items-center justify-center font-black text-xs shadow-sm shrink-0">
PDF
</div>
<div>
<h4 className="text-base font-black text-gray-900">بروشور و کاتالوگ علمی Canina آلمان</h4>
<p className="text-xs text-gray-500 font-medium mt-0.5">شامل مقالات رفرنس، جدول ترکیبات دقیق و سرتیفیکیت رسمی</p>
</div>
</div>
<a
href={product.pdfUrl}
target="_blank"
rel="noreferrer"
className="bg-canina-blue hover:bg-blue-700 text-white px-6 py-3 rounded-2xl font-black text-xs transition-all shadow-md shadow-canina-blue/20 flex items-center gap-2 shrink-0 cursor-pointer"
>
<Download className="w-4 h-4" />
<span>دانلود مستقیم فایل PDF</span>
</a>
</div>
)}
</div>
</section>
)}
{/* FAQ Section */}
{product.faqs && product.faqs.length > 0 && (
<section className="pt-10">

View File

@ -61,6 +61,9 @@ export interface Product {
specialist: Specialist;
image: string;
images?: string[];
podcastUrl?: string;
videoUrl?: string;
pdfUrl?: string;
relatedProducts?: string[];
faqs?: FAQ[];
}

View File

@ -39,6 +39,9 @@ interface BackendProduct {
imageUrl?: string;
image?: string;
images?: string[];
podcastUrl?: string;
videoUrl?: string;
pdfUrl?: string;
productGroup?: string;
ingredientList?: Array<{ ingredient: string }>;
ingredients?: string;
@ -147,6 +150,27 @@ export class ProductService {
specialist: safeParse<Product['specialist']>(data.specialist, defaultSpecialist),
calculateDosage: local?.calculateDosage,
podcastUrl: (() => {
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
if (!data.podcastUrl) return undefined;
if (data.podcastUrl.startsWith('http://') || data.podcastUrl.startsWith('https://')) return data.podcastUrl;
if (data.podcastUrl.startsWith('/uploads/')) return `${apiBase}${data.podcastUrl}`;
return data.podcastUrl;
})(),
videoUrl: (() => {
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
if (!data.videoUrl) return undefined;
if (data.videoUrl.startsWith('http://') || data.videoUrl.startsWith('https://')) return data.videoUrl;
if (data.videoUrl.startsWith('/uploads/')) return `${apiBase}${data.videoUrl}`;
return data.videoUrl;
})(),
pdfUrl: (() => {
const apiBase = (process.env.NEXT_PUBLIC_API_URL || '').replace(/\/api$/, '');
if (!data.pdfUrl) return undefined;
if (data.pdfUrl.startsWith('http://') || data.pdfUrl.startsWith('https://')) return data.pdfUrl;
if (data.pdfUrl.startsWith('/uploads/')) return `${apiBase}${data.pdfUrl}`;
return data.pdfUrl;
})(),
};
}

View File

@ -1,293 +1,289 @@
{
"0": "SettingsService",
"1": "AdminController",
"2": "UsersService",
"3": "ProductService",
"4": "admin.module.ts",
"5": "auth.service.ts",
"6": "OrdersService",
"7": "app.module.ts",
"8": "CmsController",
"9": "tickets.controller.ts",
"10": "useSettingsStore",
"11": "UserDashboard.tsx",
"12": "CreateVideoDto",
"13": "compilerOptions",
"14": "CreateReviewDto",
"0": "AdminService",
"1": "ProductService",
"2": "CmsController",
"3": "app.module.ts",
"4": "CreateReviewDto",
"5": "tickets.controller.ts",
"6": "UserDashboard.tsx",
"7": "Spinner.tsx",
"8": "admin.module.ts",
"9": "useCartStore",
"10": "DoctorsService",
"11": "useSettingsStore",
"12": "adminRoutes.tsx",
"13": "PrismaService",
"14": "PaginationDto",
"15": "ProductsService",
"16": "PrismaService",
"17": "lib/services/api.ts",
"18": "devDependencies",
"19": "JwtAuthGuard",
"16": "lib/services/api.ts",
"17": "CreateVideoDto",
"18": "src/services/api.ts",
"19": "devDependencies",
"20": "BE-001",
"21": "src/services/api.ts",
"22": "RedisService",
"23": "WholesaleApplyDto",
"24": "FE-001",
"25": "ADM-001",
"26": "DB-001",
"27": "TS-001",
"28": "TEST-001",
"29": "DEVOPS-001",
"30": "DOC-001",
"31": "adminRoutes.tsx",
"32": "userStore.ts",
"33": "Spinner.tsx",
"34": "PetProfile.tsx",
"35": "main.ts",
"36": "B2BService",
"37": "ZibalService",
"38": "راهنمای تست سیستم (Software Testing)",
"39": "CategoriesController",
"40": "MediaController",
"41": "What You Must Do When Invoked",
"42": "SslController",
"43": "BannersService",
"44": "What You Must Do When Invoked",
"45": "UITexts.tsx",
"46": "IngredientsService",
"47": ".update",
"48": "HomeClient.tsx",
"21": "Roles",
"22": "FE-001",
"23": "ADM-001",
"24": "DB-001",
"25": "TS-001",
"26": "TEST-001",
"27": "DEVOPS-001",
"28": "DOC-001",
"29": "wholesale.controller.ts",
"30": "main.ts",
"31": "JwtAuthGuard",
"32": "ZibalService",
"33": "راهنمای تست سیستم (Software Testing)",
"34": "CategoriesController",
"35": "B2BService",
"36": "What You Must Do When Invoked",
"37": "BannersService",
"38": "UsersService",
"39": "What You Must Do When Invoked",
"40": "SslController",
"41": "toPersian",
"42": "IngredientsService",
"43": "AuthService",
"44": "MediaController",
"45": "ConfirmModal.tsx",
"46": "auth.controller.ts",
"47": "SmsService",
"48": "PrescriptionsService",
"49": "SmartAdvisorService",
"50": "TestimonialsService",
"51": "Role & Core Objective",
"52": "PaginationDto",
"53": "ContactService",
"52": "ContactService",
"53": "PaymentController",
"54": "compilerOptions",
"55": "Reports.tsx",
"55": "Media.tsx",
"56": "compilerOptions",
"57": "dependencies",
"58": "PrescriptionsService",
"59": "BlogsController",
"60": "Required Review Group Closures",
"61": "devDependencies",
"62": "Coupons.tsx",
"63": "Operational Rules & Boundaries",
"64": "Operational Rules & Boundaries",
"65": "WikiController",
"66": "PaymentController",
"67": "pets/pets.controller.ts",
"68": "seo.module.ts",
"69": "admin.service.ts",
"70": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"71": "Operational Rules & Boundaries",
"72": "Operational Rules & Boundaries",
"73": "HomeController",
"74": "scripts",
"75": "Role & Core Objective",
"76": ".handleZibalCallback",
"77": "zibal.service.ts",
"78": "Orders.tsx",
"79": "devDependencies",
"80": "exclude",
"81": "seed-products.ts",
"82": "Reconciled Audit Roles & Assignments",
"83": "20260526145407_init/migration.sql",
"84": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"85": "dependencies",
"86": "SmsService",
"87": "SafeImage.tsx",
"88": "compilerOptions",
"89": "scripts",
"90": "AdminService",
"91": "Deep Audit Summary Report",
"92": "dependencies",
"93": "Operational Rules & Boundaries",
"94": "jest",
"95": "PetsController",
"96": "Comprehensive Change Log",
"97": "Operational Rules & Boundaries",
"98": "BlogsController",
"99": "PetsService",
"100": "1. Summary of Integrity Repairs Performed",
"101": "Operational Rules & Boundaries",
"102": "Operational Rules & Boundaries",
"103": "Operational Rules & Boundaries",
"104": "AppService",
"105": "AdminTransactionFilterDto",
"106": "Vazirmatn Changelog",
"107": "Vazirmatn Font فونت وزیرمتن",
"57": "PetsController",
"58": "WikiController",
"59": "dependencies",
"60": "compilerOptions",
"61": "HomeClient.tsx",
"62": "BlogsController",
"63": "AuthController",
"64": "PetsService",
"65": "Required Review Group Closures",
"66": "Coupons.tsx",
"67": "Operational Rules & Boundaries",
"68": "Operational Rules & Boundaries",
"69": "WikiController",
"70": "pets/pets.controller.ts",
"71": "PetsController",
"72": "seo.module.ts",
"73": "BlogsService",
"74": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"75": "Operational Rules & Boundaries",
"76": "Operational Rules & Boundaries",
"77": "scripts",
"78": "Role & Core Objective",
"79": "HomeController",
"80": "zibal.service.ts",
"81": "devDependencies",
"82": "api",
"83": "Orders.tsx",
"84": "devDependencies",
"85": "seed-products.ts",
"86": "Reconciled Audit Roles & Assignments",
"87": "20260526145407_init/migration.sql",
"88": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"89": "dependencies",
"90": "VetGallery.tsx",
"91": "compilerOptions",
"92": "scripts",
"93": "Deep Audit Summary Report",
"94": "dependencies",
"95": "Operational Rules & Boundaries",
"96": "exclude",
"97": "jest",
"98": "WikiService",
"99": "Comprehensive Change Log",
"100": "Operational Rules & Boundaries",
"101": ".findAll",
"102": "SmsLogQueryDto",
"103": "ZibalCallbackQueryDto",
"104": "VerifyOtpDto",
"105": "1. Summary of Integrity Repairs Performed",
"106": "CreateHealthLogDto",
"107": "Operational Rules & Boundaries",
"108": "Operational Rules & Boundaries",
"109": "backend/README.md",
"110": "Repository Map",
"111": "validate_integrity.js",
"112": "admin-panel/package.json",
"113": "Sahel-Font",
"114": "Sahel-Font",
"115": "Role & Core Objective",
"116": "orchestrate.py",
"117": "backend/package.json",
"118": "graphify reference: extra exports and benchmark",
"119": "Phase 2 Final Quality Gate Summary Report",
"120": "Task Modifications Log",
"121": "Install",
"122": "ErrorBoundary",
"123": "application/package.json",
"124": "generate-openapi.js",
"125": "InitiatePaymentDto",
"126": "SmsLogQueryDto",
"127": "AuthService",
"128": "System Discovery",
"129": "Product Requirement Document (PRD)",
"130": "videos.controller.ts",
"131": "CreateHealthLogDto",
"132": "CreateReminderDto",
"133": "reviews.controller.ts",
"134": "Baseline Command Plan & Reconciled Command History",
"135": "SmsSettingsPage.tsx",
"136": "ErrorPages.tsx",
"137": "compilerOptions",
"138": "with-vpn.sh",
"139": "Architecture Specification",
"140": "Project Health Audit Report",
"141": "nest-cli.json",
"142": "graphify reference: query, path, explain",
"143": "Open Questions",
"144": "Final Phase 2 Audit Closure Report",
"145": "Transactions.tsx",
"146": "open-browsers.js",
"147": "start-dev.js",
"148": "📝 Active Agent Working Scratchpad",
"149": "🔍 Code Health Audit Review (01_auditor)",
"150": "paginated-response.schema.ts",
"151": "PetsController",
"152": "Vazirmatn Font README",
"153": "Omitted File Inspection Report",
"154": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"155": "Phase 3 Audit Traceability Matrix",
"156": "rebuild_honest_ledger.js",
"157": "validate_evidence_grade.js",
"158": "api",
"159": "Reviews.tsx",
"160": "compilerOptions",
"161": "blog/[slug]/page.tsx",
"162": "WikiService",
"163": "API Contract Specification",
"164": "⚙️ Backend Technical Review (05_dev_backend)",
"165": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"166": "🚀 SEO & Content Strategy Review (12_seo_content)",
"167": "@types/node",
"168": "typescript",
"169": "seed-ui-texts.ts",
"170": "seed-wiki.ts",
"171": "update-blog.dto.ts",
"172": "update-home.dto.ts",
"173": "update-wiki.dto.ts",
"174": "graphify reference: add a URL and watch a folder",
"175": "graphify reference: commit hook and native CLAUDE.md integration",
"176": "graphify reference: incremental update and cluster-only",
"177": "Raw Finding Verification & Disposition Report",
"178": "React + TypeScript + Vite",
"179": "Select.tsx",
"180": "wiki/[slug]/page.tsx",
"181": "application/README.md",
"182": "NetworkBanner.tsx",
"183": "deploy.sh",
"184": "🔒 Security & Performance Review (09_devops_security)",
"185": "👁️ UX & Persona Interface Review (08_visual_qa)",
"186": "@eslint/eslintrc",
"187": "typescript-eslint",
"188": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"189": "prisma/scientificTerms.ts",
"190": "seed-blogs.ts",
"191": "seed-custom.ts",
"192": "graphify reference: GitHub clone and cross-repo merge",
"193": "graphify reference: transcribe video and audio",
"194": "Compiler Diagnostic Dispositions",
"195": "Master Task Backlog (Phase 3.3)",
"196": "build_manifest.js",
"197": "generate_classification.js",
"198": "generate_evidence.js",
"199": "generate_ledger.js",
"200": "generate_manifest.js",
"201": "sync_honest_manifest.js",
"202": "sync_manifest.js",
"203": "eslint-plugin-prettier",
"204": "globals",
"205": "tailwindcss",
"206": "@nestjs/cli",
"207": "FormField.tsx",
"208": "Input.tsx",
"209": "Textarea.tsx",
"210": "admin-panel/tsconfig.json",
"211": "about/page.tsx",
"212": "privacy/page.tsx",
"213": "next.config.ts",
"214": "Shabnam Font README",
"215": "AGENTS.md",
"216": "rules/graphify.md",
"217": ".agents/workflows/graphify.md",
"218": "instructions.md",
"219": "prettier",
"220": "prisma",
"221": "supertest",
"222": "ts-node",
"223": "@types/express",
"224": "@types/jest",
"225": "@types/js-yaml",
"226": "@types/multer",
"227": "@types/passport-jwt",
"228": "eslint-config-next",
"229": "eslint-plugin-react-hooks",
"230": "eslint-plugin-react-refresh",
"231": "ts-loader",
"232": "@tailwindcss/postcss",
"233": "@types/bcrypt",
"234": "@types/supertest",
"235": "blog.entity.ts",
"236": "home.entity.ts",
"237": "wiki.entity.ts",
"238": "User Profile Photo",
"239": "CLAUDE.md",
"240": ".claude/CLAUDE.md",
"241": "extraction-spec.md",
"242": "Products Table",
"243": "Users Table",
"244": "Architectural Audit Findings",
"245": "Cross Boundary Dependencies & Backend Architecture Specification",
"246": "Next.js Agent Rules & Brand Guidelines",
"247": "robots.ts",
"248": "application/eslint.config.mjs",
"249": "postcss.config.mjs",
"250": "vitest.setup.ts",
"251": "backup_db.sh",
"252": "start.sh",
"253": "reviews/README.md",
"254": "backend/eslint.config.mjs",
"255": "User Login API",
"256": "User Logout API",
"257": "generate-openapi.d.ts",
"258": "ApiOperation",
"259": "typescript",
"260": "@testing-library/jest-dom",
"261": "@testing-library/react",
"262": "@types/react",
"263": "typescript",
"264": "Roles",
"265": "Get",
"266": "vitest",
"267": "axios",
"268": "tailwindcss",
"279": "Canina Pharma GmbH",
"280": "Pets Table",
"281": "Canina Iran Project Introduction",
"282": "Developer Standards and Architecture",
"283": "Frontend & Admin Architecture Route Map Specification",
"284": "Project Backlog and Tasks",
"285": "eslint.config.js",
"286": "postcss.config.js",
"287": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"288": "shabnam-font-v5.0.1/CHANGELOG.md",
"289": "tailwind.config.js",
"290": "vite.config.ts",
"291": "application/CLAUDE.md",
"292": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"293": "Sahel Font Sample",
"294": "Shabnam Font Changelog",
"295": "Vazirmatn Changelog",
"296": "vitest.config.ts",
"297": "Sahel Font Variable Sample",
"298": "Shabnam Font Sample",
"299": "Production Docker Compose",
"300": "Staging Docker Compose"
"109": "Operational Rules & Boundaries",
"110": "AppService",
"111": "CreateReminderDto",
"112": "@eslint/eslintrc",
"113": "Vazirmatn Changelog",
"114": "Vazirmatn Font فونت وزیرمتن",
"115": "Operational Rules & Boundaries",
"116": "compilerOptions",
"117": "compilerOptions",
"118": "backend/README.md",
"119": "eslint-plugin-prettier",
"120": "Repository Map",
"121": "validate_integrity.js",
"122": "admin-panel/package.json",
"123": "Sahel-Font",
"124": "Reports.tsx",
"125": "Sahel-Font",
"126": "Role & Core Objective",
"127": "orchestrate.py",
"128": "backend/package.json",
"129": "RegisterDto",
"130": "graphify reference: extra exports and benchmark",
"131": "Phase 2 Final Quality Gate Summary Report",
"132": "Task Modifications Log",
"133": "Install",
"134": "ErrorBoundary",
"135": "application/package.json",
"136": "generate-openapi.js",
"137": "payment.service.ts",
"138": "InitiatePaymentDto",
"139": "System Discovery",
"140": "Product Requirement Document (PRD)",
"141": "AdminLoginDto",
"142": "globals",
"143": "@nestjs/cli",
"144": "Baseline Command Plan & Reconciled Command History",
"145": "SmsSettingsPage.tsx",
"146": "ErrorPages.tsx",
"147": "with-vpn.sh",
"148": "Architecture Specification",
"149": "Project Health Audit Report",
"150": "nest-cli.json",
"151": "prettier",
"152": "graphify reference: query, path, explain",
"153": "Open Questions",
"154": "Final Phase 2 Audit Closure Report",
"155": "prisma",
"156": "open-browsers.js",
"157": "start-dev.js",
"158": "📝 Active Agent Working Scratchpad",
"159": "🔍 Code Health Audit Review (01_auditor)",
"160": "paginated-response.schema.ts",
"161": "Vazirmatn Font README",
"162": "Omitted File Inspection Report",
"163": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"164": "Phase 3 Audit Traceability Matrix",
"165": "rebuild_honest_ledger.js",
"166": "validate_evidence_grade.js",
"167": "supertest",
"168": "blog/[slug]/page.tsx",
"169": "API Contract Specification",
"170": "⚙️ Backend Technical Review (05_dev_backend)",
"171": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"172": "🚀 SEO & Content Strategy Review (12_seo_content)",
"173": "@types/node",
"174": "typescript",
"175": "seed-ui-texts.ts",
"176": "seed-wiki.ts",
"177": "update-blog.dto.ts",
"178": "update-home.dto.ts",
"179": "update-wiki.dto.ts",
"180": "graphify reference: add a URL and watch a folder",
"181": "graphify reference: commit hook and native CLAUDE.md integration",
"182": "graphify reference: incremental update and cluster-only",
"183": "Raw Finding Verification & Disposition Report",
"184": "React + TypeScript + Vite",
"185": "Select.tsx",
"186": "wiki/[slug]/page.tsx",
"187": "ts-node",
"188": "application/README.md",
"189": "@types/express",
"190": "deploy.sh",
"191": "🔒 Security & Performance Review (09_devops_security)",
"192": "👁️ UX & Persona Interface Review (08_visual_qa)",
"193": "@types/jest",
"194": "typescript-eslint",
"195": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"196": "prisma/scientificTerms.ts",
"197": "seed-blogs.ts",
"198": "seed-custom.ts",
"199": "@types/js-yaml",
"200": "graphify reference: GitHub clone and cross-repo merge",
"201": "graphify reference: transcribe video and audio",
"202": "Compiler Diagnostic Dispositions",
"203": "Master Task Backlog (Phase 3.3)",
"204": "build_manifest.js",
"205": "generate_classification.js",
"206": "generate_evidence.js",
"207": "generate_ledger.js",
"208": "generate_manifest.js",
"209": "sync_honest_manifest.js",
"210": "sync_manifest.js",
"211": "@types/multer",
"212": "@types/passport-jwt",
"213": "tailwindcss",
"214": "MetricsController",
"215": "eslint-config-next",
"216": "FormField.tsx",
"217": "Input.tsx",
"218": "Textarea.tsx",
"219": "admin-panel/tsconfig.json",
"220": "about/page.tsx",
"221": "privacy/page.tsx",
"222": "next.config.ts",
"223": "Shabnam Font README",
"224": "AGENTS.md",
"225": "rules/graphify.md",
"226": ".agents/workflows/graphify.md",
"227": "instructions.md",
"228": "eslint-plugin-react-hooks",
"229": "eslint-plugin-react-refresh",
"230": "@tailwindcss/postcss",
"231": "typescript",
"232": "@testing-library/jest-dom",
"233": "@testing-library/react",
"234": "@types/react",
"235": "typescript",
"236": "vitest",
"237": "axios",
"238": "tailwindcss",
"240": "ts-loader",
"242": "@types/bcrypt",
"243": "@types/supertest",
"244": "blog.entity.ts",
"245": "home.entity.ts",
"246": "wiki.entity.ts",
"247": "User Profile Photo",
"248": "CLAUDE.md",
"249": ".claude/CLAUDE.md",
"250": "extraction-spec.md",
"251": "Products Table",
"252": "Users Table",
"253": "Architectural Audit Findings",
"254": "Cross Boundary Dependencies & Backend Architecture Specification",
"255": "Next.js Agent Rules & Brand Guidelines",
"256": "robots.ts",
"257": "application/eslint.config.mjs",
"258": "postcss.config.mjs",
"259": "vitest.setup.ts",
"260": "backup_db.sh",
"261": "start.sh",
"262": "reviews/README.md",
"263": "backend/eslint.config.mjs",
"264": "User Login API",
"265": "User Logout API",
"266": "generate-openapi.d.ts",
"267": "Canina Pharma GmbH",
"268": "Pets Table",
"269": "Canina Iran Project Introduction",
"270": "Developer Standards and Architecture",
"271": "Frontend & Admin Architecture Route Map Specification",
"272": "Project Backlog and Tasks",
"273": "eslint.config.js",
"274": "postcss.config.js",
"275": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"276": "shabnam-font-v5.0.1/CHANGELOG.md",
"277": "tailwind.config.js",
"278": "vite.config.ts",
"279": "application/CLAUDE.md",
"280": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"281": "Sahel Font Sample",
"282": "Shabnam Font Changelog",
"283": "Vazirmatn Changelog",
"284": "vitest.config.ts",
"285": "Sahel Font Variable Sample",
"286": "Shabnam Font Sample",
"287": "Production Docker Compose",
"288": "Staging Docker Compose"
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
{
"0": "admin.service.ts",
"1": "ProductService",
"2": "CmsController",
"3": "app.module.ts",
"4": "reviews.controller.ts",
"5": "tickets.controller.ts",
"6": "UserDashboard.tsx",
"7": "Spinner.tsx",
"8": "admin.module.ts",
"9": "PetProfile.tsx",
"10": "userStore.ts",
"11": "useSettingsStore",
"12": "adminRoutes.tsx",
"13": "PrismaService",
"14": "PaginationDto",
"15": "ProductsService",
"16": "lib/services/api.ts",
"17": "CreateVideoDto",
"18": "src/services/api.ts",
"19": "devDependencies",
"20": "BE-001",
"21": "Roles",
"22": "FE-001",
"23": "ADM-001",
"24": "DB-001",
"25": "TS-001",
"26": "TEST-001",
"27": "DEVOPS-001",
"28": "DOC-001",
"29": "WholesaleService",
"30": "main.ts",
"31": "JwtAuthGuard",
"32": "ZibalService",
"33": "راهنمای تست سیستم (Software Testing)",
"34": "CategoriesController",
"35": "B2BService",
"36": "What You Must Do When Invoked",
"37": "BannersService",
"38": "UsersController",
"39": "What You Must Do When Invoked",
"40": "SslController",
"41": "SettingsService",
"42": "IngredientsService",
"43": "RedisService",
"44": "MediaController",
"45": "auth.module.ts",
"46": "auth.service.ts",
"47": "OrdersService",
"48": "PrescriptionsService",
"49": "SmartAdvisorService",
"50": "TestimonialsService",
"51": "Role & Core Objective",
"52": "ContactService",
"53": "PaymentController",
"54": "compilerOptions",
"55": "UITexts.tsx",
"56": "compilerOptions",
"57": "PetsController",
"58": "WikiController",
"59": "dependencies",
"60": "compilerOptions",
"61": "HomeClient.tsx",
"62": "BlogsController",
"63": "AuthController",
"64": "SmsService",
"65": "Required Review Group Closures",
"66": "Coupons.tsx",
"67": "Operational Rules & Boundaries",
"68": "Operational Rules & Boundaries",
"69": "WikiController",
"70": "pets/pets.controller.ts",
"71": "PetsController",
"72": "seo.module.ts",
"73": "UsersService",
"74": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"75": "Operational Rules & Boundaries",
"76": "Operational Rules & Boundaries",
"77": "scripts",
"78": "Role & Core Objective",
"79": "HomeController",
"80": "zibal.service.ts",
"81": "devDependencies",
"82": "api",
"83": "Orders.tsx",
"84": "devDependencies",
"85": "seed-products.ts",
"86": "Reconciled Audit Roles & Assignments",
"87": "20260526145407_init/migration.sql",
"88": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"89": "dependencies",
"90": "SafeImage.tsx",
"91": "compilerOptions",
"92": "scripts",
"93": "Deep Audit Summary Report",
"94": "dependencies",
"95": "Operational Rules & Boundaries",
"96": "exclude",
"97": "jest",
"98": "ApiOperation",
"99": "Comprehensive Change Log",
"100": "Operational Rules & Boundaries",
"101": "BlogsController",
"102": "SmsLogQueryDto",
"103": "ZibalCallbackQueryDto",
"104": "Body",
"105": "1. Summary of Integrity Repairs Performed",
"106": "include",
"107": "Operational Rules & Boundaries",
"108": "Operational Rules & Boundaries",
"109": "Operational Rules & Boundaries",
"110": "AppService",
"111": "AdminController",
"112": "AdminService",
"113": "Vazirmatn Changelog",
"114": "Vazirmatn Font فونت وزیرمتن",
"115": "Operational Rules & Boundaries",
"116": "compilerOptions",
"117": "compilerOptions",
"118": "backend/README.md",
"119": "ProductDto",
"120": "Repository Map",
"121": "validate_integrity.js",
"122": "admin-panel/package.json",
"123": "Sahel-Font",
"124": "Reports.tsx",
"125": "Sahel-Font",
"126": "Role & Core Objective",
"127": "orchestrate.py",
"128": "backend/package.json",
"129": "RegisterDto",
"130": "graphify reference: extra exports and benchmark",
"131": "Phase 2 Final Quality Gate Summary Report",
"132": "Task Modifications Log",
"133": "Install",
"134": "ErrorBoundary",
"135": "application/package.json",
"136": "generate-openapi.js",
"137": "AdminTransactionFilterDto",
"138": "InitiatePaymentDto",
"139": "System Discovery",
"140": "Product Requirement Document (PRD)",
"141": "AdminLoginDto",
"142": "AddressDto",
"143": "WholesaleApplyDto",
"144": "Baseline Command Plan & Reconciled Command History",
"145": "SmsSettingsPage.tsx",
"146": "ErrorPages.tsx",
"147": "with-vpn.sh",
"148": "Architecture Specification",
"149": "Project Health Audit Report",
"150": "nest-cli.json",
"151": "Get",
"152": "graphify reference: query, path, explain",
"153": "Open Questions",
"154": "Final Phase 2 Audit Closure Report",
"155": "Transactions.tsx",
"156": "open-browsers.js",
"157": "start-dev.js",
"158": "📝 Active Agent Working Scratchpad",
"159": "🔍 Code Health Audit Review (01_auditor)",
"160": "paginated-response.schema.ts",
"161": "Vazirmatn Font README",
"162": "Omitted File Inspection Report",
"163": "Phase 3.2 / 3.3 — Implementation Readiness & Architectural Finalization Report",
"164": "Phase 3 Audit Traceability Matrix",
"165": "rebuild_honest_ledger.js",
"166": "validate_evidence_grade.js",
"167": "Reviews.tsx",
"168": "blog/[slug]/page.tsx",
"169": "API Contract Specification",
"170": "⚙️ Backend Technical Review (05_dev_backend)",
"171": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"172": "🚀 SEO & Content Strategy Review (12_seo_content)",
"173": "@types/node",
"174": "typescript",
"175": "seed-ui-texts.ts",
"176": "seed-wiki.ts",
"177": "update-blog.dto.ts",
"178": "update-home.dto.ts",
"179": "update-wiki.dto.ts",
"180": "graphify reference: add a URL and watch a folder",
"181": "graphify reference: commit hook and native CLAUDE.md integration",
"182": "graphify reference: incremental update and cluster-only",
"183": "Raw Finding Verification & Disposition Report",
"184": "React + TypeScript + Vite",
"185": "Select.tsx",
"186": "wiki/[slug]/page.tsx",
"187": "NetworkBanner.tsx",
"188": "application/README.md",
"189": "lib",
"190": "deploy.sh",
"191": "🔒 Security & Performance Review (09_devops_security)",
"192": "👁️ UX & Persona Interface Review (08_visual_qa)",
"193": "@eslint/js",
"194": "typescript-eslint",
"195": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
"196": "prisma/scientificTerms.ts",
"197": "seed-blogs.ts",
"198": "seed-custom.ts",
"199": "backend/tsconfig.json",
"200": "graphify reference: GitHub clone and cross-repo merge",
"201": "graphify reference: transcribe video and audio",
"202": "Compiler Diagnostic Dispositions",
"203": "Master Task Backlog (Phase 3.3)",
"204": "build_manifest.js",
"205": "generate_classification.js",
"206": "generate_evidence.js",
"207": "generate_ledger.js",
"208": "generate_manifest.js",
"209": "sync_honest_manifest.js",
"210": "sync_manifest.js",
"211": "react-dom",
"212": "zustand",
"213": "tailwindcss",
"214": "MetricsController",
"215": "@types/react-dom",
"216": "FormField.tsx",
"217": "Input.tsx",
"218": "Textarea.tsx",
"219": "admin-panel/tsconfig.json",
"220": "about/page.tsx",
"221": "privacy/page.tsx",
"222": "next.config.ts",
"223": "Shabnam Font README",
"224": "AGENTS.md",
"225": "rules/graphify.md",
"226": ".agents/workflows/graphify.md",
"227": "instructions.md",
"228": "bcryptjs",
"229": "helmet",
"230": "js-yaml",
"231": "@nestjs/core",
"232": "@nestjs/jwt",
"233": "@nestjs/swagger",
"234": "@nestjs/throttler",
"235": "passport-jwt",
"236": "@prisma/client",
"237": "swagger-ui-express",
"238": "@nestjs/schematics",
"239": "ts-jest",
"240": "ts-loader",
"241": "tsconfig-paths",
"242": "@types/bcrypt",
"243": "@types/supertest",
"244": "blog.entity.ts",
"245": "home.entity.ts",
"246": "wiki.entity.ts",
"247": "User Profile Photo",
"248": "CLAUDE.md",
"249": ".claude/CLAUDE.md",
"250": "extraction-spec.md",
"251": "Products Table",
"252": "Users Table",
"253": "Architectural Audit Findings",
"254": "Cross Boundary Dependencies & Backend Architecture Specification",
"255": "Next.js Agent Rules & Brand Guidelines",
"256": "robots.ts",
"257": "application/eslint.config.mjs",
"258": "postcss.config.mjs",
"259": "vitest.setup.ts",
"260": "backup_db.sh",
"261": "start.sh",
"262": "reviews/README.md",
"263": "backend/eslint.config.mjs",
"264": "User Login API",
"265": "User Logout API",
"266": "generate-openapi.d.ts",
"267": "Canina Pharma GmbH",
"268": "Pets Table",
"269": "Canina Iran Project Introduction",
"270": "Developer Standards and Architecture",
"271": "Frontend & Admin Architecture Route Map Specification",
"272": "Project Backlog and Tasks",
"273": "eslint.config.js",
"274": "postcss.config.js",
"275": "admin-panel/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"276": "shabnam-font-v5.0.1/CHANGELOG.md",
"277": "tailwind.config.js",
"278": "vite.config.ts",
"279": "application/CLAUDE.md",
"280": "application/public/fonts/sahel-font-v3.4.0/CHANGELOG.md",
"281": "Sahel Font Sample",
"282": "Shabnam Font Changelog",
"283": "Vazirmatn Changelog",
"284": "vitest.config.ts",
"285": "Sahel Font Variable Sample",
"286": "Shabnam Font Sample",
"287": "Production Docker Compose",
"288": "Staging Docker Compose",
"289": ".updateSettings",
"290": "ApiBearerAuth",
"291": "ApiOperation",
"292": "ApiQuery",
"293": "ApiTags",
"294": "Body",
"295": "Controller",
"296": "Delete",
"297": "Get",
"298": "Param",
"299": "Post",
"300": "Put",
"301": "Query",
"302": "UseGuards",
"303": "Injectable"
}

View File

@ -0,0 +1 @@
{"output_tokens": 7105}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_scripts_deploy_sh", "label": "deploy.sh", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "file"}}, {"id": "$graphify-root$_scripts_deploy_sh__entry", "label": "deploy.sh script", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L1", "metadata": {"language": "bash", "kind": "bash_entrypoint"}}, {"id": "$graphify-root$_scripts_deploy_docker_buildkit", "label": "DOCKER_BUILDKIT", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L4", "metadata": {"language": "bash", "kind": "code"}}, {"id": "$graphify-root$_scripts_deploy_compose_docker_cli_build", "label": "COMPOSE_DOCKER_CLI_BUILD", "file_type": "code", "source_file": "scripts/deploy.sh", "source_location": "L5", "metadata": {"language": "bash", "kind": "code"}}], "edges": [{"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_sh__entry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_docker_buildkit", "relation": "defines", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L4", "weight": 1.0}, {"source": "$graphify-root$_scripts_deploy_sh", "target": "$graphify-root$_scripts_deploy_compose_docker_cli_build", "relation": "defines", "confidence": "EXTRACTED", "source_file": "scripts/deploy.sh", "source_location": "L5", "weight": 1.0}], "raw_calls": [{"language": "bash", "callee": "set", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L2"}, {"language": "bash", "callee": "docker", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L8"}, {"language": "bash", "callee": "true", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L8"}, {"language": "bash", "callee": "echo", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L18"}, {"language": "bash", "callee": "mkdir", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L38"}, {"language": "bash", "callee": "git", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L42"}, {"language": "bash", "callee": "cd", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L45"}, {"language": "bash", "callee": "cp", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L55"}, {"language": "bash", "callee": "sed", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L56"}, {"language": "bash", "callee": "sleep", "caller_nid": "$graphify-root$_scripts_deploy_sh__entry", "source_file": "scripts/deploy.sh", "source_location": "L76"}], "bash_sources": []}

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