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,311 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { ImageCropper } from '../components/ImageCropper'
import { ImageUploader } from '../components/ImageUploader'
import { TagInput } from '../components/TagInput'
import { RichTextEditor } from '../components/RichTextEditor'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { ApiError } from '../lib/api'
import { listProductCategories, mapProductCategoryToUi } from '../services/productCategoryService'
import { listAllBrands, mapBrandToSelectOption } from '../services/brandService'
import {
resolveDataUrlToMediaId,
resolveDataUrlsToMediaIds,
} from '../services/mediaService'
import {
createProduct,
getProduct,
mapProductApiToFormState,
updateProduct,
} from '../services/productService'
import type { Category, FlatCategory } from '../types/category'
import { flattenCategories } from '../utils/categories'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
export function AddNewProductPage() {
const { id } = useParams()
const navigate = useNavigate()
const isEdit = Boolean(id)
const [categories, setCategories] = useState<Category[]>([])
const [brandOptions, setBrandOptions] = useState<FlatCategory[]>([])
const [categoryId, setCategoryId] = useState('')
const [brandId, setBrandId] = useState('')
const [nameFa, setNameFa] = useState('')
const [nameEn, setNameEn] = useState('')
const [summary, setSummary] = useState('')
const [description, setDescription] = useState('')
const [thumbnail, setThumbnail] = useState<string | null>(null)
const [images, setImages] = useState<string[]>([])
const [tags, setTags] = useState<string[]>([])
const [thumbnailMediaId, setThumbnailMediaId] = useState<string | null>(null)
const [galleryMediaIds, setGalleryMediaIds] = useState<string[]>([])
const [loaded, setLoaded] = useState(!isEdit)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
useEffect(() => {
const controller = new AbortController()
async function loadFormOptions() {
try {
const [categoryItems, brandItems] = await Promise.all([
listProductCategories(controller.signal),
listAllBrands(controller.signal),
])
setCategories(categoryItems.map(mapProductCategoryToUi))
setBrandOptions(brandItems.map(mapBrandToSelectOption))
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setError(err.message)
} else {
setError('Unable to load form options.')
}
}
}
void loadFormOptions()
return () => controller.abort()
}, [])
useEffect(() => {
if (!isEdit || !id) return
const controller = new AbortController()
async function loadProduct() {
setError('')
try {
const product = await getProduct(id!, controller.signal)
const form = mapProductApiToFormState(product)
setCategoryId(form.categoryId)
setBrandId(form.brandId)
setNameFa(form.nameFa)
setNameEn(form.nameEn)
setSummary(form.summary)
setDescription(form.description)
setThumbnail(form.thumbnail)
setImages(form.images)
setTags(form.tags)
setThumbnailMediaId(form.thumbnailMediaId)
setGalleryMediaIds(form.galleryMediaIds)
setLoaded(true)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
navigate('/products/list', { replace: true })
}
}
void loadProduct()
return () => controller.abort()
}, [isEdit, id, navigate])
if (!loaded) return null
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsSubmitting(true)
setError('')
try {
const featuredMediaId = await resolveDataUrlToMediaId(
thumbnail,
'product-thumbnail.jpg',
thumbnailMediaId,
)
const nextGalleryMediaIds = await resolveDataUrlsToMediaIds(
images,
galleryMediaIds,
)
const payload = {
title: nameEn.trim(),
nameFa: nameFa.trim(),
summary: summary.trim(),
descriptionHtml: description,
categoryId: categoryId || undefined,
brandId: brandId || undefined,
featuredMediaId: featuredMediaId ?? undefined,
galleryMediaIds: nextGalleryMediaIds,
tags,
status: 'published' as const,
}
if (isEdit && id) {
await updateProduct(id, {
...payload,
categoryId: categoryId || null,
brandId: brandId || null,
featuredMediaId,
})
} else {
await createProduct(payload)
}
navigate('/products/list')
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else if (err instanceof Error) {
setError(err.message)
} else {
setError('Unable to save product.')
}
} finally {
setIsSubmitting(false)
}
}
return (
<main className={pageStyles.content}>
<Breadcrumbs
items={
isEdit
? [
{ label: 'Dashboard', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'My Products', href: '/products/list' },
{ label: 'Edit Product' },
]
: [
{ label: 'Dashboard', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Add a New Product' },
]
}
/>
<div className={pageStyles.pageHeader}>
<div>
<h2 className={pageStyles.pageTitle}>
{isEdit ? 'Edit Product' : 'Add a New Product'}
</h2>
<p className={pageStyles.pageSubtitle}>
{isEdit
? 'Update product details and save changes.'
: 'Create and publish a new product to your store.'}
</p>
</div>
</div>
{error && (
<div className={styles.error} role="alert">
{error}
</div>
)}
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.formGrid}>
<div className={`${styles.field} ${styles.col2} ${styles.thumbnailField}`}>
<label>Thumbnail Image</label>
<ImageCropper value={thumbnail} onChange={setThumbnail} />
</div>
<div className={styles.col10}>
<div className={styles.topFields}>
<div className={`${styles.field} ${styles.fieldCategory}`}>
<label>Category</label>
<SearchableSelect
options={categoryOptions}
value={categoryId}
onChange={setCategoryId}
placeholder="Search and select category..."
/>
</div>
<div className={`${styles.field} ${styles.fieldBrand}`}>
<label>
Brand <span className={styles.optional}>(optional)</span>
</label>
<SearchableSelect
options={brandOptions}
value={brandId}
onChange={setBrandId}
placeholder="Search and select brand..."
/>
</div>
<div className={`${styles.field} ${styles.fieldHalf}`}>
<label htmlFor="nameFa">Name (FA)</label>
<input
id="nameFa"
name="nameFa"
type="text"
dir="rtl"
className="faText"
placeholder="نام محصول"
value={nameFa}
onChange={(e) => setNameFa(e.target.value)}
required
disabled={isSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.fieldHalf}`}>
<label htmlFor="nameEn">Name (EN)</label>
<input
id="nameEn"
name="nameEn"
type="text"
dir="ltr"
placeholder="Product name"
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
required
disabled={isSubmitting}
/>
</div>
<div className={`${styles.field} ${styles.fieldFull} ${styles.summaryField}`}>
<label htmlFor="summary">Summary</label>
<textarea
id="summary"
name="summary"
rows={4}
placeholder="Short summary shown in product listings"
value={summary}
onChange={(e) => setSummary(e.target.value)}
required
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className={`${styles.field} ${styles.col12}`}>
<label>Description</label>
<RichTextEditor
value={description}
onChange={setDescription}
placeholder="Full product description with formatting..."
/>
</div>
<div className={`${styles.field} ${styles.col12}`}>
<label>Product Images</label>
<ImageUploader images={images} onChange={setImages} />
</div>
<div className={`${styles.field} ${styles.col12}`}>
<label>Tags</label>
<TagInput tags={tags} onChange={setTags} />
</div>
</div>
<div className={styles.actions}>
<Link to="/products/list" className={styles.cancelBtn}>
Cancel
</Link>
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : isEdit ? 'Update Product' : 'Save Product'}
</button>
</div>
</form>
</main>
)
}