Add e-payment UI, checkout payment result, and legacy migrate domain lookup.
Wire Mellat settings in business dashboard, customer bank return page with payment details, and super-admin migrate modal domain confirmation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a11f9643bb
commit
cc94b929e0
@@ -47,7 +47,7 @@ export function mapCommentApiToUi(comment: CommentApi): ProductComment {
|
||||
}
|
||||
|
||||
async function listEntityComments(
|
||||
entityType: 'product' | 'blog' | 'portfolio',
|
||||
entityType: 'product' | 'blog' | 'portfolio' | 'video' | 'instruction',
|
||||
entityId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
@@ -98,6 +98,24 @@ export async function listPortfolioComments(
|
||||
return listEntityComments('portfolio', portfolioId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function listVideoComments(
|
||||
videoId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return listEntityComments('video', videoId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function listInstructionComments(
|
||||
instructionId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return listEntityComments('instruction', instructionId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function updateCommentApproval(commentId: string, isApproved: boolean) {
|
||||
const data = await apiRequest<{ comment: CommentApi }>(commentsPath(`/${commentId}`), {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryFormData,
|
||||
CreateCategoryPayload,
|
||||
ProductCategoriesResponse,
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
|
||||
const ENTITY_TYPE = 'instruction' as const
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export async function listInstructionCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<ProductCategoriesResponse>(
|
||||
`${businessPath()}?entityType=${ENTITY_TYPE}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function createInstructionCategory(payload: CreateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { ...payload, entityType: ENTITY_TYPE },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateInstructionCategory(categoryId: string, payload: UpdateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteInstructionCategory(categoryId: string) {
|
||||
return apiRequest<{ message: string; deletedIds: string[] }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function toInstructionCategoryFormPayload(data: CategoryFormData): CreateCategoryPayload {
|
||||
return {
|
||||
parentId: data.parentId || undefined,
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapInstructionCategoryToUi(category: ProductCategory) {
|
||||
return {
|
||||
id: category.id,
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
Instruction,
|
||||
InstructionAuthor,
|
||||
InstructionDetail,
|
||||
InstructionStatus,
|
||||
VideoProvider,
|
||||
} from '../types/instruction'
|
||||
|
||||
export const INSTRUCTIONS_PER_PAGE = 12
|
||||
|
||||
export interface InstructionApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
titleFa: string
|
||||
titleEn: string | null
|
||||
slug: string
|
||||
abstract: string
|
||||
descriptionHtml: string
|
||||
videoProvider: VideoProvider | null
|
||||
embedCode: string
|
||||
status: InstructionStatus
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
coverImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
author?: InstructionAuthor | null
|
||||
commentCount: number
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface InstructionsListResponse {
|
||||
items: InstructionApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListInstructionsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: InstructionStatus
|
||||
provider?: VideoProvider
|
||||
categoryId?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/instructions${suffix}`
|
||||
}
|
||||
|
||||
export interface CreateInstructionPayload {
|
||||
titleFa: string
|
||||
titleEn?: string
|
||||
abstract?: string
|
||||
descriptionHtml?: string
|
||||
videoProvider?: VideoProvider
|
||||
embedCode?: string
|
||||
categoryId?: string
|
||||
status?: InstructionStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateInstructionPayload {
|
||||
titleFa?: string
|
||||
titleEn?: string | null
|
||||
abstract?: string | null
|
||||
descriptionHtml?: string | null
|
||||
videoProvider?: VideoProvider | null
|
||||
embedCode?: string | null
|
||||
categoryId?: string | null
|
||||
status?: InstructionStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export function resolveInstructionTitle(
|
||||
instruction: Pick<Instruction, 'titleFa' | 'titleEn' | 'title'>,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (locale === 'fa') {
|
||||
return instruction.titleFa || instruction.title
|
||||
}
|
||||
return instruction.titleEn?.trim() || instruction.titleFa || instruction.title
|
||||
}
|
||||
|
||||
export function mapInstructionApiToUi(instruction: InstructionApi): Instruction {
|
||||
return {
|
||||
id: instruction.id,
|
||||
title: instruction.title,
|
||||
titleFa: instruction.titleFa || instruction.title,
|
||||
titleEn: instruction.titleEn,
|
||||
slug: instruction.slug,
|
||||
abstract: instruction.abstract,
|
||||
videoProvider: instruction.videoProvider,
|
||||
embedCode: instruction.embedCode,
|
||||
status: instruction.status,
|
||||
categoryId: instruction.categoryId,
|
||||
categoryName: instruction.categoryName,
|
||||
tags: instruction.tags,
|
||||
coverImageUrl: instruction.coverImageUrl,
|
||||
commentCount: instruction.commentCount,
|
||||
author: instruction.author ?? null,
|
||||
publishedAt: instruction.publishedAt,
|
||||
createdAt: instruction.createdAt,
|
||||
updatedAt: instruction.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapInstructionDetailFromApi(instruction: InstructionApi): InstructionDetail {
|
||||
return {
|
||||
...mapInstructionApiToUi(instruction),
|
||||
descriptionHtml: instruction.descriptionHtml,
|
||||
author: instruction.author ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatInstructionAuthor(author: InstructionAuthor | null): string {
|
||||
if (!author) return 'Unknown author'
|
||||
const name = [author.firstName, author.lastName].filter(Boolean).join(' ').trim()
|
||||
return name || author.email || 'Unknown author'
|
||||
}
|
||||
|
||||
export function formatInstructionDate(
|
||||
value: string | null,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatInstructionCardDate(
|
||||
value: string | null,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (!value) return locale === 'fa' ? 'منتشر نشده' : 'Not published'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function mapInstructionApiToFormState(instruction: InstructionApi) {
|
||||
return {
|
||||
titleFa: instruction.titleFa || instruction.title,
|
||||
titleEn: instruction.titleEn ?? '',
|
||||
abstract: instruction.abstract,
|
||||
descriptionHtml: instruction.descriptionHtml,
|
||||
videoProvider: instruction.videoProvider ?? 'youtube',
|
||||
embedCode: instruction.embedCode,
|
||||
categoryId: instruction.categoryId ?? '',
|
||||
tags: instruction.tags,
|
||||
coverImage: instruction.coverImageUrl,
|
||||
featuredMediaId: instruction.featuredMediaId,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listInstructions(
|
||||
page = 1,
|
||||
pageSize = INSTRUCTIONS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: Omit<ListInstructionsParams, 'page' | 'pageSize'>,
|
||||
) {
|
||||
const q = new URLSearchParams()
|
||||
q.set('page', String(page))
|
||||
q.set('pageSize', String(pageSize))
|
||||
if (filters?.status) q.set('status', filters.status)
|
||||
if (filters?.provider) q.set('provider', filters.provider)
|
||||
if (filters?.categoryId) q.set('categoryId', filters.categoryId)
|
||||
if (filters?.title) q.set('title', filters.title)
|
||||
|
||||
const data = await apiRequest<InstructionsListResponse>(`${businessPath()}?${q.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapInstructionApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInstruction(instructionId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ instruction: InstructionApi }>(businessPath(`/${instructionId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.instruction
|
||||
}
|
||||
|
||||
export async function getInstructionDetail(instructionId: string, signal?: AbortSignal) {
|
||||
const instruction = await getInstruction(instructionId, signal)
|
||||
return mapInstructionDetailFromApi(instruction)
|
||||
}
|
||||
|
||||
export async function createInstruction(payload: CreateInstructionPayload) {
|
||||
return apiRequest<{ message: string; instruction: InstructionApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateInstruction(
|
||||
instructionId: string,
|
||||
payload: UpdateInstructionPayload,
|
||||
) {
|
||||
return apiRequest<{ message: string; instruction: InstructionApi }>(
|
||||
businessPath(`/${instructionId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteInstruction(instructionId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${instructionId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -20,9 +20,60 @@ export interface OrderProcessStep {
|
||||
color: string
|
||||
}
|
||||
|
||||
export type PaymentGatewayId =
|
||||
| 'mellat'
|
||||
| 'sep'
|
||||
| 'snappay'
|
||||
| 'digipay'
|
||||
| 'zarinpal'
|
||||
|
||||
export interface MellatGatewaySettings {
|
||||
enabled: boolean
|
||||
terminalId: string
|
||||
username: string
|
||||
/** Always empty in API responses; use passwordSet. */
|
||||
password: string
|
||||
passwordSet?: boolean
|
||||
}
|
||||
|
||||
export interface StubGatewaySettings {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface EPaymentSettings {
|
||||
enabled: boolean
|
||||
defaultGateway: PaymentGatewayId | null
|
||||
gateways: {
|
||||
mellat: MellatGatewaySettings
|
||||
sep: StubGatewaySettings
|
||||
snappay: StubGatewaySettings
|
||||
digipay: StubGatewaySettings
|
||||
zarinpal: StubGatewaySettings
|
||||
}
|
||||
}
|
||||
|
||||
export interface StoreSettings {
|
||||
onlineSellEnabled: boolean
|
||||
orderProcessSteps: OrderProcessStep[]
|
||||
ePayment: EPaymentSettings
|
||||
}
|
||||
|
||||
export const DEFAULT_EPAYMENT_SETTINGS: EPaymentSettings = {
|
||||
enabled: false,
|
||||
defaultGateway: null,
|
||||
gateways: {
|
||||
mellat: {
|
||||
enabled: false,
|
||||
terminalId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
passwordSet: false,
|
||||
},
|
||||
sep: { enabled: false },
|
||||
snappay: { enabled: false },
|
||||
digipay: { enabled: false },
|
||||
zarinpal: { enabled: false },
|
||||
},
|
||||
}
|
||||
|
||||
export interface ModulesSettings {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryFormData,
|
||||
CreateCategoryPayload,
|
||||
ProductCategoriesResponse,
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
|
||||
const ENTITY_TYPE = 'video' as const
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export async function listVideoCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<ProductCategoriesResponse>(
|
||||
`${businessPath()}?entityType=${ENTITY_TYPE}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function createVideoCategory(payload: CreateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { ...payload, entityType: ENTITY_TYPE },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateVideoCategory(categoryId: string, payload: UpdateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteVideoCategory(categoryId: string) {
|
||||
return apiRequest<{ message: string; deletedIds: string[] }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function toVideoCategoryFormPayload(data: CategoryFormData): CreateCategoryPayload {
|
||||
return {
|
||||
parentId: data.parentId || undefined,
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapVideoCategoryToUi(category: ProductCategory) {
|
||||
return {
|
||||
id: category.id,
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
Video,
|
||||
VideoAuthor,
|
||||
VideoDetail,
|
||||
VideoProvider,
|
||||
VideoStatus,
|
||||
} from '../types/video'
|
||||
|
||||
export const VIDEOS_PER_PAGE = 12
|
||||
|
||||
export interface VideoApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
titleFa: string
|
||||
titleEn: string | null
|
||||
slug: string
|
||||
abstract: string
|
||||
descriptionHtml: string
|
||||
videoProvider: VideoProvider
|
||||
embedCode: string
|
||||
status: VideoStatus
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
coverImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
author?: VideoAuthor | null
|
||||
commentCount: number
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface VideosListResponse {
|
||||
items: VideoApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListVideosParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: VideoStatus
|
||||
provider?: VideoProvider
|
||||
categoryId?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/videos${suffix}`
|
||||
}
|
||||
|
||||
export interface CreateVideoPayload {
|
||||
titleFa: string
|
||||
titleEn?: string
|
||||
abstract?: string
|
||||
descriptionHtml?: string
|
||||
videoProvider: VideoProvider
|
||||
embedCode: string
|
||||
categoryId?: string
|
||||
status?: VideoStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateVideoPayload {
|
||||
titleFa?: string
|
||||
titleEn?: string | null
|
||||
abstract?: string | null
|
||||
descriptionHtml?: string | null
|
||||
videoProvider?: VideoProvider
|
||||
embedCode?: string
|
||||
categoryId?: string | null
|
||||
status?: VideoStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export function resolveVideoTitle(
|
||||
video: Pick<Video, 'titleFa' | 'titleEn' | 'title'>,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (locale === 'fa') {
|
||||
return video.titleFa || video.title
|
||||
}
|
||||
return video.titleEn?.trim() || video.titleFa || video.title
|
||||
}
|
||||
|
||||
export function mapVideoApiToUi(video: VideoApi): Video {
|
||||
return {
|
||||
id: video.id,
|
||||
title: video.title,
|
||||
titleFa: video.titleFa || video.title,
|
||||
titleEn: video.titleEn,
|
||||
slug: video.slug,
|
||||
abstract: video.abstract,
|
||||
videoProvider: video.videoProvider,
|
||||
embedCode: video.embedCode,
|
||||
status: video.status,
|
||||
categoryId: video.categoryId,
|
||||
categoryName: video.categoryName,
|
||||
tags: video.tags,
|
||||
coverImageUrl: video.coverImageUrl,
|
||||
commentCount: video.commentCount,
|
||||
author: video.author ?? null,
|
||||
publishedAt: video.publishedAt,
|
||||
createdAt: video.createdAt,
|
||||
updatedAt: video.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapVideoDetailFromApi(video: VideoApi): VideoDetail {
|
||||
return {
|
||||
...mapVideoApiToUi(video),
|
||||
descriptionHtml: video.descriptionHtml,
|
||||
author: video.author ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatVideoAuthor(author: VideoAuthor | null): string {
|
||||
if (!author) return 'Unknown author'
|
||||
const name = [author.firstName, author.lastName].filter(Boolean).join(' ').trim()
|
||||
return name || author.email || 'Unknown author'
|
||||
}
|
||||
|
||||
export function formatVideoDate(
|
||||
value: string | null,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatVideoCardDate(
|
||||
value: string | null,
|
||||
locale: 'en' | 'fa' = 'en',
|
||||
): string {
|
||||
if (!value) return locale === 'fa' ? 'منتشر نشده' : 'Not published'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(locale === 'fa' ? 'fa-IR' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function mapVideoApiToFormState(video: VideoApi) {
|
||||
return {
|
||||
titleFa: video.titleFa || video.title,
|
||||
titleEn: video.titleEn ?? '',
|
||||
abstract: video.abstract,
|
||||
descriptionHtml: video.descriptionHtml,
|
||||
videoProvider: video.videoProvider,
|
||||
embedCode: video.embedCode,
|
||||
categoryId: video.categoryId ?? '',
|
||||
tags: video.tags,
|
||||
coverImage: video.coverImageUrl,
|
||||
featuredMediaId: video.featuredMediaId,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listVideos(
|
||||
page = 1,
|
||||
pageSize = VIDEOS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: Omit<ListVideosParams, 'page' | 'pageSize'>,
|
||||
) {
|
||||
const q = new URLSearchParams()
|
||||
q.set('page', String(page))
|
||||
q.set('pageSize', String(pageSize))
|
||||
if (filters?.status) q.set('status', filters.status)
|
||||
if (filters?.provider) q.set('provider', filters.provider)
|
||||
if (filters?.categoryId) q.set('categoryId', filters.categoryId)
|
||||
if (filters?.title) q.set('title', filters.title)
|
||||
|
||||
const data = await apiRequest<VideosListResponse>(`${businessPath()}?${q.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapVideoApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVideo(videoId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ video: VideoApi }>(businessPath(`/${videoId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.video
|
||||
}
|
||||
|
||||
export async function getVideoDetail(videoId: string, signal?: AbortSignal) {
|
||||
const video = await getVideo(videoId, signal)
|
||||
return mapVideoDetailFromApi(video)
|
||||
}
|
||||
|
||||
export async function createVideo(payload: CreateVideoPayload) {
|
||||
return apiRequest<{ message: string; video: VideoApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateVideo(videoId: string, payload: UpdateVideoPayload) {
|
||||
return apiRequest<{ message: string; video: VideoApi }>(businessPath(`/${videoId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteVideo(videoId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${videoId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user