diff --git a/.env.example b/.env.example index c82173b..ad0bad0 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ VITE_AUTH_BASE_URL=https://accounts-staging.gooyal.ir VITE_AUTH_CLIENT_ID= VITE_AUTH_CLIENT_SECRET= +VITE_APP_BASE_URL= diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..344de72 --- /dev/null +++ b/.env.staging @@ -0,0 +1 @@ +VITE_APP_BASE_URL=https://reservation-staging.winsoo.ir/ diff --git a/package.json b/package.json index ee7f152..5b8b69e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:staging": "vite build --mode staging", "lint": "eslint .", "typecheck": "tsc --noEmit", "preview": "vite preview" diff --git a/src/App.tsx b/src/App.tsx index 10f9c63..8421a84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,8 +9,10 @@ import CafeOwnerPanel from './pages/CafeOwnerPanel' import AddEvent from './pages/AddEvent' import AddEventSuccess from './pages/AddEventSuccess' import EventReservations from './pages/EventReservations' +import MyReservations from './pages/MyReservations' import Login from './pages/Login' import LoginOtp from './pages/LoginOtp' +import RequireRole from './components/RequireRole' function ScrollToTop() { const { pathname } = useLocation() @@ -27,16 +29,18 @@ export default function App() { - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> + } /> } /> diff --git a/src/components/BookIcon.tsx b/src/components/BookIcon.tsx new file mode 100644 index 0000000..0115d55 --- /dev/null +++ b/src/components/BookIcon.tsx @@ -0,0 +1,10 @@ +export default function BookIcon() { + return ( + + + + + + + ) +} diff --git a/src/components/RequireRole.tsx b/src/components/RequireRole.tsx new file mode 100644 index 0000000..1bae174 --- /dev/null +++ b/src/components/RequireRole.tsx @@ -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 + } + return <>{children} +} diff --git a/src/data/mockMyReservations.ts b/src/data/mockMyReservations.ts new file mode 100644 index 0000000..d1b5a63 --- /dev/null +++ b/src/data/mockMyReservations.ts @@ -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: 'بی‌بای', + }, +] diff --git a/src/pages/CafeOwnerPanel.tsx b/src/pages/CafeOwnerPanel.tsx index 463652c..880be8a 100644 --- a/src/pages/CafeOwnerPanel.tsx +++ b/src/pages/CafeOwnerPanel.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react' import { useNavigate } from 'react-router-dom' +import BookIcon from '../components/BookIcon' import { mockCafeEvents } from '../data/mockCafeEvents' function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) { @@ -20,17 +21,6 @@ function ProfileCircleIcon() { ) } -function BookIcon() { - return ( - - - - - - - ) -} - function CalendarIcon() { return ( diff --git a/src/pages/CafeRegisterSuccess.tsx b/src/pages/CafeRegisterSuccess.tsx index 5beb089..fb5846c 100644 --- a/src/pages/CafeRegisterSuccess.tsx +++ b/src/pages/CafeRegisterSuccess.tsx @@ -12,8 +12,7 @@ export default function CafeRegisterSuccess() {

کافه شما با موفقیت ثبت شد!

- با اضافه کردن ایونت بازی‌ها، کافه شما در کمپین نمایش داده می‌شود و کاربران می‌توانند برای هر مسابقه رزرو ثبت - کنند. + با اضافه کردن ایونت بازی‌ها، کافه شما در کمپین نمایش داده می‌شود و کاربران می‌توانند برای تماشای هر مسابقه در کافه شما، میز رزرو کنند.

diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 376a05d..c438e7b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' +import BookIcon from '../components/BookIcon' import { mockCafes, Cafe } from '../data/mockCafes' const cityOptions = ['تهران', 'اصفهان', 'مشهد'] @@ -9,6 +10,14 @@ const matchOptions = [ 'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)', ] +function ChevronIcon({ className = '' }: { className?: string }) { + return ( + + + + ) +} + function ChevronDownIcon({ className = '' }: { className?: string }) { return ( navigate('/reservations')} + className="flex w-full items-center justify-between px-4 py-6" + > +
+ + رزروهای شما +
+ + + ) +} + function CafeCard({ cafe }: { cafe: Cafe }) { const navigate = useNavigate() @@ -136,6 +163,8 @@ export default function Home() {
+ + diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 9f27e87..7101b1c 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,8 +1,9 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { requestOtp } from "../services/authApi"; import { toPersianDigits } from "../utils/formatNumber"; import { toE164 } from "../utils/phone"; +import { setUserRole } from "../utils/role"; function CheckIcon() { return ( @@ -24,6 +25,11 @@ export default function Login() { const navigate = useNavigate(); const location = useLocation(); 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 [acceptedTerms, setAcceptedTerms] = useState(false); @@ -89,7 +95,7 @@ export default function Login() { {acceptedTerms && }

- + قوانین و شرایط {" "} را می‌پذیرم. diff --git a/src/pages/LoginOtp.tsx b/src/pages/LoginOtp.tsx index c828a8b..4b8384e 100644 --- a/src/pages/LoginOtp.tsx +++ b/src/pages/LoginOtp.tsx @@ -33,6 +33,7 @@ export default function LoginOtp() { const location = useLocation() const state = location.state as { phone?: string; ttl?: number } | null const phone = state?.phone ?? '' + const loginPath = getUserRole() === 'cafe_owner' ? '/cafe/login' : '/login' const [otp, setOtp] = useState(Array(OTP_LENGTH).fill('')) const [secondsLeft, setSecondsLeft] = useState(Math.round(state?.ttl ?? DEFAULT_RESEND_SECONDS)) @@ -44,9 +45,9 @@ export default function LoginOtp() { useEffect(() => { if (!phone) { - navigate('/login', { replace: true }) + navigate(loginPath, { replace: true }) } - }, [phone, navigate]) + }, [phone, loginPath, navigate]) useEffect(() => { const timer = setInterval(() => { @@ -118,7 +119,7 @@ export default function LoginOtp() {

کد ارسال شده را وارد کنید:

{phone} -
diff --git a/src/pages/MyReservations.tsx b/src/pages/MyReservations.tsx new file mode 100644 index 0000000..75ae8ab --- /dev/null +++ b/src/pages/MyReservations.tsx @@ -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 ( + + + + ) +} + +function MyReservationCard({ reservation, onClick }: { reservation: MyReservation; onClick: () => void }) { + return ( + + ) +} + +function DetailRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +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( +
+
e.stopPropagation()} + > + +

جزئیات رزرو

+ +
+ + + + + + + +
+ +
+ +

+ کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات ورود و کد دیگری برای دقایقی شما پیامک می‌شود. +

+
+ + +
+
, + document.body, + ) +} + +export default function MyReservations() { + const navigate = useNavigate() + const [selectedReservation, setSelectedReservation] = useState(null) + + return ( +
+
+ + + +
+ {mockMyReservations.map((reservation) => ( + setSelectedReservation(reservation)} /> + ))} +
+ + {selectedReservation && ( + setSelectedReservation(null)} /> + )} +
+ ) +} diff --git a/src/utils/role.ts b/src/utils/role.ts index 4080b40..4db59dd 100644 --- a/src/utils/role.ts +++ b/src/utils/role.ts @@ -2,8 +2,11 @@ export type UserRole = 'customer' | 'cafe_owner' const ROLE_STORAGE_KEY = 'role' -// dev-only helper until real auth/role assignment exists — set via -// localStorage.setItem('role', 'cafe_owner') in devtools to test that flow +// set by Login.tsx based on whether the user entered via /login or /cafe/login +export function setUserRole(role: UserRole) { + localStorage.setItem(ROLE_STORAGE_KEY, role) +} + export function getUserRole(): UserRole { return localStorage.getItem(ROLE_STORAGE_KEY) === 'cafe_owner' ? 'cafe_owner' : 'customer' }