mirror of
https://git.meshkee.com/BaloutPastry/dashboards.git
synced 2026-08-11 22:30:59 +04:30
315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||
import { ChevronRight, ChevronDown, Search } from 'lucide-react'
|
||
import { Header } from '../components/Header'
|
||
import { ImageCropper } from '../components/ImageCropper'
|
||
import { ImageGallery } from '../components/ImageGallery'
|
||
import { PriceInput } from '../components/PriceInput'
|
||
import { RichTextArea } from '../components/RichTextArea'
|
||
import { TagInput } from '../components/TagInput'
|
||
import {
|
||
findCategory,
|
||
flattenCategories,
|
||
initialCategories,
|
||
} from '../data/categories'
|
||
import type { SellUnit } from '../data/products'
|
||
import { parsePriceNumber } from '../utils/price'
|
||
import styles from './HomePage.module.css'
|
||
import pageStyles from './AddProductPage.module.css'
|
||
|
||
export function AddProductPage() {
|
||
const location = useLocation()
|
||
const navigate = useNavigate()
|
||
const formId = useId()
|
||
|
||
const [mainImage, setMainImage] = useState<string | null>(null)
|
||
const [categoryId, setCategoryId] = useState<string | null>(null)
|
||
const [nameFa, setNameFa] = useState('')
|
||
const [nameEn, setNameEn] = useState('')
|
||
const [sellUnit, setSellUnit] = useState<SellUnit>('unit')
|
||
const [price, setPrice] = useState('')
|
||
const [intro, setIntro] = useState('')
|
||
const [description, setDescription] = useState('')
|
||
const [gallery, setGallery] = useState<string[]>([])
|
||
const [tags, setTags] = useState<string[]>([])
|
||
const [error, setError] = useState('')
|
||
|
||
const [categoryQuery, setCategoryQuery] = useState('')
|
||
const [categoryOpen, setCategoryOpen] = useState(false)
|
||
const categoryRef = useRef<HTMLDivElement>(null)
|
||
|
||
const categoryOptions = useMemo(
|
||
() => flattenCategories(initialCategories),
|
||
[],
|
||
)
|
||
|
||
const filteredCategories = useMemo(() => {
|
||
const q = categoryQuery.trim().toLowerCase()
|
||
if (!q) return categoryOptions
|
||
return categoryOptions.filter(
|
||
(option) =>
|
||
option.labelFa.includes(categoryQuery.trim()) ||
|
||
option.labelEn.toLowerCase().includes(q),
|
||
)
|
||
}, [categoryOptions, categoryQuery])
|
||
|
||
const selectedCategory = categoryId
|
||
? findCategory(initialCategories, categoryId)
|
||
: null
|
||
|
||
useEffect(() => {
|
||
function handlePointerDown(event: MouseEvent) {
|
||
if (!categoryRef.current?.contains(event.target as Node)) {
|
||
setCategoryOpen(false)
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', handlePointerDown)
|
||
return () => document.removeEventListener('mousedown', handlePointerDown)
|
||
}, [])
|
||
|
||
function handleSubmit(event: React.FormEvent) {
|
||
event.preventDefault()
|
||
if (!mainImage) {
|
||
setError('تصویر اصلی الزامی است')
|
||
return
|
||
}
|
||
if (!categoryId) {
|
||
setError('انتخاب دستهبندی الزامی است')
|
||
return
|
||
}
|
||
if (!nameFa.trim() || !nameEn.trim()) {
|
||
setError('نام فارسی و انگلیسی الزامی است')
|
||
return
|
||
}
|
||
const numericPrice = parsePriceNumber(price)
|
||
if (numericPrice === null || numericPrice <= 0) {
|
||
setError('قیمت معتبر الزامی است')
|
||
return
|
||
}
|
||
|
||
setError('')
|
||
window.alert('محصول با موفقیت ثبت شد (نسخه نمایشی)')
|
||
navigate('/products/list')
|
||
}
|
||
|
||
return (
|
||
<div className={styles.page}>
|
||
<Header />
|
||
|
||
<main className={styles.main}>
|
||
<section key={`welcome-${location.key}`} className={styles.welcome}>
|
||
<span className={styles.enBackdrop} aria-hidden>
|
||
New Product
|
||
</span>
|
||
<div className={styles.welcomeContent}>
|
||
<p className={styles.greeting}>
|
||
<Link to="/products" className={styles.backLink}>
|
||
<ChevronRight size={18} strokeWidth={1.75} />
|
||
بازگشت به محصولات
|
||
</Link>
|
||
</p>
|
||
<h1 className={styles.headline}>ثبت محصول جدید</h1>
|
||
<p className={styles.lead}>
|
||
اطلاعات محصول را تکمیل کنید و ذخیره نمایید.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<form
|
||
className={pageStyles.form}
|
||
onSubmit={handleSubmit}
|
||
aria-labelledby={`${formId}-title`}
|
||
>
|
||
<h2 id={`${formId}-title`} className={pageStyles.srOnly}>
|
||
فرم ثبت محصول
|
||
</h2>
|
||
|
||
{error && (
|
||
<div className={pageStyles.error} role="alert">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className={pageStyles.panel}>
|
||
<section className={pageStyles.section}>
|
||
<div className={pageStyles.topGrid}>
|
||
<div>
|
||
<h3 className={pageStyles.sectionTitle}>تصویر اصلی</h3>
|
||
<ImageCropper
|
||
value={mainImage}
|
||
onChange={setMainImage}
|
||
aspect={1}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className={pageStyles.sectionTitle}>اطلاعات پایه</h3>
|
||
|
||
<div className={pageStyles.field} ref={categoryRef}>
|
||
<label htmlFor="product-category">دستهبندی</label>
|
||
<div className={pageStyles.searchWrap}>
|
||
<Search size={16} className={pageStyles.searchIcon} />
|
||
<input
|
||
id="product-category"
|
||
type="text"
|
||
placeholder="جستجوی دستهبندی..."
|
||
value={
|
||
categoryOpen
|
||
? categoryQuery
|
||
: selectedCategory
|
||
? `${selectedCategory.nameFa} — ${selectedCategory.nameEn}`
|
||
: categoryQuery
|
||
}
|
||
onChange={(e) => {
|
||
setCategoryQuery(e.target.value)
|
||
setCategoryOpen(true)
|
||
if (categoryId) setCategoryId(null)
|
||
}}
|
||
onFocus={() => {
|
||
setCategoryOpen(true)
|
||
setCategoryQuery('')
|
||
}}
|
||
autoComplete="off"
|
||
/>
|
||
<ChevronDown size={16} className={pageStyles.chevron} />
|
||
</div>
|
||
|
||
{categoryOpen && (
|
||
<ul className={pageStyles.dropdown} role="listbox">
|
||
{filteredCategories.length === 0 ? (
|
||
<li className={pageStyles.emptyOption}>موردی یافت نشد</li>
|
||
) : (
|
||
filteredCategories.map((option) => (
|
||
<li key={option.id}>
|
||
<button
|
||
type="button"
|
||
className={`${pageStyles.option} ${categoryId === option.id ? pageStyles.optionActive : ''}`}
|
||
onClick={() => {
|
||
setCategoryId(option.id)
|
||
setCategoryQuery('')
|
||
setCategoryOpen(false)
|
||
}}
|
||
>
|
||
<span className={pageStyles.optionFa}>
|
||
{option.labelFa}
|
||
</span>
|
||
<span className={pageStyles.optionEn}>
|
||
{option.labelEn}
|
||
</span>
|
||
</button>
|
||
</li>
|
||
))
|
||
)}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
|
||
<div className={pageStyles.field}>
|
||
<label htmlFor="product-name-fa">نام فارسی</label>
|
||
<input
|
||
id="product-name-fa"
|
||
type="text"
|
||
placeholder="مثال: شیرینی پسته"
|
||
value={nameFa}
|
||
onChange={(e) => setNameFa(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className={pageStyles.field}>
|
||
<label htmlFor="product-name-en">نام انگلیسی</label>
|
||
<input
|
||
id="product-name-en"
|
||
type="text"
|
||
dir="ltr"
|
||
placeholder="Example: Pistachio Cookie"
|
||
value={nameEn}
|
||
onChange={(e) => setNameEn(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className={pageStyles.priceRow}>
|
||
<div className={pageStyles.field}>
|
||
<label htmlFor="product-sell-unit">نوع قیمت</label>
|
||
<select
|
||
id="product-sell-unit"
|
||
value={sellUnit}
|
||
onChange={(e) =>
|
||
setSellUnit(e.target.value as SellUnit)
|
||
}
|
||
>
|
||
<option value="unit">برای واحد</option>
|
||
<option value="kilo">برای وزن (کیلو)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className={pageStyles.field}>
|
||
<label htmlFor="product-price">قیمت (تومان)</label>
|
||
<PriceInput
|
||
id="product-price"
|
||
value={price}
|
||
onChange={setPrice}
|
||
placeholder="۰"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className={pageStyles.field}>
|
||
<label htmlFor="product-intro">معرفی کوتاه</label>
|
||
<textarea
|
||
id="product-intro"
|
||
rows={3}
|
||
placeholder="یک معرفی کوتاه از محصول بنویسید..."
|
||
value={intro}
|
||
onChange={(e) => setIntro(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className={`${pageStyles.section} ${pageStyles.sectionDivider}`}>
|
||
<RichTextArea
|
||
id="product-description"
|
||
label="توضیحات"
|
||
value={description}
|
||
onChange={setDescription}
|
||
placeholder="توضیحات کامل محصول را بنویسید..."
|
||
/>
|
||
</section>
|
||
|
||
<section className={`${pageStyles.section} ${pageStyles.sectionBand}`}>
|
||
<ImageGallery
|
||
label="گالری تصاویر"
|
||
images={gallery}
|
||
onChange={setGallery}
|
||
/>
|
||
</section>
|
||
|
||
<section className={pageStyles.section}>
|
||
<TagInput
|
||
label="تگها"
|
||
tags={tags}
|
||
onChange={setTags}
|
||
placeholder="تگ را بنویسید و Enter بزنید"
|
||
/>
|
||
</section>
|
||
</div>
|
||
|
||
<div className={pageStyles.actions}>
|
||
<button
|
||
type="button"
|
||
className={pageStyles.cancelBtn}
|
||
onClick={() => navigate('/products')}
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button type="submit" className={pageStyles.submitBtn}>
|
||
ذخیره محصول
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|