This commit is contained in:
hamid zarghami
2025-10-25 12:53:58 +03:30
parent 8e81321add
commit 4621233901
11 changed files with 453 additions and 4 deletions
+4 -1
View File
@@ -14,7 +14,10 @@ export const Paths = {
detail: '/announcement/', detail: '/announcement/',
}, },
criticisms: '/criticisms', criticisms: '/criticisms',
learning: '/learning', learning: {
list: '/learning/list',
detail: '/learning/',
},
auth: { auth: {
login: '/auth/login', login: '/auth/login',
}, },
@@ -1,5 +1,5 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { CreateCriticismsTypes } from "../types/CriticismsTypes"; import { type CreateCriticismsTypes } from "../types/CriticismsTypes";
import * as api from "../service/CriticismsService"; import * as api from "../service/CriticismsService";
export const useCreateCriticisms = () => { export const useCreateCriticisms = () => {
@@ -1,5 +1,5 @@
import axios from "../../../config/axios"; import axios from "../../../config/axios";
import { CreateCriticismsTypes } from "../types/CriticismsTypes"; import { type CreateCriticismsTypes } from "../types/CriticismsTypes";
export const createCriticisms = async (params: CreateCriticismsTypes) => { export const createCriticisms = async (params: CreateCriticismsTypes) => {
const { data } = await axios.post(`/criticisms`, params); const { data } = await axios.post(`/criticisms`, params);
+90
View File
@@ -0,0 +1,90 @@
import { type FC } from 'react'
import { useParams } from 'react-router-dom'
import { useGetLearningDetail } from './hooks/useLearningData'
import PageLoading from '../../components/PageLoading'
// import DanakLearning from '../home/components/DanakLearning'
import { Calendar2, Clock, Element3 } from 'iconsax-react'
import moment from 'moment-jalaali'
import { Helmet } from 'react-helmet-async'
import { type LearningItemType } from './types/LearningTypes'
const LearningDetail: FC = () => {
const { id } = useParams()
const getLearningDetail = useGetLearningDetail(id ? id : '')
return (
<div className='mt-4'>
<Helmet>
<title>
داناک | آموزش
</title>
</Helmet>
{
getLearningDetail.isPending ?
<PageLoading />
:
<div className='xl:mt-9 mt-4 flex flex-wrap xl:gap-8 gap-6 text-sm'>
<div className='flex-1'>
<div className='bg-white p-8 rounded-3xl'>
<video className='w-full rounded-3xl' controls>
<source src={(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.videoUrl} />
</video>
<div className='mt-7 flex xl:flex-row flex-col gap-4 justify-between'>
<div className='xl:text-lg'>
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.title}
</div>
<div className='flex xl:flex-row flex-col xl:max-h-5 gap-3 text-description text-xs'>
<div className='flex gap-2 items-center'>
<Clock
size={18}
color={'#888888'}
/>
<div>
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.videoDuration + ' دقیقه'}
</div>
</div>
<div className='flex gap-2 items-center xl:border-r xl:pr-3 border-description'>
<Element3
size={18}
color={'#888888'}
/>
<div>
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.category?.name}
</div>
</div>
<div className='flex gap-2 items-center xl:border-r xl:pr-3 border-description'>
<Calendar2
size={18}
color={'#888888'}
/>
<div>
{moment((getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.createdAt).format('jYYYY-jMM-jDD')}
</div>
</div>
</div>
</div>
<div className='mt-6 text-description'>
{(getLearningDetail.data as { data: { learning: LearningItemType } })?.data?.learning?.description}
</div>
</div>
</div>
{/* <DanakLearning /> */}
</div>
}
</div>
)
}
export default LearningDetail
+106
View File
@@ -0,0 +1,106 @@
import { type FC, useState } from 'react'
// import { useTranslation } from 'react-i18next'
import BannerImage from '../../assets/images/support1.png'
import Input from '../../components/Input'
import { useGetLearnings } from './hooks/useLearningData'
import PageLoading from '../../components/PageLoading'
import { type LearningItemType } from './types/LearningTypes'
import moment from 'moment-jalaali'
import { Link } from 'react-router-dom'
// import { Pages } from '../../config/Pages'
import { Helmet } from 'react-helmet-async'
const LearningList: FC = () => {
// const { t } = useTranslation('global')
const [search, setSearch] = useState<string>('')
const getLearnings = useGetLearnings(search)
return (
<div className='w-full flex gap-6 mt-4'>
<Helmet>
<title>
داناک | آموزش
</title>
</Helmet>
<div className='flex-1'>
<div>
آموزش
</div>
<div className='xl:mt-8 mt-4 flex xl:gap-6 gap-4 items-center'>
<div className='xl:w-[300px] w-full'>
<Input
variant='search'
placeholder='جستجو...'
className='bg-white w-full'
onChangeSearchFinal={(value) => setSearch(value)}
/>
</div>
{/* <div>
<Select
items={[
{ label: t('all'), value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
]}
className='max-w-[100px]'
/>
</div> */}
</div>
{
getLearnings.isPending ?
<div className='mt-5'>
<PageLoading />
</div>
:
<div className='xl:mt-9 mt-4 flex flex-wrap xl:gap-8 gap-6 text-sm'>
{
(getLearnings.data as { data: { learnings: LearningItemType[] } })?.data?.learnings?.map((item: LearningItemType) => {
return (
<Link to={`/learning/${item.id}`} className='bg-white p-4 rounded-3xl xl:min-w-[30%] min-w-full flex-1'>
<img src={item.coverUrl} className='w-full rounded-[20px]' />
<div className='mt-4'>
{item.title}
</div>
<div className='flex gap-1 text-xs text-description mt-2'>
<div>{item.category.name} ,</div>
<div>{item.videoDuration} دقیقه</div>
</div>
<div className='mt-3 text-xs text-description'>
{moment(item.createdAt).format('jYYYY/jMM/jDD')}
</div>
</Link>
)
})
}
<div className='min-w-[30%] flex-1 px-4'></div>
<div className='min-w-[30%] flex-1 px-4'></div>
</div>
}
<div className='h-14 xl:hidden'></div>
</div>
<div className='bg-white w-[300px] hidden xl:block h-fit rounded-3xl overflow-hidden relative'>
<img
src={BannerImage}
className='w-full backdrop-blur-[100px] h-[550px] object-cover'
/>
<div className='absolute flex items-center px-4 w-full bottom-0 h-[86px] bg-primary bg-opacity-5 backdrop-blur-[14px]'>
<div className='max-w-[80%] text-sm leading-6 text-white'>
سفارش نرمافزار اختصاصی: سرمایه‌ای برای آینده یا فقط هزینهای برای امروز؟
</div>
</div>
</div>
</div>
)
}
export default LearningList
+138
View File
@@ -0,0 +1,138 @@
// import type { LearningItemType } from "../types/LearningTypes";
export const staticLearningsData = {
data: {
learnings: [
{
id: "1",
title: "آموزش کامل React و TypeScript",
description:
"در این دوره شما با مفاهیم پایه و پیشرفته React و TypeScript آشنا خواهید شد و پروژه‌های عملی انجام خواهید داد.",
coverUrl: "/src/assets/images/new_order.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4",
videoDuration: "120",
createdAt: "2024-01-15T10:30:00Z",
updatedAt: "2024-01-15T10:30:00Z",
category: {
id: "1",
name: "Frontend",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
{
id: "2",
title: "Node.js و Express.js از صفر تا صد",
description:
"یادگیری کامل Node.js و Express.js برای توسعه سمت سرور و ایجاد API های قدرتمند.",
coverUrl: "/src/assets/images/support1.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_2mb.mp4",
videoDuration: "180",
createdAt: "2024-01-20T14:15:00Z",
updatedAt: "2024-01-20T14:15:00Z",
category: {
id: "2",
name: "Backend",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
{
id: "3",
title: "آموزش MongoDB و Mongoose",
description:
"کار با پایگاه داده NoSQL MongoDB و کتابخانه Mongoose برای مدیریت داده‌ها.",
coverUrl: "/src/assets/images/logo.svg",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_5mb.mp4",
videoDuration: "90",
createdAt: "2024-01-25T09:45:00Z",
updatedAt: "2024-01-25T09:45:00Z",
category: {
id: "3",
name: "Database",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
{
id: "4",
title: "Docker و Containerization",
description:
"آموزش کامل Docker برای containerization و deployment اپلیکیشن‌ها.",
coverUrl: "/src/assets/images/avatar_image.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_10mb.mp4",
videoDuration: "150",
createdAt: "2024-02-01T16:20:00Z",
updatedAt: "2024-02-01T16:20:00Z",
category: {
id: "4",
name: "DevOps",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
{
id: "5",
title: "آموزش Git و GitHub",
description: "مدیریت نسخه‌ها با Git و همکاری تیمی با GitHub.",
coverUrl: "/src/assets/images/new_order.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4",
videoDuration: "75",
createdAt: "2024-02-05T11:10:00Z",
updatedAt: "2024-02-05T11:10:00Z",
category: {
id: "5",
name: "Tools",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
{
id: "6",
title: "آموزش CSS و Tailwind CSS",
description:
"طراحی و استایل‌دهی صفحات وب با CSS و فریمورک Tailwind CSS.",
coverUrl: "/src/assets/images/support1.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_2mb.mp4",
videoDuration: "105",
createdAt: "2024-02-10T13:30:00Z",
updatedAt: "2024-02-10T13:30:00Z",
category: {
id: "6",
name: "Styling",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
],
},
};
export const staticLearningDetailData = {
data: {
learning: {
id: "1",
title: "آموزش کامل React و TypeScript",
description:
"در این دوره شما با مفاهیم پایه و پیشرفته React و TypeScript آشنا خواهید شد و پروژه‌های عملی انجام خواهید داد. این دوره شامل موارد زیر است:\n\n• نصب و راه‌اندازی محیط توسعه\n• آشنایی با JSX و Components\n• State و Props در React\n• Hooks و Lifecycle Methods\n• TypeScript و Type Safety\n• Routing و Navigation\n• State Management\n• Testing و Debugging\n• Deployment و Production",
coverUrl: "/src/assets/images/new_order.png",
videoUrl:
"https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4",
videoDuration: "120",
createdAt: "2024-01-15T10:30:00Z",
updatedAt: "2024-01-15T10:30:00Z",
category: {
id: "1",
name: "Frontend",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
},
},
};
@@ -0,0 +1,78 @@
import { useQuery } from "@tanstack/react-query";
// import * as api from "../service/LearningService";
import {
staticLearningsData,
staticLearningDetailData,
} from "../data/staticData";
export const useGetLearnings = (search: string, limit?: number) => {
return useQuery({
queryKey: ["learnings", search, limit],
// queryFn: () => api.getLearnings(search, limit),
queryFn: () => {
// شبیه‌سازی delay سرور
return new Promise((resolve) => {
setTimeout(() => {
let filteredData = staticLearningsData;
// فیلتر کردن بر اساس جستجو
if (search) {
filteredData = {
data: {
learnings: staticLearningsData.data.learnings.filter(
(item) =>
item.title.toLowerCase().includes(search.toLowerCase()) ||
item.description
.toLowerCase()
.includes(search.toLowerCase()) ||
item.category.name
.toLowerCase()
.includes(search.toLowerCase())
),
},
};
}
// محدود کردن تعداد نتایج
if (limit) {
filteredData = {
data: {
learnings: filteredData.data.learnings.slice(0, limit),
},
};
}
resolve(filteredData);
}, 500);
});
},
});
};
export const useGetLearningDetail = (id: string) => {
return useQuery({
queryKey: ["learning", id],
// queryFn: () => api.getLearningDetail(id),
queryFn: () => {
// شبیه‌سازی delay سرور
return new Promise((resolve) => {
setTimeout(() => {
// پیدا کردن آموزش بر اساس ID
const learning = staticLearningsData.data.learnings.find(
(item) => item.id === id
);
if (learning) {
resolve({
data: {
learning: learning,
},
});
} else {
resolve(staticLearningDetailData);
}
}, 300);
});
},
enabled: !!id,
});
};
@@ -0,0 +1,14 @@
// import axios from "../../../config/axios";
// Service calls کامنت شده - در حال حاضر از دیتای استاتیک استفاده می‌شود
// export const getLearnings = async (search: string, limit?: number) => {
// const { data } = await axios.get(
// `/learnings?q=${search}&limit=${limit ? limit : 20}`
// );
// return data;
// };
// export const getLearningDetail = async (id: string) => {
// const { data } = await axios.get(`/learnings/${id}`);
// return data;
// };
+16
View File
@@ -0,0 +1,16 @@
export type LearningItemType = {
id: string;
title: string;
description: string;
coverUrl: string;
videoUrl: string;
videoDuration: string;
createdAt: string;
updatedAt: string;
category: {
id: string;
name: string;
createdAt: string;
updatedAt: string;
};
};
+4
View File
@@ -14,6 +14,8 @@ import TicketDetail from '@/pages/ticket/Detail'
import AnnouncementList from '@/pages/annoncement/List' import AnnouncementList from '@/pages/annoncement/List'
import AnnouncementDetail from '@/pages/annoncement/Detail' import AnnouncementDetail from '@/pages/annoncement/Detail'
import AddCriticisms from '@/pages/criticisms/Add' import AddCriticisms from '@/pages/criticisms/Add'
import LearningList from '@/pages/learning/List'
import LearningDetail from '@/pages/learning/Detail'
const MainRouter: FC = () => { const MainRouter: FC = () => {
return ( return (
@@ -37,6 +39,8 @@ const MainRouter: FC = () => {
<Route path={Paths.announcement.list} element={<AnnouncementList />} /> <Route path={Paths.announcement.list} element={<AnnouncementList />} />
<Route path={`${Paths.announcement.detail}:id`} element={<AnnouncementDetail />} /> <Route path={`${Paths.announcement.detail}:id`} element={<AnnouncementDetail />} />
<Route path={Paths.criticisms} element={<AddCriticisms />} /> <Route path={Paths.criticisms} element={<AddCriticisms />} />
<Route path={Paths.learning.list} element={<LearningList />} />
<Route path={`${Paths.learning.detail}:id`} element={<LearningDetail />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+1 -1
View File
@@ -96,7 +96,7 @@ const SideBar: FC = () => {
icon={<Teacher size={iconSizeSideBar} color='#4F5260' />} icon={<Teacher size={iconSizeSideBar} color='#4F5260' />}
title={t('sidebar.learning')} title={t('sidebar.learning')}
isActive={isActive('/learning')} isActive={isActive('/learning')}
link={Paths.learning} link={Paths.learning.list}
/> />
</div> </div>