diff --git a/.ai_agency/memory/backlog.json b/.ai_agency/memory/backlog.json
index 35252c7..571c586 100644
--- a/.ai_agency/memory/backlog.json
+++ b/.ai_agency/memory/backlog.json
@@ -648,14 +648,90 @@
"sub_steps": [
{ "index": 1, "name": "update_refill_dashboard_and_ai_monitor", "description": "Render course reserve badge and next-order voucher info in dashboard, pet profile, and admin panel", "status": "done" }
]
+ },
+ {
+ "id": "EPIC-17-TASK-01",
+ "title": "PDF Catalog Link Replacement to Online Catalog Route",
+ "description": "Replace all static /canina.pdf links in Footer.tsx and SmartAdvisor.tsx with /catalog route.",
+ "architectural_layer": "presentation_ui",
+ "assigned_role": "06_dev_frontend",
+ "priority": "HIGH",
+ "status": "completed",
+ "max_files_allowed": 3,
+ "estimated_minutes": 10,
+ "dependency_task_ids": ["EPIC-16-TASK-03"],
+ "acceptance_criteria": [
+ "انتقال تمام لینکهای کاتالوگ در فوتور و مشاور هوشمند به مسیر /catalog",
+ "عدم وجود لینک شکستهشده به فایل pdf محلی"
+ ],
+ "sub_steps": [
+ { "index": 1, "name": "replace_pdf_catalog_links", "description": "Update Footer.tsx and SmartAdvisor.tsx hrefs to /catalog", "status": "done" }
+ ]
+ },
+ {
+ "id": "EPIC-17-TASK-02",
+ "title": "Backend Video Module, Prisma Schema & Initial Seed Data",
+ "description": "Create Video model in Prisma schema and build NestJS Video Module, Controller, and Service with public/admin CRUD endpoints.",
+ "architectural_layer": "business_logic",
+ "assigned_role": "05_dev_backend",
+ "priority": "HIGH",
+ "status": "completed",
+ "max_files_allowed": 5,
+ "estimated_minutes": 25,
+ "dependency_task_ids": ["EPIC-17-TASK-01"],
+ "acceptance_criteria": [
+ "ایجاد جدول Video در دیتابیس PostgreSQL و Prisma",
+ "ارائه APIهای کامل دریافت، ایجاد، ویرایش و حذف ویدئوها"
+ ],
+ "sub_steps": [
+ { "index": 1, "name": "create_backend_video_module", "description": "Add Video model in schema.prisma and build NestJS VideoModule", "status": "done" }
+ ]
+ },
+ {
+ "id": "EPIC-17-TASK-03",
+ "title": "Admin Panel Video Management Page & Navigation Integration",
+ "description": "Build Videos.tsx in admin-panel featuring video table, create/edit video modal, delete confirmation, and sidebar menu link.",
+ "architectural_layer": "presentation_ui",
+ "assigned_role": "06_dev_frontend",
+ "priority": "HIGH",
+ "status": "completed",
+ "max_files_allowed": 4,
+ "estimated_minutes": 25,
+ "dependency_task_ids": ["EPIC-17-TASK-02"],
+ "acceptance_criteria": [
+ "افزوده شدن گزینه مدیریت ویدئوها به منوی اصلی پنل مدیریت",
+ "مدیریت کامل ویدئوها (ایجاد، مشاهده، تغییر پوستر، ویدئو، توضیحات و فیلد ویژه)"
+ ],
+ "sub_steps": [
+ { "index": 1, "name": "build_admin_videos_page", "description": "Create Videos.tsx and link in Sidebar.tsx and App.tsx", "status": "done" }
+ ]
+ },
+ {
+ "id": "EPIC-17-TASK-04",
+ "title": "Frontend Dynamic Video Fetching & Synchronization",
+ "description": "Create videoService.ts and update VetGallery.tsx and VideosPage.tsx to fetch dynamic video data from NestJS API instead of hardcoded mock arrays.",
+ "architectural_layer": "presentation_ui",
+ "assigned_role": "06_dev_frontend",
+ "priority": "HIGH",
+ "status": "completed",
+ "max_files_allowed": 4,
+ "estimated_minutes": 20,
+ "dependency_task_ids": ["EPIC-17-TASK-03"],
+ "acceptance_criteria": [
+ "بارگذاری پویا ویدئوها در صفحه اصلی و صفحه اختصاصی /videos از دیتابیس بکاند",
+ "هماهنگی ۱۰۰٪ دادههای ویدئو بین تمام صفحات سایت و پنل مدیریت"
+ ],
+ "sub_steps": [
+ { "index": 1, "name": "connect_frontend_videos_api", "description": "Create videoService.ts and wire VetGallery.tsx and VideosPage.tsx to NestJS backend API", "status": "done" }
+ ]
}
],
"metadata": {
- "total": 33,
- "completed": 33,
+ "total": 37,
+ "completed": 37,
"in_progress": 0,
"pending": 0,
- "generated_at": "2026-07-27T11:45:00Z",
- "decomposition_pass": 14
+ "generated_at": "2026-07-27T11:55:00Z",
+ "decomposition_pass": 15
}
}
\ No newline at end of file
diff --git a/.ai_agency/memory/state.json b/.ai_agency/memory/state.json
index da504a5..bc008b9 100644
--- a/.ai_agency/memory/state.json
+++ b/.ai_agency/memory/state.json
@@ -7,12 +7,12 @@
"status": "COMPLETE",
"checkpoint": {
"active_agent": "01_auditor",
- "current_ticket_id": "EPIC-16-TASK-03",
+ "current_ticket_id": "EPIC-17-TASK-04",
"sub_step": {
"index": 1,
"total": 1,
- "name": "update_refill_dashboard_and_ai_monitor",
- "description": "Smart Refill Course Reservation model, next-order 5% cashback voucher, and abuse prevention complete and 100% verified"
+ "name": "connect_frontend_videos_api",
+ "description": "PDF catalog link replacement to /catalog and Full-stack Video Management System (NestJS API, Prisma DB, Admin Videos page, and frontend sync) complete and 100% verified"
}
},
"review_phase": {
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 1670507..a26455e 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -374,3 +374,19 @@ model SmartAdvisorRule {
@@map("smart_advisor_rules")
}
+
+model Video {
+ id String @id @default(uuid()) @db.Uuid
+ title String @db.VarChar(200)
+ doctor String @db.VarChar(150)
+ duration String @default("02:00") @db.VarChar(20)
+ thumbnail String? @db.Text
+ videoUrl String @map("video_url") @db.Text
+ description String? @db.Text
+ viewsCount Int @default(0) @map("views_count")
+ isFeatured Boolean @default(false) @map("is_featured")
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
+
+ @@map("videos")
+}
diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts
index 7b8a7e7..dc595c5 100644
--- a/backend/prisma/seed.ts
+++ b/backend/prisma/seed.ts
@@ -315,6 +315,58 @@ async function main() {
console.error('Failed to seed home components:', err);
}
+ console.log('Seeding Initial Videos...');
+ await prisma.video.deleteMany();
+ await prisma.video.createMany({
+ data: [
+ {
+ title: "نحوه آمادهسازی کانیهیدروکس GAG",
+ doctor: "دکتر کلاوس هنینگ",
+ duration: "۰۱:۴۵",
+ thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=600",
+ videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
+ description: "آموزش گام به گام آمادهسازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش.",
+ isFeatured: true,
+ },
+ {
+ title: "اهمیت صدف لبسبز برای بازسازی غضروف",
+ doctor: "دکتر النا اشمیت",
+ duration: "۰۲:۲۰",
+ thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=600",
+ videoUrl: "https://www.w3schools.com/html/movie.mp4",
+ description: "چرا صدف لبسبز نیوزیلندی نایابترین و موثرترین ماده برای بازسازی مفصل است؟",
+ isFeatured: true,
+ },
+ {
+ title: "تغذیه هوشمند تولهسگهای نژاد بزرگ",
+ doctor: "دکتر هلگا فیشر",
+ duration: "۰۳:۱۰",
+ thumbnail: "/assets/images/regenerated_image_1779109861747.png",
+ videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
+ description: "دوز مصرفی مکملهای کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلیهای استخوانی.",
+ isFeatured: true,
+ },
+ {
+ title: "تاثیر امگا ۳ بر ریزش مو فصلی",
+ doctor: "دکتر ماری کونیگ",
+ duration: "۰۵:۱۵",
+ thumbnail: "https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&q=80&w=600",
+ videoUrl: "https://www.w3schools.com/html/movie.mp4",
+ description: "چربیهای ضروری چگونه لایه محافظ پوست را بازسازی میکنند.",
+ isFeatured: false,
+ },
+ {
+ title: "سمزدایی گوارشی با اسید هومیک",
+ doctor: "دکتر ریکاردو کوخ",
+ duration: "۰۴:۰۰",
+ thumbnail: "https://images.unsplash.com/photo-1581594693702-fbdc51b2763b?auto=format&fit=crop&q=80&w=600",
+ videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
+ description: "مکانیزم عمل Moor-Tranke در جذب توکسینهای رودهای.",
+ isFeatured: false,
+ }
+ ]
+ });
+
console.log('All Seeding completed successfully!');
}
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index d4d4240..e3a212f 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -18,6 +18,7 @@ import { WikiModule } from './wiki/wiki.module';
import { SeoModule } from './seo/seo.module';
import { CmsModule } from './cms/cms.module';
import { WholesaleModule } from './wholesale/wholesale.module';
+import { VideosModule } from './videos/videos.module';
@Module({
imports: [
@@ -40,6 +41,7 @@ import { WholesaleModule } from './wholesale/wholesale.module';
SeoModule,
CmsModule,
WholesaleModule,
+ VideosModule,
],
controllers: [MetricsController],
providers: [
diff --git a/backend/src/videos/dto/create-video.dto.ts b/backend/src/videos/dto/create-video.dto.ts
new file mode 100644
index 0000000..ccad3d5
--- /dev/null
+++ b/backend/src/videos/dto/create-video.dto.ts
@@ -0,0 +1,41 @@
+import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+
+export class CreateVideoDto {
+ @ApiProperty({ description: 'عنوان ویدئوی آموزشی', example: 'نحوه آمادهسازی کانیهیدروکس GAG' })
+ @IsString()
+ @IsNotEmpty()
+ title: string;
+
+ @ApiProperty({ description: 'نام دکتر/ارائهدهنده', example: 'دکتر کلاوس هنینگ' })
+ @IsString()
+ @IsNotEmpty()
+ doctor: string;
+
+ @ApiPropertyOptional({ description: 'مدت زمان ویدئو', example: '۰۲:۳۰' })
+ @IsString()
+ @IsOptional()
+ duration?: string;
+
+ @ApiPropertyOptional({ description: 'آدرس تصویر کاور', example: 'https://example.com/thumb.jpg' })
+ @IsString()
+ @IsOptional()
+ thumbnail?: string;
+
+ @ApiProperty({ description: 'آدرس فایل ویدئو', example: 'https://example.com/video.mp4' })
+ @IsString()
+ @IsNotEmpty()
+ videoUrl: string;
+
+ @ApiPropertyOptional({ description: 'توضیحات تکمیلی ویدئو' })
+ @IsString()
+ @IsOptional()
+ description?: string;
+
+ @ApiPropertyOptional({ description: 'آیا ویدئوی ویژه/صفحه اصلی است؟', example: true })
+ @IsBoolean()
+ @IsOptional()
+ isFeatured?: boolean;
+}
+
+export class UpdateVideoDto extends CreateVideoDto {}
diff --git a/backend/src/videos/videos.controller.ts b/backend/src/videos/videos.controller.ts
new file mode 100644
index 0000000..aa33e06
--- /dev/null
+++ b/backend/src/videos/videos.controller.ts
@@ -0,0 +1,51 @@
+import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
+import { VideosService } from './videos.service';
+import { CreateVideoDto } from './dto/create-video.dto';
+import { JwtAuthGuard } from '../auth/jwt-auth.guard';
+import { ApiTags, ApiOperation, ApiQuery, ApiBearerAuth } from '@nestjs/swagger';
+
+@ApiTags('Videos - مشاوره ویدئویی و آکادمی')
+@Controller('videos')
+export class VideosController {
+ constructor(private readonly videosService: VideosService) {}
+
+ @Get()
+ @ApiOperation({ summary: 'دریافت لیست ویدئوها (عمومی)' })
+ @ApiQuery({ name: 'page', required: false })
+ @ApiQuery({ name: 'limit', required: false })
+ @ApiQuery({ name: 'search', required: false })
+ @ApiQuery({ name: 'featured', required: false })
+ findAll(@Query() query: any) {
+ return this.videosService.findAll(query);
+ }
+
+ @Get(':id')
+ @ApiOperation({ summary: 'دریافت جزئیات یک ویدئو' })
+ findOne(@Param('id') id: string) {
+ return this.videosService.findOne(id);
+ }
+
+ @Post()
+ @UseGuards(JwtAuthGuard)
+ @ApiBearerAuth()
+ @ApiOperation({ summary: 'افزودن ویدئوی جدید (ادمین)' })
+ create(@Body() createVideoDto: CreateVideoDto) {
+ return this.videosService.create(createVideoDto);
+ }
+
+ @Put(':id')
+ @UseGuards(JwtAuthGuard)
+ @ApiBearerAuth()
+ @ApiOperation({ summary: 'ویرایش ویدئو (ادمین)' })
+ update(@Param('id') id: string, @Body() updateVideoDto: Partial) {
+ return this.videosService.update(id, updateVideoDto);
+ }
+
+ @Delete(':id')
+ @UseGuards(JwtAuthGuard)
+ @ApiBearerAuth()
+ @ApiOperation({ summary: 'حذف ویدئو (ادمین)' })
+ remove(@Param('id') id: string) {
+ return this.videosService.remove(id);
+ }
+}
diff --git a/backend/src/videos/videos.module.ts b/backend/src/videos/videos.module.ts
new file mode 100644
index 0000000..12f3faa
--- /dev/null
+++ b/backend/src/videos/videos.module.ts
@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+import { VideosController } from './videos.controller';
+import { VideosService } from './videos.service';
+
+@Module({
+ controllers: [VideosController],
+ providers: [VideosService],
+ exports: [VideosService],
+})
+export class VideosModule {}
diff --git a/backend/src/videos/videos.service.ts b/backend/src/videos/videos.service.ts
new file mode 100644
index 0000000..0c64fff
--- /dev/null
+++ b/backend/src/videos/videos.service.ts
@@ -0,0 +1,98 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { PrismaService } from '../prisma/prisma.service';
+import { CreateVideoDto } from './dto/create-video.dto';
+
+@Injectable()
+export class VideosService {
+ constructor(private readonly prisma: PrismaService) {}
+
+ async findAll(query: any) {
+ const page = Number(query.page) || 1;
+ const limit = Number(query.limit) || 20;
+ const skip = (page - 1) * limit;
+
+ const where: any = {};
+ if (query.search) {
+ where.OR = [
+ { title: { contains: query.search, mode: 'insensitive' } },
+ { doctor: { contains: query.search, mode: 'insensitive' } },
+ { description: { contains: query.search, mode: 'insensitive' } },
+ ];
+ }
+ if (query.featured !== undefined) {
+ where.isFeatured = query.featured === 'true' || query.featured === true;
+ }
+
+ const [data, total] = await Promise.all([
+ this.prisma.video.findMany({
+ where,
+ skip,
+ take: limit,
+ orderBy: [{ isFeatured: 'desc' }, { createdAt: 'desc' }],
+ }),
+ this.prisma.video.count({ where }),
+ ]);
+
+ return {
+ data,
+ meta: {
+ total,
+ page,
+ limit,
+ lastPage: Math.ceil(total / limit),
+ },
+ };
+ }
+
+ async findOne(id: string) {
+ const video = await this.prisma.video.findUnique({
+ where: { id },
+ });
+ if (!video) {
+ throw new NotFoundException('ویدئوی مورد نظر یافت نشد');
+ }
+ // Increment view count
+ await this.prisma.video.update({
+ where: { id },
+ data: { viewsCount: { increment: 1 } },
+ });
+ return video;
+ }
+
+ async create(dto: CreateVideoDto) {
+ return this.prisma.video.create({
+ data: {
+ title: dto.title,
+ doctor: dto.doctor,
+ duration: dto.duration || '۰۲:۰۰',
+ thumbnail: dto.thumbnail,
+ videoUrl: dto.videoUrl,
+ description: dto.description,
+ isFeatured: dto.isFeatured ?? false,
+ },
+ });
+ }
+
+ async update(id: string, dto: Partial) {
+ await this.findOne(id);
+ return this.prisma.video.update({
+ where: { id },
+ data: {
+ title: dto.title,
+ doctor: dto.doctor,
+ duration: dto.duration,
+ thumbnail: dto.thumbnail,
+ videoUrl: dto.videoUrl,
+ description: dto.description,
+ isFeatured: dto.isFeatured,
+ },
+ });
+ }
+
+ async remove(id: string) {
+ await this.findOne(id);
+ return this.prisma.video.delete({
+ where: { id },
+ });
+ }
+}
diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx
index d470c42..603ec41 100644
--- a/frontend/admin-panel/src/App.tsx
+++ b/frontend/admin-panel/src/App.tsx
@@ -18,6 +18,7 @@ import UITexts from './pages/UITexts';
import Media from './pages/Media';
import CMS from './pages/CMS';
import WholesaleApplications from './pages/WholesaleApplications';
+import Videos from './pages/Videos';
function App() {
return (
@@ -37,6 +38,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/admin-panel/src/components/Sidebar.tsx b/frontend/admin-panel/src/components/Sidebar.tsx
index ea6fc7a..3b539d2 100644
--- a/frontend/admin-panel/src/components/Sidebar.tsx
+++ b/frontend/admin-panel/src/components/Sidebar.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
-import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image } from 'lucide-react';
+import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image, Video } from 'lucide-react';
import api from '../services/api';
const menuGroups = [
@@ -33,6 +33,7 @@ const menuGroups = [
items: [
{ icon: FileText, label: 'وبلاگ', path: '/blogs' },
{ icon: BookOpen, label: 'دانشنامه', path: '/wiki' },
+ { icon: Video, label: 'مدیریت ویدئوها', path: '/videos' },
{ icon: Image, label: 'بنرها و نظرات (CMS)', path: '/cms' },
]
},
diff --git a/frontend/admin-panel/src/pages/Videos.tsx b/frontend/admin-panel/src/pages/Videos.tsx
new file mode 100644
index 0000000..601ba79
--- /dev/null
+++ b/frontend/admin-panel/src/pages/Videos.tsx
@@ -0,0 +1,341 @@
+import React, { useState, useEffect } from 'react';
+import { Plus, Search, Edit2, Trash2, Video as VideoIcon, Star, CheckCircle, X } from 'lucide-react';
+import api from '../services/api';
+
+interface Video {
+ id: string;
+ title: string;
+ doctor: string;
+ duration: string;
+ thumbnail: string;
+ videoUrl: string;
+ description: string;
+ viewsCount: number;
+ isFeatured: boolean;
+ createdAt: string;
+}
+
+export default function Videos() {
+ const [videos, setVideos] = useState
diff --git a/frontend/application/components/VetGallery.tsx b/frontend/application/components/VetGallery.tsx
index ec80392..fee4401 100644
--- a/frontend/application/components/VetGallery.tsx
+++ b/frontend/application/components/VetGallery.tsx
@@ -5,23 +5,23 @@ import { motion, AnimatePresence } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSettingsStore } from "../lib/store/settingsStore";
-import SafeImage from "./SafeImage";
+import { videoService, Video } from "../lib/services/videoService";
-const VIDEOS = [
+const FALLBACK_VIDEOS = [
{
id: "v1",
- title: "نحوه آمادهسازی کانیهیدروکس",
+ title: "نحوه آمادهسازی کانیهیدروکس GAG",
doctor: "دکتر کلاوس هنینگ",
- duration: "۱:۴۵",
+ duration: "۰۱:۴۵",
thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400",
- videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", // Placeholder real-looking video
+ videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: "آموزش گام به گام آمادهسازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش."
},
{
id: "v2",
title: "اهمیت صدف لبسبز",
doctor: "دکتر النا اشمیت",
- duration: "۲:۲۰",
+ duration: "۰۲:۲۰",
thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=400",
videoUrl: "https://www.w3schools.com/html/movie.mp4",
description: "چرا صدف لبسبز نیوزیلندی نایابترین و موثرترین ماده برای بازسازی مفصل است؟"
@@ -30,7 +30,7 @@ const VIDEOS = [
id: "v3",
title: "تغذیه هوشمند تولهسگها",
doctor: "دکتر هلگا فیشر",
- duration: "۳:۱۰",
+ duration: "۰۳:۱۰",
thumbnail: "/assets/images/regenerated_image_1779109861747.png",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: "دوز مصرفی مکملهای کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلیهای استخوانی."
@@ -40,18 +40,30 @@ const VIDEOS = [
export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) {
const router = useRouter();
const getText = useSettingsStore(state => state.getText);
- const displayItems = testimonials.length > 0 ? testimonials.map((t, idx) => ({
- id: t.id || `t-${idx}`,
- title: t.clinicName ? `${t.vetName} — ${t.clinicName}` : t.vetName,
- doctor: t.vetName,
- duration: "۲:۰۰",
- thumbnail: t.imageUrl || "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400",
- videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
- description: t.quote,
- quote: t.quote
- })) : VIDEOS;
+ const [apiVideos, setApiVideos] = useState([]);
const [selectedVideo, setSelectedVideo] = useState(null);
+ React.useEffect(() => {
+ videoService.getVideos({ featured: true, limit: 3 }).then((data) => {
+ if (data && data.length > 0) {
+ setApiVideos(data);
+ }
+ });
+ }, []);
+
+ const displayItems = testimonials.length > 0
+ ? testimonials.map((t, idx) => ({
+ id: t.id || `t-${idx}`,
+ title: t.clinicName ? `${t.vetName} — ${t.clinicName}` : t.vetName,
+ doctor: t.vetName,
+ duration: "۰۲:۰۰",
+ thumbnail: t.imageUrl || "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400",
+ videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
+ description: t.quote,
+ quote: t.quote
+ }))
+ : (apiVideos.length > 0 ? apiVideos : FALLBACK_VIDEOS);
+
return (
diff --git a/frontend/application/components/VideosPage.tsx b/frontend/application/components/VideosPage.tsx
index 9d9aecb..2008503 100644
--- a/frontend/application/components/VideosPage.tsx
+++ b/frontend/application/components/VideosPage.tsx
@@ -5,21 +5,24 @@ import { ChevronRight, PlayCircle, Star, ShieldCheck, X, Search, Clock, User } f
import SafeImage from "./SafeImage";
-const ALL_VIDEOS = [
+import { videoService, Video } from "../lib/services/videoService";
+import { useRouter } from 'next/navigation';
+
+const FALLBACK_VIDEOS: Video[] = [
{
id: "v1",
- title: "نحوه آمادهسازی کانیهیدروکس",
+ title: "نحوه آمادهسازی کانیهیدروکس GAG",
doctor: "دکتر کلاوس هنینگ",
- duration: "۱:۴۵",
+ duration: "۰۱:۴۵",
thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=600",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: "آموزش گام به گام آمادهسازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش."
},
{
id: "v2",
- title: "اهمیت صدف لبسبز برای غضروف",
+ title: "اهمیت صدف لبسبز برای بازسازی غضروف",
doctor: "دکتر النا اشمیت",
- duration: "۲:۲۰",
+ duration: "۰۲:۲۰",
thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=600",
videoUrl: "https://www.w3schools.com/html/movie.mp4",
description: "چرا صدف لبسبز نیوزیلندی نایابترین و موثرترین ماده برای بازسازی مفصل است؟"
@@ -28,7 +31,7 @@ const ALL_VIDEOS = [
id: "v3",
title: "تغذیه هوشمند تولهسگهای نژاد بزرگ",
doctor: "دکتر هلگا فیشر",
- duration: "۳:۱۰",
+ duration: "۰۳:۱۰",
thumbnail: "/assets/images/regenerated_image_1779109861747.png",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: "دوز مصرفی مکملهای کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلیهای استخوانی."
@@ -37,7 +40,7 @@ const ALL_VIDEOS = [
id: "v4",
title: "تاثیر امگا ۳ بر ریزش مو فصلی",
doctor: "دکتر ماری کونیگ",
- duration: "۵:۱۵",
+ duration: "۰۵:۱۵",
thumbnail: "https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&q=80&w=600",
videoUrl: "https://www.w3schools.com/html/movie.mp4",
description: "چربیهای ضروری چگونه لایه محافظ پوست را بازسازی میکنند."
@@ -46,21 +49,28 @@ const ALL_VIDEOS = [
id: "v5",
title: "سمزدایی گوارشی با اسید هومیک",
doctor: "دکتر ریکاردو کوخ",
- duration: "۴:۰۰",
+ duration: "۰۴:۰۰",
thumbnail: "https://images.unsplash.com/photo-1581594693702-fbdc51b2763b?auto=format&fit=crop&q=80&w=600",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: "مکانیزم عمل Moor-Tranke در جذب توکسینهای رودهای."
}
];
-import { useRouter } from 'next/navigation';
-
export default function VideosPage() {
const router = useRouter();
- const [selectedVideo, setSelectedVideo] = useState(null);
+ const [videos, setVideos] = useState(FALLBACK_VIDEOS);
+ const [selectedVideo, setSelectedVideo] = useState(null);
const [search, setSearch] = useState("");
- const filteredVideos = ALL_VIDEOS.filter(v =>
+ React.useEffect(() => {
+ videoService.getVideos({ limit: 100 }).then((data) => {
+ if (data && data.length > 0) {
+ setVideos(data);
+ }
+ });
+ }, []);
+
+ const filteredVideos = videos.filter(v =>
v.title.includes(search) || v.doctor.includes(search)
);
diff --git a/frontend/application/lib/services/videoService.ts b/frontend/application/lib/services/videoService.ts
new file mode 100644
index 0000000..1b6786e
--- /dev/null
+++ b/frontend/application/lib/services/videoService.ts
@@ -0,0 +1,35 @@
+import api from './api';
+
+export interface Video {
+ id: string;
+ title: string;
+ doctor: string;
+ duration: string;
+ thumbnail: string;
+ videoUrl: string;
+ description: string;
+ viewsCount?: number;
+ isFeatured?: boolean;
+}
+
+export const videoService = {
+ getVideos: async (params?: { featured?: boolean; limit?: number; search?: string }) => {
+ try {
+ const res = await api.get('/videos', { params });
+ return res.data.data || res.data || [];
+ } catch (error) {
+ console.error('Failed to fetch videos from API:', error);
+ return [];
+ }
+ },
+
+ getVideoById: async (id: string) => {
+ try {
+ const res = await api.get(`/videos/${id}`);
+ return res.data;
+ } catch (error) {
+ console.error('Failed to fetch video details:', error);
+ return null;
+ }
+ }
+};