diff --git a/src/api/playerHome.ts b/src/api/playerHome.ts
index 3597c2c4..36798da1 100644
--- a/src/api/playerHome.ts
+++ b/src/api/playerHome.ts
@@ -1,4 +1,4 @@
-import { request } from "./http";
+import { type AuthScheme, request } from "./http";
import type { PaginatedResponse } from "./player";
export interface CoachSummary {
@@ -328,13 +328,15 @@ export const getCheckLocation = async ({
export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
+ authScheme?: AuthScheme;
}
-export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
+export const fetchPlayerDetails = async ({ token, userId, authScheme }: FetchPlayerDetailsParams) =>
request>(
"/player/surveys/getchecklocation/specific_user",
{
token,
+ authScheme,
query: {
userId,
},
diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx
index b54a35e5..fa500796 100644
--- a/src/pages/PlayerMatchProfilePage.tsx
+++ b/src/pages/PlayerMatchProfilePage.tsx
@@ -1,6 +1,9 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { Check, Clock, MapPin, Target } from "lucide-react";
import MainLayout from "../components/MainLayout";
+import { fetchPlayerDetails } from "../api/playerHome";
+import { getPersonalDetails } from "../services/auth";
+import { getStoredAuthToken } from "../services/authToken";
import "./PlayerSettingsPages.css";
@@ -13,6 +16,20 @@ const availabilitySlots = [
"Weekend evenings",
];
+type PlayerMatchProfileResponse = {
+ availability?: unknown;
+ lookingFor?: unknown;
+ matchPreferences?: unknown;
+ preferred_formats?: unknown;
+ matchIntensity?: unknown;
+ intensity?: unknown;
+ match_intensity?: unknown;
+ playerCourtLocations?: unknown;
+ playerLocations?: unknown;
+ homeBase?: unknown;
+ home_court?: unknown;
+};
+
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" },
@@ -27,14 +44,149 @@ const preferredFormats = [
{ id: "fitness", label: "Cardio tennis" },
];
+const toStringArray = (value: unknown): string[] => {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
+ .map((item) => item.split(",").map((part) => part.trim()))
+ .flat()
+ .filter((item) => item.length > 0);
+ }
+ if (typeof value === "string") {
+ return value
+ .split(",")
+ .map((item) => item.trim())
+ .filter((item) => item.length > 0);
+ }
+ return [];
+};
+
+const matchAvailability = (value: string) =>
+ availabilitySlots.find((slot) => slot.toLowerCase() === value.trim().toLowerCase());
+
+const normalizeAvailability = (value: unknown) => {
+ const matches = toStringArray(value)
+ .map((item) => matchAvailability(item) ?? null)
+ .filter((item): item is string => Boolean(item));
+ return Array.from(new Set(matches));
+};
+
+const normalizeFormats = (value: unknown) => {
+ const entries = toStringArray(value)
+ .map((item) => {
+ const normalized = item.toLowerCase();
+ const match = preferredFormats.find(
+ (format) =>
+ format.id.toLowerCase() === normalized || format.label.toLowerCase() === normalized,
+ );
+ return match?.id ?? null;
+ })
+ .filter((item): item is string => Boolean(item));
+
+ return Array.from(new Set(entries));
+};
+
+const normalizeIntensity = (value: unknown) => {
+ if (typeof value !== "string") return null;
+ const normalized = value.trim().toLowerCase();
+ const match = matchIntensities.find(
+ (option) => option.id === normalized || option.label.toLowerCase() === normalized,
+ );
+ return match?.id ?? null;
+};
+
+const formatLabel = (id: string) => preferredFormats.find((format) => format.id === id)?.label ?? id;
+
+const intensityLabel = (id: string) => matchIntensities.find((option) => option.id === id)?.label ?? id;
+
+const normalizeHomeBase = (record: PlayerMatchProfileResponse) => {
+ const courtLocations =
+ record.playerCourtLocations ?? record.playerLocations ?? record.homeBase ?? record.home_court;
+ const courts = toStringArray(courtLocations);
+ return courts[0] ?? "";
+};
+
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 [selectedAvailability, setSelectedAvailability] = useState([]);
+ const [intensity, setIntensity] = useState("");
+ const [formats, setFormats] = useState([]);
+ const [homeBase, setHomeBase] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const statusMessage = error
+ ? "Unable to load profile"
+ : loading
+ ? "Loading..."
+ : "Synced with your profile";
+
+ useEffect(() => {
+ let isCancelled = false;
+
+ const loadProfile = async () => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const authToken = getStoredAuthToken({ defaultScheme: "token", preferScheme: "token" });
+ if (!authToken) {
+ setError("Sign in to view your match profile.");
+ return;
+ }
+
+ const tokenCredentials = authToken.replace(/^\s*[A-Za-z]+\s+/, "").trim();
+ const accessToken = tokenCredentials || authToken;
+
+ const personalDetails = await getPersonalDetails();
+ const userId =
+ personalDetails?.id ?? personalDetails?.userId ?? personalDetails?.user_id ?? null;
+
+ if (!userId) {
+ setError("We couldn't determine your player id. Please try again.");
+ return;
+ }
+
+ const payload =
+ (await fetchPlayerDetails({
+ token: accessToken,
+ authScheme: "token",
+ userId,
+ })) as PlayerMatchProfileResponse;
+
+ if (isCancelled || !payload) return;
+
+ const normalizedAvailability = normalizeAvailability(payload.availability);
+ const normalizedFormats = normalizeFormats(
+ payload.lookingFor ?? payload.matchPreferences ?? payload.preferred_formats,
+ );
+ const normalizedIntensity = normalizeIntensity(
+ payload.matchIntensity ?? payload.intensity ?? payload.match_intensity,
+ );
+ const normalizedHomeBase = normalizeHomeBase(payload);
+
+ setSelectedAvailability(
+ normalizedAvailability.length > 0 ? normalizedAvailability : [],
+ );
+ setFormats(normalizedFormats.length > 0 ? normalizedFormats : []);
+ setIntensity(normalizedIntensity ?? "");
+ setHomeBase(normalizedHomeBase ?? "");
+ } catch (err) {
+ if (isCancelled) return;
+ const message = err instanceof Error ? err.message : "Unable to load match profile.";
+ setError(message);
+ } finally {
+ if (!isCancelled) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadProfile();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, []);
const toggleAvailability = (slot: string) => {
setSelectedAvailability((current) =>
@@ -63,6 +215,81 @@ const PlayerMatchProfilePage = () => {
+
+
+
+
Your saved preferences
+
Here's what other players see
+
+
{statusMessage}
+
+
+
+
+
Availability
+
+ {selectedAvailability.length === 0 ? (
+ Not provided
+ ) : (
+ selectedAvailability.map((slot) => (
+
+ {slot}
+
+ ))
+ )}
+
+
+
+
+
Match intensity
+
+ {intensity ? (
+ {intensityLabel(intensity)}
+ ) : (
+ Not provided
+ )}
+
+
+
+
+
Preferred formats
+
+ {formats.length === 0 ? (
+ Not provided
+ ) : (
+ formats.map((format) => (
+
+ {formatLabel(format)}
+
+ ))
+ )}
+
+
+
+
+
Home courts
+
+ {homeBase ? (
+ {homeBase}
+ ) : (
+ Not provided
+ )}
+
+
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {!error && loading ? (
+
+ Loading your match profile...
+
+ ) : null}
+
@@ -190,7 +417,7 @@ const PlayerMatchProfilePage = () => {
-
diff --git a/src/pages/PlayerSettingsPages.css b/src/pages/PlayerSettingsPages.css
index 31344ef1..b32e445e 100644
--- a/src/pages/PlayerSettingsPages.css
+++ b/src/pages/PlayerSettingsPages.css
@@ -166,6 +166,103 @@
gap: 24px;
}
+.match-summary {
+ border-radius: 28px;
+ border: 1px solid rgba(99, 102, 241, 0.22);
+ background: linear-gradient(135deg, rgba(224, 231, 255, 0.55), rgba(240, 253, 244, 0.65));
+ padding: 24px;
+ box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
+ display: grid;
+ gap: 18px;
+}
+
+.match-summary__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.match-summary__eyebrow {
+ margin: 0 0 6px;
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #4338ca;
+}
+
+.match-summary__title {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.match-summary__status {
+ margin: 0;
+ padding: 8px 14px;
+ border-radius: 999px;
+ background: rgba(99, 102, 241, 0.16);
+ color: #312e81;
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.match-summary__grid {
+ display: grid;
+ gap: 14px;
+}
+
+@media (min-width: 640px) {
+ .match-summary__grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+.match-summary__card {
+ border-radius: 20px;
+ border: 1px solid rgba(148, 163, 184, 0.32);
+ background: #ffffff;
+ padding: 16px 18px;
+ display: grid;
+ gap: 10px;
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06);
+}
+
+.match-summary__label {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #475569;
+}
+
+.match-summary__chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.match-summary__chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-radius: 12px;
+ background: rgba(14, 165, 233, 0.08);
+ color: #0f172a;
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.match-summary__chip--empty {
+ background: rgba(148, 163, 184, 0.18);
+ color: #475569;
+}
+
@media (min-width: 1024px) {
.match-profile__layout {
grid-template-columns: minmax(0, 1fr) 320px;