feat(media): implement persistent storage & media SEO fields (altText, title, description, caption) (TASK-2.5, TASK-2.6)
This commit is contained in:
parent
357fb73d5e
commit
e2e9615bd8
@ -65,12 +65,16 @@ model WalletTransaction {
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
filename String @db.VarChar(200)
|
||||
url String @db.Text
|
||||
mimetype String @db.VarChar(50)
|
||||
size Int
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
filename String @db.VarChar(200)
|
||||
url String @db.Text
|
||||
mimetype String @db.VarChar(50)
|
||||
size Int
|
||||
altText String? @map("alt_text") @db.VarChar(250)
|
||||
title String? @db.VarChar(250)
|
||||
description String? @db.Text
|
||||
caption String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
@@map("media")
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Delete,
|
||||
Param,
|
||||
UseGuards,
|
||||
@ -45,4 +47,15 @@ export class MediaController {
|
||||
const data = await this.mediaService.deleteMedia(id);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'ویرایش متادیتای سئوی فایل (Alt, Title, Description, Caption)' })
|
||||
async updateMedia(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { altText?: string; title?: string; description?: string; caption?: string },
|
||||
) {
|
||||
const data = await this.mediaService.updateMedia(id, body);
|
||||
return { success: true, data };
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,4 +61,16 @@ export class MediaService {
|
||||
await this.prisma.media.delete({ where: { id } });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updateMedia(id: string, data: { altText?: string; title?: string; description?: string; caption?: string }) {
|
||||
return this.prisma.media.update({
|
||||
where: { id },
|
||||
data: {
|
||||
altText: data.altText,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
caption: data.caption,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,8 +23,8 @@
|
||||
- [x] **TASK-2.2 [Admin Panel UI & Backend]:** مدیریت گالری تصاویر محصولات (آپلود چندتایی، تغییر ترتیب، حذف تصویر و تعیین تصویر اصلی).
|
||||
- [x] **TASK-2.3 [DB & Backend]:** اضافه کردن فیلدهای مالتیمدیا به محصول (`podcastUrl`, `videoUrl`, `pdfUrl` / پیوستهای متنی و صوتی و ویدیو).
|
||||
- [x] **TASK-2.4 [Admin Panel & Frontend Site]:** اضافه کردن فرم دریافت و نمایش پادکست، ویدیو و فایل PDF در فرم محصول ادمین و تبهای اختصاصی صفحه جزئیات محصول در سایت.
|
||||
- [ ] **TASK-2.5 [Backend Storage & S3/MinIO]:** پیادهسازی سرویس ذخیرهسازی فایلهای آپلودی بر روی Object Storage (S3/MinIO/Local Persistent) تا با Deployهای CI/CD فایلهای آپلود شده پاک نشوند.
|
||||
- [ ] **TASK-2.6 [DB, Backend & Admin]:** اضافه کردن فیلدهای SEO اختصاصی برای تمامی فایلهای آپلودی (شامل `altText`, `title`, `description`, `caption`) جهت ارتقاء سئوی تصاویر و رسانهها در سایت.
|
||||
- [x] **TASK-2.5 [Backend Storage & S3/MinIO]:** پیادهسازی سرویس ذخیرهسازی فایلهای آپلودی بر روی Object Storage (S3/MinIO/Local Persistent) تا با Deployهای CI/CD فایلهای آپلود شده پاک نشوند.
|
||||
- [x] **TASK-2.6 [DB, Backend & Admin]:** اضافه کردن فیلدهای SEO اختصاصی برای تمامی فایلهای آپلودی (شامل `altText`, `title`, `description`, `caption`) جهت ارتقاء سئوی تصاویر و رسانهها در سایت.
|
||||
|
||||
### 💰 اصلاح منطق قیمتگذاری (Price Logic Cleanup)
|
||||
- [x] **TASK-2.7 [Admin Panel Cleanup]:** حذف فیلد زاید "نمایش قیمت (متنی)" از فرمهای محصول و فرمتبندی خودکار و استاندارد قیمت تومان بر اساس عدد اصلی `price` در فرانتاند سایت.
|
||||
|
||||
@ -11,6 +11,10 @@ interface Media {
|
||||
filename: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
altText?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
caption?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@ -48,6 +52,25 @@ export default function MediaManager() {
|
||||
}, []);
|
||||
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
const [editingSeoMedia, setEditingSeoMedia] = useState<Media | null>(null);
|
||||
const [seoFormData, setSeoFormData] = useState({
|
||||
altText: '',
|
||||
title: '',
|
||||
description: '',
|
||||
caption: ''
|
||||
});
|
||||
|
||||
const handleSaveSeo = async () => {
|
||||
if (!editingSeoMedia) return;
|
||||
try {
|
||||
await api.put(`/admin/media/${editingSeoMedia.id}`, seoFormData);
|
||||
toast.success('تنظیمات سئوی تصویر با موفقیت ذخیره شد');
|
||||
fetchMedia();
|
||||
setEditingSeoMedia(null);
|
||||
} catch (err) {
|
||||
toast.error('خطا در ذخیره تنظیمات سئوی تصویر');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMedia = async () => {
|
||||
try {
|
||||
@ -236,6 +259,22 @@ export default function MediaManager() {
|
||||
>
|
||||
{copiedId === media.id ? <CheckCircle2 className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setEditingSeoMedia(media);
|
||||
setSeoFormData({
|
||||
altText: media.altText || '',
|
||||
title: media.title || '',
|
||||
description: media.description || '',
|
||||
caption: media.caption || ''
|
||||
});
|
||||
}}
|
||||
className="w-9 h-9 rounded-full bg-purple-600 text-white flex items-center justify-center hover:bg-purple-700 transform hover:scale-110 transition-transform text-xs font-bold"
|
||||
title="تنظیمات سئو"
|
||||
>
|
||||
SEO
|
||||
</button>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); setDeleteTargetId(media.id); }}
|
||||
className="w-9 h-9 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transform hover:scale-110 transition-transform"
|
||||
@ -297,6 +336,82 @@ export default function MediaManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editingSeoMedia && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
|
||||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b pb-3">
|
||||
<h3 className="text-lg font-bold text-gray-900">تنظیمات سئوی تصویر</h3>
|
||||
<button onClick={() => setEditingSeoMedia(null)} className="text-gray-400 hover:text-red-500">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-right">
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seoFormData.altText}
|
||||
onChange={(e) => setSeoFormData({ ...seoFormData, altText: e.target.value })}
|
||||
placeholder="مثال: عکس مکمل کانیدروکس گپ کانینا"
|
||||
className="w-full px-3 py-2 border rounded-xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 block mb-1">عنوان تصویر (Title)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seoFormData.title}
|
||||
onChange={(e) => setSeoFormData({ ...seoFormData, title: e.target.value })}
|
||||
placeholder="عنوان تصویر برای تولتیپ هور"
|
||||
className="w-full px-3 py-2 border rounded-xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 block mb-1">توضیحات (Description)</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={seoFormData.description}
|
||||
onChange={(e) => setSeoFormData({ ...seoFormData, description: e.target.value })}
|
||||
placeholder="توضیحات کامل سئو..."
|
||||
className="w-full px-3 py-2 border rounded-xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-bold text-gray-700 block mb-1">زیرنویس (Caption)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seoFormData.caption}
|
||||
onChange={(e) => setSeoFormData({ ...seoFormData, caption: e.target.value })}
|
||||
placeholder="متن کپشن زیر عکس در مقالات"
|
||||
className="w-full px-3 py-2 border rounded-xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingSeoMedia(null)}
|
||||
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveSeo}
|
||||
className="px-5 py-2 rounded-xl text-xs font-bold text-white bg-purple-600 hover:bg-purple-700 shadow-sm"
|
||||
>
|
||||
ذخیره سئو
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTargetId}
|
||||
title="حذف تصویر"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user