feature/535 #3
4 changed files with 272 additions and 9 deletions
|
|
@ -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<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 { 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 (
|
||||
<Card bordered={false} style={{ boxShadow: 'none', padding: 0 }}>
|
||||
<Title level={2}>بیلبوردها</Title>
|
||||
<Paragraph style={{ color: colors.textSecondary, marginTop: spacing.sm }}>
|
||||
مدیریت جدول بیلبوردها بهزودی اضافه خواهد شد.
|
||||
</Paragraph>
|
||||
</Card>
|
||||
<Table
|
||||
rowKey="uuid"
|
||||
columns={columns}
|
||||
dataSource={ads}
|
||||
loading={isLoading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
total,
|
||||
showSizeChanger: false,
|
||||
onChange: setPage,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
26
src/services/adsApi.ts
Normal file
26
src/services/adsApi.ts
Normal 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
51
src/types/ad.ts
Normal 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
17
src/utils/formatDate.ts
Normal 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(',', '');
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue