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
113 changes: 103 additions & 10 deletions src/api/playerHome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,16 +330,109 @@ export interface FetchPlayerDetailsParams extends PlayerTokenOnlyParams {
userId: number | string;
}

export const fetchPlayerDetails = async ({ token, userId }: FetchPlayerDetailsParams) =>
request<Record<string, unknown>>(
"/player/surveys/getchecklocation/specific_user",
{
token,
query: {
userId,
},
},
);
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;
};

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<Record<string, unknown>>(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;
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) => {
const bareToken = extractTokenCredentials(token);
let lastError: unknown;
for (const route of MATCH_PROFILE_SAVE_ROUTES) {
try {
return await request<Record<string, unknown>>(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;
Expand Down
31 changes: 26 additions & 5 deletions src/components/players/MatchProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -108,7 +111,9 @@ type PlacesStatus = "idle" | "loading" | "ready" | "unavailable";

let placesScriptPromise: Promise<void> | 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") {
Expand Down Expand Up @@ -147,7 +152,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<HTMLInputElement | null>(null);
Expand Down Expand Up @@ -255,6 +268,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<HTMLFormElement>) => {
Expand Down Expand Up @@ -636,17 +652,22 @@ const MatchProfileModal = ({ isOpen, onClose, onComplete, initialProfile }: Matc
Please complete your full profile before saving.
</p>
)}
{submitError ? (
<p className="match-profile-modal__submit-error" role="alert">
{submitError}
</p>
) : null}
<div className="match-profile-modal__buttons">
<button type="button" className="fc-button fc-button--secondary" onClick={onClose}>
Cancel
</button>
<button
type="submit"
className="fc-button fc-button--primary"
disabled={isSubmitDisabled}
aria-disabled={isSubmitDisabled}
disabled={isFinalSubmitDisabled}
aria-disabled={isFinalSubmitDisabled}
>
Save profile
{finalSubmitLabel}
</button>
</div>
</div>
Expand Down
Loading