From 8e9e943afd214f94a6d33c518fd490bfcb2dcb3c Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Mon, 17 Nov 2025 10:05:05 -0800 Subject: [PATCH 1/3] Fetch player match profile data from API --- src/pages/PlayerMatchProfilePage.tsx | 164 +++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 9 deletions(-) diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx index b54a35e5..9a4746ea 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,132 @@ 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 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); + + 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 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: authToken, 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 +198,17 @@ const PlayerMatchProfilePage = () => {

+ {error ? ( +

+ {error} +

+ ) : null} + {!error && loading ? ( +

+ Loading your match profile... +

+ ) : null} +
@@ -190,7 +336,7 @@ const PlayerMatchProfilePage = () => {
-
From 21976701b73ec87623c1bbc0549f63cfdaf5c9d2 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Mon, 17 Nov 2025 10:24:45 -0800 Subject: [PATCH 2/3] Show match profile data from API --- src/pages/PlayerMatchProfilePage.tsx | 74 +++++++++++++++++++++ src/pages/PlayerSettingsPages.css | 97 ++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/src/pages/PlayerMatchProfilePage.tsx b/src/pages/PlayerMatchProfilePage.tsx index 9a4746ea..183fe5c1 100644 --- a/src/pages/PlayerMatchProfilePage.tsx +++ b/src/pages/PlayerMatchProfilePage.tsx @@ -95,6 +95,10 @@ const normalizeIntensity = (value: unknown) => { 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; @@ -110,6 +114,12 @@ const PlayerMatchProfilePage = () => { 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; @@ -198,6 +208,70 @@ 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} 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; From 0422254fc85ba4050c9afd8dd429c405127a29db Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Mon, 17 Nov 2025 10:58:47 -0800 Subject: [PATCH 3/3] Fix auth token header for match profile fetch --- src/api/playerHome.ts | 6 ++++-- src/pages/PlayerMatchProfilePage.tsx | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) 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 183fe5c1..fa500796 100644 --- a/src/pages/PlayerMatchProfilePage.tsx +++ b/src/pages/PlayerMatchProfilePage.tsx @@ -134,6 +134,9 @@ const PlayerMatchProfilePage = () => { 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; @@ -144,7 +147,11 @@ const PlayerMatchProfilePage = () => { } const payload = - (await fetchPlayerDetails({ token: authToken, userId })) as PlayerMatchProfileResponse; + (await fetchPlayerDetails({ + token: accessToken, + authScheme: "token", + userId, + })) as PlayerMatchProfileResponse; if (isCancelled || !payload) return;