Business admins can configure site-wide Schema.org and optional per-item SEO title/description; mass SMS continues on failures with an end report. Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
6.0 KiB
TypeScript
231 lines
6.0 KiB
TypeScript
import { apiRequest } from '../lib/api'
|
|
import { getActiveBusinessId } from '../lib/businessContext'
|
|
import { extractFirstImageUrl } from '../utils/htmlImages'
|
|
import type { Blog, BlogAuthor, BlogDetail, BlogPostType, BlogStatus } from '../types/blog'
|
|
|
|
export const BLOGS_PER_PAGE = 12
|
|
|
|
export interface BlogApi {
|
|
id: string
|
|
businessId: string
|
|
title: string
|
|
slug: string
|
|
type: BlogPostType
|
|
abstract: string
|
|
mainTextHtml: string
|
|
status: BlogStatus
|
|
categoryId: string | null
|
|
categoryName: string
|
|
tags: string[]
|
|
seoMetaTitle?: string | null
|
|
seoMetaDescription?: string | null
|
|
titleImageUrl: string | null
|
|
featuredMediaId: string | null
|
|
authorId?: string | null
|
|
author?: BlogAuthor | null
|
|
commentCount: number
|
|
publishedAt: string | null
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
export interface BlogsListResponse {
|
|
items: BlogApi[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
}
|
|
|
|
export interface ListBlogsParams {
|
|
page?: number
|
|
pageSize?: number
|
|
status?: BlogStatus
|
|
type?: BlogPostType
|
|
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}/blogs${suffix}`
|
|
}
|
|
|
|
export interface CreateBlogPayload {
|
|
title: string
|
|
type: BlogPostType
|
|
abstract?: string
|
|
mainTextHtml?: string
|
|
categoryId?: string
|
|
status?: BlogStatus
|
|
featuredMediaId?: string
|
|
tags?: string[]
|
|
seoMetaTitle?: string | null
|
|
seoMetaDescription?: string | null
|
|
}
|
|
|
|
export interface UpdateBlogPayload {
|
|
title?: string
|
|
type?: BlogPostType
|
|
abstract?: string | null
|
|
mainTextHtml?: string | null
|
|
categoryId?: string | null
|
|
status?: BlogStatus
|
|
featuredMediaId?: string | null
|
|
tags?: string[]
|
|
seoMetaTitle?: string | null
|
|
seoMetaDescription?: string | null
|
|
}
|
|
|
|
export function resolveBlogTitleImageUrl(blog: BlogApi): string | null {
|
|
return blog.titleImageUrl?.trim() || extractFirstImageUrl(blog.mainTextHtml) || null
|
|
}
|
|
|
|
export function mapBlogApiToUi(blog: BlogApi): Blog {
|
|
return {
|
|
id: blog.id,
|
|
title: blog.title,
|
|
slug: blog.slug,
|
|
type: blog.type,
|
|
abstract: blog.abstract,
|
|
status: blog.status,
|
|
categoryId: blog.categoryId,
|
|
categoryName: blog.categoryName,
|
|
tags: blog.tags,
|
|
titleImageUrl: resolveBlogTitleImageUrl(blog),
|
|
commentCount: blog.commentCount,
|
|
author: blog.author ?? null,
|
|
publishedAt: blog.publishedAt,
|
|
createdAt: blog.createdAt,
|
|
updatedAt: blog.updatedAt,
|
|
}
|
|
}
|
|
|
|
export function mapBlogDetailFromApi(blog: BlogApi): BlogDetail {
|
|
return {
|
|
...mapBlogApiToUi(blog),
|
|
mainTextHtml: blog.mainTextHtml,
|
|
author: blog.author ?? null,
|
|
}
|
|
}
|
|
|
|
export function formatBlogAuthor(author: BlogAuthor | 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 formatBlogDate(
|
|
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 formatBlogCardDate(
|
|
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 isBlogVerified(blog: Pick<Blog, 'status'>): boolean {
|
|
return blog.status === 'published'
|
|
}
|
|
|
|
export async function setBlogVerified(blogId: string, verified: boolean) {
|
|
return updateBlog(blogId, { status: verified ? 'published' : 'draft' })
|
|
}
|
|
|
|
export function mapBlogApiToFormState(blog: BlogApi) {
|
|
return {
|
|
title: blog.title,
|
|
type: blog.type,
|
|
abstract: blog.abstract,
|
|
mainTextHtml: blog.mainTextHtml,
|
|
categoryId: blog.categoryId ?? '',
|
|
tags: blog.tags,
|
|
seoMetaTitle: blog.seoMetaTitle ?? '',
|
|
seoMetaDescription: blog.seoMetaDescription ?? '',
|
|
titleImage: resolveBlogTitleImageUrl(blog),
|
|
featuredMediaId: blog.featuredMediaId,
|
|
}
|
|
}
|
|
|
|
export async function listBlogs(
|
|
page = 1,
|
|
pageSize = BLOGS_PER_PAGE,
|
|
signal?: AbortSignal,
|
|
filters?: Omit<ListBlogsParams, '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?.type) q.set('type', filters.type)
|
|
if (filters?.categoryId) q.set('categoryId', filters.categoryId)
|
|
if (filters?.title) q.set('title', filters.title)
|
|
|
|
const data = await apiRequest<BlogsListResponse>(`${businessPath()}?${q.toString()}`, {
|
|
auth: true,
|
|
signal,
|
|
})
|
|
|
|
return {
|
|
...data,
|
|
items: data.items.map(mapBlogApiToUi),
|
|
}
|
|
}
|
|
|
|
export async function getBlog(blogId: string, signal?: AbortSignal) {
|
|
const data = await apiRequest<{ blog: BlogApi }>(businessPath(`/${blogId}`), {
|
|
auth: true,
|
|
signal,
|
|
})
|
|
return data.blog
|
|
}
|
|
|
|
export async function getBlogDetail(blogId: string, signal?: AbortSignal) {
|
|
const blog = await getBlog(blogId, signal)
|
|
return mapBlogDetailFromApi(blog)
|
|
}
|
|
|
|
export async function createBlog(payload: CreateBlogPayload) {
|
|
return apiRequest<{ message: string; blog: BlogApi }>(businessPath(), {
|
|
method: 'POST',
|
|
auth: true,
|
|
body: payload,
|
|
})
|
|
}
|
|
|
|
export async function updateBlog(blogId: string, payload: UpdateBlogPayload) {
|
|
return apiRequest<{ message: string; blog: BlogApi }>(businessPath(`/${blogId}`), {
|
|
method: 'PATCH',
|
|
auth: true,
|
|
body: payload,
|
|
})
|
|
}
|
|
|
|
export async function deleteBlog(blogId: string) {
|
|
return apiRequest<{ message: string }>(businessPath(`/${blogId}`), {
|
|
method: 'DELETE',
|
|
auth: true,
|
|
})
|
|
}
|