diff --git a/src/pages/Billboards/Billboards.tsx b/src/pages/Billboards/Billboards.tsx index bae932c..a5d93df 100644 --- a/src/pages/Billboards/Billboards.tsx +++ b/src/pages/Billboards/Billboards.tsx @@ -1,17 +1,186 @@ -import { Card, Typography } from 'antd'; +import { App, Button, Table, Tag, Tooltip, Typography } from 'antd'; +import type { TableColumnsType } from 'antd'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import { colors } from '@/design/tokens/colors'; -import { spacing } from '@/design/tokens/spacing'; +import { formatDateTime } from '@/utils/formatDate'; +import { adsApi } from '@/services/adsApi'; +import type { Ad } from '@/types/ad'; -const { Title, Paragraph } = Typography; +const { Text } = Typography; + +const PAGE_SIZE = 10; + +const AD_TYPE_LABELS: Record = { + 1: 'بیلبورد', + 2: 'بیلبورد کپچر شده', + 99: 'تبلیغ/پروموشن', + 100: 'poi', +}; + +type AdStateColor = 'default' | 'processing' | 'success' | 'warning' | 'error'; + +const AD_STATE_LABELS: Record = { + 0: { label: 'پیش‌نویس', color: 'default' }, + 2: { label: 'در حال بررسی', color: 'processing' }, + 9: { label: 'رد شده', color: 'error' }, + 18: { label: 'متوقف‌شده (توسط کاربر)', color: 'warning' }, + 19: { label: 'لغو انتشار شده', color: 'warning' }, + 20: { label: 'منتشر شده', color: 'success' }, + 100: { label: 'حذف شده', color: 'error' }, + 200: { label: 'در حال اجرا', color: 'processing' }, + 400: { label: 'پایان‌یافته', color: 'success' }, + 500: { label: 'غیرفعال', color: 'default' }, +}; + +const truncateUuid = (value: string) => `${value.slice(0, 8)}…`; + +const DESCRIPTION_MAX_LENGTH = 40; +const truncateText = (value: string, maxLength = DESCRIPTION_MAX_LENGTH) => + value.length > maxLength ? `${value.slice(0, maxLength)}…` : value; + +const EmptyValue = () => —; + +interface FetchResult { + page: number; + status: 'success' | 'error'; + ads: Ad[]; + total: number; +} const Billboards = () => { + const { message } = App.useApp(); + const [page, setPage] = useState(1); + const [result, setResult] = useState(null); + + useEffect(() => { + adsApi + .getAdminAds({ limit: PAGE_SIZE, offset: (page - 1) * PAGE_SIZE }) + .then(({ results, count }) => { + setResult({ page, status: 'success', ads: results, total: count }); + }) + .catch(() => { + setResult({ page, status: 'error', ads: [], total: 0 }); + message.error('دریافت اطلاعات بیلبوردها با خطا مواجه شد'); + }); + }, [page, message]); + + const isCurrent = result?.page === page; + const isLoading = !isCurrent; + const ads = isCurrent ? result.ads : []; + const total = isCurrent ? result.total : 0; + + const columns: TableColumnsType = [ + { + title: 'شناسه', + dataIndex: 'uuid', + key: 'uuid', + render: (value: string) => ( + + + {truncateUuid(value)} + + + ), + }, + { + title: 'کاربر (ثبت‌کننده)', + dataIndex: 'user', + key: 'user', + render: (value: string) => ( + + + {truncateUuid(value)} + + + ), + }, + { + title: 'عنوان', + dataIndex: 'title', + key: 'title', + ellipsis: true, + render: (value: string | null) => value ?? , + }, + { + title: 'نوع', + dataIndex: 'type', + key: 'type', + render: (value: number) => {AD_TYPE_LABELS[value] ?? value}, + }, + { + title: 'توضیحات', + dataIndex: 'description', + key: 'description', + render: (value: string | null) => + value ? ( + + {truncateText(value)} + + ) : ( + + ), + }, + { + title: 'برچسب‌ها', + dataIndex: 'tags', + key: 'tags', + render: (tags: string[]) => + tags?.length ? ( +
+ {tags.map((tag) => ( + {tag} + ))} +
+ ) : ( + + ), + }, + { title: 'تاریخ ایجاد', dataIndex: 'created_at', key: 'created_at', render: formatDateTime }, + { title: 'تاریخ آخرین ویرایش', dataIndex: 'updated_at', key: 'updated_at', render: formatDateTime }, + { + title: 'وضعیت', + dataIndex: 'state', + key: 'state', + render: (value: number) => { + const state = AD_STATE_LABELS[value]; + return {state?.label ?? value}; + }, + }, + { title: 'تعداد نظرات', dataIndex: 'comments_count', key: 'comments_count' }, + { title: 'تعداد لایک‌ها', dataIndex: 'likes_count', key: 'likes_count' }, + { + title: 'فعال/غیرفعال', + dataIndex: 'is_active', + key: 'is_active', + render: (value: boolean) => {value ? 'فعال' : 'غیرفعال'}, + }, + { + title: 'جزئیات', + key: 'details', + render: () => ( + + ), + }, + ]; + return ( - - بیلبوردها - - مدیریت جدول بیلبوردها به‌زودی اضافه خواهد شد. - - + ); }; diff --git a/src/services/adsApi.ts b/src/services/adsApi.ts new file mode 100644 index 0000000..1fe7b27 --- /dev/null +++ b/src/services/adsApi.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; +import { env } from '@/config/env'; +import { getAccessToken } from '@/utils/authToken'; +import { buildAuthUrl } from '@/utils/authUrl'; +import type { AdminAdsResponse } from '@/types/ad'; + +interface GetAdminAdsParams { + limit: number; + offset: number; +} + +export const adsApi = { + getAdminAds: async ({ limit, offset }: GetAdminAdsParams): Promise => { + const response = await axios.get( + buildAuthUrl('/advertising/stores/admin-ads/', env.apiBaseUrl), + { + params: { limit, offset }, + headers: { + Authorization: `Bearer ${getAccessToken()}`, + }, + }, + ); + + return response.data; + }, +}; diff --git a/src/types/ad.ts b/src/types/ad.ts new file mode 100644 index 0000000..3e48774 --- /dev/null +++ b/src/types/ad.ts @@ -0,0 +1,51 @@ +export interface AdLocation { + type: string; + coordinates: [number, number]; +} + +export interface AdMediaAsset { + uuid: string; + object_key: string; + content_type: number; + filename: string; + state: number; + url: string; +} + +export interface Ad { + uuid: string; + campaign: string | null; + title: string | null; + type: number; + details: Record; + content: string; + contents: unknown[]; + refined_contents: AdMediaAsset[]; + description: string | null; + location: AdLocation | null; + location_is_approximate: boolean; + banner: string | null; + refined_banner: AdMediaAsset | null; + reward_amount: number; + reward_count: number; + address: string | null; + url: string | null; + tags: string[]; + created_at: string; + updated_at: string; + display_time: number; + state: number; + status: string; + is_active: boolean; + user: string; + user_uuid: string; + comments_count: number; + likes_count: number; +} + +export interface AdminAdsResponse { + count: number; + next: string | null; + previous: string | null; + results: Ad[]; +} diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 0000000..3586b75 --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,17 @@ +export const formatDateTime = (value: string): string => { + const date = new Date(value); + + if (Number.isNaN(date.getTime())) { + return value; + } + + return date + .toLocaleString('en-GB', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + .replace(',', ''); +};