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,34 @@
import {
DEFAULT_BUSINESS_PRIMARY_COLOR_ID,
getBusinessPrimaryColorTokens,
type BusinessPrimaryColorId,
} from './businessPrimaryColors'
const CSS_VAR_DEFAULTS: Record<string, string> = {
'--primary': '#3b82f6',
'--primary-glow': '#3b82f6',
'--primary-light': '#dbeafe',
'--primary-dark': '#2563eb',
'--primary-rgb': '59 130 246',
'--primary-dark-rgb': '37 99 235',
}
export function applyBusinessPrimaryColor(colorId?: BusinessPrimaryColorId | null) {
const root = document.documentElement
const tokens = getBusinessPrimaryColorTokens(colorId ?? DEFAULT_BUSINESS_PRIMARY_COLOR_ID)
root.style.setProperty('--primary', tokens.primary)
root.style.setProperty('--primary-glow', tokens.primaryGlow)
root.style.setProperty('--primary-light', tokens.primaryLight)
root.style.setProperty('--primary-dark', tokens.primaryDark)
root.style.setProperty('--primary-rgb', tokens.primaryRgb)
root.style.setProperty('--primary-dark-rgb', tokens.primaryDarkRgb)
}
export function resetBusinessPrimaryColor() {
const root = document.documentElement
for (const [name, value] of Object.entries(CSS_VAR_DEFAULTS)) {
root.style.setProperty(name, value)
}
}
@@ -0,0 +1,22 @@
import type { MultiSelectOption } from '../components/MultiSelectDropdown'
import type { BusinessActivityCategory } from '../services/businessActivityCategoryService'
export function flattenBusinessActivityCategories(
categories: BusinessActivityCategory[],
): MultiSelectOption<string>[] {
const result: MultiSelectOption<string>[] = []
function walk(parentId: string | null, depth: number) {
const children = categories
.filter((category) => category.parentId === parentId)
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
for (const child of children) {
result.push({ value: child.id, label: child.name, depth })
walk(child.id, depth + 1)
}
}
walk(null, 0)
return result
}
@@ -0,0 +1,111 @@
export const BUSINESS_PRIMARY_COLOR_IDS = [
'red',
'yellow',
'black',
'cyan',
'purple',
'light-blue',
'dark-blue',
] as const
export type BusinessPrimaryColorId = (typeof BUSINESS_PRIMARY_COLOR_IDS)[number]
export const DEFAULT_BUSINESS_PRIMARY_COLOR_ID: BusinessPrimaryColorId = 'dark-blue'
export type BusinessPrimaryColorTokens = {
label: string
primary: string
primaryDark: string
primaryLight: string
primaryGlow: string
primaryRgb: string
primaryDarkRgb: string
}
export const BUSINESS_PRIMARY_COLOR_PALETTE: Record<
BusinessPrimaryColorId,
BusinessPrimaryColorTokens
> = {
red: {
label: 'Red',
primary: '#ef4444',
primaryDark: '#dc2626',
primaryLight: '#fee2e2',
primaryGlow: '#ef4444',
primaryRgb: '239 68 68',
primaryDarkRgb: '220 38 38',
},
yellow: {
label: 'Yellow',
primary: '#eab308',
primaryDark: '#ca8a04',
primaryLight: '#fef9c3',
primaryGlow: '#eab308',
primaryRgb: '234 179 8',
primaryDarkRgb: '202 138 4',
},
black: {
label: 'Black',
primary: '#1e293b',
primaryDark: '#0f172a',
primaryLight: '#e2e8f0',
primaryGlow: '#334155',
primaryRgb: '30 41 59',
primaryDarkRgb: '15 23 42',
},
cyan: {
label: 'Cyan',
primary: '#06b6d4',
primaryDark: '#0891b2',
primaryLight: '#cffafe',
primaryGlow: '#06b6d4',
primaryRgb: '6 182 212',
primaryDarkRgb: '8 145 178',
},
purple: {
label: 'Purple',
primary: '#a855f7',
primaryDark: '#9333ea',
primaryLight: '#f3e8ff',
primaryGlow: '#a855f7',
primaryRgb: '168 85 247',
primaryDarkRgb: '147 51 234',
},
'light-blue': {
label: 'Light Blue',
primary: '#38bdf8',
primaryDark: '#0ea5e9',
primaryLight: '#e0f2fe',
primaryGlow: '#38bdf8',
primaryRgb: '56 189 248',
primaryDarkRgb: '14 165 233',
},
'dark-blue': {
label: 'Dark Blue',
primary: '#3b82f6',
primaryDark: '#2563eb',
primaryLight: '#dbeafe',
primaryGlow: '#3b82f6',
primaryRgb: '59 130 246',
primaryDarkRgb: '37 99 235',
},
}
export function normalizeBusinessPrimaryColorId(value: unknown): BusinessPrimaryColorId {
if (
typeof value === 'string' &&
BUSINESS_PRIMARY_COLOR_IDS.includes(value as BusinessPrimaryColorId)
) {
return value as BusinessPrimaryColorId
}
return DEFAULT_BUSINESS_PRIMARY_COLOR_ID
}
export function getBusinessPrimaryColorTokens(
colorId: BusinessPrimaryColorId | undefined | null,
): BusinessPrimaryColorTokens {
return BUSINESS_PRIMARY_COLOR_PALETTE[
normalizeBusinessPrimaryColorId(colorId ?? DEFAULT_BUSINESS_PRIMARY_COLOR_ID)
]
}
+38
View File
@@ -0,0 +1,38 @@
import type { Category, FlatCategory } from '../types/category'
export function flattenCategories(categories: Category[]): FlatCategory[] {
const result: FlatCategory[] = []
function walk(parentId: string | null, depth: number) {
const children = categories
.filter((c) => c.parentId === parentId)
.sort((a, b) => a.nameEn.localeCompare(b.nameEn))
for (const child of children) {
result.push({ ...child, depth })
walk(child.id, depth + 1)
}
}
walk(null, 0)
return result
}
export function getDescendantIds(categories: Category[], id: string): string[] {
const ids: string[] = [id]
const children = categories.filter((c) => c.parentId === id)
for (const child of children) {
ids.push(...getDescendantIds(categories, child.id))
}
return ids
}
export function getChildren(categories: Category[], parentId: string | null): Category[] {
return categories
.filter((c) => c.parentId === parentId)
.sort((a, b) => a.nameEn.localeCompare(b.nameEn))
}
export function hasChildren(categories: Category[], id: string): boolean {
return categories.some((c) => c.parentId === id)
}
+46
View File
@@ -0,0 +1,46 @@
import type { Area } from 'react-easy-crop'
function createImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image()
image.addEventListener('load', () => resolve(image))
image.addEventListener('error', reject)
image.src = url
})
}
export async function getCroppedImage(
imageSrc: string,
pixelCrop: Area,
format: 'jpeg' | 'png' = 'jpeg',
): Promise<string> {
const image = await createImage(imageSrc)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Could not get canvas context')
canvas.width = pixelCrop.width
canvas.height = pixelCrop.height
if (format === 'png') {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height,
)
if (format === 'png') {
return canvas.toDataURL('image/png')
}
return canvas.toDataURL('image/jpeg', 0.92)
}
@@ -0,0 +1,55 @@
import { hasStoreItemDiscount } from './irtPrice'
import type { StoreProductListing } from './storeProductGroups'
export interface StoreListingFilters {
name: string
minPrice: number | null
maxPrice: number | null
onlyDiscounted: boolean
}
export const EMPTY_STORE_LISTING_FILTERS: StoreListingFilters = {
name: '',
minPrice: null,
maxPrice: null,
onlyDiscounted: false,
}
function listingEffectivePrice(listing: StoreProductListing): number | null {
if (hasStoreItemDiscount(listing.displayPrice, listing.displayDiscountedPrice)) {
return listing.displayDiscountedPrice
}
return listing.displayPrice
}
export function filterStoreListings(
listings: StoreProductListing[],
filters: StoreListingFilters,
): StoreProductListing[] {
const nameQuery = filters.name.trim().toLowerCase()
return listings.filter((listing) => {
if (nameQuery) {
const matchesTitle = listing.productTitle.toLowerCase().includes(nameQuery)
const matchesFa = listing.productNameFa?.toLowerCase().includes(nameQuery)
if (!matchesTitle && !matchesFa) return false
}
const price = listingEffectivePrice(listing)
if (filters.minPrice !== null) {
if (price === null || price < filters.minPrice) return false
}
if (filters.maxPrice !== null) {
if (price === null || price > filters.maxPrice) return false
}
if (filters.onlyDiscounted) {
const hasDiscount = listing.variants.some((variant) =>
hasStoreItemDiscount(variant.price, variant.discountedPrice),
)
if (!hasDiscount) return false
}
return true
})
}
+8
View File
@@ -0,0 +1,8 @@
export function extractFirstImageUrl(html: string): string | null {
if (!html || !html.includes('<img')) return null
const doc = new DOMParser().parseFromString(html, 'text/html')
const img = doc.querySelector('img[src]')
const src = img?.getAttribute('src')?.trim()
return src || null
}
+7
View File
@@ -0,0 +1,7 @@
export function createId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`
}
+88
View File
@@ -0,0 +1,88 @@
const ALLOWED_IMAGE_TYPES = new Set([
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/gif',
])
function parseDataUrl(dataUrl: string): { mime: string; bytes: Uint8Array } {
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/)
if (!match) {
throw new Error('Invalid image data')
}
const mime = match[1] === 'image/jpg' ? 'image/jpeg' : match[1]
const binary = atob(match[2])
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i)
}
return { mime, bytes }
}
function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image()
image.addEventListener('load', () => resolve(image))
image.addEventListener('error', () => reject(new Error('Could not load image')))
image.src = src
})
}
export function dataUrlToFile(dataUrl: string, filename: string): File {
const { mime, bytes } = parseDataUrl(dataUrl)
const copy = new Uint8Array(bytes)
return new File([copy], filename, { type: mime })
}
export async function ensureUploadFile(dataUrl: string, filename: string): Promise<File> {
if (dataUrl.startsWith('data:')) {
const mime = dataUrl.slice(5, dataUrl.indexOf(';'))
const normalized = mime === 'image/jpg' ? 'image/jpeg' : mime
if (ALLOWED_IMAGE_TYPES.has(normalized)) {
const file = dataUrlToFile(dataUrl, filename)
if (file.size > 0) {
return file
}
}
}
const image = await loadImage(dataUrl)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('Could not prepare image for upload')
}
const wantsPng = filename.toLowerCase().endsWith('.png')
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
if (wantsPng) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
ctx.drawImage(image, 0, 0)
const outputDataUrl = wantsPng
? canvas.toDataURL('image/png')
: canvas.toDataURL('image/jpeg', 0.92)
const safeName = filename.replace(/\.[^.]+$/, '') || 'image'
const extension = wantsPng ? 'png' : 'jpg'
return dataUrlToFile(outputDataUrl, `${safeName}.${extension}`)
}
export async function ensureJpegUploadFile(
dataUrl: string,
filename: string,
): Promise<File> {
return ensureUploadFile(
dataUrl,
filename.toLowerCase().endsWith('.jpg') || filename.toLowerCase().endsWith('.jpeg')
? filename
: `${filename.replace(/\.[^.]+$/, '') || 'image'}.jpg`,
)
}
+39
View File
@@ -0,0 +1,39 @@
/** 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)
}
@@ -0,0 +1,63 @@
import type { ProductApi } from '../services/productService'
export interface ProductMonthActivity {
monthKey: string
label: string
added: number
updated: number
}
function toMonthKey(iso: string): string {
const date = new Date(iso)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
return `${year}-${month}`
}
function formatMonthLabel(monthKey: string): string {
const [year, month] = monthKey.split('-').map(Number)
return new Date(year, month - 1, 1).toLocaleString('en-US', { month: 'short' })
}
export function buildLast12MonthKeys(): string[] {
const keys: string[] = []
const now = new Date()
for (let i = 11; i >= 0; i -= 1) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
keys.push(`${year}-${month}`)
}
return keys
}
export function aggregateProductActivity(products: ProductApi[]): ProductMonthActivity[] {
const monthKeys = buildLast12MonthKeys()
const added = new Map(monthKeys.map((key) => [key, 0]))
const updated = new Map(monthKeys.map((key) => [key, 0]))
for (const product of products) {
const createdKey = toMonthKey(product.createdAt)
if (added.has(createdKey)) {
added.set(createdKey, (added.get(createdKey) ?? 0) + 1)
}
const createdTime = new Date(product.createdAt).getTime()
const updatedTime = new Date(product.updatedAt).getTime()
if (updatedTime > createdTime) {
const updatedKey = toMonthKey(product.updatedAt)
if (updated.has(updatedKey)) {
updated.set(updatedKey, (updated.get(updatedKey) ?? 0) + 1)
}
}
}
return monthKeys.map((monthKey) => ({
monthKey,
label: formatMonthLabel(monthKey),
added: added.get(monthKey) ?? 0,
updated: updated.get(monthKey) ?? 0,
}))
}
+84
View File
@@ -0,0 +1,84 @@
export const STEP_COLOR_PRESETS = [
'#EF4444',
'#F97316',
'#F59E0B',
'#EAB308',
'#84CC16',
'#22C55E',
'#10B981',
'#14B8A6',
'#06B6D4',
'#0EA5E9',
'#3B82F6',
'#6366F1',
'#8B5CF6',
'#A855F7',
'#D946EF',
'#EC4899',
'#F43F5E',
'#78716C',
'#6B7280',
'#64748B',
'#111827',
'#92400E',
'#1E3A5F',
'#D4AF37',
] as const
export type StepColorPreset = (typeof STEP_COLOR_PRESETS)[number]
export const DEFAULT_STEP_COLOR: StepColorPreset = '#3B82F6'
const DEFAULT_STEP_COLORS_BY_ID: Record<string, StepColorPreset> = {
processing: '#3B82F6',
'ready-for-shipping': '#F59E0B',
shipped: '#8B5CF6',
delivered: '#22C55E',
}
export function isStepColorPreset(value: string): value is StepColorPreset {
return STEP_COLOR_PRESETS.includes(value as StepColorPreset)
}
export function normalizeStepColor(value: unknown, fallback = DEFAULT_STEP_COLOR): StepColorPreset {
if (typeof value === 'string' && isStepColorPreset(value)) {
return value
}
return fallback
}
export function defaultStepColorForId(id: string, index = 0): StepColorPreset {
return DEFAULT_STEP_COLORS_BY_ID[id] ?? STEP_COLOR_PRESETS[index % STEP_COLOR_PRESETS.length]
}
export function stepColorLabel(hex: StepColorPreset) {
const index = STEP_COLOR_PRESETS.indexOf(hex)
return index >= 0 ? `Color ${index + 1}` : 'Color'
}
function hexToRgb(hex: string) {
const normalized = hex.replace('#', '')
const value =
normalized.length === 3
? normalized
.split('')
.map((char) => char + char)
.join('')
: normalized
const int = Number.parseInt(value, 16)
return {
r: (int >> 16) & 255,
g: (int >> 8) & 255,
b: int & 255,
}
}
export function stepBadgeStyle(color: string) {
const { r, g, b } = hexToRgb(color)
return {
color,
background: `rgba(${r}, ${g}, ${b}, 0.14)`,
border: `1px solid rgba(${r}, ${g}, ${b}, 0.32)`,
} as const
}
+28
View File
@@ -0,0 +1,28 @@
import type { ProductVariationSelection } from '../services/productVariationService'
import { createId } from './id'
export interface StoreItemDraftRow {
id: string
storeItemId?: string
selections: Record<string, string>
price: string
stock: string
}
export function getVariationOptions(variation: ProductVariationSelection) {
if (variation.selectedOptionIds.length > 0) {
return variation.options.filter((option) =>
variation.selectedOptionIds.includes(option.id),
)
}
return variation.options
}
export function createEmptyStoreItemRow(): StoreItemDraftRow {
return {
id: createId(),
selections: {},
price: '',
stock: '1',
}
}
@@ -0,0 +1,98 @@
import type { StoreItem } from '../services/storeItemService'
import { hasStoreItemDiscount } from './irtPrice'
export interface StoreProductListing {
productId: string
productTitle: string
productNameFa: string
productImage: string | null
productTotalStock: number
variantCount: number
variants: StoreItem[]
representative: StoreItem
displayPrice: number | null
displayDiscountedPrice: number | null
showFestival: boolean
}
function effectivePrice(item: StoreItem): number | null {
if (hasStoreItemDiscount(item.price, item.discountedPrice)) {
return item.discountedPrice
}
return item.price
}
function pickDisplayPrice(variants: StoreItem[]) {
let displayPrice: number | null = null
let displayDiscountedPrice: number | null = null
let minEffective = Infinity
for (const variant of variants) {
const effective = effectivePrice(variant)
if (effective === null) continue
if (effective < minEffective) {
minEffective = effective
if (hasStoreItemDiscount(variant.price, variant.discountedPrice)) {
displayPrice = variant.price
displayDiscountedPrice = variant.discountedPrice
} else {
displayPrice = variant.price
displayDiscountedPrice = null
}
}
}
if (minEffective === Infinity) {
const first = variants[0]
return {
displayPrice: first?.price ?? null,
displayDiscountedPrice: first?.discountedPrice ?? null,
}
}
return { displayPrice, displayDiscountedPrice }
}
export function groupStoreItemsByProduct(items: StoreItem[]): StoreProductListing[] {
const byProduct = new Map<string, StoreItem[]>()
for (const item of items) {
const variants = byProduct.get(item.productId) ?? []
variants.push(item)
byProduct.set(item.productId, variants)
}
return [...byProduct.values()]
.map((variants) => {
const sorted = [...variants].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
const representative = sorted[0]
const { displayPrice, displayDiscountedPrice } = pickDisplayPrice(sorted)
return {
productId: representative.productId,
productTitle: representative.productTitle,
productNameFa: representative.productNameFa,
productImage: representative.productImage,
productTotalStock: representative.productTotalStock,
variantCount: sorted.length,
variants: sorted,
representative,
displayPrice,
displayDiscountedPrice,
showFestival: sorted.some(
(variant) => variant.isFestival || (variant.rewardPoints ?? 0) > 0,
),
}
})
.sort(
(a, b) =>
new Date(b.representative.createdAt).getTime() -
new Date(a.representative.createdAt).getTime(),
)
}
export function formatVariantCount(count: number): string {
return count === 1 ? '1 variant' : `${count} variants`
}
@@ -0,0 +1,46 @@
import type { StoreItem } from '../services/storeItemService'
import type { StoreSpecialStoreItem } from '../types/storeSpecial'
import {
groupStoreItemsByProduct,
type StoreProductListing,
} from './storeProductGroups'
export interface SpecialProductListing extends StoreProductListing {
storeItemId: string
}
function specialStoreItemToVariants(item: StoreSpecialStoreItem): StoreItem[] {
const productTotalStock = item.variants.reduce(
(sum, variant) => sum + (variant.stockQuantity ?? 0),
0,
)
return item.variants.map((variant) => ({
id: variant.id,
storeItemId: item.id,
productId: item.productId,
productTitle: item.productTitle,
productNameFa: item.productNameFa,
productImage: item.productImage,
productTotalStock,
selections: variant.selections,
label: variant.label,
price: variant.price,
discountedPrice: variant.discountedPrice,
stockQuantity: variant.stockQuantity,
rewardPoints: variant.rewardPoints,
isFestival: variant.isFestival,
sortOrder: variant.sortOrder,
createdAt: '',
}))
}
export function specialItemsToListings(items: StoreSpecialStoreItem[]): SpecialProductListing[] {
const variants = items.flatMap(specialStoreItemToVariants)
const listings = groupStoreItemsByProduct(variants)
return listings.map((listing) => ({
...listing,
storeItemId: listing.representative.storeItemId ?? listing.representative.id,
}))
}