feat: user panel api calls fixed

This commit is contained in:
Afra ‌ 2026-06-24 19:06:24 +03:30
parent 74fbdfdf7d
commit 5bad637b1f
7 changed files with 282 additions and 142 deletions

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { Cafe } from "../data/mockCafes";
import { Cafe } from "../services/campaignApi";
import { formatPersianNumber } from "../utils/formatNumber";
import InfoIcon from "./InfoIcon";
@ -8,6 +8,7 @@ interface ReservationModalProps {
isOpen: boolean;
onClose: () => void;
cafe: Cafe;
ticketPrice: number;
}
const mockPhone = "۰۹۱۲۳۴۵۶۷۸۹";
@ -75,6 +76,7 @@ export default function ReservationModal({
isOpen,
onClose,
cafe,
ticketPrice,
}: ReservationModalProps) {
const [fullName, setFullName] = useState("");
const [count, setCount] = useState(1);
@ -91,7 +93,7 @@ export default function ReservationModal({
if (!isOpen) return null;
const reservationAmount = cafe.entryFeeAmount * count;
const reservationAmount = ticketPrice * count;
const payableAmount = Math.max(0, reservationAmount - walletBalance);
return createPortal(
@ -105,7 +107,7 @@ export default function ReservationModal({
{cafe.name}
</h2>
<p className="mt-3 mb-5 text-right text-sm text-gray-500">
{cafe.match}
{cafe.address}
</p>
<div>

View file

@ -1,55 +0,0 @@
export interface Cafe {
id: number
name: string
match: string
entryFee: string
entryFeeAmount: number
remainingCapacity: number
tags: string[]
location: { lat: number; lng: number }
phone: string
address: string
images: string[] // empty array for now, will show gray placeholder
}
export const mockCafes: Cafe[] = [
{
id: 1,
name: 'کافه بی‌بای',
match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)',
entryFee: '۲,۰۰۰,۰۰۰ تومان',
entryFeeAmount: 2000000,
remainingCapacity: 3,
tags: ['پارکینگ دارد', 'فضای سیگار', 'امکان سفارش'],
location: { lat: 35.7219, lng: 51.3347 },
phone: '۰۲۱۹۱۰۲۰۲۱۲۰۲',
address: 'ونک، خدامی، نبش نیروی انتظامی، پلاک ۲',
images: [],
},
{
id: 2,
name: 'کافه بی‌بای شعبه ۲',
match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)',
entryFee: '۱,۵۰۰,۰۰۰ تومان',
entryFeeAmount: 1500000,
remainingCapacity: 10,
tags: ['فضای سیگار', 'امکان سفارش'],
location: { lat: 35.755, lng: 51.4165 },
phone: '۰۲۱۸۸۴۴۵۵۶۶',
address: 'جردن، خیابان آفریقا، پلاک ۱۲',
images: [],
},
{
id: 3,
name: 'کافه ستاره',
match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)',
entryFee: 'رایگان',
entryFeeAmount: 0,
remainingCapacity: 15,
tags: ['پارکینگ دارد'],
location: { lat: 35.6892, lng: 51.389 },
phone: '۰۲۱۷۷۳۳۲۲۱۱',
address: 'انقلاب، خیابان ۱۶ آذر، پلاک ۵',
images: [],
},
]

View file

@ -1,13 +0,0 @@
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: '۰۹۱۲۳۴۵۶۷۸۹' },
]

View file

@ -1,9 +1,19 @@
import { ReactNode, useState } from "react";
import { useNavigate, useParams, useLocation } from "react-router-dom";
import { ReactNode, useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { MapContainer, TileLayer, Marker, ZoomControl } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "../utils/leafletIcons";
import { mockCafes, Cafe } from "../data/mockCafes";
import {
Cafe,
CafeEventSchedule,
CafeSchedule,
fetchCafeEventSchedule,
fetchCafeSchedules,
} from "../services/campaignApi";
import { useNow } from "../hooks/useNow";
import { formatMatchLabel } from "../utils/matchCountdown";
import { formatPersianNumber } from "../utils/formatNumber";
import { fromE164 } from "../utils/phone";
import InfoIcon from "../components/InfoIcon";
import ReservationModal from "../components/ReservationModal";
@ -67,6 +77,71 @@ function MapPinIcon() {
);
}
function ChevronDownIcon({ className = "" }: { className?: string }) {
return (
<svg
className={`h-4 w-4 shrink-0 text-gray-400 transition-transform ${className}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 9l6 6 6-6" />
</svg>
);
}
function MatchPicker({
value,
options,
now,
onChange,
}: {
value: string;
options: CafeSchedule[];
now: Date;
onChange: (uuid: string) => void;
}) {
const [open, setOpen] = useState(false);
const selected = options.find((s) => s.match_event.uuid === value);
if (options.length === 0) return null;
return (
<div className="relative mt-1 inline-block">
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1 text-right text-sm text-gray-500"
>
<span>{selected ? formatMatchLabel(selected.match_event, now) : "بازی را انتخاب کنید"}</span>
<ChevronDownIcon className={open ? "rotate-180" : ""} />
</button>
{open && (
<>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute right-0 top-full z-20 mt-2 max-h-60 w-64 overflow-y-auto rounded-xl bg-white shadow-lg">
{options.map((s) => (
<button
key={s.match_event.uuid}
type="button"
onClick={() => {
onChange(s.match_event.uuid);
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"
>
{formatMatchLabel(s.match_event, now)}
</button>
))}
</div>
</>
)}
</div>
);
}
function Pill({
children,
className = "bg-gray-100 text-gray-700",
@ -117,17 +192,12 @@ function ImageCarousel({ images }: { images: string[] }) {
);
}
function toEnglishDigits(value: string) {
const persianDigits = "۰۱۲۳۴۵۶۷۸۹";
return value.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit)));
}
async function openNavigationPicker(cafe: Cafe) {
const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${cafe.location.lat},${cafe.location.lng}`;
async function openNavigationPicker(name: string, lat: number, lng: number) {
const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
if (navigator.share) {
try {
await navigator.share({ title: cafe.name, url: mapsUrl });
await navigator.share({ title: name, url: mapsUrl });
} catch {
// user dismissed the native share sheet
}
@ -137,14 +207,37 @@ async function openNavigationPicker(cafe: Cafe) {
}
export default function CafeDetail() {
const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const now = useNow();
const [isReservationOpen, setIsReservationOpen] = useState(false);
const [schedules, setSchedules] = useState<CafeSchedule[]>([]);
const [selectedEventUuid, setSelectedEventUuid] = useState("");
const [schedule, setSchedule] = useState<CafeEventSchedule | undefined>(undefined);
const cafe: Cafe | undefined =
(location.state as { cafe?: Cafe } | null)?.cafe ??
mockCafes.find((c) => c.id === Number(id));
const state = location.state as { cafe?: Cafe } | null;
const cafe = state?.cafe;
// only ever offer upcoming matches — picking a past one to "reserve" makes no sense
const upcomingSchedules = schedules
.filter((s) => new Date(s.match_event.match_datetime).getTime() > now.getTime())
.sort((a, b) => new Date(a.match_event.match_datetime).getTime() - new Date(b.match_event.match_datetime).getTime());
// defaults to the closest upcoming match, but the user can override via MatchPicker
const effectiveEventUuid = selectedEventUuid || upcomingSchedules[0]?.match_event.uuid || "";
useEffect(() => {
if (!cafe) return;
fetchCafeSchedules(cafe.uuid)
.then(setSchedules)
.catch(() => {});
}, [cafe]);
useEffect(() => {
if (!cafe || !effectiveEventUuid) return;
fetchCafeEventSchedule(cafe.uuid, effectiveEventUuid)
.then(setSchedule)
.catch(() => {});
}, [cafe, effectiveEventUuid]);
if (!cafe) {
return (
@ -154,10 +247,14 @@ export default function CafeDetail() {
);
}
const images = cafe.media.filter((m) => m.type === "image").map((m) => m.url);
const tags = cafe.features.map((f) => f.name);
const mapPosition: [number, number] = [cafe.location.coordinates[1], cafe.location.coordinates[0]];
return (
<div className="min-h-screen bg-white">
<div className="relative">
<ImageCarousel images={cafe.images} />
<ImageCarousel images={images} />
<button
type="button"
onClick={() => navigate(-1)}
@ -172,15 +269,25 @@ export default function CafeDetail() {
<h1 className="text-right text-xl font-bold text-gray-900">
{cafe.name}
</h1>
<p className="mt-1 text-right text-sm text-gray-500">{cafe.match}</p>
<MatchPicker
value={effectiveEventUuid}
options={upcomingSchedules}
now={now}
onChange={setSelectedEventUuid}
/>
<div className="mt-3 flex flex-wrap gap-2 ">
<Pill className="bg-[#ECF2F8] text-gray-700">ورودی: نفری {cafe.entryFee}</Pill>
<Pill className="bg-[#ECF2F8] text-gray-700">ظرفیت باقی مانده: {cafe.remainingCapacity}نفر</Pill>
{/* </div> */}
{/* <div className="mt-2 flex flex-wrap gap-2"> */}
{cafe.tags.map((tag) => (
{schedule && (
<>
<Pill className="bg-[#ECF2F8] text-gray-700">
ورودی: نفری {formatPersianNumber(Number(schedule.ticket_price))} تومان
</Pill>
<Pill className="bg-[#ECF2F8] text-gray-700">
ظرفیت باقی مانده: {formatPersianNumber(schedule.remaining_tickets)}نفر
</Pill>
</>
)}
{tags.map((tag) => (
<Pill key={tag} className="bg-[#ECF2F8] text-gray-700">
{tag}
</Pill>
@ -191,7 +298,7 @@ export default function CafeDetail() {
آدرس
</h2>
<MapContainer
center={[cafe.location.lat, cafe.location.lng]}
center={mapPosition}
zoom={15}
scrollWheelZoom={false}
zoomControl={false}
@ -203,7 +310,7 @@ export default function CafeDetail() {
maxZoom={19}
minZoom={8}
/>
<Marker position={[cafe.location.lat, cafe.location.lng]} />
<Marker position={mapPosition} />
<ZoomControl position="topright" />
</MapContainer>
@ -212,8 +319,8 @@ export default function CafeDetail() {
<PhoneIcon />
<div className="flex flex-col items-start gap-1">
<p>شماره تماس</p>
<a href={`tel:${toEnglishDigits(cafe.phone)}`} className="text-gray-900">
{cafe.phone}
<a href={`tel:${cafe.phone_number}`} className="text-gray-900">
{fromE164(cafe.phone_number)}
</a>
</div>
</div>
@ -227,7 +334,7 @@ export default function CafeDetail() {
<button
type="button"
onClick={() => openNavigationPicker(cafe)}
onClick={() => openNavigationPicker(cafe.name, mapPosition[0], mapPosition[1])}
className="mt-4 w-full rounded-2xl border border-[#223D5D] py-3 text-center text-gray-900"
>
مسیریابی
@ -257,6 +364,7 @@ export default function CafeDetail() {
isOpen={isReservationOpen}
onClose={() => setIsReservationOpen(false)}
cafe={cafe}
ticketPrice={schedule ? Number(schedule.ticket_price) : 0}
/>
</div>
);

View file

@ -1,7 +1,8 @@
import { useMemo, useState } from "react";
import { useEffect, 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 { EventReservation, fetchEventReservations } from "../services/campaignApi";
import { getCafeUuid } from "../utils/cafe";
import { formatPersianNumber } from "../utils/formatNumber";
function ChevronIcon({ className = "" }: { className?: string }) {
@ -60,21 +61,21 @@ function toEnglishDigits(value: string) {
);
}
function reservationMatches(reservation: Reservation, query: string) {
function reservationMatches(reservation: EventReservation, query: string) {
if (!query) return true;
const haystack = toEnglishDigits(
`${reservation.name} ${reservation.phone} ${reservation.count} ${formatPersianNumber(reservation.count)} ${reservation.code}`,
`${reservation.full_name} ${reservation.phone_number} ${reservation.people_count} ${formatPersianNumber(reservation.people_count)} ${reservation.reserve_code}`,
).toLowerCase();
return haystack.includes(toEnglishDigits(query).toLowerCase());
}
function downloadReservationsCsv(reservations: Reservation[]) {
function downloadReservationsCsv(reservations: EventReservation[]) {
const header = ["کد رزرو", "تعداد", "نام", "شماره تماس"];
const rows = reservations.map((r) => [
r.code,
String(r.count),
r.name,
toEnglishDigits(r.phone),
r.reserve_code,
String(r.people_count),
r.full_name,
toEnglishDigits(r.phone_number),
]);
const csvContent = [header, ...rows]
.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(","))
@ -96,7 +97,7 @@ function ReservationCard({
selected,
onToggle,
}: {
reservation: Reservation;
reservation: EventReservation;
selected: boolean;
onToggle: () => void;
}) {
@ -105,14 +106,14 @@ function ReservationCard({
<div className="flex flex-col items-start gap-2 text-right">
<div className="flex items-center gap-2">
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{reservation.code}
{reservation.reserve_code}
</span>
<span className="font-medium rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700 h-9 flex items-center">
{formatPersianNumber(reservation.count)} نفر
{formatPersianNumber(reservation.people_count)} نفر
</span>
</div>
<p className="text-gray-900">{reservation.name}</p>
<p className="text-gray-500">{reservation.phone}</p>
<p className="text-gray-900">{reservation.full_name}</p>
<p className="text-gray-500">{reservation.phone_number}</p>
</div>
<button
type="button"
@ -130,21 +131,30 @@ export default function EventReservations() {
const location = useLocation();
const navigate = useNavigate();
const [query, setQuery] = useState("");
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [reservations, setReservations] = useState<EventReservation[]>([]);
const event: CafeEvent | undefined =
(location.state as { event?: CafeEvent } | null)?.event ??
mockCafeEvents.find((e) => e.id === Number(id));
useEffect(() => {
const cafeUuid = getCafeUuid();
if (!cafeUuid || !id) return;
fetchEventReservations(cafeUuid, id)
.then(setReservations)
.catch(() => {});
}, [id]);
const filteredReservations = useMemo(
() =>
mockReservations.filter((reservation) =>
reservations.filter((reservation) =>
reservationMatches(reservation, query),
),
[query],
[reservations, query],
);
const toggleSelected = (reservationId: number) => {
const toggleSelected = (reservationId: string) => {
setSelectedIds((prev) =>
prev.includes(reservationId)
? prev.filter((id) => id !== reservationId)
@ -155,8 +165,8 @@ export default function EventReservations() {
const handleDownload = () => {
const selectedReservations =
selectedIds.length > 0
? mockReservations.filter((r) => selectedIds.includes(r.id))
: mockReservations;
? reservations.filter((r) => selectedIds.includes(r.uuid))
: reservations;
downloadReservationsCsv(selectedReservations);
};
@ -189,10 +199,10 @@ export default function EventReservations() {
<div className="mt-4 flex flex-col gap-3 px-4">
{filteredReservations.map((reservation) => (
<ReservationCard
key={reservation.id}
key={reservation.uuid}
reservation={reservation}
selected={selectedIds.includes(reservation.id)}
onToggle={() => toggleSelected(reservation.id)}
selected={selectedIds.includes(reservation.uuid)}
onToggle={() => toggleSelected(reservation.uuid)}
/>
))}
</div>

View file

@ -1,12 +1,14 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import BookIcon from '../components/BookIcon'
import { mockCafes, Cafe } from '../data/mockCafes'
import { MatchEvent } from '../services/campaignApi'
import { Cafe, City, fetchCafes, fetchCities, MatchEvent } from '../services/campaignApi'
import { useMatchEvents } from '../hooks/useMatchEvents'
import { formatMatchLabel } from '../utils/matchCountdown'
import { formatPersianNumber } from '../utils/formatNumber'
const cityOptions = ['تهران', 'اصفهان', 'مشهد']
function findTehran(cities: City[]): City | undefined {
return cities.find((c) => c.city_name === 'تهران' || c.city_name.toLowerCase() === 'tehran')
}
function ChevronIcon({ className = '' }: { className?: string }) {
return (
@ -67,7 +69,7 @@ function MapPlaceholder() {
)
}
function SelectorRow({
function CitySelectorRow({
label,
value,
options,
@ -75,10 +77,11 @@ function SelectorRow({
}: {
label: string
value: string
options: string[]
onChange: (value: string) => void
options: City[]
onChange: (uuid: string) => void
}) {
const [open, setOpen] = useState(false)
const selectedCity = options.find((city) => city.uuid === value)
return (
<div className="relative px-4 mt-4">
@ -88,25 +91,25 @@ function SelectorRow({
onClick={() => setOpen((o) => !o)}
className="w-full flex items-center justify-between gap-2 rounded-xl border border-gray-200 bg-white px-4 py-3"
>
<span className="text-gray-900">{value}</span>
<span className="text-gray-900">{selectedCity?.city_name ?? ''}</span>
<ChevronDownIcon className={open ? 'rotate-180' : ''} />
</button>
{open && (
<>
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute inset-x-4 top-full z-20 mt-2 overflow-hidden rounded-xl bg-white shadow-lg">
{options.map((option) => (
<div className="absolute inset-x-4 top-full z-20 mt-2 max-h-72 overflow-y-auto rounded-xl bg-white shadow-lg">
{options.map((city) => (
<button
key={option}
key={city.uuid}
type="button"
onClick={() => {
onChange(option)
onChange(city.uuid)
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}
{city.city_name}
</button>
))}
</div>
@ -188,17 +191,22 @@ function MyReservationsRow() {
function CafeCard({ cafe }: { cafe: Cafe }) {
const navigate = useNavigate()
const remainingTickets = cafe.schedules?.[0]?.remaining_tickets
return (
<button
type="button"
onClick={() => navigate(`/cafe/${cafe.id}`, { state: { cafe } })}
onClick={() => navigate(`/cafe/${cafe.uuid}`, { state: { cafe } })}
className="flex w-full items-center justify-start gap-4 rounded-2xl bg-white p-4 text-right shadow-sm"
>
<div className="h-32 w-36 shrink-0 rounded-xl bg-gray-200" />
<div className="flex flex-col items-start gap-2">
<h3 className="font-semibold text-gray-900">{cafe.name}</h3>
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600">{cafe.remainingCapacity} صندلی</span>
{remainingTickets !== undefined && (
<span className="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600">
{formatPersianNumber(remainingTickets)} صندلی
</span>
)}
</div>
</button>
)
@ -206,10 +214,32 @@ function CafeCard({ cafe }: { cafe: Cafe }) {
export default function Home() {
const [view, setView] = useState<'list' | 'map'>('list')
const [city, setCity] = useState(cityOptions[0])
const [cities, setCities] = useState<City[]>([])
const [selectedCityUuid, setSelectedCityUuid] = useState('')
const [selectedMatchUuid, setSelectedMatchUuid] = useState('')
const { matches, now } = useMatchEvents()
const effectiveMatchUuid = selectedMatchUuid || matches[0]?.uuid || ''
const [cafes, setCafes] = useState<Cafe[]>([])
const nearestMatch = [...matches].sort(
(a, b) => new Date(a.match_datetime).getTime() - new Date(b.match_datetime).getTime(),
)[0]
// defaults to Tehran + the nearest upcoming match, but the user can override either
const effectiveCityUuid = selectedCityUuid || findTehran(cities)?.uuid || ''
const effectiveMatchUuid = selectedMatchUuid || nearestMatch?.uuid || ''
useEffect(() => {
fetchCities()
.then(setCities)
.catch(() => {})
}, [])
useEffect(() => {
// wait until both real defaults are known — city_uuid/event_uuid must always be sent together
if (!effectiveCityUuid || !effectiveMatchUuid) return
fetchCafes({ city_uuid: effectiveCityUuid, event_uuid: effectiveMatchUuid })
.then(setCafes)
.catch(() => {})
}, [effectiveCityUuid, effectiveMatchUuid])
return (
<div className="min-h-screen bg-white pb-24">
@ -217,7 +247,7 @@ export default function Home() {
<MyReservationsRow />
<SelectorRow label="شهر" value={city} options={cityOptions} onChange={setCity} />
<CitySelectorRow label="شهر" value={effectiveCityUuid} options={cities} onChange={setSelectedCityUuid} />
<MatchSelectorRow
label="بازی انتخابی"
value={effectiveMatchUuid}
@ -255,8 +285,8 @@ export default function Home() {
<div className="mt-3 px-4">
{view === 'list' ? (
<div className="space-y-3">
{mockCafes.map((cafe) => (
<CafeCard key={cafe.id} cafe={cafe} />
{cafes.map((cafe) => (
<CafeCard key={cafe.uuid} cafe={cafe} />
))}
</div>
) : (

View file

@ -122,6 +122,24 @@ export async function fetchMyCafes(): Promise<MyCafe[]> {
return data
}
// the public cafe-listing response matches MyCafe plus an embedded schedule for
// whichever event_uuid was queried — NOT present on /campaign/cafes/my/, don't merge these back into one type.
// confirmed via a real response that this field can be entirely absent (not even an
// empty array) when the cafe has no schedule for the queried event — always guard with `?.`
export interface Cafe extends MyCafe {
schedules?: CafeSchedule[]
}
export interface FetchCafesParams {
city_uuid: string
event_uuid: string
}
export async function fetchCafes(params: FetchCafesParams): Promise<Cafe[]> {
const { data } = await api.get<Cafe[]>('/campaign/cafes/', { params })
return data
}
export interface CafeSchedule {
uuid: string
cafe_uuid: string
@ -148,3 +166,43 @@ export async function fetchCafeSchedules(cafeUuid: string): Promise<CafeSchedule
const { data } = await api.get<CafeSchedule[]>(`/campaign/cafes/${cafeUuid}/schedules/`)
return data
}
export interface CafeEventSchedule {
cafe_uuid: string
cafe_name: string
cafe_phone_number: string
cafe_address: string
cafe_location: GeoPoint
// confirmed via a real response: CafeFeature[], not the "string" shown in the spec
cafe_features: CafeFeature[]
cafe_media: CafeMedia[]
schedule_uuid: string
match_event: MatchEvent
total_tickets: number
remaining_tickets: number
ticket_price: string
description: string
}
export async function fetchCafeEventSchedule(cafeUuid: string, eventUuid: string): Promise<CafeEventSchedule> {
// bare single object, NOT a list — unlike most other endpoints in this file
const { data } = await api.get<CafeEventSchedule>(`/campaign/cafes/${cafeUuid}/events/${eventUuid}/`)
return data
}
export interface EventReservation {
uuid: string
user_uuid: string
cafe_match_schedule_uuid: string
reserve_code: string
people_count: number
full_name: string
phone_number: string
status: string
created_at: string
}
export async function fetchEventReservations(cafeUuid: string, eventUuid: string): Promise<EventReservation[]> {
const { data } = await api.get<EventReservation[]>(`/campaign/cafes/${cafeUuid}/events/${eventUuid}/reservations/`)
return data
}