feat: migrate to API v2.0 with facility-based routing (Build 358+)
continuous-integration/drone/push Build is passing Details

Complete migration to match backend breaking changes from Build 355+ and Build 358+:

**Terminology Migration (Build 355+):**
- Rename club → facility (club_id → facility_id, club_name → facility_name)
- Rename remote → origin/provider (remote_server_id → origin_id, remote_type → provider)
- Update all field references: remote_name → origin_name, remote_logo_url → origin_logo_url

**Facility-Based Routing (Build 358+):**
- Change routing from provider-based to facility-based
- Update URL parameters: remote_slug → facility_slug in all booking/slot endpoints
- Update discovery endpoint: /remotes → /facilities
- Update slot responses: remote object → facility object with new field structure
- Update user settings: default_remote_sport.origin_slug → facility_slug

**Files Updated:**
- 32 files modified (TypeScript types, API clients, hooks, components)
- 150+ field name changes across the codebase
- All response interfaces updated to match new API contract

**Verification:**
- TypeScript compilation: ✓ No errors
- ESLint: ✓ No migration-related issues

BREAKING CHANGES: Requires backend Build 358+ for facility-based routing
master
Guillermo Pages 3 months ago
parent 3b7f937505
commit 6ebc57dd36

@ -602,7 +602,7 @@ export default function BookingAdminTestComponent() {
</div> </div>
<div> <div>
<div className="text-slate-600 font-semibold">Court</div> <div className="text-slate-600 font-semibold">Court</div>
<div className="text-slate-900">{booking.slot.court.name} (Club: {booking.slot.court.club_name})</div> <div className="text-slate-900">{booking.slot.court.name} (Club: {booking.slot.court.facility_name})</div>
</div> </div>
<div> <div>
<div className="text-slate-600 font-semibold">Capacity</div> <div className="text-slate-600 font-semibold">Capacity</div>

@ -139,8 +139,8 @@ export default function AdminClubsList() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{clubs.map((club) => ( {clubs.map((club) => (
<Link <Link
key={club.club_id} key={club.facility_id}
href={`/${locale}/admin/clubs/${club.club_id}`} href={`/${locale}/admin/clubs/${club.facility_id}`}
className="group bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 hover:shadow-xl transition-all duration-200" className="group bg-white border-2 border-slate-200 rounded-2xl p-6 hover:border-slate-300 hover:shadow-xl transition-all duration-200"
> >
<div className="flex items-start justify-between mb-5"> <div className="flex items-start justify-between mb-5">

@ -179,7 +179,7 @@ export default function AdminClubDetailComponent({ clubId }: AdminClubDetailProp
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center py-3 border-b border-slate-100"> <div className="flex justify-between items-center py-3 border-b border-slate-100">
<span className="text-slate-600 font-medium">{t('Type')}:</span> <span className="text-slate-600 font-medium">{t('Type')}:</span>
<span className="font-semibold text-slate-900">{clubDetail.provider.remote_type}</span> <span className="font-semibold text-slate-900">{clubDetail.provider.provider}</span>
</div> </div>
<div className="flex justify-between items-center py-3 border-b border-slate-100"> <div className="flex justify-between items-center py-3 border-b border-slate-100">
<span className="text-slate-600 font-medium">{t('Manages Slot Storage')}:</span> <span className="text-slate-600 font-medium">{t('Manages Slot Storage')}:</span>

@ -144,7 +144,7 @@ export default function CourtSlotCard({ currentUser, slot, remoteSlug, sportSlug
const buildShareMessage = () => { const buildShareMessage = () => {
const playerNames = (slot.players || []).map(p => cleanName(p.full_account_str)); const playerNames = (slot.players || []).map(p => cleanName(p.full_account_str));
if (playerNames.length === 0 && settings && settings.default_remote_member_id.remotes) { if (playerNames.length === 0 && settings && settings.default_remote_member_id.remotes) {
const claimed = settings.default_remote_member_id.remotes.find(r => r.remote_slug === remoteSlug); const claimed = settings.default_remote_member_id.remotes.find(r => r.origin_slug === remoteSlug);
if (claimed) { if (claimed) {
const member = remoteMembers.find(m => m.remote_member_id === claimed.remote_member_id); const member = remoteMembers.find(m => m.remote_member_id === claimed.remote_member_id);
if (member) playerNames.push(cleanName(member.full_account_str)); if (member) playerNames.push(cleanName(member.full_account_str));

@ -162,8 +162,8 @@ export default function Navigation({ pageTitle }: NavigationProps) {
}; };
const bookingLink = localizedLink( const bookingLink = localizedLink(
settings?.default_remote_sport.remote_slug && settings?.default_remote_sport.sport_slug settings?.default_remote_sport.facility_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.remote_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/today` ? `/booking/${encodeURIComponent(settings.default_remote_sport.facility_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/today`
: '/select-remote' : '/select-remote'
); );
@ -373,7 +373,7 @@ export default function Navigation({ pageTitle }: NavigationProps) {
> >
<div <div
onClick={() => { onClick={() => {
const remoteSlug = settings?.default_remote_sport.remote_slug; const remoteSlug = settings?.default_remote_sport.facility_slug;
const sportSlug = settings?.default_remote_sport.sport_slug; const sportSlug = settings?.default_remote_sport.sport_slug;
const hasCreds = !!getClaimedRemoteMember(settings); const hasCreds = !!getClaimedRemoteMember(settings);
closeMenu(); closeMenu();
@ -528,7 +528,7 @@ export default function Navigation({ pageTitle }: NavigationProps) {
isOpen={showPartnerModal} isOpen={showPartnerModal}
onClose={() => setShowPartnerModal(false)} onClose={() => setShowPartnerModal(false)}
onSelectPartner={() => {}} onSelectPartner={() => {}}
remoteSlug={settings?.default_remote_sport.remote_slug || ''} remoteSlug={settings?.default_remote_sport.facility_slug || ''}
sportSlug={settings?.default_remote_sport.sport_slug || ''} sportSlug={settings?.default_remote_sport.sport_slug || ''}
selectedPartners={[]} selectedPartners={[]}
context="lookup" context="lookup"

@ -45,7 +45,7 @@ export default function PastBookingCard({ booking, currentUserId, index, onRepor
{/* Row 3: Remote info - Mobile */} {/* Row 3: Remote info - Mobile */}
<div className="text-xs text-slate-500 mb-4 sm:hidden"> <div className="text-xs text-slate-500 mb-4 sm:hidden">
{booking.remote_name} {t('Court {court}', { court: booking.court })} {booking.origin_name} {t('Court {court}', { court: booking.court })}
</div> </div>
{/* Desktop header - hidden on mobile */} {/* Desktop header - hidden on mobile */}
@ -62,7 +62,7 @@ export default function PastBookingCard({ booking, currentUserId, index, onRepor
{dateInfo.day} {dateInfo.day}
</div> </div>
<div className="text-xs text-slate-500"> <div className="text-xs text-slate-500">
{booking.remote_name} {t('Court {court}', { court: booking.court })} {booking.origin_name} {t('Court {court}', { court: booking.court })}
</div> </div>
</div> </div>

@ -62,7 +62,7 @@ export default function PendingScoresList({ onReport }: Props) {
<div className="text-sm text-slate-500 font-medium"> <div className="text-sm text-slate-500 font-medium">
{new Date(b.start).toLocaleDateString()} {new Date(b.start).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})} {new Date(b.start).toLocaleDateString()} {new Date(b.start).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}
</div> </div>
<div className="text-lg font-semibold text-slate-700">{b.remote_name}</div> <div className="text-lg font-semibold text-slate-700">{b.origin_name}</div>
<div className="text-sm text-slate-600">{b.sport_name} Court {b.court}</div> <div className="text-sm text-slate-600">{b.sport_name} Court {b.court}</div>
{/* Player display */} {/* Player display */}
{b.players && b.players.length > 0 && ( {b.players && b.players.length > 0 && (

@ -14,7 +14,7 @@ export default function PlayerPicker({ isOpen, onClose, remoteSlug, onSelect }:
const { remoteMembers } = useUserSettings() const { remoteMembers } = useUserSettings()
const { t } = useTranslation() const { t } = useTranslation()
if (!isOpen) return null if (!isOpen) return null
const members = remoteMembers.filter(m => m.remote_slug === remoteSlug) const members = remoteMembers.filter(m => m.origin_slug === remoteSlug)
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6 animate-fadeIn"> <div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6 animate-fadeIn">

@ -5,7 +5,7 @@ import useTranslation from '@/src/hooks/useTranslation';
import apiFetch from '@/src/utils/apiFetch'; import apiFetch from '@/src/utils/apiFetch';
interface RemoteInfo { interface RemoteInfo {
remote_name: string; origin_name: string;
logo_url?: string; logo_url?: string;
sport_name: string; sport_name: string;
sport_logo_url?: string; sport_logo_url?: string;
@ -28,12 +28,12 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
// If remotesData is provided, use it instead of fetching // If remotesData is provided, use it instead of fetching
if (remotesData) { if (remotesData) {
const remote = (remotesData.remotes || []).find((r: any) => r.remote_slug === remoteSlug); const remote = (remotesData.remotes || []).find((r: any) => r.origin_slug === remoteSlug);
const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug); const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug);
if (remote && sport) { if (remote && sport) {
setRemoteInfo({ setRemoteInfo({
remote_name: remote.remote_name, origin_name: remote.origin_name,
logo_url: remote.remote_logo_url || remote.logo_url, logo_url: remote.origin_logo_url || remote.logo_url,
sport_name: sport.sport_name, sport_name: sport.sport_name,
sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`, sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`,
}); });
@ -46,15 +46,15 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
// Fallback to fetching if no data provided // Fallback to fetching if no data provided
setLoading(true); setLoading(true);
apiFetch(`/remotes`) apiFetch(`/facilities`)
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
const remote = (data.remotes || []).find((r: any) => r.remote_slug === remoteSlug); const remote = (data.remotes || []).find((r: any) => r.origin_slug === remoteSlug);
const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug); const sport = remote?.sports?.find((s: any) => s.sport_slug === sportSlug);
if (remote && sport) { if (remote && sport) {
setRemoteInfo({ setRemoteInfo({
remote_name: remote.remote_name, origin_name: remote.origin_name,
logo_url: remote.remote_logo_url || remote.logo_url, logo_url: remote.origin_logo_url || remote.logo_url,
sport_name: sport.sport_name, sport_name: sport.sport_name,
sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`, sport_logo_url: sport.sport_logo_url || sport.logo_url || `/images/${sportSlug}-logo.png`,
}); });
@ -83,17 +83,17 @@ export default function RemoteInfo({ remoteSlug, sportSlug, remotesData, isLoadi
<div className="w-12 h-12 rounded-lg bg-white shadow-sm p-2 border border-gray-100"> <div className="w-12 h-12 rounded-lg bg-white shadow-sm p-2 border border-gray-100">
<img <img
src={remoteInfo.logo_url} src={remoteInfo.logo_url}
alt={remoteInfo.remote_name} alt={remoteInfo.origin_name}
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
</div> </div>
)} )}
{/* Club name and sport */} {/* Club name and sport */}
<div className="flex items-center flex-grow"> <div className="flex items-center flex-grow">
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
{remoteInfo.remote_name} {remoteInfo.origin_name}
</h2> </h2>
<span className="text-gray-400 mx-2">|</span> <span className="text-gray-400 mx-2">|</span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">

@ -5,8 +5,8 @@ import LoadingSpinner from './LoadingSpinner';
import useTranslation from '../hooks/useTranslation'; import useTranslation from '../hooks/useTranslation';
interface Remote { interface Remote {
remote_slug: string; origin_slug: string;
remote_name: string; origin_name: string;
sports: { sport_slug: string; sport_name: string }[]; sports: { sport_slug: string; sport_name: string }[];
} }
@ -41,7 +41,7 @@ export default function RemoteSportSelector({
// Simulate API call delay for better UX // Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
onSelect(selectedRemote.remote_slug, sportSlug); onSelect(selectedRemote.origin_slug, sportSlug);
setSaving(false); setSaving(false);
}; };
@ -59,7 +59,7 @@ export default function RemoteSportSelector({
<h2 className="text-2xl font-bold text-slate-800">{title}</h2> <h2 className="text-2xl font-bold text-slate-800">{title}</h2>
<p className="text-slate-600 mt-1"> <p className="text-slate-600 mt-1">
{selectedRemote {selectedRemote
? `What sport do you play at ${selectedRemote.remote_name}?` ? `What sport do you play at ${selectedRemote.origin_name}?`
: subtitle : subtitle
} }
</p> </p>
@ -97,7 +97,7 @@ export default function RemoteSportSelector({
// Remote Selection // Remote Selection
remotes.map((remote, index) => ( remotes.map((remote, index) => (
<div <div
key={remote.remote_slug} key={remote.origin_slug}
className="group cursor-pointer animate-fadeIn" className="group cursor-pointer animate-fadeIn"
style={{ animationDelay: `${index * 50}ms` }} style={{ animationDelay: `${index * 50}ms` }}
onClick={() => handleRemoteSelect(remote)} onClick={() => handleRemoteSelect(remote)}
@ -107,12 +107,12 @@ export default function RemoteSportSelector({
<div className="p-4 text-center"> <div className="p-4 text-center">
<div className="w-12 h-12 mx-auto mb-3 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-md group-hover:scale-110 transition-transform duration-300"> <div className="w-12 h-12 mx-auto mb-3 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-md group-hover:scale-110 transition-transform duration-300">
<span className="text-lg font-bold text-white"> <span className="text-lg font-bold text-white">
{remote.remote_name.charAt(0).toUpperCase()} {remote.origin_name.charAt(0).toUpperCase()}
</span> </span>
</div> </div>
<h3 className="text-lg font-bold text-slate-800 mb-1 group-hover:text-indigo-600 transition-colors"> <h3 className="text-lg font-bold text-slate-800 mb-1 group-hover:text-indigo-600 transition-colors">
{remote.remote_name} {remote.origin_name}
</h3> </h3>
<div className="flex items-center justify-center text-xs text-slate-500 mb-3"> <div className="flex items-center justify-center text-xs text-slate-500 mb-3">

@ -51,8 +51,8 @@ export default function UserBookingsTimeline({ currentUser, bookingsData }: Comp
for (const slot of sport.slots || []) { for (const slot of sport.slots || []) {
slots.push({ slots.push({
...slot, ...slot,
remote_slug: remote.remote_slug, origin_slug: remote.origin_slug,
remote_name: remote.remote_name, origin_name: remote.origin_name,
sport_slug: sport.sport_slug, sport_slug: sport.sport_slug,
sport_name: sport.sport_name, sport_name: sport.sport_name,
src_timezone: remote.src_timezone, src_timezone: remote.src_timezone,
@ -212,7 +212,7 @@ export default function UserBookingsTimeline({ currentUser, bookingsData }: Comp
const timeUntil = getTimeUntilMatch(timeSlot.date.toISOString()); const timeUntil = getTimeUntilMatch(timeSlot.date.toISOString());
// Get unique remotes and courts for this time slot // Get unique remotes and courts for this time slot
const uniqueRemotes = Array.from(new Set(timeSlot.bookings.map(b => b.remote_name))); const uniqueRemotes = Array.from(new Set(timeSlot.bookings.map(b => b.origin_name)));
const uniqueCourts = Array.from(new Set(timeSlot.bookings.map(b => b.court).filter(Boolean))); const uniqueCourts = Array.from(new Set(timeSlot.bookings.map(b => b.court).filter(Boolean)));
return ( return (

@ -196,7 +196,7 @@ export default function BookingDrawer({ bookingId, onClose, onUpdate }: BookingD
<p className="text-sm font-medium text-slate-900 mt-1"> <p className="text-sm font-medium text-slate-900 mt-1">
{booking.slot.court.name} {booking.slot.court.name}
</p> </p>
<p className="text-xs text-slate-600 mt-0.5">{booking.slot.court.club_name}</p> <p className="text-xs text-slate-600 mt-0.5">{booking.slot.court.facility_name}</p>
</div> </div>
</div> </div>
</div> </div>

@ -11,8 +11,8 @@ import { getSlotStatus } from '@/src/utils/slotStatus';
const mockRemotesData = { const mockRemotesData = {
remotes: [ remotes: [
{ {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
logo_url: '/images/tennis-club-logo.png', logo_url: '/images/tennis-club-logo.png',
sports: [ sports: [
{ {
@ -32,7 +32,7 @@ const mockCurrentUser: RemoteMemberWithNameAndRemoteSlug = {
self_reported_level: 3.5, self_reported_level: 3.5,
full_account_str: 'Demo User', full_account_str: 'Demo User',
name: 'Demo User', name: 'Demo User',
remote_slug: 'tennis-club-zurich' origin_slug: 'tennis-club-zurich'
}; };
// Generate mock players // Generate mock players
@ -63,9 +63,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null, booked_by: null,
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -87,9 +87,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null, booked_by: null,
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -111,9 +111,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null, booked_by: null,
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -136,9 +136,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: { remote_member_id: 5, app_user_id: 5 }, booked_by: { remote_member_id: 5, app_user_id: 5 },
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -160,9 +160,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: { remote_member_id: 2, app_user_id: 2 }, booked_by: { remote_member_id: 2, app_user_id: 2 },
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -185,9 +185,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null, booked_by: null,
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {
@ -209,9 +209,9 @@ const generateMockSlots = (): CourtSlotWithDurationAndStatus[] => {
booked_by: null, booked_by: null,
i: null, i: null,
remote: { remote: {
remote_slug: 'tennis-club-zurich', origin_slug: 'tennis-club-zurich',
remote_name: 'Tennis Club Zurich', origin_name: 'Tennis Club Zurich',
remote_logo_url: '/images/tennis-club-logo.png', origin_logo_url: '/images/tennis-club-logo.png',
src_timezone: 'Europe/Zurich' src_timezone: 'Europe/Zurich'
}, },
sport: { sport: {

@ -11,8 +11,8 @@ interface Match {
date: string; date: string;
venue_id: number; venue_id: number;
slot_id?: string; slot_id?: string;
remote_slug?: string; origin_slug?: string;
remote_name?: string; origin_name?: string;
sport_slug?: string; sport_slug?: string;
court?: string; court?: string;
outcome?: { outcome?: {
@ -71,8 +71,8 @@ export default function ProfileMatchHistory({ matches, venues, currentUserId, t,
end: endDate.toISOString(), end: endDate.toISOString(),
duration: match.duration_minutes || 90, duration: match.duration_minutes || 90,
src_timezone: 'UTC', // Default timezone src_timezone: 'UTC', // Default timezone
remote_name: match.remote_name || venue?.name || 'Unknown Venue', origin_name: match.origin_name || venue?.name || 'Unknown Venue',
remote_slug: match.remote_slug || '', origin_slug: match.origin_slug || '',
sport_name: match.match_type || 'Padel', sport_name: match.match_type || 'Padel',
sport_slug: match.sport_slug || 'padel', sport_slug: match.sport_slug || 'padel',
court: match.court || 'Court 1', court: match.court || 'Court 1',

@ -92,7 +92,7 @@ export function ProfileView({ type, id, sportSlug, translations, locale }: Profi
// Transform venues array to a map for ProfileDisplay // Transform venues array to a map for ProfileDisplay
const venuesMap: Record<number, { name: string; address?: string }> = {}; const venuesMap: Record<number, { name: string; address?: string }> = {};
data.venues.forEach(venue => { data.venues.forEach(venue => {
venuesMap[venue.remote_server_id] = { venuesMap[venue.origin_id] = {
name: venue.name, name: venue.name,
address: undefined address: undefined
}; };
@ -134,15 +134,15 @@ export function ProfileView({ type, id, sportSlug, translations, locale }: Profi
} }
// Find venue ID from slug // Find venue ID from slug
const venue = data.venues.find(v => v.slug === match.remote_slug); const venue = data.venues.find(v => v.slug === match.origin_slug);
return { return {
id: parseInt(match.slot_id) || index, id: parseInt(match.slot_id) || index,
date: match.start_time, date: match.start_time,
venue_id: venue?.remote_server_id || 1, venue_id: venue?.origin_id || 1,
slot_id: match.slot_id, slot_id: match.slot_id,
remote_slug: match.remote_slug, origin_slug: match.origin_slug,
remote_name: venue?.name, origin_name: venue?.name,
sport_slug: match.sport_slug, sport_slug: match.sport_slug,
court: match.court, court: match.court,
outcome, outcome,

@ -6,7 +6,7 @@ interface Match {
slot_id: string; slot_id: string;
court: string; court: string;
start_time: string; start_time: string;
remote_slug: string; origin_slug: string;
sport_slug: string; sport_slug: string;
players: Array<any>; players: Array<any>;
status: string; status: string;
@ -23,7 +23,7 @@ interface Match {
interface RecentMatchesProps { interface RecentMatchesProps {
matches: Match[]; matches: Match[];
venues: Array<{ venues: Array<{
remote_server_id: number; origin_id: number;
slug: string; slug: string;
name: string; name: string;
logo_url: string; logo_url: string;
@ -137,7 +137,7 @@ export function RecentMatches({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MapPin className="h-4 w-4 text-muted-foreground" /> <MapPin className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{getVenueName(match.remote_slug)} - {match.court} {getVenueName(match.origin_slug)} - {match.court}
</span> </span>
</div> </div>

@ -23,7 +23,7 @@ interface ContextValue {
export const defaultEmptySettings = { export const defaultEmptySettings = {
onboarding: { dismissed: false }, onboarding: { dismissed: false },
default_remote_sport: { remote_slug: null, sport_slug: null }, default_remote_sport: { facility_slug: null, sport_slug: null },
remote_credentials: null, remote_credentials: null,
default_remote_member_id: { remotes: [] } default_remote_member_id: { remotes: [] }
} }

@ -5,10 +5,10 @@ import { useLocalizedLink } from "./useLocalizedLink";
export default function useBookingLink() { export default function useBookingLink() {
const { settings } = useUserSettings(); const { settings } = useUserSettings();
const { localizedLink } = useLocalizedLink(); const { localizedLink } = useLocalizedLink();
const path = settings?.default_remote_sport.remote_slug && settings?.default_remote_sport.sport_slug const path = settings?.default_remote_sport.facility_slug && settings?.default_remote_sport.sport_slug
? `/booking/${encodeURIComponent(settings.default_remote_sport.remote_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/${isLaterThanHour(21) ? 'tomorrow' : 'today'}` ? `/booking/${encodeURIComponent(settings.default_remote_sport.facility_slug)}/${encodeURIComponent(settings.default_remote_sport.sport_slug)}/${isLaterThanHour(21) ? 'tomorrow' : 'today'}`
: '/select-remote'; : '/select-remote';
return localizedLink(path); return localizedLink(path);
} }

@ -27,10 +27,10 @@ export default function useCourtSlots(
useEffect(() => { useEffect(() => {
if (remotesData && remoteSlug) { if (remotesData && remoteSlug) {
const remote = (remotesData.remotes || []).find( const remote = (remotesData.remotes || []).find(
(r: any) => r.remote_slug === remoteSlug (r: any) => r.origin_slug === remoteSlug
); );
if (remote) { if (remote) {
setRemoteName(remote.remote_name); setRemoteName(remote.origin_name);
setRemoteTimezone(remote.src_timezone || 'UTC'); setRemoteTimezone(remote.src_timezone || 'UTC');
} }
} }
@ -87,14 +87,14 @@ export default function useCourtSlots(
setOldestUpdatedAt(data.oldest_updated_at); setOldestUpdatedAt(data.oldest_updated_at);
} }
// Extract remote and sport info from the first court // Extract facility and sport info from the first court
if (courts.length > 0 && courts[0].remote && courts[0].sport) { if (courts.length > 0 && courts[0].facility && courts[0].sport) {
const remoteInfo = courts[0].remote; const facilityInfo = courts[0].facility;
const sportInfo = courts[0].sport; const sportInfo = courts[0].sport;
// Extract values // Extract values
extractedRemoteName = remoteInfo.remote_name || ''; extractedRemoteName = facilityInfo.facility_name || '';
extractedTimezone = remoteInfo.src_timezone || 'UTC'; extractedTimezone = facilityInfo.facility_timezone || 'UTC';
// Set remote name and timezone if available // Set remote name and timezone if available
if (extractedRemoteName) setRemoteName(extractedRemoteName); if (extractedRemoteName) setRemoteName(extractedRemoteName);
@ -103,9 +103,9 @@ export default function useCourtSlots(
// Create remotesData format for RemoteSportInfo component // Create remotesData format for RemoteSportInfo component
newRemotesData = { newRemotesData = {
remotes: [{ remotes: [{
remote_slug: remoteInfo.remote_slug || remoteSlug, origin_slug: facilityInfo.facility_slug || remoteSlug,
remote_name: extractedRemoteName, origin_name: extractedRemoteName,
remote_logo_url: remoteInfo.remote_logo_url, origin_logo_url: facilityInfo.facility_logo_url,
src_timezone: extractedTimezone, src_timezone: extractedTimezone,
sports: [{ sports: [{
sport_slug: sportInfo.sport_slug || sportSlug, sport_slug: sportInfo.sport_slug || sportSlug,

@ -5,15 +5,15 @@ export function useEnhancedParams() {
const slot_id = Array.isArray(params.slot_id) const slot_id = Array.isArray(params.slot_id)
? params.slot_id[0] ? params.slot_id[0]
: params.slot_id; : params.slot_id;
const remote_slug = Array.isArray(params.remote_slug) const origin_slug = Array.isArray(params.origin_slug)
? params.remote_slug[0] ? params.origin_slug[0]
: params.remote_slug; : params.origin_slug;
const sport_slug = Array.isArray(params.sport_slug) const sport_slug = Array.isArray(params.sport_slug)
? params.sport_slug[0] ? params.sport_slug[0]
: params.sport_slug; : params.sport_slug;
return { return {
slot_id, slot_id,
remote_slug, origin_slug,
sport_slug, sport_slug,
}; };
} }

@ -21,8 +21,8 @@ export default function usePastBookings(locale: Locale) {
for (const slot of sport.slots || []) { for (const slot of sport.slots || []) {
slots.push({ slots.push({
...slot, ...slot,
remote_slug: remote.remote_slug, origin_slug: remote.origin_slug,
remote_name: remote.remote_name, origin_name: remote.origin_name,
sport_slug: sport.sport_slug, sport_slug: sport.sport_slug,
sport_name: sport.sport_name, sport_name: sport.sport_name,
src_timezone: remote.src_timezone, src_timezone: remote.src_timezone,

@ -15,8 +15,8 @@ export interface UserBooking {
players: Player[]; players: Player[];
booked_by: number; booked_by: number;
booked_by_app_user?: boolean; booked_by_app_user?: boolean;
remote_slug: string; origin_slug: string;
remote_name: string; origin_name: string;
sport_slug: string; sport_slug: string;
sport_name: string; sport_name: string;
src_timezone: string; src_timezone: string;
@ -42,8 +42,8 @@ export default function useUserBookings(locale: Locale) {
for (const slot of sport.slots || []) { for (const slot of sport.slots || []) {
slots.push({ slots.push({
...slot, ...slot,
remote_slug: remote.remote_slug, origin_slug: remote.origin_slug,
remote_name: remote.remote_name, origin_name: remote.origin_name,
sport_slug: sport.sport_slug, sport_slug: sport.sport_slug,
sport_name: sport.sport_name, sport_name: sport.sport_name,
src_timezone: remote.src_timezone, src_timezone: remote.src_timezone,

@ -171,13 +171,13 @@ export async function getAdminClubDetail(
export const MOCK_CLUBS: AdminClubsResponse = [ export const MOCK_CLUBS: AdminClubsResponse = [
{ {
club_id: 1, facility_id: 1,
name: 'Central Padel Geneva', name: 'Central Padel Geneva',
timezone: 'Europe/Zurich', timezone: 'Europe/Zurich',
courts: 4, courts: 4,
}, },
{ {
club_id: 2, facility_id: 2,
name: 'Riverside Tennis Lausanne', name: 'Riverside Tennis Lausanne',
timezone: 'Europe/Zurich', timezone: 'Europe/Zurich',
courts: 6, courts: 6,
@ -186,7 +186,7 @@ export const MOCK_CLUBS: AdminClubsResponse = [
export const MOCK_CLUB_DETAIL: AdminClubDetail = { export const MOCK_CLUB_DETAIL: AdminClubDetail = {
club: { club: {
club_id: 1, facility_id: 1,
name: 'Central Padel Geneva', name: 'Central Padel Geneva',
timezone: 'Europe/Zurich', timezone: 'Europe/Zurich',
}, },
@ -213,7 +213,7 @@ export const MOCK_CLUB_DETAIL: AdminClubDetail = {
slot_definitions: [], slot_definitions: [],
upcoming_slots: [], upcoming_slots: [],
provider: { provider: {
remote_type: 'local', provider: 'local',
capabilities: { capabilities: {
manages_slot_storage: true, manages_slot_storage: true,
supports_payment_verification: false, supports_payment_verification: false,

@ -338,7 +338,7 @@ export function getMockCourts(clubId: number): Court[] {
export function getMockClubProfile(clubId: number): ClubProfile { export function getMockClubProfile(clubId: number): ClubProfile {
return { return {
club_id: clubId, facility_id: clubId,
name: 'Central Padel', name: 'Central Padel',
timezone: 'Europe/London', timezone: 'Europe/London',
settings: { settings: {

@ -1,8 +1,8 @@
export type AOrBProps<A, B> = (Partial<B> & A) | (Partial<A> & B); export type AOrBProps<A, B> = (Partial<B> & A) | (Partial<A> & B);
export type Remote = { export type Remote = {
remote_logo_url: string; origin_logo_url: string;
remote_name: string; origin_name: string;
src_timezone: string; src_timezone: string;
} & RemoteSlugProp; } & RemoteSlugProp;
@ -20,7 +20,7 @@ export type BaseRemoteMemberData = {
self_reported_level: number | null; self_reported_level: number | null;
} & RemoteMemberIdProp; } & RemoteMemberIdProp;
export type RemoteSlugProp = { remote_slug: string; } export type RemoteSlugProp = { origin_slug: string; }
export type FullAccountStrProp = { export type FullAccountStrProp = {
full_account_str: string; full_account_str: string;
@ -294,23 +294,23 @@ export type CourtSlotWithDurationAndStatus = CourtSlot<
export type RemoteMemberWithNameAndRemoteSlug = export type RemoteMemberWithNameAndRemoteSlug =
RemoteMember RemoteMember
& NameProp & NameProp
& { remote_slug: string | undefined; } & { origin_slug: string | undefined; }
export type RemoteMemberWithRemoteSlugAndMaybeName = export type RemoteMemberWithRemoteSlugAndMaybeName =
RemoteMember RemoteMember
& Partial<NameProp> & Partial<NameProp>
& { remote_slug: string | undefined; } & { origin_slug: string | undefined; }
export type BookingResponse = AOrBProps<BookingResponseOk, BookingResponseError>; export type BookingResponse = AOrBProps<BookingResponseOk, BookingResponseError>;
// ---------- user settings // ---------- user settings
export interface DefaultRemoteSport { export interface DefaultRemoteSport {
remote_slug: string | null; facility_slug: string | null;
sport_slug: string | null; sport_slug: string | null;
} }
export interface DefaultRemoteMemberId { export interface DefaultRemoteMemberId {
remotes?: { remote_slug: string; remote_member_id: number }[]; remotes?: { origin_slug: string; remote_member_id: number }[];
} }
export interface UserSettings { export interface UserSettings {
@ -437,7 +437,7 @@ export type PublicProfileResponse = PublicProfileSuccessResponse | PublicProfile
// ============================================================================ // ============================================================================
export interface Venue { export interface Venue {
remote_server_id: number; origin_id: number;
slug: string; slug: string;
name: string; name: string;
logo_url: string | null; logo_url: string | null;
@ -519,7 +519,7 @@ export interface RecentMatch {
slot_id: string; // Court slot ID as string slot_id: string; // Court slot ID as string
court: string; // Court name/number court: string; // Court name/number
start_time: string | null; // ISO 8601 date string start_time: string | null; // ISO 8601 date string
remote_slug: string; // Venue slug origin_slug: string; // Venue slug
sport_slug: string; sport_slug: string;
players: MatchPlayer[]; // Array of 2-4 players depending on sport players: MatchPlayer[]; // Array of 2-4 players depending on sport
status: string; // e.g., "booked", "completed" status: string; // e.g., "booked", "completed"

@ -13,8 +13,8 @@
} }
interface Remote { interface Remote {
remote_slug: string; origin_slug: string;
remote_name: string; origin_name: string;
remote_members: RemoteMember[]; remote_members: RemoteMember[];
} }

@ -16,7 +16,7 @@
* Returns: unwrapped array of clubs * Returns: unwrapped array of clubs
*/ */
export interface AdminClub { export interface AdminClub {
club_id: number; facility_id: number;
name: string; name: string;
timezone: string; timezone: string;
courts: number; // Court count courts: number; // Court count
@ -30,7 +30,7 @@ export type AdminClubsResponse = AdminClub[];
*/ */
export interface AdminClubDetail { export interface AdminClubDetail {
club: { club: {
club_id: number; facility_id: number;
name: string; name: string;
timezone: string; timezone: string;
}; };
@ -61,7 +61,7 @@ export interface AdminSlot {
slot_id: number; // slot_instance_id slot_id: number; // slot_instance_id
court_id: number; court_id: number;
court_name: string; court_name: string;
club_id: number; facility_id: number;
starts_at: string; // ISO 8601 timestamp starts_at: string; // ISO 8601 timestamp
ends_at: string; // ISO 8601 timestamp ends_at: string; // ISO 8601 timestamp
booking: AdminSlotBooking; booking: AdminSlotBooking;
@ -91,7 +91,7 @@ export interface AdminSlotMeta {
} }
export interface AdminProviderInfo { export interface AdminProviderInfo {
remote_type: 'local' | 'fairplay'; provider: 'local' | 'fairplay';
capabilities: { capabilities: {
manages_slot_storage: boolean; manages_slot_storage: boolean;
supports_payment_verification: boolean; supports_payment_verification: boolean;

@ -42,8 +42,8 @@ export type SlotStatus = 'open' | 'booked' | 'held' | 'cancelled';
export interface CourtInfo { export interface CourtInfo {
court_id: number; court_id: number;
name: string; name: string;
club_id: number; facility_id: number;
club_name: string; facility_name: string;
} }
export interface ProviderInfo { export interface ProviderInfo {

@ -39,8 +39,8 @@ export interface BookingSlot {
court: { court: {
court_id: number; court_id: number;
name: string; name: string;
club_id: number; facility_id: number;
club_name: string; facility_name: string;
}; };
} }

@ -73,7 +73,7 @@ export interface ClubProfileSettings {
} }
export interface ClubProfile { export interface ClubProfile {
club_id: number; facility_id: number;
name: string; name: string;
timezone: string; // IANA timezone timezone: string; // IANA timezone
settings?: ClubProfileSettings; settings?: ClubProfileSettings;

@ -142,17 +142,17 @@ export const isCurrentUserGen = (
p: { remote_member_id: number } p: { remote_member_id: number }
) => currentUser && p.remote_member_id === currentUser.remote_member_id; ) => currentUser && p.remote_member_id === currentUser.remote_member_id;
export const getCurrentUser = (settings: UserSettings | null, remoteMembers: RemoteMemberWithRemoteSlugAndMaybeName[], remote_slug?: string): RemoteMemberWithNameAndRemoteSlug => { export const getCurrentUser = (settings: UserSettings | null, remoteMembers: RemoteMemberWithRemoteSlugAndMaybeName[], origin_slug?: string): RemoteMemberWithNameAndRemoteSlug => {
if (settings && settings.default_remote_member_id.remotes) { if (settings && settings.default_remote_member_id.remotes) {
const remoteSlug = remote_slug ?? settings.default_remote_sport.remote_slug; const remoteSlug = origin_slug ?? settings.default_remote_sport.facility_slug;
const entry = settings.default_remote_member_id.remotes.find( const entry = settings.default_remote_member_id.remotes.find(
r => r.remote_slug === remoteSlug r => r.origin_slug === remoteSlug
); );
const member = remoteMembers.find(m => m.remote_member_id === entry?.remote_member_id); const member = remoteMembers.find(m => m.remote_member_id === entry?.remote_member_id);
if (member) return { ...member, name: cleanName(member.full_account_str)}; if (member) return { ...member, name: cleanName(member.full_account_str)};
} }
// Use -Infinity to indicate no user (won't match any real remote_member_id) // Use -Infinity to indicate no user (won't match any real remote_member_id)
return { remote_member_id: -Infinity, full_account_str: 'You', remote_slug, name: 'You', self_reported_level: null, glicko_level: null }; return { remote_member_id: -Infinity, full_account_str: 'You', origin_slug, name: 'You', self_reported_level: null, glicko_level: null };
} }
export const getEnhancedPlayerGen = ( export const getEnhancedPlayerGen = (

@ -6,11 +6,11 @@ export function hasClaimedAnyCredentials(settings: UserSettings | null) {
export function getClaimedRemoteMember(settings: UserSettings | null) { export function getClaimedRemoteMember(settings: UserSettings | null) {
if (!settings || !settings.default_remote_member_id.remotes) return undefined; if (!settings || !settings.default_remote_member_id.remotes) return undefined;
const remoteMembers = settings.default_remote_member_id && settings.default_remote_member_id.remotes.filter(r => r.remote_slug === settings.default_remote_sport.remote_slug); const remoteMembers = settings.default_remote_member_id && settings.default_remote_member_id.remotes.filter(r => r.origin_slug === settings.default_remote_sport.facility_slug);
return remoteMembers && remoteMembers[0]; return remoteMembers && remoteMembers[0];
} }
export function hasClaimedCredentialsForRemote(settings: UserSettings | null, remoteSlug: string) { export function hasClaimedCredentialsForRemote(settings: UserSettings | null, remoteSlug: string) {
if (!settings || !settings.default_remote_member_id.remotes) return false; if (!settings || !settings.default_remote_member_id.remotes) return false;
return !!settings.default_remote_member_id.remotes.find(r => r.remote_slug === remoteSlug); return !!settings.default_remote_member_id.remotes.find(r => r.origin_slug === remoteSlug);
} }

Loading…
Cancel
Save