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,30 @@
|
||||
/** Normalize Iranian/local input to E.164 (e.g. +989121111111). */
|
||||
export function toE164CellNumber(input: string): string {
|
||||
const digits = input.replace(/\D/g, '')
|
||||
|
||||
if (!digits) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (digits.startsWith('98')) {
|
||||
return `+${digits}`
|
||||
}
|
||||
|
||||
if (digits.startsWith('0')) {
|
||||
return `+98${digits.slice(1)}`
|
||||
}
|
||||
|
||||
if (digits.length === 10 && digits.startsWith('9')) {
|
||||
return `+98${digits}`
|
||||
}
|
||||
|
||||
return `+${digits}`
|
||||
}
|
||||
|
||||
/** Display E.164 Iranian numbers as local format (e.g. 0912...). */
|
||||
export function formatCellForDisplay(cellNumber: string): string {
|
||||
if (cellNumber.startsWith('+98')) {
|
||||
return `0${cellNumber.slice(3)}`
|
||||
}
|
||||
return cellNumber
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
export interface DashboardDocumentTitleParts {
|
||||
businessName: string
|
||||
dashboardName: string
|
||||
pageLabels?: string[]
|
||||
}
|
||||
|
||||
/** Pattern: `{businessName} - {dashboardName} · {page}` */
|
||||
export function formatDashboardDocumentTitle({
|
||||
businessName,
|
||||
dashboardName,
|
||||
pageLabels = [],
|
||||
}: DashboardDocumentTitleParts): string {
|
||||
const business = businessName.trim() || 'Store'
|
||||
const dashboard = dashboardName.trim()
|
||||
const pages = pageLabels.map((label) => label.trim()).filter(Boolean)
|
||||
|
||||
if (pages.length === 0) {
|
||||
return `${business} - ${dashboard}`
|
||||
}
|
||||
|
||||
return `${business} - ${dashboard} · ${pages.join(' · ')}`
|
||||
}
|
||||
|
||||
export interface RouteTitleRule {
|
||||
match: string | RegExp
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export function normalizeRoutePath(pathname: string): string {
|
||||
const path = pathname.split('?')[0]?.split('#')[0] ?? '/'
|
||||
if (path === '/') return '/'
|
||||
return path.replace(/\/+$/, '') || '/'
|
||||
}
|
||||
|
||||
export function resolveRoutePageLabels(pathname: string, rules: RouteTitleRule[]): string[] {
|
||||
const path = normalizeRoutePath(pathname)
|
||||
|
||||
const sorted = [...rules].sort((a, b) => {
|
||||
const lenA = typeof a.match === 'string' ? a.match.length : 0
|
||||
const lenB = typeof b.match === 'string' ? b.match.length : 0
|
||||
return lenB - lenA
|
||||
})
|
||||
|
||||
for (const rule of sorted) {
|
||||
if (typeof rule.match === 'string' && rule.match === path) {
|
||||
return rule.labels
|
||||
}
|
||||
if (rule.match instanceof RegExp && rule.match.test(path)) {
|
||||
return rule.labels
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
const FAVICON_ATTR = 'data-business-favicon'
|
||||
|
||||
function removeIconLinks(root: ParentNode = document.head) {
|
||||
root
|
||||
.querySelectorAll(
|
||||
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]',
|
||||
)
|
||||
.forEach((node) => node.remove())
|
||||
}
|
||||
|
||||
/** Sets browser tab favicon links for the current tenant. Pass null to clear. */
|
||||
export function applyDocumentFavicon(url: string | null | undefined) {
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
removeIconLinks()
|
||||
|
||||
const href = url?.trim()
|
||||
if (!href) return
|
||||
|
||||
// Bust browser favicon cache when the logo/favicon media URL changes.
|
||||
const cacheBusted =
|
||||
href.includes('?') ? `${href}&v=${Date.now()}` : `${href}?v=${Date.now()}`
|
||||
|
||||
for (const rel of ['icon', 'apple-touch-icon'] as const) {
|
||||
const link = document.createElement('link')
|
||||
link.setAttribute(FAVICON_ATTR, 'true')
|
||||
link.rel = rel
|
||||
link.href = cacheBusted
|
||||
if (rel === 'icon') {
|
||||
link.type = 'image/png'
|
||||
link.sizes = '48x48'
|
||||
}
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** Format a numeric price for display in Iranian Toman (IRT). */
|
||||
export function formatIrtPrice(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '—'
|
||||
return `${Math.round(value).toLocaleString('en-US')} IRT`
|
||||
}
|
||||
|
||||
/** Format raw digits into comma-separated groups while typing. */
|
||||
export function formatIrtInput(raw: string): string {
|
||||
const digits = raw.replace(/\D/g, '')
|
||||
if (!digits) return ''
|
||||
return Number(digits).toLocaleString('en-US')
|
||||
}
|
||||
|
||||
/** Parse a formatted IRT input string back to a number. */
|
||||
export function parseIrtInput(formatted: string): number | null {
|
||||
const digits = formatted.replace(/\D/g, '')
|
||||
if (!digits) return null
|
||||
return Number(digits)
|
||||
}
|
||||
|
||||
export function hasStoreItemDiscount(
|
||||
price: number | null,
|
||||
discountedPrice: number | null,
|
||||
): boolean {
|
||||
return (
|
||||
price !== null &&
|
||||
discountedPrice !== null &&
|
||||
discountedPrice < price &&
|
||||
discountedPrice >= 0
|
||||
)
|
||||
}
|
||||
|
||||
export function calcDiscountPercent(price: number, discountedPrice: number): number {
|
||||
if (price <= 0 || discountedPrice >= price) return 0
|
||||
return Math.round((1 - discountedPrice / price) * 100)
|
||||
}
|
||||
Reference in New Issue
Block a user