Expand special-order form with selects and success screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-12 23:44:27 +03:30
co-authored by Cursor
parent 389fc6fc51
commit cc4334e048
7 changed files with 782 additions and 51 deletions
+31
View File
@@ -11,6 +11,7 @@
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-multi-date-picker": "^4.5.2",
"react-router-dom": "^7.18.2" "react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
@@ -1116,6 +1117,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-date-object": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz",
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
"license": "MIT"
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.8", "version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
@@ -1128,6 +1135,30 @@
"react": "^19.2.8" "react": "^19.2.8"
} }
}, },
"node_modules/react-element-popper": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz",
"integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-multi-date-picker": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz",
"integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==",
"license": "MIT",
"dependencies": {
"react-date-object": "^2.1.8",
"react-element-popper": "^2.1.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.2", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
+2 -1
View File
@@ -10,9 +10,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^0.511.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"lucide-react": "^0.511.0", "react-multi-date-picker": "^4.5.2",
"react-router-dom": "^7.18.2" "react-router-dom": "^7.18.2"
}, },
"devDependencies": { "devDependencies": {
+75
View File
@@ -0,0 +1,75 @@
import MultiDatePicker from 'react-multi-date-picker'
import DateObjectModule from 'react-date-object'
import persian from 'react-date-object/calendars/persian'
import persian_fa from 'react-date-object/locales/persian_fa'
const DatePicker =
(MultiDatePicker as unknown as { default?: typeof MultiDatePicker }).default ??
MultiDatePicker
const DateObject =
(DateObjectModule as unknown as { default?: typeof DateObjectModule })
.default ?? DateObjectModule
type PersianDateInputProps = {
id?: string
value: string
onChange: (isoDate: string) => void
disabled?: boolean
placeholder?: string
}
function isoToPickerValue(iso: string) {
if (!iso) return null
const date = new Date(iso)
if (Number.isNaN(date.getTime())) return null
return new DateObject({
date,
calendar: persian,
locale: persian_fa,
})
}
function pickerValueToIso(value: InstanceType<typeof DateObject> | null) {
if (!value) return ''
const date = value.toDate()
date.setHours(12, 0, 0, 0)
return date.toISOString()
}
export default function PersianDateInput({
id,
value,
onChange,
disabled = false,
placeholder = 'انتخاب تاریخ',
}: PersianDateInputProps) {
return (
<DatePicker
id={id}
value={isoToPickerValue(value)}
onChange={(
date:
| InstanceType<typeof DateObject>
| InstanceType<typeof DateObject>[]
| null,
) => {
if (date instanceof DateObject) {
onChange(pickerValueToIso(date))
return
}
onChange('')
}}
calendar={persian}
locale={persian_fa}
format="YYYY/MM/DD"
calendarPosition="bottom-center"
containerClassName="persian-date-input"
inputClass="persian-date-input__field"
portal
zIndex={200}
disabled={disabled}
placeholder={placeholder}
editable={false}
/>
)
}
+29
View File
@@ -70,6 +70,7 @@ export type CreateMyOrderPayload = {
} }
export type SpecialOrderCategory = 'specialCake' | 'specialPastry' export type SpecialOrderCategory = 'specialCake' | 'specialPastry'
export type SpecialOrderDeliveryType = 'pickup' | 'shipping'
export type SpecialOrderImageInput = { export type SpecialOrderImageInput = {
url: string url: string
@@ -80,6 +81,18 @@ export type CreateMySpecialOrderPayload = {
category: SpecialOrderCategory category: SpecialOrderCategory
name: string name: string
description?: string description?: string
neededDate?: string
receiveTime?: string
servings?: number
cakeFlavor?: string
filling?: string
baloutSuggestedCombo?: boolean
coating?: string
designStyle?: string
cakeText?: string
decorations?: string
deliveryType?: SpecialOrderDeliveryType
branchId?: string
images?: SpecialOrderImageInput[] images?: SpecialOrderImageInput[]
} }
@@ -91,9 +104,25 @@ export type MediaUploadResult = {
export type SpecialOrder = { export type SpecialOrder = {
id: string id: string
number: number
code: string
category: SpecialOrderCategory category: SpecialOrderCategory
name: string name: string
description: string description: string
neededDate: string | null
receiveTime: string
servings: number | null
cakeFlavor: string
filling: string
baloutSuggestedCombo: boolean
coating: string
designStyle: string
cakeText: string
decorations: string
deliveryType: SpecialOrderDeliveryType | null
deliveryTypeLabel: string | null
branchId: string | null
branchName: string
createdAt: string createdAt: string
images: Array<{ images: Array<{
id: string id: string
+52
View File
@@ -0,0 +1,52 @@
export const SPONGE_FLAVORS = [
'وانیلی',
'شکلاتی',
'ردولوت',
'نسکافه',
'سایر',
] as const
export const FILLINGS = [
'چیپس شکلات',
'گاناش',
'فندق',
'پسته',
'کارامل',
'توت‌فرنگی',
'موز',
'موز و گردو',
'گردو',
'سایر',
] as const
export const COATINGS = [
'خامه شکلاتی',
'خامه وانیلی',
'خامه نسکافه‌ای',
'فوندانت',
'روکش خاص',
'بدون روکش',
'انتخاب توسط قناد',
] as const
export const DESIGN_STYLES = [
'مینیمال',
'لوکس',
'کلاسیک',
'مدرن',
'رمانتیک',
'گل و طبیعت',
'فانتزی',
'کودک',
'ایرانی / سنتی',
'سازمانی',
'سایر',
] as const
export const OTHER_OPTION = 'سایر'
export function resolveSelectValue(selected: string, custom: string) {
if (!selected) return ''
if (selected === OTHER_OPTION) return custom.trim() || OTHER_OPTION
return selected
}
+180 -3
View File
@@ -15,6 +15,109 @@
gap: 20px; gap: 20px;
} }
.special-order-section {
display: flex;
flex-direction: column;
gap: 16px;
padding: 18px 16px;
border-radius: 16px;
border: 1px solid var(--color-gray-100);
background: rgba(255, 255, 255, 0.35);
}
.special-order-section__title {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--color-primary);
}
.special-order-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.special-order-hint {
margin: 0;
font-size: 0.85rem;
line-height: 1.8;
color: var(--color-secondary);
padding-inline: 4px;
}
.special-order-choice-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.special-order-choice {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--color-gray-100);
cursor: pointer;
transition: border-color 0.2s ease;
}
.special-order-choice input {
margin-top: 3px;
accent-color: var(--color-primary);
}
.special-order-choice span {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.special-order-choice strong {
font-size: 0.92rem;
font-weight: 500;
color: #000;
}
.special-order-choice em {
font-style: normal;
font-size: 0.82rem;
line-height: 1.6;
color: var(--color-secondary);
}
.special-order-choice.is-active {
border-color: var(--color-primary);
}
.persian-date-input {
width: 100%;
display: block;
}
.persian-date-input__field {
width: 100%;
border: 1px solid var(--color-gray-100);
border-radius: 12px;
background: transparent;
padding: 12px 14px;
font-family: inherit;
font-size: 15px;
color: #000;
outline: none;
cursor: pointer;
transition: border-color 0.2s ease;
direction: ltr;
text-align: center;
font-variant-numeric: tabular-nums;
}
.persian-date-input__field:focus {
border-color: var(--color-primary);
}
.special-order-form__alert { .special-order-form__alert {
padding: 12px 14px; padding: 12px 14px;
border-radius: 12px; border-radius: 12px;
@@ -31,6 +134,67 @@
color: #2f6b4f; color: #2f6b4f;
} }
.special-order-success {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 28px 24px;
border-radius: 16px;
border: 1px solid rgba(47, 107, 79, 0.18);
background: rgba(47, 107, 79, 0.06);
}
.special-order-success__icon {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #2f6b4f;
background: rgba(47, 107, 79, 0.12);
}
.special-order-success__title {
margin: 0;
font-size: clamp(20px, 3vw, 28px);
font-weight: 700;
color: #2f6b4f;
}
.special-order-success__text {
margin: 0;
font-size: 0.95rem;
line-height: 1.9;
color: var(--color-secondary);
text-align: justify;
}
.special-order-success__code {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid rgba(47, 107, 79, 0.16);
background: rgba(255, 255, 255, 0.7);
}
.special-order-success__code span {
font-size: 0.78rem;
color: var(--color-secondary);
}
.special-order-success__code strong {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: 0.04em;
color: #000;
font-variant-numeric: tabular-nums;
}
.special-order-field { .special-order-field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -47,7 +211,8 @@
} }
.special-order-field input, .special-order-field input,
.special-order-field textarea { .special-order-field textarea,
.special-order-field select {
width: 100%; width: 100%;
border: 1px solid var(--color-gray-100); border: 1px solid var(--color-gray-100);
border-radius: 12px; border-radius: 12px;
@@ -62,12 +227,20 @@
} }
.special-order-field input:focus, .special-order-field input:focus,
.special-order-field textarea:focus { .special-order-field textarea:focus,
.special-order-field select:focus {
border-color: var(--color-primary); border-color: var(--color-primary);
} }
.special-order-field input[type='time'] {
direction: ltr;
text-align: center;
font-variant-numeric: tabular-nums;
min-height: 46px;
}
.special-order-field--textarea textarea { .special-order-field--textarea textarea {
min-height: 160px; min-height: 120px;
} }
.special-order-radios { .special-order-radios {
@@ -309,6 +482,10 @@
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.special-order-row {
grid-template-columns: 1fr;
}
.special-order-gallery__grid { .special-order-gallery__grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
+412 -46
View File
@@ -2,11 +2,15 @@ import { useEffect, useState, type FormEvent } from 'react'
import { ChevronLeft, ImagePlus, LogIn, Sparkles, X } from 'lucide-react' import { ChevronLeft, ImagePlus, LogIn, Sparkles, X } from 'lucide-react'
import PageHeader from '../components/PageHeader' import PageHeader from '../components/PageHeader'
import OffersHours from '../components/OffersHours' import OffersHours from '../components/OffersHours'
import PersianDateInput from '../components/PersianDateInput'
import cakeIcon from '../assets/images/Cake.svg' import cakeIcon from '../assets/images/Cake.svg'
import { import {
createMySpecialOrder, createMySpecialOrder,
listBranches,
uploadMedia, uploadMedia,
type Branch,
type SpecialOrderCategory, type SpecialOrderCategory,
type SpecialOrderDeliveryType,
} from '../lib/api' } from '../lib/api'
import { import {
consumeAuthHashFromUrl, consumeAuthHashFromUrl,
@@ -15,6 +19,14 @@ import {
isAuthenticated, isAuthenticated,
type AuthUser, type AuthUser,
} from '../lib/auth' } from '../lib/auth'
import {
COATINGS,
DESIGN_STYLES,
FILLINGS,
OTHER_OPTION,
resolveSelectValue,
SPONGE_FLAVORS,
} from '../lib/specialOrderOptions'
import './SpecialOrderPage.css' import './SpecialOrderPage.css'
const CATEGORIES: { id: SpecialOrderCategory; label: string }[] = [ const CATEGORIES: { id: SpecialOrderCategory; label: string }[] = [
@@ -22,6 +34,11 @@ const CATEGORIES: { id: SpecialOrderCategory; label: string }[] = [
{ id: 'specialPastry', label: 'شیرینی ویژه' }, { id: 'specialPastry', label: 'شیرینی ویژه' },
] ]
const DELIVERY_OPTIONS: { id: SpecialOrderDeliveryType; label: string }[] = [
{ id: 'pickup', label: 'دریافت حضوری' },
{ id: 'shipping', label: 'ارسال با پیک' },
]
const SUBMIT_COOLDOWN_MS = 60_000 const SUBMIT_COOLDOWN_MS = 60_000
const COOLDOWN_STORAGE_KEY = 'balout.specialOrderCooldownUntil' const COOLDOWN_STORAGE_KEY = 'balout.specialOrderCooldownUntil'
@@ -31,8 +48,8 @@ const SIDE_NOTES = [
text: 'این فرم فقط برای ثبت درخواست است؛ هزینه بعداً با هماهنگی فروشگاه مشخص می‌شود.', text: 'این فرم فقط برای ثبت درخواست است؛ هزینه بعداً با هماهنگی فروشگاه مشخص می‌شود.',
}, },
{ {
title: 'کیک یا شیرینی سفارشی', title: 'جزئیات دقیق‌تر = ساخت بهتر',
text: 'ایده، تم تولد، تعداد و زمان تحویل را در توضیحات بنویسید تا دقیق‌تر راهنمایی شوید.', text: 'مناسبت، تاریخ، تعداد نفرات و طعم را کامل بنویسید تا تیم بلوط دقیق‌تر راهنمایی کند.',
}, },
{ {
title: 'تصاویر ایده', title: 'تصاویر ایده',
@@ -80,11 +97,29 @@ function cooldownSecondsLeft(until = readCooldownUntil()) {
export default function SpecialOrderPage() { export default function SpecialOrderPage() {
const [user, setUser] = useState<AuthUser | null>(() => getAuthUser()) const [user, setUser] = useState<AuthUser | null>(() => getAuthUser())
const [category, setCategory] = useState<SpecialOrderCategory>('specialCake') const [category, setCategory] = useState<SpecialOrderCategory>('specialCake')
const [name, setName] = useState('') const [occasion, setOccasion] = useState('')
const [description, setDescription] = useState('') const [neededDate, setNeededDate] = useState('')
const [receiveTime, setReceiveTime] = useState('')
const [servings, setServings] = useState('')
const [spongeFlavor, setSpongeFlavor] = useState('')
const [spongeFlavorCustom, setSpongeFlavorCustom] = useState('')
const [filling, setFilling] = useState('')
const [fillingCustom, setFillingCustom] = useState('')
const [coating, setCoating] = useState('')
const [designStyle, setDesignStyle] = useState('')
const [designStyleCustom, setDesignStyleCustom] = useState('')
const [cakeText, setCakeText] = useState('')
const [decorations, setDecorations] = useState('')
const [deliveryType, setDeliveryType] =
useState<SpecialOrderDeliveryType>('pickup')
const [branchId, setBranchId] = useState('')
const [branches, setBranches] = useState<Branch[]>([])
const [gallery, setGallery] = useState<string[]>([]) const [gallery, setGallery] = useState<string[]>([])
const [error, setError] = useState('') const [error, setError] = useState('')
const [success, setSuccess] = useState('') const [submittedOrder, setSubmittedOrder] = useState<{
id: string
code: string
} | null>(null)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [cooldownLeft, setCooldownLeft] = useState(() => cooldownSecondsLeft()) const [cooldownLeft, setCooldownLeft] = useState(() => cooldownSecondsLeft())
@@ -105,11 +140,50 @@ export default function SpecialOrderPage() {
}, [cooldownLeft]) }, [cooldownLeft])
const authenticated = Boolean(user) || isAuthenticated() const authenticated = Boolean(user) || isAuthenticated()
useEffect(() => {
if (!authenticated) {
setBranches([])
return
}
let cancelled = false
void listBranches()
.then((items) => {
if (!cancelled) setBranches(items)
})
.catch(() => {
if (!cancelled) setBranches([])
})
return () => {
cancelled = true
}
}, [authenticated])
const loginUrl = getCustomerLoginUrl( const loginUrl = getCustomerLoginUrl(
`${window.location.origin}/special-order`, `${window.location.origin}/special-order`,
) )
const submitDisabled = saving || cooldownLeft > 0 const submitDisabled = saving || cooldownLeft > 0
function resetForm() {
setCategory('specialCake')
setOccasion('')
setNeededDate('')
setReceiveTime('')
setServings('')
setSpongeFlavor('')
setSpongeFlavorCustom('')
setFilling('')
setFillingCustom('')
setCoating('')
setDesignStyle('')
setDesignStyleCustom('')
setCakeText('')
setDecorations('')
setDeliveryType('pickup')
setBranchId('')
setGallery([])
}
async function handleFiles(event: React.ChangeEvent<HTMLInputElement>) { async function handleFiles(event: React.ChangeEvent<HTMLInputElement>) {
const files = Array.from(event.target.files ?? []) const files = Array.from(event.target.files ?? [])
event.target.value = '' event.target.value = ''
@@ -121,7 +195,6 @@ export default function SpecialOrderPage() {
async function handleSubmit(event: FormEvent) { async function handleSubmit(event: FormEvent) {
event.preventDefault() event.preventDefault()
setError('') setError('')
setSuccess('')
if (!authenticated) { if (!authenticated) {
window.location.assign(loginUrl) window.location.assign(loginUrl)
@@ -131,10 +204,48 @@ export default function SpecialOrderPage() {
setError(`لطفاً ${cooldownLeft} ثانیه دیگر دوباره تلاش کنید`) setError(`لطفاً ${cooldownLeft} ثانیه دیگر دوباره تلاش کنید`)
return return
} }
if (!name.trim()) { if (!occasion.trim()) {
setError('نام سفارش الزامی است') setError('مناسبت سفارش الزامی است')
return return
} }
if (!neededDate) {
setError('تاریخ موردنیاز الزامی است')
return
}
if (!receiveTime.trim()) {
setError('ساعت دریافت الزامی است')
return
}
if (deliveryType === 'pickup' && !branchId) {
setError('برای دریافت حضوری، انتخاب شعبه الزامی است')
return
}
const servingsValue = servings.trim() ? Number(servings) : undefined
if (
servings.trim() &&
(!Number.isInteger(servingsValue) || (servingsValue ?? 0) < 1)
) {
setError('تعداد نفرات باید عدد صحیح و حداقل ۱ باشد')
return
}
if (spongeFlavor === OTHER_OPTION && !spongeFlavorCustom.trim()) {
setError('لطفاً طعم اسفنج سفارشی را بنویسید')
return
}
if (filling === OTHER_OPTION && !fillingCustom.trim()) {
setError('لطفاً فیلینگ / مغزی سفارشی را بنویسید')
return
}
if (designStyle === OTHER_OPTION && !designStyleCustom.trim()) {
setError('لطفاً سبک طراحی سفارشی را بنویسید')
return
}
const cakeFlavorValue = resolveSelectValue(spongeFlavor, spongeFlavorCustom)
const fillingValue = resolveSelectValue(filling, fillingCustom)
const designStyleValue = resolveSelectValue(designStyle, designStyleCustom)
setSaving(true) setSaving(true)
try { try {
@@ -145,20 +256,28 @@ export default function SpecialOrderPage() {
images.push({ url: uploaded.url, storageKey: uploaded.storageKey }) images.push({ url: uploaded.url, storageKey: uploaded.storageKey })
} }
await createMySpecialOrder({ const created = await createMySpecialOrder({
category, category,
name: name.trim(), name: occasion.trim(),
description: description.trim(), neededDate,
receiveTime: receiveTime.trim(),
servings: servingsValue,
cakeFlavor: cakeFlavorValue || undefined,
filling: fillingValue || undefined,
coating: coating || undefined,
designStyle: designStyleValue || undefined,
cakeText: cakeText.trim() || undefined,
decorations: decorations.trim() || undefined,
deliveryType,
branchId: branchId || undefined,
images, images,
}) })
setName('') resetForm()
setDescription('') setSubmittedOrder({
setGallery([]) id: created.id,
setCategory('specialCake') code: created.code || `BL-S-${created.number}`,
setSuccess( })
'سفارش ویژه شما ثبت شد. تیم بلوط به‌زودی برای هماهنگی با شما تماس می‌گیرد.',
)
const until = Date.now() + SUBMIT_COOLDOWN_MS const until = Date.now() + SUBMIT_COOLDOWN_MS
writeCooldownUntil(until) writeCooldownUntil(until)
setCooldownLeft(cooldownSecondsLeft(until)) setCooldownLeft(cooldownSecondsLeft(until))
@@ -189,7 +308,26 @@ export default function SpecialOrderPage() {
<div className="max-container special-order-page__body"> <div className="max-container special-order-page__body">
<div className="special-order-page__grid"> <div className="special-order-page__grid">
{authenticated ? ( {authenticated && submittedOrder ? (
<div className="special-order-success" role="status">
<div className="special-order-success__icon" aria-hidden>
<Sparkles size={28} strokeWidth={1.5} />
</div>
<h2 className="special-order-success__title">سفارش ویژه ثبت شد</h2>
<p className="special-order-success__text">
درخواست شما با موفقیت ثبت شد. تیم بلوط به‌زودی برای هماهنگی با شما
تماس می‌گیرد.
</p>
<div className="special-order-success__code">
<span>کد سفارش</span>
<strong dir="ltr">{submittedOrder.code}</strong>
</div>
<a href="/" className="btn-primary">
بازگشت به صفحه اصلی
<ChevronLeft size={14} />
</a>
</div>
) : authenticated ? (
<form <form
className="special-order-form" className="special-order-form"
onSubmit={(e) => void handleSubmit(e)} onSubmit={(e) => void handleSubmit(e)}
@@ -199,14 +337,6 @@ export default function SpecialOrderPage() {
{error} {error}
</div> </div>
) : null} ) : null}
{success ? (
<div
className="special-order-form__alert special-order-form__alert--ok"
role="status"
>
{success}
</div>
) : null}
<div className="special-order-field"> <div className="special-order-field">
<span>دسته‌بندی</span> <span>دسته‌بندی</span>
@@ -227,26 +357,262 @@ export default function SpecialOrderPage() {
</div> </div>
</div> </div>
<label className="special-order-field"> <section className="special-order-section">
<span>نام سفارش</span> <h2 className="special-order-section__title">۱. اطلاعات سفارش</h2>
<input <label className="special-order-field">
value={name} <span>مناسبت سفارش</span>
onChange={(e) => setName(e.target.value)} <input
placeholder="مثلاً کیک تولد با تم خاص" value={occasion}
disabled={saving} onChange={(e) => setOccasion(e.target.value)}
/> placeholder="مثلاً تولد، سالگرد، مراسم کاری"
</label> disabled={saving}
/>
</label>
<div className="special-order-row">
<label className="special-order-field">
<span>تاریخ موردنیاز (شمسی)</span>
<PersianDateInput
value={neededDate}
onChange={setNeededDate}
disabled={saving}
placeholder="انتخاب تاریخ"
/>
</label>
<label className="special-order-field">
<span>ساعت دریافت</span>
<input
type="time"
value={receiveTime}
onChange={(e) => setReceiveTime(e.target.value)}
disabled={saving}
/>
</label>
</div>
</section>
<label className="special-order-field special-order-field--textarea"> <section className="special-order-section">
<span>توضیحات</span> <h2 className="special-order-section__title">۲. اندازه کیک</h2>
<textarea <label className="special-order-field">
value={description} <span>تعداد نفرات</span>
onChange={(e) => setDescription(e.target.value)} <input
placeholder="جزئیات، سلیقه، تعداد، زمان تحویل و ..." type="number"
rows={6} min={1}
disabled={saving} max={500}
/> inputMode="numeric"
</label> value={servings}
onChange={(e) => setServings(e.target.value)}
placeholder="مثلاً ۱۲"
disabled={saving}
/>
</label>
</section>
<section className="special-order-section">
<h2 className="special-order-section__title">۳. طعم کیک</h2>
<label className="special-order-field">
<span>طعم اسفنج</span>
<select
value={spongeFlavor}
onChange={(e) => {
setSpongeFlavor(e.target.value)
if (e.target.value !== OTHER_OPTION) {
setSpongeFlavorCustom('')
}
}}
disabled={saving}
>
<option value="">انتخاب کنید</option>
{SPONGE_FLAVORS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
{spongeFlavor === OTHER_OPTION ? (
<label className="special-order-field">
<span>طعم اسفنج (سایر)</span>
<input
value={spongeFlavorCustom}
onChange={(e) => setSpongeFlavorCustom(e.target.value)}
placeholder="طعم موردنظر را بنویسید"
disabled={saving}
/>
</label>
) : null}
<label className="special-order-field">
<span>فیلینگ / مغزی</span>
<select
value={filling}
onChange={(e) => {
setFilling(e.target.value)
if (e.target.value !== OTHER_OPTION) {
setFillingCustom('')
}
}}
disabled={saving}
>
<option value="">انتخاب کنید</option>
{FILLINGS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
{filling === OTHER_OPTION ? (
<label className="special-order-field">
<span>فیلینگ / مغزی (سایر)</span>
<input
value={fillingCustom}
onChange={(e) => setFillingCustom(e.target.value)}
placeholder="فیلینگ موردنظر را بنویسید"
disabled={saving}
/>
</label>
) : null}
<label className="special-order-field">
<span>روکش کیک</span>
<select
value={coating}
onChange={(e) => setCoating(e.target.value)}
disabled={saving}
>
<option value="">انتخاب کنید</option>
{COATINGS.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
</section>
<section className="special-order-section">
<h2 className="special-order-section__title">۴. نوشته و تزئینات</h2>
<label className="special-order-field">
<span>متن روی کیک</span>
<input
value={cakeText}
onChange={(e) => setCakeText(e.target.value)}
placeholder="متنی که روی کیک نوشته شود"
disabled={saving}
/>
</label>
<label className="special-order-field special-order-field--textarea">
<span>تزئینات خاص در صورت نیاز</span>
<textarea
value={decorations}
onChange={(e) => setDecorations(e.target.value)}
placeholder="توضیح تم، رنگ، گل، فیگور و ..."
rows={4}
disabled={saving}
/>
</label>
</section>
<section className="special-order-section">
<h2 className="special-order-section__title">۵. سبک طراحی</h2>
<label className="special-order-field">
<span>سبک طراحی</span>
<select
value={designStyle}
onChange={(e) => {
setDesignStyle(e.target.value)
if (e.target.value !== OTHER_OPTION) {
setDesignStyleCustom('')
}
}}
disabled={saving}
>
<option value="">انتخاب کنید</option>
{DESIGN_STYLES.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
{designStyle === OTHER_OPTION ? (
<label className="special-order-field">
<span>سبک طراحی (سایر)</span>
<input
value={designStyleCustom}
onChange={(e) => setDesignStyleCustom(e.target.value)}
placeholder="سبک موردنظر را بنویسید"
disabled={saving}
/>
</label>
) : null}
</section>
<section className="special-order-section">
<h2 className="special-order-section__title">
۶. اطلاعات دریافت و تماس
</h2>
<div className="special-order-field">
<span>نحوه دریافت</span>
<div className="special-order-radios" role="radiogroup">
{DELIVERY_OPTIONS.map((item) => (
<label key={item.id} className="special-order-radio">
<input
type="radio"
name="special-order-delivery"
value={item.id}
checked={deliveryType === item.id}
onChange={() => {
setDeliveryType(item.id)
if (item.id === 'shipping') setBranchId('')
}}
disabled={saving}
/>
<span>{item.label}</span>
</label>
))}
</div>
</div>
{deliveryType === 'pickup' ? (
<div className="special-order-field">
<span>انتخاب شعبه</span>
{branches.length === 0 ? (
<p className="special-order-hint">شعبه‌ای ثبت نشده است.</p>
) : (
<div className="special-order-choice-list">
{branches.map((branch) => (
<label
key={branch.id}
className={`special-order-choice${
branchId === branch.id ? ' is-active' : ''
}`}
>
<input
type="radio"
name="special-order-branch"
checked={branchId === branch.id}
onChange={() => setBranchId(branch.id)}
disabled={saving}
/>
<span>
<strong>{branch.name}</strong>
<em>
{branch.district} — {branch.address}
</em>
</span>
</label>
))}
</div>
)}
</div>
) : (
<p className="special-order-hint">
پس از ثبت درخواست، برای هماهنگی آدرس و زمان ارسال با شما تماس
گرفته می‌شود.
</p>
)}
</section>
<div className="special-order-gallery"> <div className="special-order-gallery">
<div className="special-order-gallery__header"> <div className="special-order-gallery__header">