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
@@ -0,0 +1,35 @@
const GAP = 8
const VIEWPORT_PAD = 8
export type FixedPopoverCoords = {
top: number
left: number
}
export function fitFixedPopover(
trigger: DOMRect,
size: { width: number; height: number },
align: 'end' | 'center' = 'end',
): FixedPopoverCoords {
const vw = window.innerWidth
const vh = window.innerHeight
const width = Math.max(1, size.width)
const height = Math.max(1, size.height)
const spaceBelow = vh - trigger.bottom - VIEWPORT_PAD
const spaceAbove = trigger.top - VIEWPORT_PAD
const placeBelow = spaceBelow >= height + GAP || spaceBelow >= spaceAbove
let top = placeBelow ? trigger.bottom + GAP : trigger.top - GAP - height
top = Math.min(Math.max(top, VIEWPORT_PAD), Math.max(VIEWPORT_PAD, vh - VIEWPORT_PAD - height))
let left =
align === 'center'
? trigger.left + trigger.width / 2 - width / 2
: trigger.right - width
const maxLeft = Math.max(VIEWPORT_PAD, vw - VIEWPORT_PAD - width)
left = Math.min(Math.max(left, VIEWPORT_PAD), maxLeft)
return { top, left }
}
+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 },