feat: billboard table added

This commit is contained in:
Afra ‌ 2026-08-08 16:58:27 +03:30
parent fedc39a413
commit b85ff7cbf8
4 changed files with 272 additions and 9 deletions

View file

@ -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 { 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<number, string> = {
1: 'بیلبورد',
2: 'بیلبورد کپچر شده',
99: 'تبلیغ/پروموشن',
100: 'poi',
};
type AdStateColor = 'default' | 'processing' | 'success' | 'warning' | 'error';
const AD_STATE_LABELS: Record<number, { label: string; color: AdStateColor }> = {
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 = () => <Text style={{ color: colors.textSecondary }}>—</Text>;
interface FetchResult {
page: number;
status: 'success' | 'error';
ads: Ad[];
total: number;
}
const Billboards = () => { const Billboards = () => {
const { message } = App.useApp();
const [page, setPage] = useState(1);
const [result, setResult] = useState<FetchResult | null>(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<Ad> = [
{
title: 'شناسه',
dataIndex: 'uuid',
key: 'uuid',
render: (value: string) => (
<Tooltip title={value}>
<Text copyable={{ text: value }} style={{ direction: 'ltr', fontSize: 12 }}>
{truncateUuid(value)}
</Text>
</Tooltip>
),
},
{
title: 'کاربر (ثبت‌کننده)',
dataIndex: 'user',
key: 'user',
render: (value: string) => (
<Tooltip title={value}>
<Link to={`/profile?uuid=${value}`} style={{ direction: 'ltr', fontSize: 12 }}>
{truncateUuid(value)}
</Link>
</Tooltip>
),
},
{
title: 'عنوان',
dataIndex: 'title',
key: 'title',
ellipsis: true,
render: (value: string | null) => value ?? <EmptyValue />,
},
{
title: 'نوع',
dataIndex: 'type',
key: 'type',
render: (value: number) => <Tag color={colors.primary}>{AD_TYPE_LABELS[value] ?? value}</Tag>,
},
{
title: 'توضیحات',
dataIndex: 'description',
key: 'description',
render: (value: string | null) =>
value ? (
<Tooltip title={value}>
<span>{truncateText(value)}</span>
</Tooltip>
) : (
<EmptyValue />
),
},
{
title: 'برچسب‌ها',
dataIndex: 'tags',
key: 'tags',
render: (tags: string[]) =>
tags?.length ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
) : (
<EmptyValue />
),
},
{ 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 <Tag color={state?.color ?? 'default'}>{state?.label ?? value}</Tag>;
},
},
{ title: 'تعداد نظرات', dataIndex: 'comments_count', key: 'comments_count' },
{ title: 'تعداد لایک‌ها', dataIndex: 'likes_count', key: 'likes_count' },
{
title: 'فعال/غیرفعال',
dataIndex: 'is_active',
key: 'is_active',
render: (value: boolean) => <Tag color={value ? 'success' : 'default'}>{value ? 'فعال' : 'غیرفعال'}</Tag>,
},
{
title: 'جزئیات',
key: 'details',
render: () => (
<Button size="small" disabled>
مشاهده جزئیات
</Button>
),
},
];
return ( return (
<Card bordered={false} style={{ boxShadow: 'none', padding: 0 }}> <Table
<Title level={2}>بیلبوردها</Title> rowKey="uuid"
<Paragraph style={{ color: colors.textSecondary, marginTop: spacing.sm }}> columns={columns}
مدیریت جدول بیلبوردها به‌زودی اضافه خواهد شد. dataSource={ads}
</Paragraph> loading={isLoading}
</Card> scroll={{ x: 'max-content' }}
pagination={{
current: page,
pageSize: PAGE_SIZE,
total,
showSizeChanger: false,
onChange: setPage,
}}
/>
); );
}; };

26
src/services/adsApi.ts Normal file
View file

@ -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<AdminAdsResponse> => {
const response = await axios.get<AdminAdsResponse>(
buildAuthUrl('/advertising/stores/admin-ads/', env.apiBaseUrl),
{
params: { limit, offset },
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
},
);
return response.data;
},
};

51
src/types/ad.ts Normal file
View file

@ -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<string, unknown>;
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[];
}

17
src/utils/formatDate.ts Normal file
View file

@ -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(',', '');
};