feat: editing cafe data
This commit is contained in:
parent
c0b8710ee5
commit
9c331470b5
6 changed files with 131 additions and 28 deletions
|
|
@ -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<MatchEvent[]>([])
|
||||
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())
|
||||
|
||||
|
|
|
|||
12
src/hooks/useNow.ts
Normal file
12
src/hooks/useNow.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<CafeSchedule[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const cafeUuid = getCafeUuid()
|
||||
if (!cafeUuid) return
|
||||
fetchCafeSchedules(cafeUuid)
|
||||
.then(setSchedules)
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pb-20">
|
||||
|
|
@ -72,23 +86,47 @@ export default function CafeOwnerPanel() {
|
|||
<h2 className="mt-6 text-right font-semibold text-gray-900">رویدادهای فعال شما</h2>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-6">
|
||||
{mockCafeEvents.map((event) => (
|
||||
<div key={event.id} className="rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F]">
|
||||
{schedules.map((schedule) => {
|
||||
const isPast = new Date(schedule.match_event.match_datetime).getTime() <= now.getTime()
|
||||
const matchLabel = formatMatchLabel(schedule.match_event, now)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={schedule.uuid}
|
||||
className={`rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F] ${isPast ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<div className="pt-4">
|
||||
<p className="text-right text-gray-900 pb-3">{event.matchLabel}</p>
|
||||
<div className="flex items-center justify-between pb-3">
|
||||
<p className="text-right text-gray-900">{matchLabel}</p>
|
||||
{isPast && (
|
||||
<span className="shrink-0 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500">برگزار شده</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="mt-2 inline-block rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700">
|
||||
{event.remainingSeats} صندلی از {event.totalSeats} باقی مانده
|
||||
{formatPersianNumber(schedule.remaining_tickets)} صندلی از {formatPersianNumber(schedule.total_tickets)} باقی مانده
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<PanelRow
|
||||
icon={<CalendarIcon />}
|
||||
label="مشاهده رزروها"
|
||||
onClick={() => navigate(`/owner/events/${event.id}/reservations`, { state: { event } })}
|
||||
onClick={() =>
|
||||
navigate(`/owner/events/${schedule.uuid}/reservations`, {
|
||||
state: {
|
||||
event: {
|
||||
id: schedule.uuid,
|
||||
matchLabel,
|
||||
remainingSeats: schedule.remaining_tickets,
|
||||
totalSeats: schedule.total_tickets,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && <ArrowIcon className="h-4 w-4" />}
|
||||
{isSubmitting ? "در حال ارسال..." : isEditMode ? "ذخیره تغییرات" : "ادامه"}
|
||||
{!isSubmitting && !isEditMode && <ArrowIcon className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,20 @@ export async function registerCafe(payload: RegisterCafePayload): Promise<CafeRe
|
|||
return data
|
||||
}
|
||||
|
||||
export interface UpdateCafePayload {
|
||||
city_uuid: string
|
||||
name: string
|
||||
phone_number: string
|
||||
location: GeoPoint
|
||||
address: string
|
||||
feature_uuids: string[]
|
||||
}
|
||||
|
||||
export async function updateCafe(cafeUuid: string, payload: UpdateCafePayload): Promise<CafeResponse> {
|
||||
const { data } = await api.patch<CafeResponse>(`/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<CafeSchedule>(`/campaign/cafes/${cafeUuid}/schedules/`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchCafeSchedules(cafeUuid: string): Promise<CafeSchedule[]> {
|
||||
const { data } = await api.get<CafeSchedule[]>(`/campaign/cafes/${cafeUuid}/schedules/`)
|
||||
return data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue