Files
dashboards/src/pages/AddProductPage.tsx
T

315 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}