From 7b7cbdc6feb5318f6a397eca5700eada5c8de387 Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Fri, 24 Oct 2025 20:15:42 -0700 Subject: [PATCH 1/2] fix: normalize player ratings for api --- src/TennisMatchApp.jsx | 60 +++++++++++++++++++------------ src/components/ProfileManager.jsx | 15 +++----- src/services/player.js | 38 ++++++++++++++++++-- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index b994f5fb..512010cc 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -22,7 +22,10 @@ import { rejectInvite, } from "./services/invites"; import { login, signup, forgotPassword, getPersonalDetails } from "./services/auth"; -import { updatePlayerPersonalDetails } from "./services/player"; +import { + updatePlayerPersonalDetails, + normalizeRatingFromApi, +} from "./services/player"; import { Calendar, MapPin, @@ -730,31 +733,44 @@ const TennisMatchApp = () => { return ""; }; + const normalizedProfileDetails = + profileDetails && typeof profileDetails === "object" + ? { + ...profileDetails, + ...(profileDetails.usta_rating !== undefined + ? { usta_rating: normalizeRatingFromApi(profileDetails.usta_rating) } + : {}), + ...(profileDetails.uta_rating !== undefined + ? { uta_rating: normalizeRatingFromApi(profileDetails.uta_rating) } + : {}), + } + : profileDetails; + setCurrentUser((prev) => { if (!prev || typeof prev !== "object") return prev; const mergedProfile = { ...(prev.profile && typeof prev.profile === "object" ? prev.profile : {}), - ...profileDetails, + ...normalizedProfileDetails, }; const derivedName = pickFirstValue( - profileDetails.full_name, - profileDetails.fullName, - profileDetails.name, + normalizedProfileDetails.full_name, + normalizedProfileDetails.fullName, + normalizedProfileDetails.name, ); const derivedSkill = pickFirstValue( - profileDetails.usta_rating, - profileDetails.ustaRating, - profileDetails.skill_level, - profileDetails.skillLevel, + normalizedProfileDetails.usta_rating, + normalizedProfileDetails.ustaRating, + normalizedProfileDetails.skill_level, + normalizedProfileDetails.skillLevel, ); const derivedAvatarUrl = getAvatarUrlFromPlayer({ - profile: profileDetails, - player: profileDetails, - user: profileDetails, + profile: normalizedProfileDetails, + player: normalizedProfileDetails, + user: normalizedProfileDetails, }); const nextUser = { @@ -775,16 +791,16 @@ const TennisMatchApp = () => { } const profilePicture = pickFirstValue( - profileDetails.profile_picture, - profileDetails.profilePicture, - profileDetails.profile_picture_url, - profileDetails.profilePictureUrl, - profileDetails.photo_url, - profileDetails.photoUrl, - profileDetails.image_url, - profileDetails.imageUrl, - profileDetails.avatar_url, - profileDetails.avatarUrl, + normalizedProfileDetails.profile_picture, + normalizedProfileDetails.profilePicture, + normalizedProfileDetails.profile_picture_url, + normalizedProfileDetails.profilePictureUrl, + normalizedProfileDetails.photo_url, + normalizedProfileDetails.photoUrl, + normalizedProfileDetails.image_url, + normalizedProfileDetails.imageUrl, + normalizedProfileDetails.avatar_url, + normalizedProfileDetails.avatarUrl, ); if (profilePicture) { diff --git a/src/components/ProfileManager.jsx b/src/components/ProfileManager.jsx index e5f49f55..e309eda0 100644 --- a/src/components/ProfileManager.jsx +++ b/src/components/ProfileManager.jsx @@ -6,6 +6,7 @@ import ProfilePhotoUploader from "./ProfilePhotoUploader"; import { updatePlayerPersonalDetails, normalizeRatingForApi, + normalizeRatingFromApi, } from "../services/player"; const USTA_RATING_OPTIONS = [ @@ -72,14 +73,8 @@ const ProfileManager = ({ isOpen, onClose, onProfileUpdate }) => { date_of_birth: data?.date_of_birth ? data.date_of_birth.split("T")[0] : "", - usta_rating: - typeof data?.usta_rating === "number" && !Number.isNaN(data.usta_rating) - ? String(data.usta_rating) - : data?.usta_rating || "", - uta_rating: - typeof data?.uta_rating === "number" && !Number.isNaN(data.uta_rating) - ? String(data.uta_rating) - : data?.uta_rating || "", + usta_rating: normalizeRatingFromApi(data?.usta_rating), + uta_rating: normalizeRatingFromApi(data?.uta_rating), about_me: data?.about_me || "", }; setDetails(normalizedDetails); @@ -166,10 +161,10 @@ const ProfileManager = ({ isOpen, onClose, onProfileUpdate }) => { onProfileUpdate({ ...details, ...(normalizedUstaRating !== undefined - ? { usta_rating: normalizedUstaRating } + ? { usta_rating: normalizeRatingFromApi(normalizedUstaRating) } : {}), ...(normalizedUtaRating !== undefined - ? { uta_rating: normalizedUtaRating } + ? { uta_rating: normalizeRatingFromApi(normalizedUtaRating) } : {}), }); } diff --git a/src/services/player.js b/src/services/player.js index 8b92c5bb..52a8467a 100644 --- a/src/services/player.js +++ b/src/services/player.js @@ -20,8 +20,42 @@ export const normalizeRatingForApi = (value) => { return undefined; } - const rounded = Math.round(numeric * 10) / 10; - return rounded.toFixed(1); + const roundedToHalfStep = Math.round(numeric * 2) / 2; + const isHalfStep = Math.abs(roundedToHalfStep - numeric) < 1e-9; + if (!isHalfStep) { + return undefined; + } + + if (Number.isInteger(roundedToHalfStep)) { + return roundedToHalfStep; + } + + return Math.round(roundedToHalfStep * 10); +}; + +export const normalizeRatingFromApi = (value) => { + if (value === undefined || value === null) { + return ""; + } + + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric < 0) { + return ""; + } + + if (numeric >= 10 && Number.isInteger(numeric)) { + const scaled = numeric / 10; + if (scaled >= 0 && scaled <= 10) { + return scaled.toFixed(1); + } + } + + if (Number.isInteger(numeric)) { + return numeric.toFixed(1); + } + + const roundedToHalfStep = Math.round(numeric * 2) / 2; + return roundedToHalfStep.toFixed(1); }; export const updatePlayerPersonalDetails = async ({ From dcf11eceea801ac5c758e1979ad728ead62c7dbc Mon Sep 17 00:00:00 2001 From: Studio-18 Date: Fri, 24 Oct 2025 20:32:36 -0700 Subject: [PATCH 2/2] fix: sync ntrp rating with match profile --- src/TennisMatchApp.jsx | 7 +++ src/components/ProfileManager.jsx | 101 +++++++++++++++++++++++++++--- src/services/player.js | 76 +++++++++++++++++++--- 3 files changed, 168 insertions(+), 16 deletions(-) diff --git a/src/TennisMatchApp.jsx b/src/TennisMatchApp.jsx index 512010cc..9bf117cf 100644 --- a/src/TennisMatchApp.jsx +++ b/src/TennisMatchApp.jsx @@ -494,12 +494,15 @@ const resolveAuthSession = (data = {}, fallback = {}) => { ); const derivedSkill = pickString( + profile?.ntrp_rating, profile?.usta_rating, profile?.skill_level, profile?.skillLevel, + userFromApi?.ntrp_rating, userFromApi?.usta_rating, userFromApi?.skill_level, userFromApi?.skillLevel, + fallbackData?.ntrp_rating, fallbackData?.skillLevel, ); @@ -737,6 +740,9 @@ const TennisMatchApp = () => { profileDetails && typeof profileDetails === "object" ? { ...profileDetails, + ...(profileDetails.ntrp_rating !== undefined + ? { ntrp_rating: normalizeRatingFromApi(profileDetails.ntrp_rating) } + : {}), ...(profileDetails.usta_rating !== undefined ? { usta_rating: normalizeRatingFromApi(profileDetails.usta_rating) } : {}), @@ -761,6 +767,7 @@ const TennisMatchApp = () => { ); const derivedSkill = pickFirstValue( + normalizedProfileDetails.ntrp_rating, normalizedProfileDetails.usta_rating, normalizedProfileDetails.ustaRating, normalizedProfileDetails.skill_level, diff --git a/src/components/ProfileManager.jsx b/src/components/ProfileManager.jsx index e309eda0..52801185 100644 --- a/src/components/ProfileManager.jsx +++ b/src/components/ProfileManager.jsx @@ -5,8 +5,11 @@ import { formatPhoneNumber, formatPhoneDisplay } from "../services/phone"; import ProfilePhotoUploader from "./ProfilePhotoUploader"; import { updatePlayerPersonalDetails, + getPlayerMatchProfile, + updatePlayerMatchProfile, normalizeRatingForApi, normalizeRatingFromApi, + getMatchProfileId, } from "../services/player"; const USTA_RATING_OPTIONS = [ @@ -28,6 +31,7 @@ const USTA_RATING_OPTIONS = [ const emptyDetails = { id: null, + match_profile_id: null, full_name: "", phone: "", profile_picture: "", @@ -64,16 +68,50 @@ const ProfileManager = ({ isOpen, onClose, onProfileUpdate }) => { if (showLoader) { setLoading(true); } - const data = await getPersonalDetails(); + const [personalResult, matchProfileResult] = await Promise.allSettled([ + getPersonalDetails(), + getPlayerMatchProfile({ player: accessToken }), + ]); + + if (personalResult.status !== "fulfilled") { + throw personalResult.reason; + } + + const data = personalResult.value || {}; + if (matchProfileResult.status === "rejected") { + const reason = matchProfileResult.reason; + const status = Number(reason?.status ?? reason?.response?.status); + if (!status || status >= 400) { + if (status !== 404) { + console.warn("Failed to load match profile", reason); + } + } + } + const matchProfile = + matchProfileResult.status === "fulfilled" ? matchProfileResult.value : null; + const matchProfileId = getMatchProfileId(matchProfile); + const hasNtrpRating = + matchProfile && Object.prototype.hasOwnProperty.call(matchProfile, "ntrp_rating"); + const hasUstaRating = + matchProfile && Object.prototype.hasOwnProperty.call(matchProfile, "usta_rating"); + const matchProfileRating = hasNtrpRating + ? matchProfile.ntrp_rating + : hasUstaRating + ? matchProfile.usta_rating + : undefined; + const normalizedDetails = { id: data?.id ?? null, + match_profile_id: matchProfileId, full_name: data?.full_name || "", phone: data?.phone ? String(data.phone).replace(/\D/g, "") : "", profile_picture: data?.profile_picture || "", date_of_birth: data?.date_of_birth ? data.date_of_birth.split("T")[0] : "", - usta_rating: normalizeRatingFromApi(data?.usta_rating), + usta_rating: normalizeRatingFromApi( + matchProfileRating ?? data?.usta_rating, + ), uta_rating: normalizeRatingFromApi(data?.uta_rating), about_me: data?.about_me || "", }; @@ -81,7 +119,15 @@ const ProfileManager = ({ isOpen, onClose, onProfileUpdate }) => { setPhoneInput(formatPhoneDisplay(data?.phone) || ""); setImagePreview(normalizedDetails.profile_picture || ""); if (onProfileUpdate) { - onProfileUpdate({ ...data }); + onProfileUpdate({ + ...data, + ...(matchProfileId !== undefined && matchProfileId !== null + ? { match_profile_id: matchProfileId } + : {}), + ...(matchProfileRating !== undefined + ? { ntrp_rating: matchProfileRating, usta_rating: matchProfileRating } + : {}), + }); } } catch (err) { console.error(err); @@ -148,20 +194,57 @@ const ProfileManager = ({ isOpen, onClose, onProfileUpdate }) => { about_me: aboutMe || null, }; - if (normalizedUstaRating !== undefined) { - payload.usta_rating = normalizedUstaRating; - } - if (normalizedUtaRating !== undefined) { payload.uta_rating = normalizedUtaRating; } + const matchProfileNeedsUpdate = + normalizedUstaRating !== undefined || + (!hasValue(details.usta_rating) && + details.match_profile_id !== undefined && + details.match_profile_id !== null); + + let matchProfileResponse = null; + + if (matchProfileNeedsUpdate) { + matchProfileResponse = await updatePlayerMatchProfile({ + player: accessToken, + id: details.match_profile_id, + ntrp_rating: + normalizedUstaRating !== undefined ? normalizedUstaRating : null, + }); + } + await updatePlayerPersonalDetails(payload); + + const nextMatchProfileId = + getMatchProfileId(matchProfileResponse) ?? details.match_profile_id; + const nextMatchProfileRating = + matchProfileResponse?.ntrp_rating ?? + matchProfileResponse?.usta_rating ?? + (matchProfileNeedsUpdate + ? normalizedUstaRating ?? null + : undefined); + const nextMatchProfileRatingDisplay = + nextMatchProfileRating !== undefined + ? normalizeRatingFromApi(nextMatchProfileRating) + : undefined; + const nextUstaRatingDisplay = + normalizedUstaRating !== undefined + ? normalizeRatingFromApi(normalizedUstaRating) + : nextMatchProfileRatingDisplay; + if (onProfileUpdate) { onProfileUpdate({ ...details, - ...(normalizedUstaRating !== undefined - ? { usta_rating: normalizeRatingFromApi(normalizedUstaRating) } + match_profile_id: nextMatchProfileId, + ...(nextMatchProfileRating !== undefined + ? { + ntrp_rating: nextMatchProfileRating, + } + : {}), + ...(nextUstaRatingDisplay !== undefined + ? { usta_rating: nextUstaRatingDisplay } : {}), ...(normalizedUtaRating !== undefined ? { uta_rating: normalizeRatingFromApi(normalizedUtaRating) } diff --git a/src/services/player.js b/src/services/player.js index 52a8467a..9291410d 100644 --- a/src/services/player.js +++ b/src/services/player.js @@ -27,10 +27,10 @@ export const normalizeRatingForApi = (value) => { } if (Number.isInteger(roundedToHalfStep)) { - return roundedToHalfStep; + return Number(roundedToHalfStep.toFixed(0)); } - return Math.round(roundedToHalfStep * 10); + return Number(roundedToHalfStep.toFixed(1)); }; export const normalizeRatingFromApi = (value) => { @@ -77,14 +77,10 @@ export const updatePlayerPersonalDetails = async ({ throw new Error("Missing player token"); } - const normalizedUstaRating = normalizeRatingForApi(usta_rating); - const normalizedUtaRating = normalizeRatingForApi(uta_rating); - const params = Object.entries({ id, date_of_birth, - usta_rating: normalizedUstaRating, - uta_rating: normalizedUtaRating, + uta_rating: normalizeRatingForApi(uta_rating), full_name: fullName, phone: mobile, about_me, @@ -107,3 +103,69 @@ export const updatePlayerPersonalDetails = async ({ }), ); }; + +export const getMatchProfileId = (profile) => { + if (!profile || typeof profile !== "object") { + return null; + } + return ( + profile.id ?? + profile.match_profile_id ?? + profile.matchProfileId ?? + profile.profile_id ?? + profile.profileId ?? + null + ); +}; + +export const getPlayerMatchProfile = async ({ player = null } = {}) => { + const authHeader = normalizeAuthToken(player, { + defaultScheme: "token", + preferScheme: "token", + }); + + return unwrap( + api(`/player/match_profile`, { + authToken: authHeader, + authSchemePreference: "token", + }), + ); +}; + +export const updatePlayerMatchProfile = async ({ + player = null, + id = null, + ntrp_rating = undefined, + uta_rating = undefined, +} = {}) => { + const authHeader = normalizeAuthToken(player, { + defaultScheme: "token", + preferScheme: "token", + }); + + if (!authHeader) { + throw new Error("Missing player token"); + } + + const params = {}; + if (id !== undefined && id !== null) { + params.id = id; + } + if (ntrp_rating !== undefined) { + params.ntrp_rating = ntrp_rating; + } + if (uta_rating !== undefined) { + params.uta_rating = uta_rating; + } + + const method = id ? "PATCH" : "POST"; + + return unwrap( + api(`/player/match_profile`, { + method, + authToken: authHeader, + authSchemePreference: "token", + json: params, + }), + ); +};