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:
co-authored by
Cursor
parent
e2950fa20e
commit
6d85a217e1
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user