From 7bec61ae1d91fc6b68817b327586a6a9e9ba948e Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 07:35:44 -0800
Subject: [PATCH 01/14] feat: connect match profile page to api
---
src/pages/PlayerMatchProfilePage.tsx | 639 ++++++++++++++++++++-------
src/pages/PlayerSettingsPages.css | 212 +++++++++
2 files changed, 680 insertions(+), 171 deletions(-)
diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx
index b54a35e5..4ba6ca99 100644
--- a/src/pages/PlayerMatchProfilePage.tsx
+++ b/src/pages/PlayerMatchProfilePage.tsx
@@ -1,52 +1,479 @@
-import { useState } from "react";
-import { Check, Clock, MapPin, Target } from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { Loader2, Mail, MapPin, Phone, ShieldCheck, Target } from "lucide-react";
import MainLayout from "../components/MainLayout";
+import { fetchPlayerDetails } from "../api/playerHome";
+import { getStoredAuthToken } from "../services/authToken";
+import { getPersonalDetails } from "../services/auth";
+import { formatPhoneDisplay } from "../services/phone";
import "./PlayerSettingsPages.css";
-const availabilitySlots = [
- "Early mornings",
- "Weekday afternoons",
- "Weekday evenings",
- "Weekend mornings",
- "Weekend afternoons",
- "Weekend evenings",
-];
-
-const matchIntensities = [
- { id: "competitive", label: "Competitive play", description: "USTA league or tournament focused" },
- { id: "balanced", label: "Balanced", description: "Mix of rally sessions and competitive sets" },
- { id: "casual", label: "Casual hits", description: "Easy going hits with rally focus" },
-];
-
-const preferredFormats = [
- { id: "singles", label: "Singles" },
- { id: "doubles", label: "Doubles" },
- { id: "mixed", label: "Mixed doubles" },
- { id: "drills", label: "Live-ball drills" },
- { id: "fitness", label: "Cardio tennis" },
-];
+type RawMatchProfileRecord = {
+ userId?: number;
+ full_name?: string;
+ email?: string;
+ phone?: string;
+ profile_picture?: string;
+ skillLevel?: string;
+ availability?: string[] | string;
+ playerLocations?: string[] | string;
+ playerCourtLocations?: string[] | string;
+ lookingFor?: string[] | string;
+ gender?: string;
+ genderAdditionalText?: string;
+ about_me?: string;
+ isLevelConfirmed?: boolean;
+ verifiedLevelCount?: number | string;
+ matchFrequency?: string;
+ match_frequency?: string;
+ playerExperience?: string;
+ player_experience?: string;
+ updated_at?: string;
+ [key: string]: unknown;
+};
+
+type PersonalDetailsRecord = {
+ id?: number;
+ full_name?: string;
+ email?: string;
+ phone?: string;
+ profile_picture?: string;
+ about_me?: string;
+};
+
+type MatchProfile = {
+ id?: string;
+ name: string;
+ initials: string;
+ about?: string;
+ location?: string;
+ level?: string;
+ levelVerified: boolean;
+ verificationCount?: number;
+ availability: string[];
+ matchGoals: string[];
+ localCourts: string[];
+ locations: string[];
+ matchFrequency?: string;
+ experience?: string;
+ gender?: string;
+ email?: string;
+ phone?: string;
+ profileImageUrl?: string;
+ updatedAtLabel?: string;
+};
+
+type Status = "idle" | "loading" | "ready" | "error";
+
+const ensureStringArray = (value: unknown): string[] => {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
+ .filter((item): item is string => item.length > 0);
+ }
+ if (typeof value === "string") {
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return [];
+ }
+ return trimmed.includes(",")
+ ? trimmed
+ .split(",")
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0)
+ : [trimmed];
+ }
+ return [];
+};
+
+const pickString = (...values: unknown[]): string | undefined => {
+ for (const value of values) {
+ if (typeof value === "string") {
+ const trimmed = value.trim();
+ if (trimmed.length > 0) {
+ return trimmed;
+ }
+ }
+ }
+ return undefined;
+};
+
+const toInitials = (name?: string, email?: string): string => {
+ if (name) {
+ const parts = name
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean);
+ if (parts.length >= 2) {
+ return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
+ }
+ if (parts.length === 1) {
+ return parts[0].slice(0, 2).toUpperCase();
+ }
+ }
+ if (email) {
+ return email.slice(0, 2).toUpperCase();
+ }
+ return "MP";
+};
+
+const parseNumber = (value: unknown): number | undefined => {
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+ if (typeof value === "string") {
+ const parsed = Number.parseInt(value, 10);
+ if (Number.isFinite(parsed)) {
+ return parsed;
+ }
+ }
+ return undefined;
+};
+
+const normalizeGender = (primary?: string, fallback?: string): string | undefined => {
+ const source = pickString(primary, fallback);
+ if (!source) {
+ return undefined;
+ }
+ return source
+ .split(" ")
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())
+ .join(" ");
+};
+
+const formatDateLabel = (value?: string): string | undefined => {
+ if (!value) {
+ return undefined;
+ }
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return undefined;
+ }
+ return new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ }).format(date);
+};
+
+const normalizeMatchProfile = (
+ raw: RawMatchProfileRecord | null,
+ personal: PersonalDetailsRecord | null,
+): MatchProfile | null => {
+ if (!raw && !personal) {
+ return null;
+ }
+
+ const availability = ensureStringArray(raw?.availability);
+ const matchGoals = ensureStringArray(raw?.lookingFor);
+ const localCourts = ensureStringArray(raw?.playerCourtLocations);
+ const locations = ensureStringArray(raw?.playerLocations);
+ const about = pickString(raw?.about_me, personal?.about_me);
+ const name = pickString(raw?.full_name, personal?.full_name) ?? "Matchplay player";
+ const email = pickString(raw?.email, personal?.email);
+ const phone = pickString(raw?.phone, personal?.phone);
+ const level = pickString(raw?.skillLevel);
+ const matchFrequency = pickString(raw?.matchFrequency, raw?.match_frequency);
+ const experience = pickString(raw?.playerExperience, raw?.player_experience);
+ const gender = normalizeGender(raw?.gender, raw?.genderAdditionalText);
+ const verificationCount = parseNumber(raw?.verifiedLevelCount);
+ const profileImageUrl = pickString(raw?.profile_picture, personal?.profile_picture);
+ const location = locations[0];
+ const initials = toInitials(name, email);
+
+ return {
+ id: raw?.userId ? String(raw.userId) : personal?.id ? String(personal.id) : undefined,
+ name,
+ initials,
+ about,
+ location,
+ level,
+ levelVerified: Boolean(raw?.isLevelConfirmed),
+ verificationCount,
+ availability,
+ matchGoals,
+ localCourts,
+ locations,
+ matchFrequency,
+ experience,
+ gender,
+ email,
+ phone,
+ profileImageUrl,
+ updatedAtLabel: formatDateLabel(raw?.updated_at),
+ };
+};
const PlayerMatchProfilePage = () => {
- const [selectedAvailability, setSelectedAvailability] = useState([
- "Weekday evenings",
- "Weekend mornings",
- ]);
- const [intensity, setIntensity] = useState("balanced");
- const [formats, setFormats] = useState(["singles", "doubles"]);
- const [homeBase, setHomeBase] = useState("Austin Tennis Center");
-
- const toggleAvailability = (slot: string) => {
- setSelectedAvailability((current) =>
- current.includes(slot) ? current.filter((item) => item !== slot) : [...current, slot]
- );
+ const [profile, setProfile] = useState(null);
+ const [status, setStatus] = useState("idle");
+ const [error, setError] = useState(null);
+ const [refreshIndex, setRefreshIndex] = useState(0);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadProfile = async () => {
+ const token = getStoredAuthToken({ preferScheme: "Bearer" });
+ if (!token) {
+ setStatus("error");
+ setError("Please sign in to view your match profile.");
+ return;
+ }
+
+ setStatus("loading");
+ setError(null);
+ try {
+ const personalDetails = (await getPersonalDetails()) as PersonalDetailsRecord | null;
+ if (!personalDetails?.id) {
+ throw new Error("We couldn’t determine your player profile ID.");
+ }
+ const rawProfile = (await fetchPlayerDetails({
+ token,
+ userId: personalDetails.id,
+ })) as RawMatchProfileRecord | null;
+
+ if (cancelled) {
+ return;
+ }
+
+ const normalized = normalizeMatchProfile(rawProfile, personalDetails);
+ setProfile(normalized);
+ setStatus("ready");
+ } catch (requestError) {
+ if (cancelled) {
+ return;
+ }
+ console.error("Failed to load match profile", requestError);
+ setStatus("error");
+ setError(
+ requestError instanceof Error
+ ? requestError.message
+ : "We couldn’t load your match profile. Please try again.",
+ );
+ }
+ };
+
+ loadProfile();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [refreshIndex]);
+
+ const locationLabel = useMemo(() => {
+ if (!profile) {
+ return "Location not shared";
+ }
+ if (profile.location) {
+ return profile.location;
+ }
+ if (profile.locations.length > 0) {
+ return profile.locations[0];
+ }
+ return "Location not shared";
+ }, [profile]);
+
+ const handleRetry = () => {
+ setRefreshIndex((index) => index + 1);
};
- const toggleFormat = (id: string) => {
- setFormats((current) =>
- current.includes(id) ? current.filter((item) => item !== id) : [...current, id]
+ const availabilityContent = profile?.availability.length ? (
+
+ {profile.availability.map((slot) => (
+ -
+ {slot}
+
+ ))}
+
+ ) : (
+ No availability shared yet.
+ );
+
+ const matchGoalsContent = profile?.matchGoals.length ? (
+
+ {profile.matchGoals.map((goal) => (
+ -
+ {goal}
+
+ ))}
+
+ ) : (
+ Add the sessions you’re hoping to schedule.
+ );
+
+ const localCourtsContent = profile?.localCourts.length ? (
+
+ {profile.localCourts.map((court) => (
+ -
+
+ {court}
+
+ ))}
+
+ ) : (
+ Share the courts where you usually meet players.
+ );
+
+ const contactDetails = (
+
+ {profile?.email ? (
+ -
+
+ {profile.email}
+
+ ) : null}
+ {profile?.phone ? (
+ -
+
+ {formatPhoneDisplay(profile.phone)}
+
+ ) : null}
+
+ );
+
+ let bodyContent: JSX.Element | null = null;
+
+ if (status === "loading" || status === "idle") {
+ bodyContent = (
+
+
+
Loading your match profile…
+
);
- };
+ } else if (status === "error") {
+ bodyContent = (
+
+
{error ?? "We couldn’t load your match profile."}
+
+
+ );
+ } else if (profile) {
+ bodyContent = (
+
+
+
+
+
+
+ {profile.profileImageUrl ? (
+

+ ) : (
+
{profile.initials}
+ )}
+
+
+
Match profile owner
+
{profile.name}
+
{locationLabel}
+
+ {profile.level ? (
+
+ NTRP {profile.level}
+
+ ) : null}
+
+
+ {profile.updatedAtLabel ? `Last updated ${profile.updatedAtLabel}` : "Updated recently"}
+
+
+ {profile.about ?? "Share a short bio so partners know what you’re looking for."}
+
+
+
+
- Level verification
+ -
+ {profile.levelVerified ? (
+
+
+ Verified by the community
+
+ ) : (
+ "Level not verified yet"
+ )}
+ {profile.verificationCount ? (
+
+ {profile.verificationCount} {profile.verificationCount === 1 ? "player" : "players"} confirmed this level.
+
+ ) : null}
+
+
+
+
- Match frequency
+ - {profile.matchFrequency ?? "Not shared"}
+
+
+
- Experience
+ - {profile.experience ?? "Not shared"}
+
+
+
- Gender
+ - {profile.gender ?? "Not shared"}
+
+
+
+
+
+
+
Match availability
+
+ These are the time windows you shared while building your profile.
+
+
+ {availabilityContent}
+
+
+
+
+
Match goals & session vibes
+
+ We use this to suggest compatible partners and session formats.
+
+
+ {matchGoalsContent}
+
+
Preferred locations
+ {profile.locations.length ? (
+
+ {profile.locations.map((spot) => (
+ - {spot}
+ ))}
+
+ ) : (
+
No preferred locations shared yet.
+ )}
+
+
+
+
+
+
+
+ );
+ } else {
+ bodyContent = (
+
+
You haven’t created a match profile yet.
+
+
+ );
+ }
return (
@@ -63,137 +490,7 @@ const PlayerMatchProfilePage = () => {
-
-
-
-
-
-
-
- Match availability
-
-
Choose the windows when you're generally open to play.
-
-
- {availabilitySlots.map((slot) => {
- const selected = selectedAvailability.includes(slot);
- return (
-
- );
- })}
-
-
-
-
-
-
Match intensity
-
Let others know how competitive you'd like sessions to be.
-
-
- {matchIntensities.map((option) => {
- const selected = intensity === option.id;
- return (
-
- );
- })}
-
-
-
-
-
-
Preferred formats
-
- Highlight the type of play you're hoping to schedule with new connections.
-
-
-
- {preferredFormats.map((format) => {
- const selected = formats.includes(format.id);
- return (
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
-
+ {bodyContent}
diff --git a/src/pages/PlayerSettingsPages.css b/src/pages/PlayerSettingsPages.css
index 31344ef1..c6edc6b1 100644
--- a/src/pages/PlayerSettingsPages.css
+++ b/src/pages/PlayerSettingsPages.css
@@ -189,6 +189,37 @@
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
}
+.match-card--summary {
+ gap: 16px;
+}
+
+.match-card--state {
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ gap: 12px;
+}
+
+.match-card__spinner {
+ color: #2563eb;
+}
+
+.match-card__retry {
+ appearance: none;
+ border: none;
+ background: #2563eb;
+ color: #fff;
+ font-weight: 600;
+ padding: 10px 20px;
+ border-radius: 999px;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+}
+
+.match-card__retry:hover {
+ opacity: 0.9;
+}
+
.match-card__heading {
display: flex;
flex-direction: column;
@@ -212,6 +243,112 @@
color: #64748b;
}
+.match-profile-summary {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.match-profile-avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: 20px;
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(14, 165, 233, 0.2));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 20px;
+ color: #4338ca;
+ overflow: hidden;
+}
+
+.match-profile-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.match-profile-summary__content h2 {
+ margin: 0;
+ font-size: 22px;
+ color: #0f172a;
+}
+
+.match-profile-summary__content p {
+ margin: 2px 0 0;
+ color: #475569;
+ font-weight: 600;
+}
+
+.match-profile-summary__eyebrow {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #64748b;
+ margin: 0 0 4px;
+}
+
+.match-profile-summary__status {
+ margin: 0;
+ font-size: 13px;
+ color: #475569;
+}
+
+.match-profile-about {
+ margin: 0;
+ font-size: 15px;
+ line-height: 1.7;
+ color: #0f172a;
+}
+
+.match-profile-pill {
+ margin-left: auto;
+ padding: 8px 14px;
+ border-radius: 999px;
+ font-size: 13px;
+ font-weight: 700;
+ background: rgba(15, 118, 110, 0.1);
+ color: #0f766e;
+}
+
+.match-profile-meta {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 16px;
+ margin: 0;
+}
+
+.match-profile-meta dt {
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ color: #94a3b8;
+ margin-bottom: 4px;
+}
+
+.match-profile-meta dd {
+ margin: 0;
+ font-size: 15px;
+ color: #0f172a;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.match-profile-meta__verified {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 700;
+ color: #047857;
+}
+
+.match-profile-meta small {
+ color: #64748b;
+}
+
.match-availability {
display: grid;
gap: 16px;
@@ -315,6 +452,55 @@
gap: 12px;
}
+.match-chip-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.match-chip {
+ padding: 10px 16px;
+ border-radius: 999px;
+ background: #f8fafc;
+ color: #0f172a;
+ font-weight: 600;
+ border: 1px solid rgba(148, 163, 184, 0.5);
+}
+
+.match-chip--accent {
+ background: rgba(99, 102, 241, 0.12);
+ border-color: rgba(99, 102, 241, 0.3);
+ color: #4338ca;
+}
+
+.match-profile-empty {
+ margin: 0;
+ font-size: 14px;
+ color: #94a3b8;
+}
+
+.match-profile-locations {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.match-profile-locations h3 {
+ margin: 0;
+ font-size: 15px;
+ color: #0f172a;
+}
+
+.match-profile-locations ul {
+ margin: 0;
+ padding-left: 18px;
+ color: #475569;
+ font-weight: 600;
+}
+
.match-format-chip {
padding: 10px 18px;
border-radius: 999px;
@@ -357,6 +543,11 @@
gap: 16px;
}
+.match-sidebar__card--info {
+ border-color: rgba(99, 102, 241, 0.28);
+ background: linear-gradient(135deg, rgba(224, 231, 255, 0.6), rgba(199, 210, 254, 0.7));
+}
+
.match-sidebar__title {
margin: 0;
display: inline-flex;
@@ -432,6 +623,27 @@
color: #475569;
}
+.match-sidebar__list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.match-sidebar__list li {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ color: #0f172a;
+ font-weight: 600;
+}
+
+.match-sidebar__list svg {
+ color: #2563eb;
+}
+
/* Payment methods */
.billing-grid {
From 340ea46eb02f1486137cc3d03ee3bb5b33a6e114 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 07:52:02 -0800
Subject: [PATCH 02/14] Fix match profile fetch
---
src/api/playerHome.ts | 5 +----
src/pages/PlayerMatchProfilePage.tsx | 9 ++++++++-
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 3597c2c4..76c63190 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -332,12 +332,9 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
request>(
- "/player/surveys/getchecklocation/specific_user",
+ `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
{
token,
- query: {
- userId,
- },
},
);
diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx
index 4ba6ca99..0f4ba909 100644
--- a/src/pages/PlayerMatchProfilePage.tsx
+++ b/src/pages/PlayerMatchProfilePage.tsx
@@ -160,7 +160,7 @@ const normalizeMatchProfile = (
raw: RawMatchProfileRecord | null,
personal: PersonalDetailsRecord | null,
): MatchProfile | null => {
- if (!raw && !personal) {
+ if (!raw) {
return null;
}
@@ -244,6 +244,13 @@ const PlayerMatchProfilePage = () => {
if (cancelled) {
return;
}
+ const statusCode = (requestError as { status?: number })?.status;
+ if (statusCode === 404) {
+ setProfile(null);
+ setStatus("ready");
+ setError(null);
+ return;
+ }
console.error("Failed to load match profile", requestError);
setStatus("error");
setError(
From 292df0bf41a9baf2113d3da01438af5cfcb89ca5 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 08:00:10 -0800
Subject: [PATCH 03/14] Retry match profile endpoint
---
src/api/playerHome.ts | 39 ++++++++++++++++++++++++++++++++++-----
1 file changed, 34 insertions(+), 5 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 76c63190..133a82e3 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -330,13 +330,42 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
}
-export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
- request>(
- `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
+export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
+ const attempts: Array<{
+ path: string;
+ query?: Record;
+ }> = [
{
- token,
+ path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
},
- );
+ {
+ path: "/player/surveys/getchecklocation/specific_user",
+ query: { userId },
+ },
+ ];
+
+ let lastError: Error & { status?: number } | undefined;
+
+ for (const attempt of attempts) {
+ try {
+ return await request>(attempt.path, {
+ token,
+ query: attempt.query,
+ });
+ } catch (error) {
+ lastError = error as Error & { status?: number };
+ if (lastError?.status !== 404) {
+ throw lastError;
+ }
+ }
+ }
+
+ if (lastError) {
+ throw lastError;
+ }
+
+ throw new Error("Unable to load match profile");
+};
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
token: string;
From 9c194a7f24ea9652036de0dbd58caf90adab9e67 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 08:21:13 -0800
Subject: [PATCH 04/14] Allow creating a match profile from settings
---
src/api/playerHome.ts | 28 ++++
src/components/players/MatchProfileModal.tsx | 27 +++-
src/pages/PlayerMatchProfilePage.tsx | 139 +++++++++++++++++--
src/pages/PlayerSettingsPages.css | 49 +++++--
4 files changed, 222 insertions(+), 21 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 133a82e3..86c35120 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -367,6 +367,34 @@ export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsPa
throw new Error("Unable to load match profile");
};
+export interface PlayerMatchProfilePayload {
+ about_me?: string;
+ skillLevel?: string;
+ lookingFor?: string[];
+ availability?: string[];
+ playerCourtLocations?: string[];
+ gender?: string;
+}
+
+export interface SavePlayerMatchProfileParams extends PlayerTokenOnlyParams {
+ userId: number | string;
+ profile: PlayerMatchProfilePayload;
+}
+
+export const savePlayerMatchProfile = async ({
+ token,
+ userId,
+ profile,
+}: SavePlayerMatchProfileParams) =>
+ request>("/player/surveys/getchecklocation", {
+ method: "POST",
+ token,
+ body: buildBody({
+ userId,
+ ...profile,
+ }),
+ });
+
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
token: string;
search?: string;
diff --git a/src/components/players/MatchProfileModal.tsx b/src/components/players/MatchProfileModal.tsx
index dddf4993..bc24e521 100644
--- a/src/components/players/MatchProfileModal.tsx
+++ b/src/components/players/MatchProfileModal.tsx
@@ -91,6 +91,9 @@ type MatchProfileModalProps = {
onClose: () => void;
onComplete: (profile: MatchProfileDetails) => void;
initialProfile?: MatchProfileDetails | null;
+ isSubmitting?: boolean;
+ submitError?: string | null;
+ submitLabel?: string;
};
const DEFAULT_LEVEL = "3.0";
@@ -147,7 +150,15 @@ const loadGooglePlacesScript = () => {
return placesScriptPromise;
};
-const MatchProfileModal = ({ isOpen, onClose, onComplete, initialProfile }: MatchProfileModalProps) => {
+const MatchProfileModal = ({
+ isOpen,
+ onClose,
+ onComplete,
+ initialProfile,
+ isSubmitting = false,
+ submitError = null,
+ submitLabel,
+}: MatchProfileModalProps) => {
const titleId = useId();
const descriptionId = useId();
const fileInputRef = useRef(null);
@@ -255,6 +266,9 @@ const MatchProfileModal = ({ isOpen, onClose, onComplete, initialProfile }: Matc
);
}, [about, gender, availability, requiresCourtVerification]);
+ const isFinalSubmitDisabled = isSubmitDisabled || isSubmitting;
+ const finalSubmitLabel = isSubmitting ? "Saving…" : submitLabel ?? "Save profile";
+
const showCompletionError = touched && isSubmitDisabled;
const handleSubmit = (event: FormEvent) => {
@@ -636,6 +650,11 @@ const MatchProfileModal = ({ isOpen, onClose, onComplete, initialProfile }: Matc
Please complete your full profile before saving.
)}
+ {submitError ? (
+
+ {submitError}
+
+ ) : null}
diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx
index 0f4ba909..46a74441 100644
--- a/src/pages/PlayerMatchProfilePage.tsx
+++ b/src/pages/PlayerMatchProfilePage.tsx
@@ -1,7 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { Loader2, Mail, MapPin, Phone, ShieldCheck, Target } from "lucide-react";
import MainLayout from "../components/MainLayout";
-import { fetchPlayerDetails } from "../api/playerHome";
+import MatchProfileModal, { type MatchProfileDetails } from "../components/players/MatchProfileModal";
+import {
+ fetchPlayerDetails,
+ savePlayerMatchProfile,
+ type PlayerMatchProfilePayload,
+} from "../api/playerHome";
import { getStoredAuthToken } from "../services/authToken";
import { getPersonalDetails } from "../services/auth";
import { formatPhoneDisplay } from "../services/phone";
@@ -204,11 +209,59 @@ const normalizeMatchProfile = (
};
};
+const DEFAULT_MODAL_LEVEL = "3.0";
+
+const matchProfileToModalDetails = (profile: MatchProfile | null): MatchProfileDetails | null => {
+ if (!profile) {
+ return null;
+ }
+ return {
+ about: profile.about ?? "",
+ level: profile.level ?? DEFAULT_MODAL_LEVEL,
+ playStyles: [...profile.matchGoals],
+ gender: profile.gender ?? "",
+ localCourts: profile.localCourts.join(", "),
+ availability: [...profile.availability],
+ };
+};
+
+const splitCommaList = (value: string) =>
+ value
+ .split(",")
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0);
+
+const matchProfileDetailsToPayload = (details: MatchProfileDetails): PlayerMatchProfilePayload => {
+ const trimmedAbout = details.about.trim();
+ const trimmedLevel = details.level?.trim();
+ const trimmedGender = details.gender?.trim();
+ const playStyles = details.playStyles
+ .map((style) => style.trim())
+ .filter((style) => style.length > 0);
+ const availability = details.availability
+ .map((slot) => slot.trim())
+ .filter((slot) => slot.length > 0);
+ const playerCourts = splitCommaList(details.localCourts);
+
+ return {
+ about_me: trimmedAbout || undefined,
+ skillLevel: trimmedLevel || undefined,
+ gender: trimmedGender || undefined,
+ lookingFor: playStyles.length ? playStyles : undefined,
+ availability: availability.length ? availability : undefined,
+ playerCourtLocations: playerCourts.length ? playerCourts : undefined,
+ };
+};
+
const PlayerMatchProfilePage = () => {
const [profile, setProfile] = useState(null);
+ const [personalDetails, setPersonalDetails] = useState(null);
const [status, setStatus] = useState("idle");
const [error, setError] = useState(null);
const [refreshIndex, setRefreshIndex] = useState(0);
+ const [isProfileModalOpen, setProfileModalOpen] = useState(false);
+ const [isSavingProfile, setSavingProfile] = useState(false);
+ const [modalError, setModalError] = useState(null);
useEffect(() => {
let cancelled = false;
@@ -224,20 +277,21 @@ const PlayerMatchProfilePage = () => {
setStatus("loading");
setError(null);
try {
- const personalDetails = (await getPersonalDetails()) as PersonalDetailsRecord | null;
- if (!personalDetails?.id) {
+ const accountDetails = (await getPersonalDetails()) as PersonalDetailsRecord | null;
+ if (!accountDetails?.id) {
throw new Error("We couldn’t determine your player profile ID.");
}
+ setPersonalDetails(accountDetails);
const rawProfile = (await fetchPlayerDetails({
token,
- userId: personalDetails.id,
+ userId: accountDetails.id,
})) as RawMatchProfileRecord | null;
if (cancelled) {
return;
}
- const normalized = normalizeMatchProfile(rawProfile, personalDetails);
+ const normalized = normalizeMatchProfile(rawProfile, accountDetails);
setProfile(normalized);
setStatus("ready");
} catch (requestError) {
@@ -285,6 +339,41 @@ const PlayerMatchProfilePage = () => {
setRefreshIndex((index) => index + 1);
};
+ const modalInitialProfile = useMemo(() => matchProfileToModalDetails(profile), [profile]);
+
+ const handleProfileModalComplete = async (details: MatchProfileDetails) => {
+ if (isSavingProfile) {
+ return;
+ }
+ const userId = personalDetails?.id;
+ if (!userId) {
+ setModalError("We couldn’t determine your player profile ID.");
+ return;
+ }
+ const token = getStoredAuthToken({ preferScheme: "Bearer" });
+ if (!token) {
+ setModalError("Please sign in again to save your match profile.");
+ return;
+ }
+
+ setSavingProfile(true);
+ setModalError(null);
+
+ try {
+ const payload = matchProfileDetailsToPayload(details);
+ await savePlayerMatchProfile({ token, userId, profile: payload });
+ setProfileModalOpen(false);
+ setRefreshIndex((index) => index + 1);
+ } catch (saveError) {
+ console.error("Failed to save match profile", saveError);
+ setModalError(
+ saveError instanceof Error ? saveError.message : "We couldn’t save your match profile. Please try again.",
+ );
+ } finally {
+ setSavingProfile(false);
+ }
+ };
+
const availabilityContent = profile?.availability.length ? (
{profile.availability.map((slot) => (
@@ -352,7 +441,7 @@ const PlayerMatchProfilePage = () => {
bodyContent = (
{error ?? "We couldn’t load your match profile."}
-
+
Try again
@@ -475,9 +564,25 @@ const PlayerMatchProfilePage = () => {
bodyContent = (
You haven’t created a match profile yet.
-
- Refresh
-
+
+ Share your level, vibe, and preferred courts so other players know how to connect.
+
+
+ {
+ setModalError(null);
+ setProfileModalOpen(true);
+ }}
+ disabled={!personalDetails?.id || isSavingProfile}
+ >
+ Build my match profile
+
+
+ Refresh
+
+
);
}
@@ -500,6 +605,22 @@ const PlayerMatchProfilePage = () => {
{bodyContent}
+
+ {
+ if (isSavingProfile) {
+ return;
+ }
+ setProfileModalOpen(false);
+ setModalError(null);
+ }}
+ initialProfile={modalInitialProfile}
+ isSubmitting={isSavingProfile}
+ submitError={modalError}
+ submitLabel={profile ? "Update profile" : "Save profile"}
+ onComplete={handleProfileModalComplete}
+ />
);
};
diff --git a/src/pages/PlayerSettingsPages.css b/src/pages/PlayerSettingsPages.css
index c6edc6b1..e8ca5464 100644
--- a/src/pages/PlayerSettingsPages.css
+++ b/src/pages/PlayerSettingsPages.css
@@ -200,15 +200,9 @@
gap: 12px;
}
-.match-card__spinner {
- color: #2563eb;
-}
-
-.match-card__retry {
+.match-card__button {
appearance: none;
border: none;
- background: #2563eb;
- color: #fff;
font-weight: 600;
padding: 10px 20px;
border-radius: 999px;
@@ -216,10 +210,49 @@
transition: opacity 0.2s ease;
}
-.match-card__retry:hover {
+.match-card__button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.match-card__spinner {
+ color: #2563eb;
+}
+
+.match-card__retry {
+ background: #2563eb;
+ color: #fff;
+}
+
+.match-card__retry:not(:disabled):hover {
opacity: 0.9;
}
+.match-card__action {
+ background: linear-gradient(120deg, #0f172a, #1d4ed8);
+ color: #fff;
+ box-shadow: 0 6px 18px rgba(37, 99, 235, 0.25);
+}
+
+.match-card__action:not(:disabled):hover {
+ opacity: 0.95;
+}
+
+.match-card__actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.match-card__helper {
+ margin: 0 auto;
+ max-width: 460px;
+ color: #475569;
+ font-size: 14px;
+}
+
.match-card__heading {
display: flex;
flex-direction: column;
From 9091cc4517a6284fc60e3a5464c162c7e2516c90 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 08:31:00 -0800
Subject: [PATCH 05/14] Harden match profile save endpoint
---
src/api/playerHome.ts | 71 +++++++++++++++++++++++++++++++++++++------
1 file changed, 62 insertions(+), 9 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 86c35120..717d62b2 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -385,15 +385,68 @@ export const savePlayerMatchProfile = async ({
token,
userId,
profile,
-}: SavePlayerMatchProfileParams) =>
- request>("/player/surveys/getchecklocation", {
- method: "POST",
- token,
- body: buildBody({
- userId,
- ...profile,
- }),
- });
+}: SavePlayerMatchProfileParams) => {
+ const attempts: Array<{
+ path: string;
+ method?: string;
+ query?: Record;
+ includeUserId?: boolean;
+ }> = [
+ {
+ path: "/player/surveys/getchecklocation",
+ method: "POST",
+ includeUserId: true,
+ },
+ {
+ path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
+ method: "POST",
+ includeUserId: true,
+ },
+ {
+ path: "/player/surveys/getchecklocation/specific_user",
+ method: "POST",
+ query: { userId },
+ includeUserId: true,
+ },
+ {
+ path: `/player/surveys/getchecklocation/${encodeURIComponent(userId)}`,
+ method: "PATCH",
+ includeUserId: true,
+ },
+ ];
+
+ let lastError: Error & { status?: number } | undefined;
+
+ for (const attempt of attempts) {
+ try {
+ return await request>(attempt.path, {
+ method: attempt.method ?? "POST",
+ token,
+ query: attempt.query,
+ body: buildBody({
+ ...(attempt.includeUserId
+ ? {
+ userId,
+ user_id: userId,
+ }
+ : {}),
+ ...profile,
+ }),
+ });
+ } catch (error) {
+ lastError = error as Error & { status?: number };
+ if (lastError?.status !== 404) {
+ throw lastError;
+ }
+ }
+ }
+
+ if (lastError) {
+ throw lastError;
+ }
+
+ throw new Error("Unable to save match profile");
+};
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
token: string;
From ee2680830b30802b8f6d958882659af45c419872 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 08:37:09 -0800
Subject: [PATCH 06/14] Use existing Google Places key for match profile
---
src/components/players/MatchProfileModal.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/players/MatchProfileModal.tsx b/src/components/players/MatchProfileModal.tsx
index bc24e521..d121b3bf 100644
--- a/src/components/players/MatchProfileModal.tsx
+++ b/src/components/players/MatchProfileModal.tsx
@@ -111,7 +111,9 @@ type PlacesStatus = "idle" | "loading" | "ready" | "unavailable";
let placesScriptPromise: Promise | null = null;
-const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY as string | undefined;
+const GOOGLE_PLACES_API_KEY =
+ (import.meta.env.VITE_GOOGLE_PLACES_API_KEY as string | undefined) ??
+ (import.meta.env.VITE_GOOGLE_API_KEY as string | undefined);
const loadGooglePlacesScript = () => {
if (typeof window === "undefined") {
From 0de71e7149fe671bae3a05bfad8cfe8f7927aa03 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 08:53:56 -0800
Subject: [PATCH 07/14] Expand match profile API fallbacks
---
src/api/playerHome.ts | 56 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 55 insertions(+), 1 deletion(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 717d62b2..fea0526f 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -331,16 +331,48 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
}
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
+ const userQuery = { userId } as Record;
+ const userBody = { userId, user_id: userId };
const attempts: Array<{
path: string;
+ method?: string;
query?: Record;
+ body?: Record;
}> = [
{
path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
},
{
path: "/player/surveys/getchecklocation/specific_user",
- query: { userId },
+ query: userQuery,
+ },
+ {
+ path: `/player/surveys/getchecklocation/${encodeURIComponent(userId)}`,
+ },
+ {
+ path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
+ },
+ {
+ path: "/player/getchecklocation/specific_user",
+ query: userQuery,
+ },
+ {
+ path: `/player/getchecklocation/${encodeURIComponent(userId)}`,
+ },
+ {
+ path: "/player/getchecklocation/specific_user",
+ method: "POST",
+ body: userBody,
+ },
+ {
+ path: "/player/surveys/getchecklocation/specific_user",
+ method: "POST",
+ body: userBody,
+ },
+ {
+ path: "/player/getchecklocation",
+ method: "POST",
+ body: userBody,
},
];
@@ -349,8 +381,10 @@ export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsPa
for (const attempt of attempts) {
try {
return await request>(attempt.path, {
+ method: attempt.method ?? "GET",
token,
query: attempt.query,
+ body: attempt.body ? buildBody(attempt.body) : undefined,
});
} catch (error) {
lastError = error as Error & { status?: number };
@@ -392,6 +426,26 @@ export const savePlayerMatchProfile = async ({
query?: Record;
includeUserId?: boolean;
}> = [
+ {
+ path: "/player/getchecklocation",
+ method: "POST",
+ includeUserId: true,
+ },
+ {
+ path: `/player/getchecklocation/${encodeURIComponent(userId)}`,
+ method: "PATCH",
+ includeUserId: true,
+ },
+ {
+ path: "/player/getchecklocation/specific_user",
+ method: "POST",
+ includeUserId: true,
+ },
+ {
+ path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
+ method: "POST",
+ includeUserId: true,
+ },
{
path: "/player/surveys/getchecklocation",
method: "POST",
From 3db1ee448066e82cca4bcd781ee451702fa172cd Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 09:00:24 -0800
Subject: [PATCH 08/14] Expand match profile endpoint fallbacks
---
src/api/playerHome.ts | 62 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 60 insertions(+), 2 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index fea0526f..980f633f 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -331,7 +331,7 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
}
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
- const userQuery = { userId } as Record;
+ const userQuery = { userId, user_id: userId } as Record;
const userBody = { userId, user_id: userId };
const attempts: Array<{
path: string;
@@ -342,38 +342,75 @@ export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsPa
{
path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
},
+ {
+ path: `/player/surveys/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
+ },
{
path: "/player/surveys/getchecklocation/specific_user",
query: userQuery,
},
+ {
+ path: "/player/surveys/getchecklocation/specific-user",
+ query: userQuery,
+ },
{
path: `/player/surveys/getchecklocation/${encodeURIComponent(userId)}`,
},
+ {
+ path: "/player/surveys/getchecklocation",
+ query: userQuery,
+ },
{
path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
},
+ {
+ path: `/player/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
+ },
{
path: "/player/getchecklocation/specific_user",
query: userQuery,
},
+ {
+ path: "/player/getchecklocation/specific-user",
+ query: userQuery,
+ },
{
path: `/player/getchecklocation/${encodeURIComponent(userId)}`,
},
+ {
+ path: "/player/getchecklocation",
+ query: userQuery,
+ },
{
path: "/player/getchecklocation/specific_user",
method: "POST",
body: userBody,
},
+ {
+ path: "/player/getchecklocation/specific-user",
+ method: "POST",
+ body: userBody,
+ },
{
path: "/player/surveys/getchecklocation/specific_user",
method: "POST",
body: userBody,
},
+ {
+ path: "/player/surveys/getchecklocation/specific-user",
+ method: "POST",
+ body: userBody,
+ },
{
path: "/player/getchecklocation",
method: "POST",
body: userBody,
},
+ {
+ path: "/player/surveys/getchecklocation",
+ method: "POST",
+ body: userBody,
+ },
];
let lastError: Error & { status?: number } | undefined;
@@ -441,11 +478,21 @@ export const savePlayerMatchProfile = async ({
method: "POST",
includeUserId: true,
},
+ {
+ path: "/player/getchecklocation/specific-user",
+ method: "POST",
+ includeUserId: true,
+ },
{
path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
method: "POST",
includeUserId: true,
},
+ {
+ path: `/player/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
+ method: "POST",
+ includeUserId: true,
+ },
{
path: "/player/surveys/getchecklocation",
method: "POST",
@@ -459,7 +506,18 @@ export const savePlayerMatchProfile = async ({
{
path: "/player/surveys/getchecklocation/specific_user",
method: "POST",
- query: { userId },
+ query: { userId, user_id: userId },
+ includeUserId: true,
+ },
+ {
+ path: "/player/surveys/getchecklocation/specific-user",
+ method: "POST",
+ query: { userId, user_id: userId },
+ includeUserId: true,
+ },
+ {
+ path: `/player/surveys/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
+ method: "POST",
includeUserId: true,
},
{
From cae1d71ccc8abcec737baa7ebed87c6a53b2c812 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 09:34:18 -0800
Subject: [PATCH 09/14] Expand match profile endpoint permutations
---
src/api/playerHome.ts | 198 +++++++++++++-----------------------------
1 file changed, 58 insertions(+), 140 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 980f633f..2ae52c32 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -330,88 +330,50 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
}
+const MATCH_PROFILE_ROUTE_PREFIXES = ["/player/surveys", "/player"] as const;
+const MATCH_PROFILE_ROUTE_RESOURCES = ["getchecklocation", "get-check-location", "get_check_location"] as const;
+const MATCH_PROFILE_SPECIFIC_SEGMENTS = ["specific_user", "specific-user"] as const;
+
+const MATCH_PROFILE_BASE_ROUTES = MATCH_PROFILE_ROUTE_PREFIXES.flatMap((prefix) =>
+ MATCH_PROFILE_ROUTE_RESOURCES.map((resource) => `${prefix}/${resource}`),
+);
+
+const shouldRetryMatchProfileError = (error?: { status?: number }) =>
+ error?.status === 404 || error?.status === 500;
+
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
const userQuery = { userId, user_id: userId } as Record;
const userBody = { userId, user_id: userId };
+ const encodedUserId = encodeURIComponent(userId);
const attempts: Array<{
path: string;
method?: string;
query?: Record;
body?: Record;
- }> = [
- {
- path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
- },
- {
- path: `/player/surveys/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
- },
- {
- path: "/player/surveys/getchecklocation/specific_user",
- query: userQuery,
- },
- {
- path: "/player/surveys/getchecklocation/specific-user",
- query: userQuery,
- },
- {
- path: `/player/surveys/getchecklocation/${encodeURIComponent(userId)}`,
- },
- {
- path: "/player/surveys/getchecklocation",
- query: userQuery,
- },
- {
- path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
- },
- {
- path: `/player/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
- },
- {
- path: "/player/getchecklocation/specific_user",
- query: userQuery,
- },
- {
- path: "/player/getchecklocation/specific-user",
- query: userQuery,
- },
- {
- path: `/player/getchecklocation/${encodeURIComponent(userId)}`,
- },
- {
- path: "/player/getchecklocation",
- query: userQuery,
- },
- {
- path: "/player/getchecklocation/specific_user",
- method: "POST",
- body: userBody,
- },
- {
- path: "/player/getchecklocation/specific-user",
- method: "POST",
- body: userBody,
- },
- {
- path: "/player/surveys/getchecklocation/specific_user",
- method: "POST",
- body: userBody,
- },
- {
- path: "/player/surveys/getchecklocation/specific-user",
- method: "POST",
- body: userBody,
- },
- {
- path: "/player/getchecklocation",
- method: "POST",
- body: userBody,
- },
- {
- path: "/player/surveys/getchecklocation",
- method: "POST",
- body: userBody,
- },
- ];
+ }> = [];
+
+ for (const base of MATCH_PROFILE_BASE_ROUTES) {
+ for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
+ attempts.push({ path: `${base}/${specific}/${encodedUserId}` });
+ attempts.push({ path: `${base}/${specific}`, query: userQuery });
+ }
+ attempts.push({ path: `${base}/${encodedUserId}` });
+ attempts.push({ path: base, query: userQuery });
+ }
+
+ for (const base of MATCH_PROFILE_BASE_ROUTES) {
+ attempts.push({ path: base, method: "POST", body: userBody });
+ for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
+ attempts.push({ path: `${base}/${specific}`, method: "POST", body: userBody });
+ attempts.push({
+ path: `${base}/${specific}`,
+ method: "POST",
+ query: userQuery,
+ body: userBody,
+ });
+ attempts.push({ path: `${base}/${specific}/${encodedUserId}`, method: "POST", body: userBody });
+ }
+ }
let lastError: Error & { status?: number } | undefined;
@@ -425,7 +387,7 @@ export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsPa
});
} catch (error) {
lastError = error as Error & { status?: number };
- if (lastError?.status !== 404) {
+ if (!shouldRetryMatchProfileError(lastError)) {
throw lastError;
}
}
@@ -457,75 +419,31 @@ export const savePlayerMatchProfile = async ({
userId,
profile,
}: SavePlayerMatchProfileParams) => {
+ const encodedUserId = encodeURIComponent(userId);
+ const userQuery = { userId, user_id: userId } as Record;
const attempts: Array<{
path: string;
method?: string;
query?: Record;
includeUserId?: boolean;
- }> = [
- {
- path: "/player/getchecklocation",
- method: "POST",
- includeUserId: true,
- },
- {
- path: `/player/getchecklocation/${encodeURIComponent(userId)}`,
- method: "PATCH",
- includeUserId: true,
- },
- {
- path: "/player/getchecklocation/specific_user",
- method: "POST",
- includeUserId: true,
- },
- {
- path: "/player/getchecklocation/specific-user",
- method: "POST",
- includeUserId: true,
- },
- {
- path: `/player/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
- method: "POST",
- includeUserId: true,
- },
- {
- path: `/player/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
- method: "POST",
- includeUserId: true,
- },
- {
- path: "/player/surveys/getchecklocation",
- method: "POST",
- includeUserId: true,
- },
- {
- path: `/player/surveys/getchecklocation/specific_user/${encodeURIComponent(userId)}`,
- method: "POST",
- includeUserId: true,
- },
- {
- path: "/player/surveys/getchecklocation/specific_user",
- method: "POST",
- query: { userId, user_id: userId },
- includeUserId: true,
- },
- {
- path: "/player/surveys/getchecklocation/specific-user",
- method: "POST",
- query: { userId, user_id: userId },
- includeUserId: true,
- },
- {
- path: `/player/surveys/getchecklocation/specific-user/${encodeURIComponent(userId)}`,
- method: "POST",
- includeUserId: true,
- },
- {
- path: `/player/surveys/getchecklocation/${encodeURIComponent(userId)}`,
- method: "PATCH",
- includeUserId: true,
- },
- ];
+ }> = [];
+
+ for (const base of MATCH_PROFILE_BASE_ROUTES) {
+ attempts.push({ path: base, method: "POST", includeUserId: true });
+ attempts.push({ path: `${base}/${encodedUserId}`, method: "POST", includeUserId: true });
+ for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
+ const specificPath = `${base}/${specific}`;
+ attempts.push({ path: specificPath, method: "POST", includeUserId: true });
+ attempts.push({
+ path: specificPath,
+ method: "POST",
+ query: userQuery,
+ includeUserId: true,
+ });
+ attempts.push({ path: `${specificPath}/${encodedUserId}`, method: "POST", includeUserId: true });
+ }
+ attempts.push({ path: `${base}/${encodedUserId}`, method: "PATCH", includeUserId: true });
+ }
let lastError: Error & { status?: number } | undefined;
@@ -547,7 +465,7 @@ export const savePlayerMatchProfile = async ({
});
} catch (error) {
lastError = error as Error & { status?: number };
- if (lastError?.status !== 404) {
+ if (!shouldRetryMatchProfileError(lastError)) {
throw lastError;
}
}
From 06a0c0ec2347ac303ba719c5136566b5f58ca456 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 09:46:26 -0800
Subject: [PATCH 10/14] Use documented match profile endpoints
---
src/api/playerHome.ts | 139 ++++--------------------------------------
1 file changed, 11 insertions(+), 128 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 2ae52c32..6c34e14d 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -330,75 +330,11 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
}
-const MATCH_PROFILE_ROUTE_PREFIXES = ["/player/surveys", "/player"] as const;
-const MATCH_PROFILE_ROUTE_RESOURCES = ["getchecklocation", "get-check-location", "get_check_location"] as const;
-const MATCH_PROFILE_SPECIFIC_SEGMENTS = ["specific_user", "specific-user"] as const;
-
-const MATCH_PROFILE_BASE_ROUTES = MATCH_PROFILE_ROUTE_PREFIXES.flatMap((prefix) =>
- MATCH_PROFILE_ROUTE_RESOURCES.map((resource) => `${prefix}/${resource}`),
-);
-
-const shouldRetryMatchProfileError = (error?: { status?: number }) =>
- error?.status === 404 || error?.status === 500;
-
-export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
- const userQuery = { userId, user_id: userId } as Record;
- const userBody = { userId, user_id: userId };
- const encodedUserId = encodeURIComponent(userId);
- const attempts: Array<{
- path: string;
- method?: string;
- query?: Record;
- body?: Record;
- }> = [];
-
- for (const base of MATCH_PROFILE_BASE_ROUTES) {
- for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
- attempts.push({ path: `${base}/${specific}/${encodedUserId}` });
- attempts.push({ path: `${base}/${specific}`, query: userQuery });
- }
- attempts.push({ path: `${base}/${encodedUserId}` });
- attempts.push({ path: base, query: userQuery });
- }
-
- for (const base of MATCH_PROFILE_BASE_ROUTES) {
- attempts.push({ path: base, method: "POST", body: userBody });
- for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
- attempts.push({ path: `${base}/${specific}`, method: "POST", body: userBody });
- attempts.push({
- path: `${base}/${specific}`,
- method: "POST",
- query: userQuery,
- body: userBody,
- });
- attempts.push({ path: `${base}/${specific}/${encodedUserId}`, method: "POST", body: userBody });
- }
- }
-
- let lastError: Error & { status?: number } | undefined;
-
- for (const attempt of attempts) {
- try {
- return await request>(attempt.path, {
- method: attempt.method ?? "GET",
- token,
- query: attempt.query,
- body: attempt.body ? buildBody(attempt.body) : undefined,
- });
- } catch (error) {
- lastError = error as Error & { status?: number };
- if (!shouldRetryMatchProfileError(lastError)) {
- throw lastError;
- }
- }
- }
-
- if (lastError) {
- throw lastError;
- }
-
- throw new Error("Unable to load match profile");
-};
+export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
+ request>("/player/surveys/getchecklocation/specific_user", {
+ token,
+ query: { userId },
+ });
export interface PlayerMatchProfilePayload {
about_me?: string;
@@ -418,65 +354,12 @@ export const savePlayerMatchProfile = async ({
token,
userId,
profile,
-}: SavePlayerMatchProfileParams) => {
- const encodedUserId = encodeURIComponent(userId);
- const userQuery = { userId, user_id: userId } as Record;
- const attempts: Array<{
- path: string;
- method?: string;
- query?: Record;
- includeUserId?: boolean;
- }> = [];
-
- for (const base of MATCH_PROFILE_BASE_ROUTES) {
- attempts.push({ path: base, method: "POST", includeUserId: true });
- attempts.push({ path: `${base}/${encodedUserId}`, method: "POST", includeUserId: true });
- for (const specific of MATCH_PROFILE_SPECIFIC_SEGMENTS) {
- const specificPath = `${base}/${specific}`;
- attempts.push({ path: specificPath, method: "POST", includeUserId: true });
- attempts.push({
- path: specificPath,
- method: "POST",
- query: userQuery,
- includeUserId: true,
- });
- attempts.push({ path: `${specificPath}/${encodedUserId}`, method: "POST", includeUserId: true });
- }
- attempts.push({ path: `${base}/${encodedUserId}`, method: "PATCH", includeUserId: true });
- }
-
- let lastError: Error & { status?: number } | undefined;
-
- for (const attempt of attempts) {
- try {
- return await request>(attempt.path, {
- method: attempt.method ?? "POST",
- token,
- query: attempt.query,
- body: buildBody({
- ...(attempt.includeUserId
- ? {
- userId,
- user_id: userId,
- }
- : {}),
- ...profile,
- }),
- });
- } catch (error) {
- lastError = error as Error & { status?: number };
- if (!shouldRetryMatchProfileError(lastError)) {
- throw lastError;
- }
- }
- }
-
- if (lastError) {
- throw lastError;
- }
-
- throw new Error("Unable to save match profile");
-};
+}: SavePlayerMatchProfileParams) =>
+ request>("/player/surveys/getchecklocation/specific_user", {
+ method: "POST",
+ token,
+ body: buildBody({ userId, ...profile }),
+ });
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
token: string;
From 542e3895f01d430f419e2ff140e13f2c3679d2e9 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 10:00:08 -0800
Subject: [PATCH 11/14] Ensure match profile requests send token auth
---
src/api/playerHome.ts | 2 ++
src/pages/PlayerMatchProfilePage.tsx | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 6c34e14d..ca629435 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -333,6 +333,7 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
request>("/player/surveys/getchecklocation/specific_user", {
token,
+ authScheme: "token",
query: { userId },
});
@@ -358,6 +359,7 @@ export const savePlayerMatchProfile = async ({
request>("/player/surveys/getchecklocation/specific_user", {
method: "POST",
token,
+ authScheme: "token",
body: buildBody({ userId, ...profile }),
});
diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx
index 46a74441..1125d26d 100644
--- a/src/pages/PlayerMatchProfilePage.tsx
+++ b/src/pages/PlayerMatchProfilePage.tsx
@@ -267,7 +267,7 @@ const PlayerMatchProfilePage = () => {
let cancelled = false;
const loadProfile = async () => {
- const token = getStoredAuthToken({ preferScheme: "Bearer" });
+ const token = getStoredAuthToken({ defaultScheme: "token", preferScheme: "token" });
if (!token) {
setStatus("error");
setError("Please sign in to view your match profile.");
@@ -350,7 +350,7 @@ const PlayerMatchProfilePage = () => {
setModalError("We couldn’t determine your player profile ID.");
return;
}
- const token = getStoredAuthToken({ preferScheme: "Bearer" });
+ const token = getStoredAuthToken({ defaultScheme: "token", preferScheme: "token" });
if (!token) {
setModalError("Please sign in again to save your match profile.");
return;
From 472d07f686f75001bd8027a2fb78de7f9540204f Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 10:08:05 -0800
Subject: [PATCH 12/14] Include user_id when loading/saving match profiles
---
src/api/playerHome.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index ca629435..523f3955 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -334,7 +334,7 @@ export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsPa
request>("/player/surveys/getchecklocation/specific_user", {
token,
authScheme: "token",
- query: { userId },
+ query: { userId, user_id: userId },
});
export interface PlayerMatchProfilePayload {
@@ -360,7 +360,8 @@ export const savePlayerMatchProfile = async ({
method: "POST",
token,
authScheme: "token",
- body: buildBody({ userId, ...profile }),
+ query: { userId },
+ body: buildBody({ userId, user_id: userId, ...profile }),
});
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
From 4374025c530ca123d5342fc438b5483ab224ed38 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 10:13:35 -0800
Subject: [PATCH 13/14] Normalize match profile auth tokens
---
src/api/playerHome.ts | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 523f3955..c18a5498 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -330,9 +330,25 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
}
+const extractTokenCredentials = (token?: string | null) => {
+ if (!token) {
+ return undefined;
+ }
+ const trimmed = token.trim();
+ if (!trimmed) {
+ return undefined;
+ }
+ const match = trimmed.match(/^[A-Za-z]+\s+(.+)$/);
+ if (match) {
+ const [, credentials] = match;
+ return credentials?.trim() || undefined;
+ }
+ return trimmed;
+};
+
export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
request>("/player/surveys/getchecklocation/specific_user", {
- token,
+ token: extractTokenCredentials(token),
authScheme: "token",
query: { userId, user_id: userId },
});
@@ -358,7 +374,7 @@ export const savePlayerMatchProfile = async ({
}: SavePlayerMatchProfileParams) =>
request>("/player/surveys/getchecklocation/specific_user", {
method: "POST",
- token,
+ token: extractTokenCredentials(token),
authScheme: "token",
query: { userId },
body: buildBody({ userId, user_id: userId, ...profile }),
From 4b864f9cdfb0ab40f34c1c5129a686c31ad3d050 Mon Sep 17 00:00:00 2001
From: Studio-18
Date: Sun, 16 Nov 2025 10:41:07 -0800
Subject: [PATCH 14/14] Add match profile endpoint fallbacks
---
src/api/playerHome.ts | 90 ++++++++++++++++++++++++++++++++++---------
1 file changed, 72 insertions(+), 18 deletions(-)
diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index c18a5498..1ad6d1e9 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -346,12 +346,57 @@ const extractTokenCredentials = (token?: string | null) => {
return trimmed;
};
-export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
- request>("/player/surveys/getchecklocation/specific_user", {
- token: extractTokenCredentials(token),
- authScheme: "token",
- query: { userId, user_id: userId },
- });
+type MatchProfileRoute = {
+ path: string;
+ method: string;
+ includeUserIdInQuery?: boolean;
+ includeUserIdInBody?: boolean;
+};
+
+const MATCH_PROFILE_FETCH_ROUTES: MatchProfileRoute[] = [
+ { path: "/player/surveys/getchecklocation/specific_user", method: "GET", includeUserIdInQuery: true },
+ { path: "/player/surveys/getchecklocation", method: "GET", includeUserIdInQuery: true },
+ { path: "/player/getchecklocation/specific_user", method: "GET", includeUserIdInQuery: true },
+ { path: "/player/getchecklocation", method: "POST", includeUserIdInBody: true },
+];
+
+const MATCH_PROFILE_SAVE_ROUTES: MatchProfileRoute[] = [
+ { path: "/player/surveys/getchecklocation/specific_user", method: "POST", includeUserIdInQuery: true, includeUserIdInBody: true },
+ { path: "/player/surveys/getchecklocation", method: "POST", includeUserIdInBody: true },
+ { path: "/player/getchecklocation/specific_user", method: "POST", includeUserIdInQuery: true, includeUserIdInBody: true },
+ { path: "/player/getchecklocation", method: "POST", includeUserIdInBody: true },
+];
+
+const buildMatchProfileQuery = (userId: number | string) => ({ userId, user_id: userId });
+const buildMatchProfileBody = (userId: number | string, profile?: PlayerMatchProfilePayload) =>
+ buildBody({ ...buildMatchProfileQuery(userId), ...(profile ?? {}) });
+
+const shouldRetryMatchProfileRoute = (error: unknown) => {
+ const status = (error as { status?: number })?.status;
+ return status === 404 || status === 500;
+};
+
+export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) => {
+ const bareToken = extractTokenCredentials(token);
+ let lastError: unknown;
+ for (const route of MATCH_PROFILE_FETCH_ROUTES) {
+ try {
+ return await request>(route.path, {
+ method: route.method,
+ token: bareToken,
+ authScheme: "token",
+ query: route.includeUserIdInQuery ? buildMatchProfileQuery(userId) : undefined,
+ body: route.includeUserIdInBody ? buildMatchProfileBody(userId) : undefined,
+ });
+ } catch (error) {
+ lastError = error;
+ if (!shouldRetryMatchProfileRoute(error)) {
+ throw error;
+ }
+ }
+ }
+ throw lastError ?? new Error("Unable to load match profile");
+};
export interface PlayerMatchProfilePayload {
about_me?: string;
@@ -367,18 +412,27 @@ export interface SavePlayerMatchProfileParams extends PlayerTokenOnlyParams {
profile: PlayerMatchProfilePayload;
}
-export const savePlayerMatchProfile = async ({
- token,
- userId,
- profile,
-}: SavePlayerMatchProfileParams) =>
- request>("/player/surveys/getchecklocation/specific_user", {
- method: "POST",
- token: extractTokenCredentials(token),
- authScheme: "token",
- query: { userId },
- body: buildBody({ userId, user_id: userId, ...profile }),
- });
+export const savePlayerMatchProfile = async ({ token, userId, profile }: SavePlayerMatchProfileParams) => {
+ const bareToken = extractTokenCredentials(token);
+ let lastError: unknown;
+ for (const route of MATCH_PROFILE_SAVE_ROUTES) {
+ try {
+ return await request>(route.path, {
+ method: route.method,
+ token: bareToken,
+ authScheme: "token",
+ query: route.includeUserIdInQuery ? buildMatchProfileQuery(userId) : undefined,
+ body: route.includeUserIdInBody ? buildMatchProfileBody(userId, profile) : buildBody(profile),
+ });
+ } catch (error) {
+ lastError = error;
+ if (!shouldRetryMatchProfileRoute(error)) {
+ throw error;
+ }
+ }
+ }
+ throw lastError ?? new Error("Unable to save match profile");
+};
export interface SuggestedPlayerCheckLocationParams extends PaginationParams {
token: string;