mirror of
https://git.meshkee.com/BaloutPastry/dashboards.git
synced 2026-08-11 22:30:59 +04:30
Initial commit of Balout Pastry admin dashboards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user