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 (
+
+
بازی
+
setOpen((o) => !o)}
+ className="flex h-14 w-full items-center justify-between rounded-2xl border border-gray-200 px-4 text-right"
+ >
+ {value || 'بازی مورد نظر را انتخاب کنید'}
+
+
+
+ {open && (
+ <>
+
setOpen(false)} />
+
+ {matchOptions.map((option) => (
+ {
+ 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}
+
+ ))}
+
+ >
+ )}
+
+ )
+}
+
+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 (
+
+
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]'}`}
+ >
+
+
+
+ {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}
+
+ )}
+
+
onChange(value + step)} className="flex h-6 w-6 items-center justify-center text-[#223D5D]">
+
+
+
+ )
+}
+
+function DescriptionField({ value, onChange }: { value: string; onChange: (value: string) => void }) {
+ return (
+
+
توضیحات (اختیاری)
+
+
+
+ )
+}
+
+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 (
+
+
+
+
navigate(-1)} className="mt-4 flex w-full items-center justify-start gap-2 px-4">
+
+ افزودن ایونت
+
+
+
+
+
+
+ ظرفیت
+
+
+
+
+ ورودی
+
+
+
+
+
+
+
+ navigate('/owner/events/success')}
+ className="flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white"
+ >
+ ادامه
+
+
+
+
+ )
+}
diff --git a/src/pages/AddEventSuccess.tsx b/src/pages/AddEventSuccess.tsx
new file mode 100644
index 0000000..014d3b4
--- /dev/null
+++ b/src/pages/AddEventSuccess.tsx
@@ -0,0 +1,34 @@
+import { useNavigate } from 'react-router-dom'
+import SuccessIcon from '../components/SuccessIcon'
+
+export default function AddEventSuccess() {
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
+
+
ایونت با موفقیت ثبت شد!
+
+
+
+ navigate('/owner/events/new')}
+ className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
+ >
+ افزودن ایونت
+
+ navigate('/owner/panel')}
+ className="w-full rounded-2xl border border-[#223D5D] py-4 text-lg font-semibold text-[#223D5D]"
+ >
+ مشاهده پنل
+
+
+
+ )
+}
diff --git a/src/pages/CafeOwnerPanel.tsx b/src/pages/CafeOwnerPanel.tsx
new file mode 100644
index 0000000..463652c
--- /dev/null
+++ b/src/pages/CafeOwnerPanel.tsx
@@ -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 (
+
+
+
+ )
+}
+
+function ProfileCircleIcon() {
+ return (
+
+
+
+
+
+ )
+}
+
+function BookIcon() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+function CalendarIcon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function PanelRow({ icon, label, onClick }: { icon: ReactNode; label: string; onClick?: () => void }) {
+ return (
+
+
+ {icon}
+ {label}
+
+
+
+ )
+}
+
+export default function CafeOwnerPanel() {
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
+
پنل کافه
+
+
+
}
+ label="اطلاعات کافه"
+ onClick={() => navigate('/owner/register', { state: { step: 2 } })}
+ />
+
} label="افزودن رویداد" onClick={() => navigate('/owner/events/new')} />
+
+
+
رویدادهای فعال شما
+
+
+ {mockCafeEvents.map((event) => (
+
+
+
{event.matchLabel}
+
+ {event.remainingSeats} صندلی از {event.totalSeats} باقی مانده
+
+
+
+
}
+ label="مشاهده رزروها"
+ onClick={() => navigate(`/owner/events/${event.id}/reservations`, { state: { event } })}
+ />
+
+
+ ))}
+
+
+
+
+ پشتیبانی
+
+
+ )
+}
diff --git a/src/pages/CafeOwnerRegister.tsx b/src/pages/CafeOwnerRegister.tsx
index 37d7a22..f15c5c7 100644
--- a/src/pages/CafeOwnerRegister.tsx
+++ b/src/pages/CafeOwnerRegister.tsx
@@ -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() {
لطفاً یک{" "}
نماینده {" "}
- برای کافه معرفی کنید. نماینده باید در اپلیکیشن ثبتنام کند و
- پرداختها و واریز به کیف پول یا واریز میشود. شماره موبایل، کد
- ملی و کارت بانکی باید متعلق به یک نفر باشند.
+ برای کافه معرفی کنید. نماینده باید در اپلیکیشن
+ ثبتنام کند و پرداختها به کیف پول او واریز میشود. شماره
+ موبایل، کد ملی و کارت بانکی باید متعلق به یک نفر باشند.
@@ -270,7 +274,9 @@ export default function CafeOwnerRegister() {
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"
>
ادامه
diff --git a/src/pages/CafeRegisterSuccess.tsx b/src/pages/CafeRegisterSuccess.tsx
new file mode 100644
index 0000000..5beb089
--- /dev/null
+++ b/src/pages/CafeRegisterSuccess.tsx
@@ -0,0 +1,38 @@
+import { useNavigate } from 'react-router-dom'
+import SuccessIcon from '../components/SuccessIcon'
+
+export default function CafeRegisterSuccess() {
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
+
+
کافه شما با موفقیت ثبت شد!
+
+ با اضافه کردن ایونت بازیها، کافه شما در کمپین نمایش داده میشود و کاربران میتوانند برای هر مسابقه رزرو ثبت
+ کنند.
+
+
+
+
+ navigate('/owner/events/new')}
+ className="w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
+ >
+ افزودن ایونت
+
+ navigate('/owner/panel')}
+ className="w-full rounded-2xl border border-[#223D5D] py-4 text-lg font-semibold text-[#223D5D]"
+ >
+ مشاهده پنل
+
+
+
+ )
+}
diff --git a/src/pages/EventReservations.tsx b/src/pages/EventReservations.tsx
new file mode 100644
index 0000000..d3b7500
--- /dev/null
+++ b/src/pages/EventReservations.tsx
@@ -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 (
+
+
+
+ );
+}
+
+function SearchIcon({ className = "" }: { className?: string }) {
+ return (
+
+
+
+
+ );
+}
+
+function CheckIcon() {
+ return (
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+ {reservation.code}
+
+
+ {formatPersianNumber(reservation.count)} نفر
+
+
+
{reservation.name}
+
{reservation.phone}
+
+
+ {selected && }
+
+
+ );
+}
+
+export default function EventReservations() {
+ const { id } = useParams();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const [query, setQuery] = useState("");
+ const [selectedIds, setSelectedIds] = useState([]);
+
+ 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 (
+
+
+
+
navigate(-1)}
+ className="mt-4 flex w-full items-center justify-start gap-2 px-4"
+ >
+
+
+ رزروهای {event?.matchLabel ?? ""}
+
+
+
+
+ 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]"
+ />
+
+
+
+
+ {filteredReservations.map((reservation) => (
+ toggleSelected(reservation.id)}
+ />
+ ))}
+
+
+
+
+ دانلود لیست
+
+
+
+ );
+}
diff --git a/src/pages/PaymentSuccess.tsx b/src/pages/PaymentSuccess.tsx
index 1f16916..37b9f3f 100644
--- a/src/pages/PaymentSuccess.tsx
+++ b/src/pages/PaymentSuccess.tsx
@@ -1,20 +1,6 @@
import { useNavigate } from 'react-router-dom'
import InfoIcon from '../components/InfoIcon'
-
-function SuccessIcon() {
- return (
-
-
-
-
- )
-}
+import SuccessIcon from '../components/SuccessIcon'
function DetailRow({ label, value }: { label: string; value: string }) {
return (