diff --git a/src/hooks/useMatchEvents.ts b/src/hooks/useMatchEvents.ts index 432d46a..377d795 100644 --- a/src/hooks/useMatchEvents.ts +++ b/src/hooks/useMatchEvents.ts @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react' import { fetchMatchEvents, MatchEvent } from '../services/campaignApi' +import { useNow } from './useNow' export function useMatchEvents() { const [matches, setMatches] = useState([]) - const [now, setNow] = useState(() => new Date()) + const now = useNow() useEffect(() => { fetchMatchEvents() @@ -11,11 +12,6 @@ export function useMatchEvents() { .catch(() => {}) }, []) - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000) - return () => clearInterval(timer) - }, []) - // drop matches whose kickoff has already passed — only upcoming ones should ever be selectable const upcomingMatches = matches.filter((match) => new Date(match.match_datetime).getTime() > now.getTime()) diff --git a/src/hooks/useNow.ts b/src/hooks/useNow.ts new file mode 100644 index 0000000..86e5db1 --- /dev/null +++ b/src/hooks/useNow.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'react' + +export function useNow() { + const [now, setNow] = useState(() => new Date()) + + useEffect(() => { + const timer = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(timer) + }, []) + + return now +} diff --git a/src/pages/CafeOwnerPanel.tsx b/src/pages/CafeOwnerPanel.tsx index 880be8a..48f3927 100644 --- a/src/pages/CafeOwnerPanel.tsx +++ b/src/pages/CafeOwnerPanel.tsx @@ -1,7 +1,11 @@ -import { ReactNode } from 'react' +import { ReactNode, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import BookIcon from '../components/BookIcon' -import { mockCafeEvents } from '../data/mockCafeEvents' +import { useNow } from '../hooks/useNow' +import { CafeSchedule, fetchCafeSchedules } from '../services/campaignApi' +import { getCafeUuid } from '../utils/cafe' +import { formatPersianNumber } from '../utils/formatNumber' +import { formatMatchLabel } from '../utils/matchCountdown' function ChevronIcon({ className = 'h-5 w-5 text-gray-400' }: { className?: string }) { return ( @@ -52,6 +56,16 @@ function PanelRow({ icon, label, onClick }: { icon: ReactNode; label: string; on export default function CafeOwnerPanel() { const navigate = useNavigate() + const now = useNow() + const [schedules, setSchedules] = useState([]) + + useEffect(() => { + const cafeUuid = getCafeUuid() + if (!cafeUuid) return + fetchCafeSchedules(cafeUuid) + .then(setSchedules) + .catch(() => {}) + }, []) return (
@@ -72,23 +86,47 @@ export default function CafeOwnerPanel() {

رویدادهای فعال شما

- {mockCafeEvents.map((event) => ( -
-
-

{event.matchLabel}

- - {event.remainingSeats} صندلی از {event.totalSeats} باقی مانده - + {schedules.map((schedule) => { + const isPast = new Date(schedule.match_event.match_datetime).getTime() <= now.getTime() + const matchLabel = formatMatchLabel(schedule.match_event, now) + + return ( +
+
+
+

{matchLabel}

+ {isPast && ( + برگزار شده + )} +
+ + {formatPersianNumber(schedule.remaining_tickets)} صندلی از {formatPersianNumber(schedule.total_tickets)} باقی مانده + +
+
+ } + label="مشاهده رزروها" + onClick={() => + navigate(`/owner/events/${schedule.uuid}/reservations`, { + state: { + event: { + id: schedule.uuid, + matchLabel, + remainingSeats: schedule.remaining_tickets, + totalSeats: schedule.total_tickets, + }, + }, + }) + } + /> +
-
- } - label="مشاهده رزروها" - onClick={() => navigate(`/owner/events/${event.id}/reservations`, { state: { event } })} - /> -
-
- ))} + ) + })}
diff --git a/src/pages/CafeOwnerRegister.tsx b/src/pages/CafeOwnerRegister.tsx index d0568bb..462ccc3 100644 --- a/src/pages/CafeOwnerRegister.tsx +++ b/src/pages/CafeOwnerRegister.tsx @@ -9,12 +9,14 @@ import { City, fetchCafeFeatures, fetchCities, + fetchMyCafes, registerCafe, registerCafeOwner, + updateCafe, } from "../services/campaignApi"; import { getCafeUuid, setCafeUuid } from "../utils/cafe"; import { toEnglishDigits } from "../utils/formatNumber"; -import { toE164 } from "../utils/phone"; +import { fromE164, toE164 } from "../utils/phone"; // Tehran — default pin until the owner picks their cafe's actual location const DEFAULT_CAFE_LOCATION = { lat: 35.6892, lng: 51.389 }; @@ -298,6 +300,9 @@ export default function CafeOwnerRegister() { const navigate = useNavigate(); const location = useLocation(); const initialStep = (location.state as { step?: 1 | 2 } | null)?.step ?? 1; + // the panel's "اطلاعات کافه" link is the only place that lands here with step 2 directly — + // treat that as "editing an existing cafe" rather than "continuing a fresh registration" + const isEditMode = initialStep === 2; const [step, setStep] = useState<1 | 2>(initialStep); const [fullName, setFullName] = useState(""); @@ -334,6 +339,22 @@ export default function CafeOwnerRegister() { .catch(() => {}); }, []); + useEffect(() => { + if (!isEditMode) return; + fetchMyCafes() + .then((cafes) => { + const cafe = cafes[0]; + if (!cafe) return; + setCafeName(cafe.name); + setSelectedCityUuid(cafe.city.uuid); + setAddress(cafe.address); + setCafePhone(fromE164(cafe.phone_number)); + setCafeLocation({ lat: cafe.location.coordinates[1], lng: cafe.location.coordinates[0] }); + setSelectedFeatures(cafe.features.map((feature) => feature.uuid)); + }) + .catch(() => {}); + }, [isEditMode]); + const toggleFeature = (feature: string) => { setSelectedFeatures((prev) => prev.includes(feature) @@ -355,6 +376,17 @@ export default function CafeOwnerRegister() { }); setOwnerUuid(owner.uuid); setStep(2); + } else if (isEditMode) { + const cafeUuid = getCafeUuid() ?? ""; + await updateCafe(cafeUuid, { + city_uuid: selectedCityUuid, + name: cafeName, + phone_number: cafePhone ? toE164(cafePhone) : "", + location: { type: "Point", coordinates: [cafeLocation.lng, cafeLocation.lat] }, + address, + feature_uuids: selectedFeatures, + }); + navigate("/owner/panel"); } else { const cafe = await registerCafe({ owner_uuid: ownerUuid, @@ -488,8 +520,8 @@ export default function CafeOwnerRegister() { onClick={handleContinue} className="pointer-events-auto flex items-center gap-2 rounded-full bg-[#223D5D] px-6 py-3 font-semibold text-white disabled:opacity-50" > - {isSubmitting ? "در حال ارسال..." : "ادامه"} - {!isSubmitting && } + {isSubmitting ? "در حال ارسال..." : isEditMode ? "ذخیره تغییرات" : "ادامه"} + {!isSubmitting && !isEditMode && }
diff --git a/src/services/campaignApi.ts b/src/services/campaignApi.ts index 844eedb..6cd6e11 100644 --- a/src/services/campaignApi.ts +++ b/src/services/campaignApi.ts @@ -81,6 +81,20 @@ export async function registerCafe(payload: RegisterCafePayload): Promise { + const { data } = await api.patch(`/campaign/cafes/${cafeUuid}/`, payload) + return data +} + export interface CafeMedia { uuid: string cafe_uuid: string @@ -129,3 +143,8 @@ export async function createCafeSchedule(cafeUuid: string, payload: CreateCafeSc const { data } = await api.post(`/campaign/cafes/${cafeUuid}/schedules/`, payload) return data } + +export async function fetchCafeSchedules(cafeUuid: string): Promise { + const { data } = await api.get(`/campaign/cafes/${cafeUuid}/schedules/`) + return data +} diff --git a/src/utils/phone.ts b/src/utils/phone.ts index 7bd8a48..f958fef 100644 --- a/src/utils/phone.ts +++ b/src/utils/phone.ts @@ -1,7 +1,13 @@ -import { toEnglishDigits } from './formatNumber' +import { toEnglishDigits, toPersianDigits } from './formatNumber' export function toE164(localPhone: string): string { const digitsOnly = toEnglishDigits(localPhone).replace(/\D/g, '') const withoutLeadingZero = digitsOnly.replace(/^0+/, '') return `+98${withoutLeadingZero}` } + +export function fromE164(e164Phone: string): string { + if (!e164Phone) return '' + const local = e164Phone.startsWith('+98') ? `0${e164Phone.slice(3)}` : e164Phone + return toPersianDigits(local) +}