Initial commit of Balout Pastry admin dashboards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-02 17:23:00 +03:30
co-authored by Cursor
commit af11d96fd0
103 changed files with 14651 additions and 0 deletions
+314
View File
@@ -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>
)
}