Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/api/playerHome.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { request } from "./http";
import { type AuthScheme, request } from "./http";
import type { PaginatedResponse } from "./player";

export interface CoachSummary {
Expand Down Expand Up @@ -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<Record<string, unknown>>(
"/player/surveys/getchecklocation/specific_user",
{
token,
authScheme,
query: {
userId,
},
Expand Down
245 changes: 236 additions & 9 deletions src/pages/PlayerMatchProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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" },
Expand All @@ -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<string[]>([
"Weekday evenings",
"Weekend mornings",
]);
const [intensity, setIntensity] = useState("balanced");
const [formats, setFormats] = useState<string[]>(["singles", "doubles"]);
const [homeBase, setHomeBase] = useState("Austin Tennis Center");
const [selectedAvailability, setSelectedAvailability] = useState<string[]>([]);
const [intensity, setIntensity] = useState("");
const [formats, setFormats] = useState<string[]>([]);
const [homeBase, setHomeBase] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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) =>
Expand Down Expand Up @@ -63,6 +215,81 @@ const PlayerMatchProfilePage = () => {
</p>
</header>

<section className="match-summary" aria-live="polite">
<div className="match-summary__header">
<div>
<p className="match-summary__eyebrow">Your saved preferences</p>
<h2 className="match-summary__title">Here&apos;s what other players see</h2>
</div>
<p className="match-summary__status">{statusMessage}</p>
</div>

<div className="match-summary__grid">
<div className="match-summary__card">
<p className="match-summary__label">Availability</p>
<div className="match-summary__chips">
{selectedAvailability.length === 0 ? (
<span className="match-summary__chip match-summary__chip--empty">Not provided</span>
) : (
selectedAvailability.map((slot) => (
<span key={slot} className="match-summary__chip">
{slot}
</span>
))
)}
</div>
</div>

<div className="match-summary__card">
<p className="match-summary__label">Match intensity</p>
<div className="match-summary__chips">
{intensity ? (
<span className="match-summary__chip">{intensityLabel(intensity)}</span>
) : (
<span className="match-summary__chip match-summary__chip--empty">Not provided</span>
)}
</div>
</div>

<div className="match-summary__card">
<p className="match-summary__label">Preferred formats</p>
<div className="match-summary__chips">
{formats.length === 0 ? (
<span className="match-summary__chip match-summary__chip--empty">Not provided</span>
) : (
formats.map((format) => (
<span key={format} className="match-summary__chip">
{formatLabel(format)}
</span>
))
)}
</div>
</div>

<div className="match-summary__card">
<p className="match-summary__label">Home courts</p>
<div className="match-summary__chips">
{homeBase ? (
<span className="match-summary__chip">{homeBase}</span>
) : (
<span className="match-summary__chip match-summary__chip--empty">Not provided</span>
)}
</div>
</div>
</div>
</section>

{error ? (
<p role="alert" style={{ color: "#b91c1c", margin: "0 0 1rem" }}>
{error}
</p>
) : null}
{!error && loading ? (
<p role="status" style={{ color: "#4b5563", margin: "0 0 1rem" }}>
Loading your match profile...
</p>
) : null}

<section className="settings-section">
<div className="match-profile__layout">
<div className="match-profile__main">
Expand Down Expand Up @@ -190,7 +417,7 @@ const PlayerMatchProfilePage = () => {
</section>

<div className="settings-save">
<button type="button" className="settings-save__button">
<button type="button" className="settings-save__button" disabled={loading}>
Save match profile
</button>
</div>
Expand Down
Loading