feat: cafe panel completed

This commit is contained in:
Afra ‌ 2026-06-21 11:17:05 +03:30
parent c2382b23f1
commit d02a96cce0
15 changed files with 706 additions and 20 deletions

View file

@ -4,6 +4,11 @@ import Home from './pages/Home'
import CafeDetail from './pages/CafeDetail'
import PaymentSuccess from './pages/PaymentSuccess'
import CafeOwnerRegister from './pages/CafeOwnerRegister'
import CafeRegisterSuccess from './pages/CafeRegisterSuccess'
import CafeOwnerPanel from './pages/CafeOwnerPanel'
import AddEvent from './pages/AddEvent'
import AddEventSuccess from './pages/AddEventSuccess'
import EventReservations from './pages/EventReservations'
function ScrollToTop() {
const { pathname } = useLocation()
@ -24,6 +29,11 @@ export default function App() {
<Route path="/cafe/:id" element={<CafeDetail />} />
<Route path="/payment/success" element={<PaymentSuccess />} />
<Route path="/owner/register" element={<CafeOwnerRegister />} />
<Route path="/owner/register/success" element={<CafeRegisterSuccess />} />
<Route path="/owner/panel" element={<CafeOwnerPanel />} />
<Route path="/owner/events/new" element={<AddEvent />} />
<Route path="/owner/events/success" element={<AddEventSuccess />} />
<Route path="/owner/events/:id/reservations" element={<EventReservations />} />
</Routes>
</BrowserRouter>
)

6
src/assets/book.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 16.7397V4.6697C22 3.4697 21.02 2.5797 19.83 2.6797H19.77C17.67 2.8597 14.48 3.9297 12.7 5.0497L12.53 5.1597C12.24 5.3397 11.76 5.3397 11.47 5.1597L11.22 5.0097C9.44 3.8997 6.26 2.8397 4.16 2.6697C2.97 2.5697 2 3.4697 2 4.6597V16.7397C2 17.6997 2.78 18.5997 3.74 18.7197L4.03 18.7597C6.2 19.0497 9.55 20.1497 11.47 21.1997L11.51 21.2197C11.78 21.3697 12.21 21.3697 12.47 21.2197C14.39 20.1597 17.75 19.0497 19.93 18.7597L20.26 18.7197C21.22 18.5997 22 17.6997 22 16.7397Z" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5.49023V20.4902" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.75 8.49023H5.5" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 11.4902H5.5" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1,013 B

12
src/assets/calendar.svg Normal file
View file

@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2V5" stroke="#4D4D4D" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 2V5" stroke="#4D4D4D" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.5 9.08984H20.5" stroke="#4D4D4D" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" stroke="#4D4D4D" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.6937 13.6992H15.7027" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.6937 16.6992H15.7027" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9945 13.6992H12.0035" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.9945 16.6992H12.0035" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.29529 13.6992H8.30427" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.29529 16.6992H8.30427" stroke="#4D4D4D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1207 12.7805C12.0507 12.7705 11.9607 12.7705 11.8807 12.7805C10.1207 12.7205 8.7207 11.2805 8.7207 9.51047C8.7207 7.70047 10.1807 6.23047 12.0007 6.23047C13.8107 6.23047 15.2807 7.70047 15.2807 9.51047C15.2707 11.2805 13.8807 12.7205 12.1207 12.7805Z" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.7398 19.3805C16.9598 21.0105 14.5998 22.0005 11.9998 22.0005C9.39977 22.0005 7.03977 21.0105 5.25977 19.3805C5.35977 18.4405 5.95977 17.5205 7.02977 16.8005C9.76977 14.9805 14.2498 14.9805 16.9698 16.8005C18.0398 17.5205 18.6398 18.4405 18.7398 19.3805Z" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1,015 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke="#666666" stroke-width="1.3835" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 22L20 20" stroke="#666666" stroke-width="1.3835" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

View file

@ -0,0 +1,14 @@
export default function SuccessIcon({ className = 'mx-auto h-28 w-28' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 119 119" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M87.6843 35.2269C86.0089 35.1574 84.1303 35.9432 82.6821 37.4859L49.8018 72.5197L36.1683 59.574C33.8655 57.3862 30.7441 56.96 29.1707 58.6174L28.278 59.556C26.7046 61.2137 27.2868 64.3147 29.5952 66.4964L46.7361 82.7002C48.7203 84.5114 52.1374 83.9803 54.4546 81.5114L89.6437 44.0199C91.961 41.5509 92.2732 38.1059 90.3456 36.2964C89.6222 35.6179 88.6899 35.2685 87.6843 35.2269Z"
fill="#57A327"
/>
<path
d="M59.5005 0C43.7202 0 28.586 6.26823 17.4275 17.4268C6.26893 28.5857 0 43.7192 0 59.5002C0 75.2805 6.26893 90.4147 17.4275 101.573C28.586 112.731 43.7202 119 59.5005 119C75.2808 119 90.4144 112.73 101.573 101.573C112.732 90.4137 119 75.2805 119 59.5002C119 43.7199 112.732 28.5857 101.573 17.4268C90.4144 6.26893 75.2808 0 59.5005 0ZM59.5005 9.34306C72.803 9.34306 85.5608 14.6275 94.9663 24.0337C104.372 33.4403 109.657 46.197 109.657 59.5002C109.657 72.8017 104.372 85.5593 94.9663 94.9656C85.5608 104.372 72.803 109.656 59.5005 109.656C46.1984 109.656 33.4407 104.372 24.0344 94.9656C14.6282 85.5593 9.34377 72.8017 9.34377 59.5002C9.34377 46.197 14.6282 33.4403 24.0344 24.0337C33.4407 14.6275 46.1984 9.34306 59.5005 9.34306Z"
fill="#57A327"
/>
</svg>
)
}

View file

@ -0,0 +1,11 @@
export interface CafeEvent {
id: number
matchLabel: string
remainingSeats: number
totalSeats: number
}
export const mockCafeEvents: CafeEvent[] = [
{ id: 1, matchLabel: 'بازی برزیل-مراکش (۳ روز دیگر-۲:۰۰)', remainingSeats: 8, totalSeats: 8 },
{ id: 2, matchLabel: 'بازی برزیل-مراکش (۳ روز دیگر-۲:۰۰)', remainingSeats: 0, totalSeats: 8 },
]

View file

@ -0,0 +1,13 @@
export interface Reservation {
id: number
code: string
count: number
name: string
phone: string
}
export const mockReservations: Reservation[] = [
{ id: 1, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' },
{ id: 2, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' },
{ id: 3, code: 'QC-۸۸۸۷', count: 3, name: 'وینسو وینسوزاده', phone: '۰۹۱۲۳۴۵۶۷۸۹' },
]

226
src/pages/AddEvent.tsx Normal file
View file

@ -0,0 +1,226 @@
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { formatPersianNumber } from '../utils/formatNumber'
const matchOptions = [
'برزیل-مراکش (۳ روز دیگر-۲:۰۰)',
'آرژانتین-فرانسه (۲ روز دیگر-۱۸:۰۰)',
'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)',
]
const ENTRY_FEE_STEP = 10000
const DESCRIPTION_MAX_LENGTH = 200
function toEnglishDigits(value: string) {
const persianDigits = '۰۱۲۳۴۵۶۷۸۹'
return value.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit)))
}
function ChevronIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M15.0898 4.07992L8.56984 10.5999C7.79984 11.3699 7.79984 12.6299 8.56984 13.3999L15.0898 19.9199" />
</svg>
)
}
function ChevronDownIcon({ className = '' }: { className?: string }) {
return (
<svg className={`h-5 w-5 shrink-0 text-gray-400 transition-transform ${className}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 9l6 6 6-6" />
</svg>
)
}
function MinusIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33398 8H12.6673" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
function PlusIcon({ className = '' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.75 2C7.94891 2 8.13968 2.07902 8.28033 2.21967C8.42098 2.36032 8.5 2.55109 8.5 2.75V7H12.75C12.9489 7 13.1397 7.07902 13.2803 7.21967C13.421 7.36032 13.5 7.55109 13.5 7.75C13.5 7.94891 13.421 8.13968 13.2803 8.28033C13.1397 8.42098 12.9489 8.5 12.75 8.5H8.5V12.75C8.5 12.9489 8.42098 13.1397 8.28033 13.2803C8.13968 13.421 7.94891 13.5 7.75 13.5C7.55109 13.5 7.36032 13.421 7.21967 13.2803C7.07902 13.1397 7 12.9489 7 12.75V8.5H2.75C2.55109 8.5 2.36032 8.42098 2.21967 8.28033C2.07902 8.13968 2 7.94891 2 7.75C2 7.55109 2.07902 7.36032 2.21967 7.21967C2.36032 7.07902 2.55109 7 2.75 7H7V2.75C7 2.55109 7.07902 2.36032 7.21967 2.21967C7.36032 2.07902 7.55109 2 7.75 2Z"
fill="currentColor"
/>
</svg>
)
}
function MatchSelect({ value, onChange }: { value: string; onChange: (value: string) => void }) {
const [open, setOpen] = useState(false)
return (
<div className="relative mt-5">
<label className="mb-3 block text-right text-lg font-normal text-gray-900">بازی</label>
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex h-14 w-full items-center justify-between rounded-2xl border border-gray-200 px-4 text-right"
>
<span className={value ? 'text-gray-900' : 'font-light text-[#999999]'}>{value || 'بازی مورد نظر را انتخاب کنید'}</span>
<ChevronDownIcon className={open ? 'rotate-180' : ''} />
</button>
{open && (
<>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute inset-x-0 top-full z-20 mt-2 overflow-hidden rounded-xl bg-white shadow-lg">
{matchOptions.map((option) => (
<button
key={option}
type="button"
onClick={() => {
onChange(option)
setOpen(false)
}}
className="block w-full border-b border-gray-100 px-4 py-3 text-right text-gray-900 last:border-b-0 hover:bg-gray-50"
>
{option}
</button>
))}
</div>
</>
)}
</div>
)
}
function Stepper({
value,
step,
min = 0,
suffix,
editable = false,
onChange,
}: {
value: number
step: number
min?: number
suffix: string
editable?: boolean
onChange: (next: number) => void
}) {
const atMin = value <= min
const measureRef = useRef<HTMLSpanElement>(null)
const [inputWidth, setInputWidth] = useState<number>(50)
useEffect(() => {
if (editable && measureRef.current) {
setInputWidth(measureRef.current.offsetWidth)
}
}, [editable, value])
const handleInputChange = (raw: string) => {
const digitsOnly = toEnglishDigits(raw).replace(/[^\d]/g, '')
onChange(digitsOnly === '' ? min : Math.max(min, Number(digitsOnly)))
}
return (
<div dir="ltr" className="flex h-14 w-full items-center justify-between rounded-2xl border border-gray-200 px-4 py-3">
<button
type="button"
onClick={() => onChange(Math.max(min, value - step))}
disabled={atMin}
className={`flex h-6 w-6 items-center justify-center ${atMin ? 'cursor-not-allowed text-[#999999]' : 'text-[#223D5D]'}`}
>
<MinusIcon className="h-4 w-4" />
</button>
{editable ? (
<div dir="rtl" className="flex flex-1 gap-2 items-center justify-center">
<span ref={measureRef} className="invisible absolute font-medium whitespace-pre">
{formatPersianNumber(value)}
</span>
<input
type="text"
inputMode="numeric"
value={formatPersianNumber(value)}
onChange={(e) => handleInputChange(e.target.value)}
style={inputWidth ? { width: `${inputWidth}px` } : undefined}
className="bg-transparent text-left font-medium text-gray-900 focus:outline-none"
/>
<span className="shrink-0 font-medium text-gray-900">{suffix}</span>
</div>
) : (
<span dir="rtl" className="text-center font-medium text-gray-900">
{formatPersianNumber(value)} {suffix}
</span>
)}
<button type="button" onClick={() => onChange(value + step)} className="flex h-6 w-6 items-center justify-center text-[#223D5D]">
<PlusIcon className="h-4 w-4" />
</button>
</div>
)
}
function DescriptionField({ value, onChange }: { value: string; onChange: (value: string) => void }) {
return (
<div className="mt-5">
<label className="mb-3 block text-right text-lg font-normal text-gray-900">توضیحات (اختیاری)</label>
<div className="relative rounded-2xl border border-gray-200">
<textarea
value={value}
onChange={(e) => onChange(e.target.value.slice(0, DESCRIPTION_MAX_LENGTH))}
placeholder="مثال: ورودی شامل نوشیدنی و پاپ‌کورن"
rows={5}
className="w-full resize-none rounded-2xl bg-transparent px-4 py-3 text-right placeholder:font-light placeholder:text-[#999999] focus:outline-none"
/>
<span dir="ltr" className="absolute left-4 top-3 text-sm text-[#999999]">
{formatPersianNumber(value.length)}/{formatPersianNumber(DESCRIPTION_MAX_LENGTH)}
</span>
</div>
</div>
)
}
export default function AddEvent() {
const navigate = useNavigate()
const [match, setMatch] = useState('')
const [capacity, setCapacity] = useState(1)
const [entryFee, setEntryFee] = useState(100000)
const [description, setDescription] = useState('')
return (
<div className="min-h-screen bg-white pb-24">
<div className="h-16 w-full bg-gray-200" />
<button type="button" onClick={() => navigate(-1)} className="mt-4 flex w-full items-center justify-start gap-2 px-4">
<ChevronIcon className="h-5 w-5 -scale-x-100 text-gray-900" />
<span className="text-lg font-bold text-gray-900">افزودن ایونت</span>
</button>
<div className="px-4">
<MatchSelect value={match} onChange={setMatch} />
<div className="mt-5">
<label className="mb-3 block text-right text-lg font-normal text-gray-900">ظرفیت</label>
<Stepper value={capacity} step={1} min={1} suffix="نفر" onChange={setCapacity} />
</div>
<div className="mt-5">
<label className="mb-3 block text-right text-lg font-normal text-gray-900">ورودی</label>
<Stepper value={entryFee} step={ENTRY_FEE_STEP} min={0} suffix="تومان" editable onChange={setEntryFee} />
</div>
<DescriptionField value={description} onChange={setDescription} />
</div>
<div className="fixed inset-x-0 bottom-0 flex justify-end bg-white px-4 py-4">
<button
type="button"
onClick={() => navigate('/owner/events/success')}
className="flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white"
>
ادامه
<ChevronIcon className="h-4 w-4" />
</button>
</div>
</div>
)
}

View file

@ -0,0 +1,34 @@
import { useNavigate } from 'react-router-dom'
import SuccessIcon from '../components/SuccessIcon'
export default function AddEventSuccess() {
const navigate = useNavigate()
return (
<div className="flex min-h-screen flex-col bg-white pb-32">
<div className="h-16 w-full bg-gray-200" />
<div className="flex flex-1 flex-col items-center justify-center px-6 text-center">
<SuccessIcon />
<h1 className="mt-6 text-xl font-bold text-gray-900">ایونت با موفقیت ثبت شد!</h1>
</div>
<div className="fixed inset-x-0 bottom-0 flex flex-col gap-3 bg-white px-4 py-4">
<button
type="button"
onClick={() => navigate('/owner/events/new')}
className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
افزودن ایونت
</button>
<button
type="button"
onClick={() => navigate('/owner/panel')}
className="w-full rounded-2xl border border-[#223D5D] py-4 text-lg font-semibold text-[#223D5D]"
>
مشاهده پنل
</button>
</div>
</div>
)
}

View file

@ -0,0 +1,110 @@
import { ReactNode } from 'react'
import { useNavigate } from 'react-router-dom'
import { mockCafeEvents } from '../data/mockCafeEvents'
function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M15.0898 4.07992L8.56984 10.5999C7.79984 11.3699 7.79984 12.6299 8.56984 13.3999L15.0898 19.9199" />
</svg>
)
}
function ProfileCircleIcon() {
return (
<svg className="h-5 w-5 text-[#4D4D4D]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M12.1207 12.7805C12.0507 12.7705 11.9607 12.7705 11.8807 12.7805C10.1207 12.7205 8.7207 11.2805 8.7207 9.51047C8.7207 7.70047 10.1807 6.23047 12.0007 6.23047C13.8107 6.23047 15.2807 7.70047 15.2807 9.51047C15.2707 11.2805 13.8807 12.7205 12.1207 12.7805Z" />
<path d="M18.7398 19.3805C16.9598 21.0105 14.5998 22.0005 11.9998 22.0005C9.39977 22.0005 7.03977 21.0105 5.25977 19.3805C5.35977 18.4405 5.95977 17.5205 7.02977 16.8005C9.76977 14.9805 14.2498 14.9805 16.9698 16.8005C18.0398 17.5205 18.6398 18.4405 18.7398 19.3805Z" />
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" />
</svg>
)
}
function BookIcon() {
return (
<svg className="h-5 w-5 text-[#4D4D4D]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M22 16.7397V4.6697C22 3.4697 21.02 2.5797 19.83 2.6797H19.77C17.67 2.8597 14.48 3.9297 12.7 5.0497L12.53 5.1597C12.24 5.3397 11.76 5.3397 11.47 5.1597L11.22 5.0097C9.44 3.8997 6.26 2.8397 4.16 2.6697C2.97 2.5697 2 3.4697 2 4.6597V16.7397C2 17.6997 2.78 18.5997 3.74 18.7197L4.03 18.7597C6.2 19.0497 9.55 20.1497 11.47 21.1997L11.51 21.2197C11.78 21.3697 12.21 21.3697 12.47 21.2197C14.39 20.1597 17.75 19.0497 19.93 18.7597L20.26 18.7197C21.22 18.5997 22 17.6997 22 16.7397Z" />
<path d="M12 5.49023V20.4902" />
<path d="M7.75 8.49023H5.5" />
<path d="M8.5 11.4902H5.5" />
</svg>
)
}
function CalendarIcon() {
return (
<svg className="h-5 w-5 text-[#4D4D4D]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
<path d="M8 2V5" />
<path d="M16 2V5" />
<path d="M3.5 9.08984H20.5" />
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" />
<path strokeWidth={2} d="M15.6937 13.6992H15.7027" />
<path strokeWidth={2} d="M15.6937 16.6992H15.7027" />
<path strokeWidth={2} d="M11.9945 13.6992H12.0035" />
<path strokeWidth={2} d="M11.9945 16.6992H12.0035" />
<path strokeWidth={2} d="M8.29529 13.6992H8.30427" />
<path strokeWidth={2} d="M8.29529 16.6992H8.30427" />
</svg>
)
}
function PanelRow({ icon, label, onClick }: { icon: ReactNode; label: string; onClick?: () => void }) {
return (
<button type="button" onClick={onClick} className="flex w-full items-center justify-between py-4">
<div className="flex items-center gap-2">
{icon}
<span className="text-gray-900">{label}</span>
</div>
<ChevronIcon />
</button>
)
}
export default function CafeOwnerPanel() {
const navigate = useNavigate()
return (
<div className="min-h-screen bg-white pb-20">
<div className="h-16 w-full bg-gray-200" />
<div className="px-4">
<h1 className="mt-4 text-right text-lg font-bold text-gray-900">پنل کافه</h1>
<div className="mt-2 divide-y divide-gray-100">
<PanelRow
icon={<ProfileCircleIcon />}
label="اطلاعات کافه"
onClick={() => navigate('/owner/register', { state: { step: 2 } })}
/>
<PanelRow icon={<BookIcon />} label="افزودن رویداد" onClick={() => navigate('/owner/events/new')} />
</div>
<h2 className="mt-6 text-right font-semibold text-gray-900">رویدادهای فعال شما</h2>
<div className="mt-3 flex flex-col gap-6">
{mockCafeEvents.map((event) => (
<div key={event.id} className="rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F]">
<div className="pt-4">
<p className="text-right text-gray-900 pb-3">{event.matchLabel}</p>
<span className="mt-2 inline-block rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700">
{event.remainingSeats} صندلی از {event.totalSeats} باقی مانده
</span>
</div>
<div>
<PanelRow
icon={<CalendarIcon />}
label="مشاهده رزروها"
onClick={() => navigate(`/owner/events/${event.id}/reservations`, { state: { event } })}
/>
</div>
</div>
))}
</div>
</div>
<button type="button" className="fixed bottom-4 right-4 rounded-full bg-[#223D5D] px-5 py-3 text-white shadow-lg">
پشتیبانی
</button>
</div>
)
}

View file

@ -1,4 +1,5 @@
import { ReactNode, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import InfoIcon from "../components/InfoIcon";
import { mockCafeFeatures } from "../data/mockCafeFeatures";
@ -153,7 +154,10 @@ function FeatureChip({
}
export default function CafeOwnerRegister() {
const [step, setStep] = useState<1 | 2>(1);
const navigate = useNavigate();
const location = useLocation();
const initialStep = (location.state as { step?: 1 | 2 } | null)?.step ?? 1;
const [step, setStep] = useState<1 | 2>(initialStep);
const [fullName, setFullName] = useState("");
const [phone, setPhone] = useState("");
@ -187,9 +191,9 @@ export default function CafeOwnerRegister() {
<p className="text-right text-md leading-relaxed text-gray-600">
لطفاً یک{" "}
<span className="font-semibold text-gray-900">نماینده</span>{" "}
برای کافه معرفی کنید. نماینده باید در اپلیکیشن ثبت‌نام کند و
پرداختها و واریز به کیف پول یا واریز می‌شود. شماره موبایل، کد
ملی و کارت بانکی باید متعلق به یک نفر باشند.
برای کافه معرفی کنید. نماینده باید در اپلیکیشن
ثبت‌نام کند و پرداخت‌ها به کیف پول او واریز می‌شود. شماره
موبایل، کد ملی و کارت بانکی باید متعلق به یک نفر باشند.
</p>
</div>
@ -270,7 +274,9 @@ export default function CafeOwnerRegister() {
</button>
<button
type="button"
onClick={() => step === 1 && setStep(2)}
onClick={() =>
step === 1 ? setStep(2) : navigate("/owner/register/success")
}
className="flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white"
>
ادامه

View file

@ -0,0 +1,38 @@
import { useNavigate } from 'react-router-dom'
import SuccessIcon from '../components/SuccessIcon'
export default function CafeRegisterSuccess() {
const navigate = useNavigate()
return (
<div className="flex min-h-screen flex-col bg-white pb-32">
<div className="h-16 w-full bg-gray-200" />
<div className="flex flex-1 flex-col items-center justify-center px-6 text-center">
<SuccessIcon />
<h1 className="mt-6 text-xl font-bold text-gray-900">کافه شما با موفقیت ثبت شد!</h1>
<p className="mt-3 leading-relaxed text-gray-500">
با اضافه کردن ایونت بازی‌ها، کافه شما در کمپین نمایش داده می‌شود و کاربران می‌توانند برای هر مسابقه رزرو ثبت
کنند.
</p>
</div>
<div className="fixed inset-x-0 bottom-0 flex flex-col gap-3 bg-white px-4 py-4">
<button
type="button"
onClick={() => navigate('/owner/events/new')}
className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
افزودن ایونت
</button>
<button
type="button"
onClick={() => navigate('/owner/panel')}
className="w-full rounded-2xl border border-[#223D5D] py-4 text-lg font-semibold text-[#223D5D]"
>
مشاهده پنل
</button>
</div>
</div>
)
}

View file

@ -0,0 +1,211 @@
import { useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { CafeEvent, mockCafeEvents } from "../data/mockCafeEvents";
import { mockReservations, Reservation } from "../data/mockReservations";
import { formatPersianNumber } from "../utils/formatNumber";
function ChevronIcon({ className = "" }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15.0898 4.07992L8.56984 10.5999C7.79984 11.3699 7.79984 12.6299 8.56984 13.3999L15.0898 19.9199" />
</svg>
);
}
function SearchIcon({ className = "" }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.3835}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" />
<path d="M22 22L20 20" />
</svg>
);
}
function CheckIcon() {
return (
<svg
className="h-3 w-3 text-white"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2.5 6.5L4.5 8.5L9.5 3.5" />
</svg>
);
}
function toEnglishDigits(value: string) {
const persianDigits = "۰۱۲۳۴۵۶۷۸۹";
return value.replace(/[۰-۹]/g, (digit) =>
String(persianDigits.indexOf(digit)),
);
}
function reservationMatches(reservation: Reservation, query: string) {
if (!query) return true;
const haystack = toEnglishDigits(
`${reservation.name} ${reservation.phone} ${reservation.count} ${formatPersianNumber(reservation.count)} ${reservation.code}`,
).toLowerCase();
return haystack.includes(toEnglishDigits(query).toLowerCase());
}
function downloadReservationsCsv(reservations: Reservation[]) {
const header = ["کد رزرو", "تعداد", "نام", "شماره تماس"];
const rows = reservations.map((r) => [
r.code,
String(r.count),
r.name,
toEnglishDigits(r.phone),
]);
const csvContent = [header, ...rows]
.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(","))
.join("\n");
const blob = new Blob(["" + csvContent], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "reservations.csv";
link.click();
URL.revokeObjectURL(url);
}
function ReservationCard({
reservation,
selected,
onToggle,
}: {
reservation: Reservation;
selected: boolean;
onToggle: () => void;
}) {
return (
<div className="flex items-center justify-between rounded-2xl shadow-[0px_4px_12px_0px_#0000001F] p-6">
<div className="flex flex-col items-start gap-2 text-right">
<div className="flex items-center gap-2">
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{reservation.code}
</span>
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{formatPersianNumber(reservation.count)} نفر
</span>
</div>
<p className="text-gray-900">{reservation.name}</p>
<p className="text-gray-500">{reservation.phone}</p>
</div>
<button
type="button"
onClick={onToggle}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 border-[#223D5D] ${selected ? "bg-[#223D5D]" : "bg-transparent"}`}
>
{selected && <CheckIcon />}
</button>
</div>
);
}
export default function EventReservations() {
const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const [query, setQuery] = useState("");
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const event: CafeEvent | undefined =
(location.state as { event?: CafeEvent } | null)?.event ??
mockCafeEvents.find((e) => e.id === Number(id));
const filteredReservations = useMemo(
() =>
mockReservations.filter((reservation) =>
reservationMatches(reservation, query),
),
[query],
);
const toggleSelected = (reservationId: number) => {
setSelectedIds((prev) =>
prev.includes(reservationId)
? prev.filter((id) => id !== reservationId)
: [...prev, reservationId],
);
};
const handleDownload = () => {
const selectedReservations =
selectedIds.length > 0
? mockReservations.filter((r) => selectedIds.includes(r.id))
: mockReservations;
downloadReservationsCsv(selectedReservations);
};
return (
<div className="min-h-screen bg-white pb-24">
<div className="h-16 w-full bg-gray-200" />
<button
type="button"
onClick={() => navigate(-1)}
className="mt-4 flex w-full items-center justify-start gap-2 px-4"
>
<ChevronIcon className="h-5 w-5 -scale-x-100 text-gray-900" />
<span className="text-md font-semibold text-gray-900">
رزروهای {event?.matchLabel ?? ""}
</span>
</button>
<div className="relative mt-4 px-4">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="جستجو"
className="w-full rounded-2xl shadow-[0px_4px_12px_0px_#0000001F] bg-white py-3 pr-6 pl-4 text-right placeholder:font-light placeholder:text-[#999999]"
/>
<SearchIcon className="pointer-events-none absolute left-8 top-1/2 h-5 w-5 -translate-y-1/2 text-[#666666]" />
</div>
<div className="mt-4 flex flex-col gap-3 px-4">
{filteredReservations.map((reservation) => (
<ReservationCard
key={reservation.id}
reservation={reservation}
selected={selectedIds.includes(reservation.id)}
onToggle={() => toggleSelected(reservation.id)}
/>
))}
</div>
<div className="fixed inset-x-0 bottom-0 bg-white px-4 py-4">
<button
type="button"
onClick={handleDownload}
className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
دانلود لیست
</button>
</div>
</div>
);
}

View file

@ -1,20 +1,6 @@
import { useNavigate } from 'react-router-dom'
import InfoIcon from '../components/InfoIcon'
function SuccessIcon() {
return (
<svg className="mx-auto h-28 w-28" viewBox="0 0 119 119" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M87.6843 35.2269C86.0089 35.1574 84.1303 35.9432 82.6821 37.4859L49.8018 72.5197L36.1683 59.574C33.8655 57.3862 30.7441 56.96 29.1707 58.6174L28.278 59.556C26.7046 61.2137 27.2868 64.3147 29.5952 66.4964L46.7361 82.7002C48.7203 84.5114 52.1374 83.9803 54.4546 81.5114L89.6437 44.0199C91.961 41.5509 92.2732 38.1059 90.3456 36.2964C89.6222 35.6179 88.6899 35.2685 87.6843 35.2269Z"
fill="#57A327"
/>
<path
d="M59.5005 0C43.7202 0 28.586 6.26823 17.4275 17.4268C6.26893 28.5857 0 43.7192 0 59.5002C0 75.2805 6.26893 90.4147 17.4275 101.573C28.586 112.731 43.7202 119 59.5005 119C75.2808 119 90.4144 112.73 101.573 101.573C112.732 90.4137 119 75.2805 119 59.5002C119 43.7199 112.732 28.5857 101.573 17.4268C90.4144 6.26893 75.2808 0 59.5005 0ZM59.5005 9.34306C72.803 9.34306 85.5608 14.6275 94.9663 24.0337C104.372 33.4403 109.657 46.197 109.657 59.5002C109.657 72.8017 104.372 85.5593 94.9663 94.9656C85.5608 104.372 72.803 109.656 59.5005 109.656C46.1984 109.656 33.4407 104.372 24.0344 94.9656C14.6282 85.5593 9.34377 72.8017 9.34377 59.5002C9.34377 46.197 14.6282 33.4403 24.0344 24.0337C33.4407 14.6275 46.1984 9.34306 59.5005 9.34306Z"
fill="#57A327"
/>
</svg>
)
}
import SuccessIcon from '../components/SuccessIcon'
function DetailRow({ label, value }: { label: string; value: string }) {
return (