perf: modified the reservation page
This commit is contained in:
parent
92cf39f723
commit
3b6d973846
14 changed files with 233 additions and 29 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
VITE_AUTH_BASE_URL=https://accounts-staging.gooyal.ir
|
VITE_AUTH_BASE_URL=https://accounts-staging.gooyal.ir
|
||||||
VITE_AUTH_CLIENT_ID=
|
VITE_AUTH_CLIENT_ID=
|
||||||
VITE_AUTH_CLIENT_SECRET=
|
VITE_AUTH_CLIENT_SECRET=
|
||||||
|
VITE_APP_BASE_URL=
|
||||||
|
|
|
||||||
1
.env.staging
Normal file
1
.env.staging
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_APP_BASE_URL=https://reservation-staging.winsoo.ir/
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:staging": "vite build --mode staging",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
|
|
|
||||||
22
src/App.tsx
22
src/App.tsx
|
|
@ -9,8 +9,10 @@ import CafeOwnerPanel from './pages/CafeOwnerPanel'
|
||||||
import AddEvent from './pages/AddEvent'
|
import AddEvent from './pages/AddEvent'
|
||||||
import AddEventSuccess from './pages/AddEventSuccess'
|
import AddEventSuccess from './pages/AddEventSuccess'
|
||||||
import EventReservations from './pages/EventReservations'
|
import EventReservations from './pages/EventReservations'
|
||||||
|
import MyReservations from './pages/MyReservations'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import LoginOtp from './pages/LoginOtp'
|
import LoginOtp from './pages/LoginOtp'
|
||||||
|
import RequireRole from './components/RequireRole'
|
||||||
|
|
||||||
function ScrollToTop() {
|
function ScrollToTop() {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
|
|
@ -27,16 +29,18 @@ export default function App() {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<RequireRole role="customer"><Home /></RequireRole>} />
|
||||||
<Route path="/cafe/:id" element={<CafeDetail />} />
|
<Route path="/cafe/:id" element={<RequireRole role="customer"><CafeDetail /></RequireRole>} />
|
||||||
<Route path="/payment/success" element={<PaymentSuccess />} />
|
<Route path="/payment/success" element={<RequireRole role="customer"><PaymentSuccess /></RequireRole>} />
|
||||||
<Route path="/owner/register" element={<CafeOwnerRegister />} />
|
<Route path="/reservations" element={<RequireRole role="customer"><MyReservations /></RequireRole>} />
|
||||||
<Route path="/owner/register/success" element={<CafeRegisterSuccess />} />
|
<Route path="/owner/register" element={<RequireRole role="cafe_owner"><CafeOwnerRegister /></RequireRole>} />
|
||||||
<Route path="/owner/panel" element={<CafeOwnerPanel />} />
|
<Route path="/owner/register/success" element={<RequireRole role="cafe_owner"><CafeRegisterSuccess /></RequireRole>} />
|
||||||
<Route path="/owner/events/new" element={<AddEvent />} />
|
<Route path="/owner/panel" element={<RequireRole role="cafe_owner"><CafeOwnerPanel /></RequireRole>} />
|
||||||
<Route path="/owner/events/success" element={<AddEventSuccess />} />
|
<Route path="/owner/events/new" element={<RequireRole role="cafe_owner"><AddEvent /></RequireRole>} />
|
||||||
<Route path="/owner/events/:id/reservations" element={<EventReservations />} />
|
<Route path="/owner/events/success" element={<RequireRole role="cafe_owner"><AddEventSuccess /></RequireRole>} />
|
||||||
|
<Route path="/owner/events/:id/reservations" element={<RequireRole role="cafe_owner"><EventReservations /></RequireRole>} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/cafe/login" element={<Login />} />
|
||||||
<Route path="/login/otp" element={<LoginOtp />} />
|
<Route path="/login/otp" element={<LoginOtp />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
10
src/components/BookIcon.tsx
Normal file
10
src/components/BookIcon.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/components/RequireRole.tsx
Normal file
11
src/components/RequireRole.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
|
import { getUserRole, UserRole } from '../utils/role'
|
||||||
|
|
||||||
|
export default function RequireRole({ role, children }: { role: UserRole; children: ReactNode }) {
|
||||||
|
const userRole = getUserRole()
|
||||||
|
if (userRole !== role) {
|
||||||
|
return <Navigate to={userRole === 'cafe_owner' ? '/cafe/login' : '/login'} replace />
|
||||||
|
}
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
33
src/data/mockMyReservations.ts
Normal file
33
src/data/mockMyReservations.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export interface MyReservation {
|
||||||
|
id: number
|
||||||
|
matchLabel: string
|
||||||
|
dateTimeLabel: string
|
||||||
|
count: number
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
cafeName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mockMyReservations: MyReservation[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
matchLabel: 'برزیل-مراکش',
|
||||||
|
dateTimeLabel: 'سهشنبه ۲۰ خرداد - ساعت ۰۰:۳۰',
|
||||||
|
count: 3,
|
||||||
|
code: 'QC-۸۸۸۷',
|
||||||
|
name: 'وینسو وینسوزاده',
|
||||||
|
phone: '۰۹۱۲۳۴۵۶۷۸۹',
|
||||||
|
cafeName: 'بیبای',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
matchLabel: 'برزیل-مراکش',
|
||||||
|
dateTimeLabel: 'سهشنبه ۲۰ خرداد - ساعت ۰۰:۳۰',
|
||||||
|
count: 3,
|
||||||
|
code: 'QC-۸۸۸۷',
|
||||||
|
name: 'وینسو وینسوزاده',
|
||||||
|
phone: '۰۹۱۲۳۴۵۶۷۸۹',
|
||||||
|
cafeName: 'بیبای',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import BookIcon from '../components/BookIcon'
|
||||||
import { mockCafeEvents } from '../data/mockCafeEvents'
|
import { mockCafeEvents } from '../data/mockCafeEvents'
|
||||||
|
|
||||||
function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) {
|
function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) {
|
||||||
|
|
@ -20,17 +21,6 @@ function ProfileCircleIcon() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
function CalendarIcon() {
|
||||||
return (
|
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">
|
<svg className="h-5 w-5 text-[#4D4D4D]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ export default function CafeRegisterSuccess() {
|
||||||
<SuccessIcon />
|
<SuccessIcon />
|
||||||
<h1 className="mt-6 text-xl font-bold text-gray-900">کافه شما با موفقیت ثبت شد!</h1>
|
<h1 className="mt-6 text-xl font-bold text-gray-900">کافه شما با موفقیت ثبت شد!</h1>
|
||||||
<p className="mt-3 leading-relaxed text-gray-500">
|
<p className="mt-3 leading-relaxed text-gray-500">
|
||||||
با اضافه کردن ایونت بازیها، کافه شما در کمپین نمایش داده میشود و کاربران میتوانند برای هر مسابقه رزرو ثبت
|
با اضافه کردن ایونت بازیها، کافه شما در کمپین نمایش داده میشود و کاربران میتوانند برای تماشای هر مسابقه در کافه شما، میز رزرو کنند.
|
||||||
کنند.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import BookIcon from '../components/BookIcon'
|
||||||
import { mockCafes, Cafe } from '../data/mockCafes'
|
import { mockCafes, Cafe } from '../data/mockCafes'
|
||||||
|
|
||||||
const cityOptions = ['تهران', 'اصفهان', 'مشهد']
|
const cityOptions = ['تهران', 'اصفهان', 'مشهد']
|
||||||
|
|
@ -9,6 +10,14 @@ const matchOptions = [
|
||||||
'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)',
|
'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 }) {
|
function ChevronDownIcon({ className = '' }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -109,6 +118,24 @@ function SelectorRow({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MyReservationsRow() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate('/reservations')}
|
||||||
|
className="flex w-full items-center justify-between px-4 py-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookIcon />
|
||||||
|
<span className="text-gray-900">رزروهای شما</span>
|
||||||
|
</div>
|
||||||
|
<ChevronIcon className="h-5 w-5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function CafeCard({ cafe }: { cafe: Cafe }) {
|
function CafeCard({ cafe }: { cafe: Cafe }) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
@ -136,6 +163,8 @@ export default function Home() {
|
||||||
<div className="min-h-screen bg-white pb-24">
|
<div className="min-h-screen bg-white pb-24">
|
||||||
<MapPlaceholder />
|
<MapPlaceholder />
|
||||||
|
|
||||||
|
<MyReservationsRow />
|
||||||
|
|
||||||
<SelectorRow label="شهر" value={city} options={cityOptions} onChange={setCity} />
|
<SelectorRow label="شهر" value={city} options={cityOptions} onChange={setCity} />
|
||||||
<SelectorRow label="بازی انتخابی" value={match} options={matchOptions} onChange={setMatch} />
|
<SelectorRow label="بازی انتخابی" value={match} options={matchOptions} onChange={setMatch} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { requestOtp } from "../services/authApi";
|
import { requestOtp } from "../services/authApi";
|
||||||
import { toPersianDigits } from "../utils/formatNumber";
|
import { toPersianDigits } from "../utils/formatNumber";
|
||||||
import { toE164 } from "../utils/phone";
|
import { toE164 } from "../utils/phone";
|
||||||
|
import { setUserRole } from "../utils/role";
|
||||||
|
|
||||||
function CheckIcon() {
|
function CheckIcon() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -24,6 +25,11 @@ export default function Login() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const initialPhone = (location.state as { phone?: string } | null)?.phone ?? "";
|
const initialPhone = (location.state as { phone?: string } | null)?.phone ?? "";
|
||||||
|
const isOwnerFlow = location.pathname.startsWith("/cafe/login");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUserRole(isOwnerFlow ? "cafe_owner" : "customer");
|
||||||
|
}, [isOwnerFlow]);
|
||||||
|
|
||||||
const [phone, setPhone] = useState(initialPhone);
|
const [phone, setPhone] = useState(initialPhone);
|
||||||
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
||||||
|
|
@ -89,7 +95,7 @@ export default function Login() {
|
||||||
{acceptedTerms && <CheckIcon />}
|
{acceptedTerms && <CheckIcon />}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
<a href="#" className="text-[#223D5D] underline">
|
<a href="https://winsoo.ir/terms/" className="text-[#223D5D] underline">
|
||||||
قوانین و شرایط
|
قوانین و شرایط
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
را میپذیرم.
|
را میپذیرم.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export default function LoginOtp() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const state = location.state as { phone?: string; ttl?: number } | null
|
const state = location.state as { phone?: string; ttl?: number } | null
|
||||||
const phone = state?.phone ?? ''
|
const phone = state?.phone ?? ''
|
||||||
|
const loginPath = getUserRole() === 'cafe_owner' ? '/cafe/login' : '/login'
|
||||||
|
|
||||||
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''))
|
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''))
|
||||||
const [secondsLeft, setSecondsLeft] = useState(Math.round(state?.ttl ?? DEFAULT_RESEND_SECONDS))
|
const [secondsLeft, setSecondsLeft] = useState(Math.round(state?.ttl ?? DEFAULT_RESEND_SECONDS))
|
||||||
|
|
@ -44,9 +45,9 @@ export default function LoginOtp() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!phone) {
|
if (!phone) {
|
||||||
navigate('/login', { replace: true })
|
navigate(loginPath, { replace: true })
|
||||||
}
|
}
|
||||||
}, [phone, navigate])
|
}, [phone, loginPath, navigate])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
|
|
@ -118,7 +119,7 @@ export default function LoginOtp() {
|
||||||
<p className="text-right text-gray-900">کد ارسال شده را وارد کنید:</p>
|
<p className="text-right text-gray-900">کد ارسال شده را وارد کنید:</p>
|
||||||
<div className="mt-2 flex items-center justify-start gap-2">
|
<div className="mt-2 flex items-center justify-start gap-2">
|
||||||
<span className="text-[#223D5D]">{phone}</span>
|
<span className="text-[#223D5D]">{phone}</span>
|
||||||
<button type="button" onClick={() => navigate('/login', { state: { phone } })} aria-label="ویرایش شماره تماس">
|
<button type="button" onClick={() => navigate(loginPath, { state: { phone } })} aria-label="ویرایش شماره تماس">
|
||||||
<EditIcon className="h-5 w-5 text-[#223D5D]" />
|
<EditIcon className="h-5 w-5 text-[#223D5D]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
115
src/pages/MyReservations.tsx
Normal file
115
src/pages/MyReservations.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import InfoIcon from '../components/InfoIcon'
|
||||||
|
import SuccessIcon from '../components/SuccessIcon'
|
||||||
|
import { mockMyReservations, MyReservation } from '../data/mockMyReservations'
|
||||||
|
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 MyReservationCard({ reservation, onClick }: { reservation: MyReservation; onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className="flex w-full flex-col items-start gap-3 rounded-2xl p-6 text-right shadow-[0px_4px_12px_0px_#0000001F]"
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-xl text-gray-900">{reservation.matchLabel}</h3>
|
||||||
|
<p className="text-lg text-gray-500">{reservation.dateTimeLabel}</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-normal rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
|
||||||
|
{formatPersianNumber(reservation.count)} نفر
|
||||||
|
</span>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-3">
|
||||||
|
<span className="text-gray-500">{label}</span>
|
||||||
|
<span className="text-gray-900">{value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReservationDetailModal({ reservation, onClose }: { reservation: MyReservation; onClose: () => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
const previousOverflow = document.body.style.overflow
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = previousOverflow
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed inset-0 z-1100 flex items-center justify-center bg-black/70 p-4" onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className="max-h-[90vh] w-full overflow-y-auto rounded-3xl bg-white p-6"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<SuccessIcon className="mx-auto h-24 w-24" />
|
||||||
|
<h2 className="mt-4 text-right font-semibold text-gray-900">جزئیات رزرو</h2>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<DetailRow label="تعداد" value={`${formatPersianNumber(reservation.count)} نفر`} />
|
||||||
|
<DetailRow label="به نام" value={reservation.name} />
|
||||||
|
<DetailRow label="شماره تماس" value={reservation.phone} />
|
||||||
|
<DetailRow label="کافه" value={reservation.cafeName} />
|
||||||
|
<DetailRow label="بازی" value={reservation.matchLabel} />
|
||||||
|
<DetailRow label="تاریخ و ساعت" value={reservation.dateTimeLabel} />
|
||||||
|
<DetailRow label="کد رزرو" value={reservation.code} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-center justify-end gap-2 rounded-2xl bg-[#ECF2F8] px-4 py-3">
|
||||||
|
<InfoIcon />
|
||||||
|
<p className="text-right text-sm text-gray-500">
|
||||||
|
کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات ورود و کد دیگری برای دقایقی شما پیامک میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" onClick={onClose} className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white">
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MyReservations() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [selectedReservation, setSelectedReservation] = useState<MyReservation | null>(null)
|
||||||
|
|
||||||
|
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="mt-4 flex flex-col gap-3 px-4">
|
||||||
|
{mockMyReservations.map((reservation) => (
|
||||||
|
<MyReservationCard key={reservation.id} reservation={reservation} onClick={() => setSelectedReservation(reservation)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedReservation && (
|
||||||
|
<ReservationDetailModal reservation={selectedReservation} onClose={() => setSelectedReservation(null)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,11 @@ export type UserRole = 'customer' | 'cafe_owner'
|
||||||
|
|
||||||
const ROLE_STORAGE_KEY = 'role'
|
const ROLE_STORAGE_KEY = 'role'
|
||||||
|
|
||||||
// dev-only helper until real auth/role assignment exists — set via
|
// set by Login.tsx based on whether the user entered via /login or /cafe/login
|
||||||
// localStorage.setItem('role', 'cafe_owner') in devtools to test that flow
|
export function setUserRole(role: UserRole) {
|
||||||
|
localStorage.setItem(ROLE_STORAGE_KEY, role)
|
||||||
|
}
|
||||||
|
|
||||||
export function getUserRole(): UserRole {
|
export function getUserRole(): UserRole {
|
||||||
return localStorage.getItem(ROLE_STORAGE_KEY) === 'cafe_owner' ? 'cafe_owner' : 'customer'
|
return localStorage.getItem(ROLE_STORAGE_KEY) === 'cafe_owner' ? 'cafe_owner' : 'customer'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue