diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 3bf0d29..a7664eb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -149,8 +149,17 @@ model Product { imageUrl String @map("image_url") @db.Text images String[] @default([]) podcastUrl String? @map("podcast_url") @db.Text + podcastTitle String? @map("podcast_title") @db.VarChar(250) + podcastDescription String? @map("podcast_description") @db.Text + podcastCover String? @map("podcast_cover") @db.Text videoUrl String? @map("video_url") @db.Text + videoTitle String? @map("video_title") @db.VarChar(250) + videoDescription String? @map("video_description") @db.Text + videoCover String? @map("video_cover") @db.Text pdfUrl String? @map("pdf_url") @db.Text + pdfTitle String? @map("pdf_title") @db.VarChar(250) + pdfDescription String? @map("pdf_description") @db.Text + pdfCover String? @map("pdf_cover") @db.Text metaTitle String? @map("meta_title") @db.VarChar(200) metaDescription String? @map("meta_description") @db.Text canonicalUrl String? @map("canonical_url") @db.Text diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 59658c6..7545ce7 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -379,8 +379,17 @@ export class AdminService { ? [data.images] : [], podcastUrl: data.podcastUrl || null, + podcastTitle: data.podcastTitle || null, + podcastDescription: data.podcastDescription || null, + podcastCover: data.podcastCover || null, videoUrl: data.videoUrl || null, + videoTitle: data.videoTitle || null, + videoDescription: data.videoDescription || null, + videoCover: data.videoCover || null, pdfUrl: data.pdfUrl || null, + pdfTitle: data.pdfTitle || null, + pdfDescription: data.pdfDescription || null, + pdfCover: data.pdfCover || null, metaTitle: data.metaTitle || '', metaDescription: data.metaDescription || '', keywords: data.keywords || '', @@ -435,8 +444,17 @@ export class AdminService { ? [data.images] : undefined, podcastUrl: data.podcastUrl !== undefined ? data.podcastUrl : undefined, + podcastTitle: data.podcastTitle !== undefined ? data.podcastTitle : undefined, + podcastDescription: data.podcastDescription !== undefined ? data.podcastDescription : undefined, + podcastCover: data.podcastCover !== undefined ? data.podcastCover : undefined, videoUrl: data.videoUrl !== undefined ? data.videoUrl : undefined, + videoTitle: data.videoTitle !== undefined ? data.videoTitle : undefined, + videoDescription: data.videoDescription !== undefined ? data.videoDescription : undefined, + videoCover: data.videoCover !== undefined ? data.videoCover : undefined, pdfUrl: data.pdfUrl !== undefined ? data.pdfUrl : undefined, + pdfTitle: data.pdfTitle !== undefined ? data.pdfTitle : undefined, + pdfDescription: data.pdfDescription !== undefined ? data.pdfDescription : undefined, + pdfCover: data.pdfCover !== undefined ? data.pdfCover : undefined, metaTitle: data.metaTitle, metaDescription: data.metaDescription, keywords: data.keywords, diff --git a/backend/src/admin/dto/product.dto.ts b/backend/src/admin/dto/product.dto.ts index 03799b6..da25703 100644 --- a/backend/src/admin/dto/product.dto.ts +++ b/backend/src/admin/dto/product.dto.ts @@ -113,16 +113,61 @@ export class ProductDto { @IsString() podcastUrl?: string | null; + @ApiPropertyOptional({ description: 'عنوان اختصاصی پادکست' }) + @IsOptional() + @IsString() + podcastTitle?: string | null; + + @ApiPropertyOptional({ description: 'توضیحات پادکست' }) + @IsOptional() + @IsString() + podcastDescription?: string | null; + + @ApiPropertyOptional({ description: 'تصویر کاور پادکست' }) + @IsOptional() + @IsString() + podcastCover?: string | null; + @ApiPropertyOptional({ description: 'لینک ویدیو' }) @IsOptional() @IsString() videoUrl?: string | null; + @ApiPropertyOptional({ description: 'عنوان اختصاصی ویدیو' }) + @IsOptional() + @IsString() + videoTitle?: string | null; + + @ApiPropertyOptional({ description: 'توضیحات ویدیو' }) + @IsOptional() + @IsString() + videoDescription?: string | null; + + @ApiPropertyOptional({ description: 'تصویر کاور/پوستر ویدیو' }) + @IsOptional() + @IsString() + videoCover?: string | null; + @ApiPropertyOptional({ description: 'لینک فایل PDF' }) @IsOptional() @IsString() pdfUrl?: string | null; + @ApiPropertyOptional({ description: 'عنوان فایل PDF' }) + @IsOptional() + @IsString() + pdfTitle?: string | null; + + @ApiPropertyOptional({ description: 'توضیحات فایل PDF' }) + @IsOptional() + @IsString() + pdfDescription?: string | null; + + @ApiPropertyOptional({ description: 'تصویر پیش‌نمایش فایل PDF' }) + @IsOptional() + @IsString() + pdfCover?: string | null; + @ApiPropertyOptional({ description: 'عنوان سئو' }) @IsOptional() @IsString() diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index 2c80344..a43557d 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -41,10 +41,18 @@ export interface Product { keywords?: string; canonicalUrl?: string; slug?: string; - images?: string[]; podcastUrl?: string; + podcastTitle?: string; + podcastDescription?: string; + podcastCover?: string; videoUrl?: string; + videoTitle?: string; + videoDescription?: string; + videoCover?: string; pdfUrl?: string; + pdfTitle?: string; + pdfDescription?: string; + pdfCover?: string; symptoms?: Array<{ symptom: string } | string>; isPreorder?: boolean; preorderDeposit?: string | number; @@ -136,7 +144,7 @@ export default function Products() { // Modal State const [isModalOpen, setIsModalOpen] = useState(false); const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false); - const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'podcastUrl' | 'videoUrl' | 'pdfUrl'>('imageUrl'); + const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'podcastUrl' | 'podcastCover' | 'videoUrl' | 'videoCover' | 'pdfUrl' | 'pdfCover' | 'gallery'>('imageUrl'); const [editingProduct, setEditingProduct] = useState(null); const [deleteTargetId, setDeleteTargetId] = useState(null); const [activeTab, setActiveTab] = useState('general'); @@ -167,8 +175,17 @@ export default function Products() { slug: '', images: [] as string[], podcastUrl: '', + podcastTitle: '', + podcastDescription: '', + podcastCover: '', videoUrl: '', + videoTitle: '', + videoDescription: '', + videoCover: '', pdfUrl: '', + pdfTitle: '', + pdfDescription: '', + pdfCover: '', symptoms: [] as string[], isPreorder: false, preorderDeposit: '' as number | string @@ -254,11 +271,19 @@ export default function Products() { metaDescription: product.metaDescription || '', keywords: product.keywords || '', canonicalUrl: product.canonicalUrl || '', - slug: product.slug || '', images: Array.isArray(product.images) ? product.images : [], podcastUrl: product.podcastUrl || '', + podcastTitle: product.podcastTitle || '', + podcastDescription: product.podcastDescription || '', + podcastCover: product.podcastCover || '', videoUrl: product.videoUrl || '', + videoTitle: product.videoTitle || '', + videoDescription: product.videoDescription || '', + videoCover: product.videoCover || '', pdfUrl: product.pdfUrl || '', + pdfTitle: product.pdfTitle || '', + pdfDescription: product.pdfDescription || '', + pdfCover: product.pdfCover || '', symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : [], isPreorder: Boolean(product.isPreorder), preorderDeposit: product.preorderDeposit || '' @@ -270,7 +295,10 @@ export default function Products() { buyPrice: '', priceValue: 0, wholesalePrice: '', priceValueMarginPercent: '', wholesaleMarginPercent: '', priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه', imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '', - images: [], podcastUrl: '', videoUrl: '', pdfUrl: '', + images: [], + podcastUrl: '', podcastTitle: '', podcastDescription: '', podcastCover: '', + videoUrl: '', videoTitle: '', videoDescription: '', videoCover: '', + pdfUrl: '', pdfTitle: '', pdfDescription: '', pdfCover: '', symptoms: [], isPreorder: false, preorderDeposit: '' }); } @@ -1277,79 +1305,268 @@ export default function Products() { )} - {/* Multimedia Attachments (TASK-2.3 & TASK-2.4) */} -
-

پیوست‌های مالتی‌مدیا و مستندات علمی

+ {/* Multimedia Attachments */} +
+
+

پیوست‌های مالتی‌مدیا و مستندات علمی تخصصی

+

مدیریت فایل‌های صوتی، تصویری و کاتالوگ‌های اختصاصی این محصول همراه با عنوان، توضیحات و کاور اختصاصی.

+
-
-
- -
- setFormData({ ...formData, podcastUrl: e.target.value })} - placeholder="https://... MP3 or Soundcloud" - className="w-full px-3 py-2 border rounded-xl text-xs font-mono" - dir="ltr" - /> - + {/* Section 1: Podcast Management Card */} +
+
+ + 🎧 تنظیمات پادکست و بررسی صوتی بالینی + + {formData.podcastUrl && ( + + فایل صوتی متصل است + + )} +
+ +
+
+ +
+ setFormData({ ...formData, podcastUrl: e.target.value })} + placeholder="https://... /uploads/audio.mp3" + className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white" + dir="ltr" + /> + +
+
+ +
+ +
+ setFormData({ ...formData, podcastCover: e.target.value })} + placeholder="در صورت خالی بودن، تصویر اصلی محصول استفاده می‌شود" + className="w-full px-3 py-2 border rounded-xl text-xs font-mono bg-white" + dir="ltr" + /> + +
-
- -
- setFormData({ ...formData, videoUrl: e.target.value })} - placeholder="https://... MP4 or Aparat/YouTube iframe" - className="w-full px-3 py-2 border rounded-xl text-xs font-mono" - dir="ltr" - /> - + +
+ + setFormData({ ...formData, podcastTitle: e.target.value })} + placeholder="مثلاً: بررسی بالینی اثر مکمل بر سلامت مفاصل سگ‌ها" + className="w-full px-3 py-2 border rounded-xl text-xs bg-white font-medium" + /> +
+ +
+ +