Add OTP-first customer signup and product catalog PDF field.

Registration collects name and phone, verifies SMS, then asks for a password only for new Meshkee accounts. Product add/edit supports an optional catalog PDF.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-27 08:57:08 +03:30
co-authored by Cursor
parent e2950fa20e
commit 6d85a217e1
12 changed files with 746 additions and 292 deletions
@@ -87,6 +87,64 @@
color: var(--text-muted);
}
.catalogHint {
margin: 0 0 8px;
font-size: 12px;
color: var(--text-muted);
}
.catalogRow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.catalogInput {
max-width: 100%;
font-size: 13px;
}
.catalogSelected {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: var(--radius-sm);
background: rgba(148, 163, 184, 0.12);
font-size: 13px;
color: var(--text-secondary);
min-width: 0;
}
.catalogSelected span,
.catalogLink {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 280px;
}
.catalogLink {
color: var(--primary);
text-decoration: underline;
}
.catalogClear {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
border-radius: 4px;
color: var(--text-muted);
flex-shrink: 0;
}
.catalogClear:hover {
color: var(--text);
background: rgba(148, 163, 184, 0.2);
}
.summaryField textarea {
min-height: calc((var(--field-height) + 8px) * 2);
padding-inline-end: 12px;
+101 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { Sparkles } from 'lucide-react'
import { FileText, Sparkles, X } from 'lucide-react'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { SearchableSelect } from '../components/SearchableSelect'
import { ImageCropper } from '../components/ImageCropper'
@@ -18,6 +18,7 @@ import {
galleryMediaIdsAfterChange,
resolveDataUrlToMediaId,
resolveDataUrlsToMediaIds,
uploadCatalogPdf,
} from '../services/mediaService'
import {
createProduct,
@@ -34,12 +35,15 @@ import aiStyles from '../styles/ai.module.css'
import styles from './AddNewProductPage.module.css'
import { useT } from '../i18n/useT'
const CATALOG_PDF_MAX_BYTES = 2 * 1024 * 1024
export function AddNewProductPage() {
const { id } = useParams()
const navigate = useNavigate()
const t = useT()
const { showToast } = useToast()
const isEdit = Boolean(id)
const catalogInputRef = useRef<HTMLInputElement>(null)
const [categories, setCategories] = useState<Category[]>([])
const [brandOptions, setBrandOptions] = useState<FlatCategory[]>([])
@@ -54,6 +58,11 @@ export function AddNewProductPage() {
const [tags, setTags] = useState<string[]>([])
const [thumbnailMediaId, setThumbnailMediaId] = useState<string | null>(null)
const [galleryMediaIds, setGalleryMediaIds] = useState<string[]>([])
const [catalogMediaId, setCatalogMediaId] = useState<string | null>(null)
const [catalogUrl, setCatalogUrl] = useState<string | null>(null)
const [catalogFileName, setCatalogFileName] = useState<string | null>(null)
const [catalogFile, setCatalogFile] = useState<File | null>(null)
const [catalogRemoved, setCatalogRemoved] = useState(false)
const [loaded, setLoaded] = useState(!isEdit)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
@@ -172,6 +181,11 @@ export function AddNewProductPage() {
setTags(form.tags)
setThumbnailMediaId(form.thumbnailMediaId)
setGalleryMediaIds(form.galleryMediaIds)
setCatalogMediaId(form.catalogMediaId)
setCatalogUrl(form.catalogUrl)
setCatalogFileName(form.catalogFileName)
setCatalogFile(null)
setCatalogRemoved(false)
setLoaded(true)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
@@ -185,6 +199,39 @@ export function AddNewProductPage() {
if (!loaded) return null
function clearCatalogFile() {
setCatalogFile(null)
setCatalogMediaId(null)
setCatalogUrl(null)
setCatalogFileName(null)
setCatalogRemoved(true)
if (catalogInputRef.current) {
catalogInputRef.current.value = ''
}
}
function handleCatalogFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0] ?? null
if (!file) return
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
setError(t('products.form.catalog.errorType'))
e.target.value = ''
return
}
if (file.size > CATALOG_PDF_MAX_BYTES) {
setError(t('products.form.catalog.errorSize'))
e.target.value = ''
return
}
setError('')
setCatalogFile(file)
setCatalogFileName(file.name)
setCatalogUrl(null)
setCatalogRemoved(false)
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsSubmitting(true)
@@ -208,6 +255,14 @@ export function AddNewProductPage() {
galleryMediaIds,
)
let nextCatalogMediaId: string | null = catalogMediaId
if (catalogFile) {
const uploaded = await uploadCatalogPdf(catalogFile)
nextCatalogMediaId = uploaded.id
} else if (catalogRemoved) {
nextCatalogMediaId = null
}
const payload = {
title: nameEn.trim(),
nameFa: nameFa.trim(),
@@ -217,6 +272,7 @@ export function AddNewProductPage() {
brandId: brandId || undefined,
featuredMediaId: featuredMediaId ?? undefined,
galleryMediaIds: nextGalleryMediaIds,
catalogMediaId: nextCatalogMediaId ?? undefined,
tags,
status: 'published' as const,
}
@@ -227,6 +283,7 @@ export function AddNewProductPage() {
categoryId: categoryId || null,
brandId: brandId || null,
featuredMediaId,
catalogMediaId: nextCatalogMediaId,
})
} else {
await createProduct(payload)
@@ -434,6 +491,48 @@ export function AddNewProductPage() {
/>
</div>
<div className={`${styles.field} ${styles.col12}`}>
<label htmlFor="product-catalog">{t('products.form.catalog')}</label>
<p className={styles.catalogHint}>{t('products.form.catalogHint')}</p>
<div className={styles.catalogRow}>
<input
ref={catalogInputRef}
id="product-catalog"
type="file"
accept="application/pdf,.pdf"
onChange={handleCatalogFileChange}
disabled={isSubmitting}
className={styles.catalogInput}
/>
{(catalogFileName || catalogUrl) && (
<div className={styles.catalogSelected}>
<FileText size={16} aria-hidden />
{catalogUrl && !catalogFile ? (
<a
href={catalogUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.catalogLink}
>
{catalogFileName || t('products.form.catalogView')}
</a>
) : (
<span>{catalogFileName}</span>
)}
<button
type="button"
className={styles.catalogClear}
onClick={clearCatalogFile}
disabled={isSubmitting}
aria-label={t('products.form.catalogRemove')}
>
<X size={16} />
</button>
</div>
)}
</div>
</div>
<div className={`${styles.field} ${styles.col12}`}>
<label>{t('products.form.tags')}</label>
<TagInput