Add customer SMS/Excel tools, selection bar, and sticky import toasts.

Bulk and single SMS from Users, row selection with overflow summary, persistent large import toasts, and business SMS sender in Super Admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 14:12:57 +03:30
co-authored by Cursor
parent 2a32c46b2e
commit 4c3bf8324e
30 changed files with 1597 additions and 149 deletions
+80
View File
@@ -2,6 +2,14 @@ import { getBusinessDomain } from '../lib/config'
export type WebsiteLinkKind = 'product' | 'storeItem' | 'blog' | 'portfolio' | 'video' | 'workshop'
export type WebsiteContentKind =
| 'product'
| 'blog'
| 'portfolio'
| 'video'
| 'instruction'
| 'workshop'
export type WebsiteLinkFilter = 'category' | 'tag'
export const WEBSITE_LINK_KINDS: WebsiteLinkKind[] = [
@@ -22,12 +30,84 @@ export const WEBSITE_LINK_KIND_PATH: Record<WebsiteLinkKind, string> = {
workshop: '/workshops',
}
/** Matches backend `slugifyForUrl` used by website sitemaps / SEO paths. */
export function slugifyForWebsitePath(value: string, fallback = 'item'): string {
const slug = value
.trim()
.normalize('NFC')
.replace(/\s+/g, '-')
.replace(
/[^\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFFa-zA-Z0-9-]+/g,
'',
)
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
return slug || fallback
}
export function getWebsiteOrigin(domain = getBusinessDomain()): string {
const host = domain.trim().replace(/^https?:\/\//i, '').replace(/\/$/, '')
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:'
return `${protocol}//${host}`
}
/**
* Public storefront detail URL for a content item (canonical Meshkee website paths).
* Returns '' when a required slug/title is missing.
*/
export function buildWebsiteContentUrl(
kind: WebsiteContentKind,
item: {
id: string
slug?: string | null
title?: string | null
titleFa?: string | null
nameFa?: string | null
nameEn?: string | null
},
domain = getBusinessDomain(),
): string {
const origin = getWebsiteOrigin(domain)
const id = item.id?.trim()
if (!id) return ''
switch (kind) {
case 'product': {
const pathSlug = slugifyForWebsitePath(
item.nameFa?.trim() || item.nameEn?.trim() || item.title?.trim() || '',
'product',
)
return `${origin}/products/${encodeURIComponent(id)}/${encodeURIComponent(pathSlug)}`
}
case 'blog': {
const pathSlug = slugifyForWebsitePath(item.title?.trim() || '', 'blog')
return `${origin}/blog/${encodeURIComponent(id)}/${encodeURIComponent(pathSlug)}`
}
case 'portfolio': {
const pathSlug = slugifyForWebsitePath(
item.titleFa?.trim() || item.title?.trim() || '',
'portfolio',
)
return `${origin}/portfolios/${encodeURIComponent(id)}/${encodeURIComponent(pathSlug)}`
}
case 'video': {
const slug = item.slug?.trim()
return slug ? `${origin}/videos/${encodeURIComponent(slug)}` : ''
}
case 'instruction': {
const slug = item.slug?.trim()
return slug ? `${origin}/instructions/${encodeURIComponent(slug)}` : ''
}
case 'workshop': {
const slug = item.slug?.trim()
return slug ? `${origin}/workshops/${encodeURIComponent(slug)}` : ''
}
default:
return ''
}
}
export function buildWebsiteCategoryUrl(
kind: WebsiteLinkKind,
category: { id: string; slug?: string | null },