announcement
This commit is contained in:
+9
-4
@@ -3,19 +3,24 @@ import '@/assets/fonts/irancell/style.css'
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import 'react-loading-skeleton/dist/skeleton.css'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
// import { getToken } from './config/func'
|
||||
import MainRouter from './router/MainRouter'
|
||||
// import AuthRouter from './router/AuthRouter'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
// const isLoggedIn = getToken()
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
{/* {isLoggedIn ? <MainRouter /> : <AuthRouter />} */}
|
||||
<MainRouter />
|
||||
</BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
{/* {isLoggedIn ? <MainRouter /> : <AuthRouter />} */}
|
||||
<MainRouter />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
const PageLoading: FC = () => {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-[200px]'>
|
||||
<div className='flex flex-col items-center gap-4'>
|
||||
<div className='w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin' />
|
||||
<div className='text-sm text-gray-500'>در حال بارگذاری...</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageLoading
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type FC } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
|
||||
type Props = {
|
||||
variant: 'error' | 'success' | 'warning' | 'info'
|
||||
text: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const StatusWithText: FC<Props> = ({ variant, text, className }) => {
|
||||
const variantStyles = {
|
||||
error: 'bg-red-50 text-red-600',
|
||||
success: 'bg-green-50 text-green-600',
|
||||
warning: 'bg-orange-50 text-orange-600',
|
||||
info: 'bg-blue-50 text-blue-600',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clx(
|
||||
'px-2 py-1 rounded-lg text-xs font-medium whitespace-nowrap',
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusWithText
|
||||
@@ -11,6 +11,7 @@ export const Paths = {
|
||||
},
|
||||
announcement: {
|
||||
list: '/announcement',
|
||||
detail: '/announcement/',
|
||||
},
|
||||
criticisms: '/criticisms',
|
||||
learning: '/learning',
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ textarea::placeholder {
|
||||
|
||||
@theme {
|
||||
--color-primary: #ffa800;
|
||||
--color-border: #f5f7fc;
|
||||
--color-border: #d0d0d0;
|
||||
--color-secondary: #ebedf5;
|
||||
--color-description: #c3c7dd;
|
||||
--color-desc: #8c90a3;
|
||||
|
||||
+9
-1
@@ -7,13 +7,21 @@ export const fa = {
|
||||
myOrders: "سفارشات من",
|
||||
preFactors: "پیش فاکتورها",
|
||||
tickets: "تیکت ها",
|
||||
announcement: "انتشارات",
|
||||
announcement: "اطلاعیه",
|
||||
criticisms: "انتقادات",
|
||||
learning: "آموزش",
|
||||
submitYourOrder: "سفارش خودرا ثبت کنید",
|
||||
submitYourOrderDescription: "کارت ویزیت ، کاتالوگ ، بروشور و...",
|
||||
newOrder: "سفارش جدید",
|
||||
},
|
||||
announcement: {
|
||||
announcements: "اطلاعیه ها",
|
||||
search: "جستجو",
|
||||
important: "مهم",
|
||||
new: "جدید",
|
||||
text_announcement: "متن اطلاعیه",
|
||||
},
|
||||
back: "بازگشت",
|
||||
notif: {
|
||||
natification: "اعلانها",
|
||||
all_read: "خواندن همه",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type FC } from 'react'
|
||||
import StatusWithText from '../../components/StatusWithText'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { ArrowLeft2 } from 'iconsax-react'
|
||||
import { useGetAnnoncementDetail } from './hooks/useAnnoncementData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { t } from '@/locale'
|
||||
|
||||
const AnnouncementDetail: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const getAnnoncementDetail = useGetAnnoncementDetail(id ? id : '')
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex justify-between'>
|
||||
<div>
|
||||
{t('announcement.text_announcement')}
|
||||
</div>
|
||||
<div onClick={() => navigate(-1)} className='text-xs flex gap-0.5 items-center font-extralight'>
|
||||
{t('back')}
|
||||
<ArrowLeft2 size={12} color='black' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getAnnoncementDetail.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='flex-1 mt-4 bg-white py-6 px-4 rounded-2xl'>
|
||||
<div className='flex justify-between items-start'>
|
||||
<div className='text-sm max-w-[70%] leading-67'>
|
||||
{getAnnoncementDetail.data?.data?.announcement?.title}
|
||||
</div>
|
||||
{
|
||||
getAnnoncementDetail.data?.data?.announcement.isImportant &&
|
||||
<StatusWithText
|
||||
variant='error'
|
||||
text={t('announcement.important')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: getAnnoncementDetail.data?.data?.announcement?.content || '' }} className='mt-5 leading-7 text-xs font-light'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnnouncementDetail
|
||||
@@ -0,0 +1,146 @@
|
||||
import { type FC, Fragment, useState } from 'react'
|
||||
import Input from '@/components/Input'
|
||||
import StatusWithText from '@/components/StatusWithText'
|
||||
import { clx } from '@/helpers/utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import { useGetAnnoncement, useGetAnnoncementDetail } from './hooks/useAnnoncementData'
|
||||
import type { AnnoncementItemType } from './types/AnnoncementTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import AnouncementSkeleton from './components/AnouncementSkeleton'
|
||||
import { t } from '@/locale'
|
||||
|
||||
const AnnouncementtList: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const [itemSelected, setItemSelected] = useState<AnnoncementItemType>()
|
||||
const getAnnoncements = useGetAnnoncement(search)
|
||||
useGetAnnoncementDetail(itemSelected ? itemSelected.announcement.id : '')
|
||||
|
||||
|
||||
const handleChange = (item: AnnoncementItemType) => {
|
||||
if (window.innerWidth < 1280) {
|
||||
navigate(Paths.announcement.detail + item.announcement.id)
|
||||
} else {
|
||||
setItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-4 text-sm'>
|
||||
<div className='text-base'>
|
||||
{t('announcement.announcements')}
|
||||
</div>
|
||||
|
||||
<div className='mt-10 flex gap-8'>
|
||||
<div className='bg-white xl:w-[400px] w-full rounded-2xl p-6'>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('announcement.search')}
|
||||
className='-mt-1'
|
||||
onChangeSearchFinal={(value) => setSearch(value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<Select
|
||||
items={[
|
||||
{ value: '1', label: 'همه' },
|
||||
{ value: '2', label: 'فعال' },
|
||||
{ value: '3', label: 'غیر فعال' },
|
||||
]}
|
||||
className='bg-[#EEF0F7] w-[68px]'
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<Fragment>
|
||||
<div className='mt-8'>
|
||||
{
|
||||
getAnnoncements.isPending ?
|
||||
<Fragment>
|
||||
<AnouncementSkeleton />
|
||||
<AnouncementSkeleton />
|
||||
<AnouncementSkeleton />
|
||||
</Fragment>
|
||||
:
|
||||
getAnnoncements.data?.data?.announcements?.map((item: AnnoncementItemType) => {
|
||||
return (
|
||||
<div onClick={() => handleChange(item)} className={clx(
|
||||
'p-4 border-b border-border cursor-pointer',
|
||||
item.id === itemSelected?.id && 'bg-[#F8F9FB]'
|
||||
)}>
|
||||
<div className='flex justify-between'>
|
||||
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
|
||||
{item.announcement.title}
|
||||
</div>
|
||||
|
||||
<div className='flex gap-1'>
|
||||
{
|
||||
item.announcement.isImportant &&
|
||||
<StatusWithText
|
||||
variant='error'
|
||||
text={t('announcement.important')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!item.isRead &&
|
||||
<StatusWithText
|
||||
variant='success'
|
||||
text={t('announcement.new')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='m-2 flex justify-between items-center text-[11px]'>
|
||||
<div dangerouslySetInnerHTML={{ __html: item.announcement.content || '' }} className='max-w-[70%] line-clamp-1 truncate text-description'>
|
||||
</div>
|
||||
<div className='text-description'>
|
||||
{moment(item.announcement.publishAt).format('jYYYY/jMM/jDD')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
|
||||
{
|
||||
itemSelected &&
|
||||
<div className='flex-1 hidden xl:block bg-white py-7 px-5 rounded-2xl'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='text-lg'>
|
||||
متن اطلاعیه
|
||||
</div>
|
||||
{
|
||||
itemSelected.announcement.isImportant &&
|
||||
<StatusWithText
|
||||
variant='error'
|
||||
text={t('announcement.important')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
{itemSelected.announcement.title}
|
||||
</div>
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: itemSelected.announcement.content || '' }} className='mt-5 leading-7 text-xs font-light'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnnouncementtList
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type FC } from 'react'
|
||||
import Skeleton from 'react-loading-skeleton'
|
||||
|
||||
const AnouncementSkeleton: FC = () => {
|
||||
return (
|
||||
<div className={'p-4 border-b'}>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
|
||||
<Skeleton
|
||||
height={7}
|
||||
width={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-1'>
|
||||
<Skeleton
|
||||
className='!w-10 !h-6 !rounded-full'
|
||||
/>
|
||||
<Skeleton
|
||||
className='!w-10 !h-6 !rounded-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='m-2 flex justify-between items-center text-[11px]'>
|
||||
<Skeleton
|
||||
height={7}
|
||||
width={150}
|
||||
/>
|
||||
<div className='text-description'>
|
||||
<Skeleton
|
||||
height={7}
|
||||
width={40}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnouncementSkeleton
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as api from "../service/AnnoncementService";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const useGetAnnoncement = (search: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["announcements", search],
|
||||
queryFn: () => api.getAnnoncements(search),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAnnoncementDetail = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["announcement", id],
|
||||
queryFn: () => api.getAnnoncementDetail(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
// import axios from "../../../config/axios";
|
||||
|
||||
// دادههای استاتیک برای نمایش
|
||||
const mockAnnoncementsData = {
|
||||
data: {
|
||||
announcements: [
|
||||
{
|
||||
id: "1",
|
||||
createdAt: "2024-01-15T10:00:00Z",
|
||||
updatedAt: "2024-01-15T10:00:00Z",
|
||||
announcement: {
|
||||
id: "ann1",
|
||||
createdAt: "2024-01-15T10:00:00Z",
|
||||
title: "بهروزرسانی سیستم اطلاعرسانی",
|
||||
content:
|
||||
"سیستم اطلاعرسانی با ویژگیهای جدید بهروزرسانی شد. لطفاً از طریق بخش تنظیمات، تنظیمات خود را بررسی کنید.",
|
||||
publishAt: "2024-01-15T10:00:00Z",
|
||||
targetService: {
|
||||
id: "1",
|
||||
name: "سرویس عمومی",
|
||||
},
|
||||
isImportant: true,
|
||||
},
|
||||
isRead: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
createdAt: "2024-01-14T08:00:00Z",
|
||||
updatedAt: "2024-01-14T08:00:00Z",
|
||||
announcement: {
|
||||
id: "ann2",
|
||||
createdAt: "2024-01-14T08:00:00Z",
|
||||
title: "نگهداری برنامهریزی شده",
|
||||
content:
|
||||
"در تاریخ ۲۰ دی ماه، بهدلیل انجام نگهداری برنامهریزی شده، سرویسها به مدت ۲ ساعت در دسترس نخواهند بود.",
|
||||
publishAt: "2024-01-14T08:00:00Z",
|
||||
targetService: {
|
||||
id: "1",
|
||||
name: "سرویس عمومی",
|
||||
},
|
||||
isImportant: false,
|
||||
},
|
||||
isRead: false,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
createdAt: "2024-01-13T15:30:00Z",
|
||||
updatedAt: "2024-01-13T15:30:00Z",
|
||||
announcement: {
|
||||
id: "ann3",
|
||||
createdAt: "2024-01-13T15:30:00Z",
|
||||
title: "ویژگی جدید: پشتیبانی از پرداخت آنلاین",
|
||||
content:
|
||||
"ویژگی پرداخت آنلاین به سیستم اضافه شد. از این پس میتوانید بهراحتی از طریق درگاههای بانکی پرداخت خود را انجام دهید.",
|
||||
publishAt: "2024-01-13T15:30:00Z",
|
||||
targetService: {
|
||||
id: "2",
|
||||
name: "سرویس پرداخت",
|
||||
},
|
||||
isImportant: false,
|
||||
},
|
||||
isRead: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnoncementDetailData = (id: string) => {
|
||||
const announcement = mockAnnoncementsData.data.announcements.find(
|
||||
(item) => item.announcement.id === id
|
||||
);
|
||||
|
||||
if (!announcement) {
|
||||
return {
|
||||
data: {
|
||||
announcement: {
|
||||
id: id,
|
||||
createdAt: "2024-01-15T10:00:00Z",
|
||||
title: "اطلاعیه نمونه",
|
||||
content:
|
||||
"<p>این یک اطلاعیه نمونه است. محتوای این اطلاعیه برای نمایش داده شده است.</p>",
|
||||
publishAt: "2024-01-15T10:00:00Z",
|
||||
targetService: {
|
||||
id: "1",
|
||||
name: "سرویس عمومی",
|
||||
},
|
||||
isImportant: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
announcement: announcement.announcement,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getAnnoncements = async (search: string) => {
|
||||
// ارتباط با سرور کامنت شده - از داده استاتیک استفاده میشود
|
||||
// const { data } = await axios.get(`/announcements/user?q=${search}`);
|
||||
// return data;
|
||||
|
||||
// شبیهسازی تاخیر شبکه
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// فیلتر بر اساس جستجو
|
||||
let filteredData = { ...mockAnnoncementsData };
|
||||
if (search && search.trim()) {
|
||||
filteredData.data.announcements = mockAnnoncementsData.data.announcements.filter(
|
||||
(item) =>
|
||||
item.announcement.title.includes(search) ||
|
||||
item.announcement.content.includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
};
|
||||
|
||||
export const getAnnoncementDetail = async (id: string) => {
|
||||
// ارتباط با سرور کامنت شده - از داده استاتیک استفاده میشود
|
||||
// const { data } = await axios.get(`/announcements/${id}`);
|
||||
// return data;
|
||||
|
||||
// شبیهسازی تاخیر شبکه
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
return mockAnnoncementDetailData(id);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export type AnnoncementItemType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
announcement: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
title: string;
|
||||
content: string;
|
||||
publishAt: string;
|
||||
targetService: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
isImportant: boolean;
|
||||
};
|
||||
isRead: boolean;
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { staticTicketsData } from './data/staticData'
|
||||
import { t } from '@/locale'
|
||||
import moment from 'moment-jalaali'
|
||||
import type { ColumnType } from '@/components/types/TableTypes'
|
||||
import { Paths } from '@/config/Paths'
|
||||
|
||||
// تعریف type برای تیکت
|
||||
interface TicketItem {
|
||||
@@ -89,7 +90,7 @@ const TicketList: FC = () => {
|
||||
title: t('ticket.detail'),
|
||||
key: 'actions',
|
||||
render: (item) => (
|
||||
<Link to={`/tickets/${item.id}`}>
|
||||
<Link to={Paths.tickets.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,8 @@ import OrderDetail from '@/pages/order/OrderDetail'
|
||||
import TicketList from '@/pages/ticket/TicketList'
|
||||
import CreateTicket from '@/pages/ticket/CreateTicket'
|
||||
import TicketDetail from '@/pages/ticket/Detail'
|
||||
import AnnouncementList from '@/pages/annoncement/List'
|
||||
import AnnouncementDetail from '@/pages/annoncement/Detail'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
return (
|
||||
@@ -31,6 +33,8 @@ const MainRouter: FC = () => {
|
||||
<Route path={Paths.tickets.list} element={<TicketList />} />
|
||||
<Route path={Paths.tickets.create} element={<CreateTicket />} />
|
||||
<Route path={`${Paths.tickets.detail}:id`} element={<TicketDetail />} />
|
||||
<Route path={Paths.announcement.list} element={<AnnouncementList />} />
|
||||
<Route path={`${Paths.announcement.detail}:id`} element={<AnnouncementDetail />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ const SideBar: FC = () => {
|
||||
)}
|
||||
>
|
||||
<div className='px-6'>
|
||||
<div className='flex border-b-2 border-border pb-10'>
|
||||
<div className='flex border-b-2 border-[#f5f7fc] pb-10'>
|
||||
<img src={LogoImage} className='w-[120px]' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user