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:
Alireza Hassani
2026-07-22 13:48:53 +03:30
co-authored by Cursor
commit f566387c61
509 changed files with 62690 additions and 0 deletions
@@ -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,
})
}