Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
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[]
|
||||
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[]
|
||||
}
|
||||
|
||||
export interface UpdateBlogPayload {
|
||||
title?: string
|
||||
type?: BlogPostType
|
||||
abstract?: string | null
|
||||
mainTextHtml?: string | null
|
||||
categoryId?: string | null
|
||||
status?: BlogStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
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): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatBlogCardDate(value: string | null): string {
|
||||
if (!value) return 'Not published'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user