From d02a96cce019e903e17d92959cbdfa48aa0a6c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sun, 21 Jun 2026 11:17:05 +0330 Subject: [PATCH] feat: cafe panel completed --- src/App.tsx | 10 ++ src/assets/book.svg | 6 + src/assets/calendar.svg | 12 ++ src/assets/profile-circle.svg | 5 + src/assets/search-normal.svg | 4 + src/components/SuccessIcon.tsx | 14 ++ src/data/mockCafeEvents.ts | 11 ++ src/data/mockReservations.ts | 13 ++ src/pages/AddEvent.tsx | 226 ++++++++++++++++++++++++++++++ src/pages/AddEventSuccess.tsx | 34 +++++ src/pages/CafeOwnerPanel.tsx | 110 +++++++++++++++ src/pages/CafeOwnerRegister.tsx | 16 ++- src/pages/CafeRegisterSuccess.tsx | 38 +++++ src/pages/EventReservations.tsx | 211 ++++++++++++++++++++++++++++ src/pages/PaymentSuccess.tsx | 16 +-- 15 files changed, 706 insertions(+), 20 deletions(-) create mode 100644 src/assets/book.svg create mode 100644 src/assets/calendar.svg create mode 100644 src/assets/profile-circle.svg create mode 100644 src/assets/search-normal.svg create mode 100644 src/components/SuccessIcon.tsx create mode 100644 src/data/mockCafeEvents.ts create mode 100644 src/data/mockReservations.ts create mode 100644 src/pages/AddEvent.tsx create mode 100644 src/pages/AddEventSuccess.tsx create mode 100644 src/pages/CafeOwnerPanel.tsx create mode 100644 src/pages/CafeRegisterSuccess.tsx create mode 100644 src/pages/EventReservations.tsx diff --git a/src/App.tsx b/src/App.tsx index 48f2aaa..7429c85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> ) diff --git a/src/assets/book.svg b/src/assets/book.svg new file mode 100644 index 0000000..a95cb42 --- /dev/null +++ b/src/assets/book.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/calendar.svg b/src/assets/calendar.svg new file mode 100644 index 0000000..ef1c748 --- /dev/null +++ b/src/assets/calendar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/profile-circle.svg b/src/assets/profile-circle.svg new file mode 100644 index 0000000..8f1f86a --- /dev/null +++ b/src/assets/profile-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/search-normal.svg b/src/assets/search-normal.svg new file mode 100644 index 0000000..c0c4ec3 --- /dev/null +++ b/src/assets/search-normal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/SuccessIcon.tsx b/src/components/SuccessIcon.tsx new file mode 100644 index 0000000..98fecf7 --- /dev/null +++ b/src/components/SuccessIcon.tsx @@ -0,0 +1,14 @@ +export default function SuccessIcon({ className = 'mx-auto h-28 w-28' }: { className?: string }) { + return ( + + + + + ) +} diff --git a/src/data/mockCafeEvents.ts b/src/data/mockCafeEvents.ts new file mode 100644 index 0000000..62862c7 --- /dev/null +++ b/src/data/mockCafeEvents.ts @@ -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 }, +] diff --git a/src/data/mockReservations.ts b/src/data/mockReservations.ts new file mode 100644 index 0000000..6e17337 --- /dev/null +++ b/src/data/mockReservations.ts @@ -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: '۰۹۱۲۳۴۵۶۷۸۹' }, +] diff --git a/src/pages/AddEvent.tsx b/src/pages/AddEvent.tsx new file mode 100644 index 0000000..d7e5c8e --- /dev/null +++ b/src/pages/AddEvent.tsx @@ -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 ( + + + + ) +} + +function ChevronDownIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function MinusIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function PlusIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + +function MatchSelect({ value, onChange }: { value: string; onChange: (value: string) => void }) { + const [open, setOpen] = useState(false) + + return ( +
+ + + + {open && ( + <> +
setOpen(false)} /> +
+ {matchOptions.map((option) => ( + + ))} +
+ + )} +
+ ) +} + +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(null) + const [inputWidth, setInputWidth] = useState(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 ( +
+ + + {editable ? ( +
+ + {formatPersianNumber(value)} + + handleInputChange(e.target.value)} + style={inputWidth ? { width: `${inputWidth}px` } : undefined} + className="bg-transparent text-left font-medium text-gray-900 focus:outline-none" + /> + {suffix} +
+ ) : ( + + {formatPersianNumber(value)} {suffix} + + )} + + +
+ ) +} + +function DescriptionField({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ( +
+ +
+