Add website schema settings, SEO meta overrides, and resilient bulk SMS.
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>
This commit is contained in:
co-authored by
Cursor
parent
15aad9f9d8
commit
c2ede07e73
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'blog' as const
|
||||
|
||||
@@ -61,6 +62,7 @@ export function toBlogCategoryFormPayload(data: CategoryFormData): CreateCategor
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ export function mapBlogCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface BlogApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
titleImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
@@ -60,6 +62,8 @@ export interface CreateBlogPayload {
|
||||
status?: BlogStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateBlogPayload {
|
||||
@@ -71,6 +75,8 @@ export interface UpdateBlogPayload {
|
||||
status?: BlogStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export function resolveBlogTitleImageUrl(blog: BlogApi): string | null {
|
||||
@@ -155,6 +161,8 @@ export function mapBlogApiToFormState(blog: BlogApi) {
|
||||
mainTextHtml: blog.mainTextHtml,
|
||||
categoryId: blog.categoryId ?? '',
|
||||
tags: blog.tags,
|
||||
seoMetaTitle: blog.seoMetaTitle ?? '',
|
||||
seoMetaDescription: blog.seoMetaDescription ?? '',
|
||||
titleImage: resolveBlogTitleImageUrl(blog),
|
||||
featuredMediaId: blog.featuredMediaId,
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ export function mapBrandToSelectOption(brand: BrandApi): FlatCategory {
|
||||
nameEn: brand.nameEn,
|
||||
nameFa: brand.nameFa ?? '',
|
||||
description: brand.about ?? '',
|
||||
seoMetaTitle: '',
|
||||
seoMetaDescription: '',
|
||||
parentId: null,
|
||||
variationCount: 0,
|
||||
depth: 0,
|
||||
|
||||
@@ -189,7 +189,10 @@ export interface SendCustomersBulkSmsResult {
|
||||
enabled: boolean
|
||||
message: string
|
||||
recipientCount: number
|
||||
sentCount?: number
|
||||
failedCount?: number
|
||||
serverIds?: string[]
|
||||
failures?: Array<{ cellNumber: string; error: string }>
|
||||
source?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'instruction' as const
|
||||
|
||||
@@ -61,6 +62,7 @@ export function toInstructionCategoryFormPayload(data: CategoryFormData): Create
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ export function mapInstructionCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface InstructionApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
coverImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
@@ -70,6 +72,8 @@ export interface CreateInstructionPayload {
|
||||
status?: InstructionStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateInstructionPayload {
|
||||
@@ -83,6 +87,8 @@ export interface UpdateInstructionPayload {
|
||||
status?: InstructionStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export function resolveInstructionTitle(
|
||||
@@ -170,6 +176,8 @@ export function mapInstructionApiToFormState(instruction: InstructionApi) {
|
||||
embedCode: instruction.embedCode,
|
||||
categoryId: instruction.categoryId ?? '',
|
||||
tags: instruction.tags,
|
||||
seoMetaTitle: instruction.seoMetaTitle ?? '',
|
||||
seoMetaDescription: instruction.seoMetaDescription ?? '',
|
||||
coverImage: instruction.coverImageUrl,
|
||||
featuredMediaId: instruction.featuredMediaId,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'portfolio' as const
|
||||
|
||||
@@ -61,6 +62,7 @@ export function toPortfolioCategoryFormPayload(data: CategoryFormData): CreateCa
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ export function mapPortfolioCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface PortfolioApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
titleImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
gallery: PortfolioGalleryItem[]
|
||||
@@ -66,6 +68,8 @@ export interface CreatePortfolioPayload {
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
@@ -82,6 +86,8 @@ export interface UpdatePortfolioPayload {
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
sortOrder?: number
|
||||
/** Pin to absolute top of the business portfolio list. */
|
||||
moveToTop?: boolean
|
||||
@@ -171,6 +177,8 @@ export function mapPortfolioApiToFormState(portfolio: PortfolioApi) {
|
||||
projectUrl: portfolio.projectUrl?.trim() || '',
|
||||
categoryId: portfolio.categoryId ?? '',
|
||||
tags: portfolio.tags,
|
||||
seoMetaTitle: portfolio.seoMetaTitle ?? '',
|
||||
seoMetaDescription: portfolio.seoMetaDescription ?? '',
|
||||
titleImage: resolvePortfolioTitleImageUrl(portfolio),
|
||||
featuredMediaId: portfolio.featuredMediaId,
|
||||
images: portfolio.gallery.map((item) => item.url),
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'product' as const
|
||||
|
||||
@@ -75,6 +76,7 @@ export function toCategoryFormPayload(data: CategoryFormData): CreateCategoryPay
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +86,8 @@ export function mapProductCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
translationCount: category.translationCount ?? 0,
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface ProductApi {
|
||||
imageUrl: string | null
|
||||
} | null
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
thumbnailUrl: string | null
|
||||
thumbnailMediaId: string | null
|
||||
image: string
|
||||
@@ -58,6 +60,8 @@ export interface CreateProductPayload {
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateProductPayload {
|
||||
@@ -71,6 +75,8 @@ export interface UpdateProductPayload {
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
@@ -145,6 +151,8 @@ export function mapProductApiToFormState(product: ProductApi) {
|
||||
thumbnail: product.thumbnailUrl ?? product.thumbnail ?? null,
|
||||
images: product.images.map((item) => item.url),
|
||||
tags: product.tags,
|
||||
seoMetaTitle: product.seoMetaTitle ?? '',
|
||||
seoMetaDescription: product.seoMetaDescription ?? '',
|
||||
thumbnailMediaId: product.thumbnailMediaId,
|
||||
galleryMediaIds: product.galleryMediaIds,
|
||||
}
|
||||
|
||||
@@ -91,6 +91,18 @@ export interface ModulesSettings {
|
||||
|
||||
export type SpecialProductsSource = 'product' | 'store_item'
|
||||
|
||||
export type WebsiteSchemaType = 'Organization' | 'LocalBusiness'
|
||||
|
||||
export interface WebsiteSchemaSettings {
|
||||
enabled: boolean
|
||||
type: WebsiteSchemaType
|
||||
name: string | null
|
||||
nameFa: string | null
|
||||
description: string | null
|
||||
priceRange: string | null
|
||||
sameAs: string[]
|
||||
}
|
||||
|
||||
export interface WebsiteSitemapConfig {
|
||||
baseUrl?: string | null
|
||||
staticPages?: Array<{
|
||||
@@ -105,6 +117,7 @@ export interface WebsiteSitemapConfig {
|
||||
export interface WebsiteSettings {
|
||||
specialProductsSource: SpecialProductsSource
|
||||
sitemapConfig?: WebsiteSitemapConfig | null
|
||||
schema?: WebsiteSchemaSettings
|
||||
}
|
||||
|
||||
export interface SmsSettings {
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface CreateUserProductInput {
|
||||
technicalValues?: UserProductTechnicalValueInput[]
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export type UpdateUserProductInput = CreateUserProductInput
|
||||
@@ -77,6 +79,8 @@ export interface UserProductDetail extends UserProductListItem {
|
||||
technicalNotes?: string | null
|
||||
description?: string | null
|
||||
titleEn?: string | null
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
priceCurrency?: string | null
|
||||
priceByExpert?: boolean
|
||||
promoted?: boolean
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'video' as const
|
||||
|
||||
@@ -61,6 +62,7 @@ export function toVideoCategoryFormPayload(data: CategoryFormData): CreateCatego
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ export function mapVideoCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface VideoApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
coverImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
@@ -70,6 +72,8 @@ export interface CreateVideoPayload {
|
||||
status?: VideoStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateVideoPayload {
|
||||
@@ -83,6 +87,8 @@ export interface UpdateVideoPayload {
|
||||
status?: VideoStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
}
|
||||
|
||||
export function resolveVideoTitle(
|
||||
@@ -170,6 +176,8 @@ export function mapVideoApiToFormState(video: VideoApi) {
|
||||
embedCode: video.embedCode,
|
||||
categoryId: video.categoryId ?? '',
|
||||
tags: video.tags,
|
||||
seoMetaTitle: video.seoMetaTitle ?? '',
|
||||
seoMetaDescription: video.seoMetaDescription ?? '',
|
||||
coverImage: video.coverImageUrl,
|
||||
featuredMediaId: video.featuredMediaId,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
import { seoMetaPayload } from '../utils/seoMeta'
|
||||
|
||||
const ENTITY_TYPE = 'workshop' as const
|
||||
|
||||
@@ -61,6 +62,7 @@ export function toWorkshopCategoryFormPayload(data: CategoryFormData): CreateCat
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,8 @@ export function mapWorkshopCategoryToUi(category: ProductCategory) {
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
seoMetaTitle: category.seoMetaTitle ?? '',
|
||||
seoMetaDescription: category.seoMetaDescription ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface WorkshopApi {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
coverImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
gallery: WorkshopGalleryItem[]
|
||||
@@ -93,6 +95,8 @@ export interface CreateWorkshopPayload {
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
eventDate?: string | null
|
||||
eventTime?: string | null
|
||||
duration?: string | null
|
||||
@@ -108,6 +112,8 @@ export interface UpdateWorkshopPayload {
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
seoMetaTitle?: string | null
|
||||
seoMetaDescription?: string | null
|
||||
eventDate?: string | null
|
||||
eventTime?: string | null
|
||||
duration?: string | null
|
||||
@@ -291,6 +297,8 @@ export function mapWorkshopApiToFormState(workshop: WorkshopApi) {
|
||||
descriptionHtml: normalizeRichTextHtml(workshop.descriptionHtml),
|
||||
categoryId: workshop.categoryId ?? '',
|
||||
tags: Array.isArray(workshop.tags) ? workshop.tags : [],
|
||||
seoMetaTitle: workshop.seoMetaTitle ?? '',
|
||||
seoMetaDescription: workshop.seoMetaDescription ?? '',
|
||||
eventDate: normalizeGregorianDate(workshop.eventDate),
|
||||
eventTime: normalizeTimeInput(workshop.eventTime),
|
||||
duration:
|
||||
|
||||
Reference in New Issue
Block a user