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,210 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { extractFirstImageUrl } from '../utils/htmlImages'
|
||||
import type { Portfolio, PortfolioDetail, PortfolioGalleryItem, PortfolioStatus } from '../types/portfolio'
|
||||
|
||||
export const PORTFOLIOS_PER_PAGE = 12
|
||||
|
||||
export interface PortfolioApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
slug: string
|
||||
abstract: string
|
||||
mainTextHtml: string
|
||||
status: PortfolioStatus
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
titleImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
gallery: PortfolioGalleryItem[]
|
||||
galleryMediaIds: string[]
|
||||
sortOrder: number
|
||||
commentCount: number
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface PortfoliosListResponse {
|
||||
items: PortfolioApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListPortfoliosParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: PortfolioStatus
|
||||
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}/portfolios${suffix}`
|
||||
}
|
||||
|
||||
export interface CreatePortfolioPayload {
|
||||
title: string
|
||||
abstract?: string
|
||||
mainTextHtml?: string
|
||||
categoryId?: string
|
||||
status?: PortfolioStatus
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export interface UpdatePortfolioPayload {
|
||||
title?: string
|
||||
abstract?: string | null
|
||||
mainTextHtml?: string | null
|
||||
categoryId?: string | null
|
||||
status?: PortfolioStatus
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export function resolvePortfolioTitleImageUrl(portfolio: PortfolioApi): string | null {
|
||||
const featured = portfolio.titleImageUrl?.trim()
|
||||
if (featured) return featured
|
||||
|
||||
const galleryUrl = portfolio.gallery?.map((item) => item.url?.trim()).find(Boolean)
|
||||
if (galleryUrl) return galleryUrl
|
||||
|
||||
return extractFirstImageUrl(portfolio.mainTextHtml) || null
|
||||
}
|
||||
|
||||
export function mapPortfolioApiToUi(portfolio: PortfolioApi): Portfolio {
|
||||
return {
|
||||
id: portfolio.id,
|
||||
title: portfolio.title,
|
||||
slug: portfolio.slug,
|
||||
abstract: portfolio.abstract,
|
||||
status: portfolio.status,
|
||||
categoryId: portfolio.categoryId,
|
||||
categoryName: portfolio.categoryName,
|
||||
tags: portfolio.tags,
|
||||
titleImageUrl: resolvePortfolioTitleImageUrl(portfolio),
|
||||
commentCount: portfolio.commentCount,
|
||||
publishedAt: portfolio.publishedAt,
|
||||
createdAt: portfolio.createdAt,
|
||||
updatedAt: portfolio.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPortfolioCardDate(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 formatPortfolioDate(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 mapPortfolioDetailFromApi(portfolio: PortfolioApi): PortfolioDetail {
|
||||
return {
|
||||
...mapPortfolioApiToUi(portfolio),
|
||||
mainTextHtml: portfolio.mainTextHtml,
|
||||
gallery: portfolio.gallery ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export function mapPortfolioApiToFormState(portfolio: PortfolioApi) {
|
||||
return {
|
||||
title: portfolio.title,
|
||||
abstract: portfolio.abstract,
|
||||
mainTextHtml: portfolio.mainTextHtml,
|
||||
categoryId: portfolio.categoryId ?? '',
|
||||
tags: portfolio.tags,
|
||||
titleImage: resolvePortfolioTitleImageUrl(portfolio),
|
||||
featuredMediaId: portfolio.featuredMediaId,
|
||||
images: portfolio.gallery.map((item) => item.url),
|
||||
galleryMediaIds: portfolio.galleryMediaIds,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPortfolios(
|
||||
page = 1,
|
||||
pageSize = PORTFOLIOS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: Omit<ListPortfoliosParams, '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?.categoryId) q.set('categoryId', filters.categoryId)
|
||||
if (filters?.title) q.set('title', filters.title)
|
||||
|
||||
const data = await apiRequest<PortfoliosListResponse>(`${businessPath()}?${q.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapPortfolioApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPortfolio(portfolioId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ portfolio: PortfolioApi }>(businessPath(`/${portfolioId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.portfolio
|
||||
}
|
||||
|
||||
export async function getPortfolioDetail(portfolioId: string, signal?: AbortSignal) {
|
||||
const portfolio = await getPortfolio(portfolioId, signal)
|
||||
return mapPortfolioDetailFromApi(portfolio)
|
||||
}
|
||||
|
||||
export async function createPortfolio(payload: CreatePortfolioPayload) {
|
||||
return apiRequest<{ message: string; portfolio: PortfolioApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updatePortfolio(portfolioId: string, payload: UpdatePortfolioPayload) {
|
||||
return apiRequest<{ message: string; portfolio: PortfolioApi }>(
|
||||
businessPath(`/${portfolioId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deletePortfolio(portfolioId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${portfolioId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user