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
155 changes: 146 additions & 9 deletions src/components/InviteScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,127 @@ import { buildRecentPartnerSuggestions } from "../utils/inviteSuggestions";
import PlayerAvatar from "./PlayerAvatar";
import { getAvatarInitials, getAvatarUrlFromPlayer } from "../utils/avatar";

const SUGGESTED_PLAYER_ID_KEYS = [
"user_id",
"id",
"player_id",
"playerId",
"member_id",
"memberId",
"membership_id",
"membershipId",
"participant_id",
"participantId",
"match_participant_id",
"matchParticipantId",
];

const nestedPlayerIdKeys = [
["profile", "user_id"],
["profile", "id"],
["profile", "player_id"],
["profile", "playerId"],
["profile", "member_id"],
["profile", "memberId"],
["profile", "membership_id"],
["profile", "membershipId"],
["profile", "participant_id"],
["profile", "participantId"],
["profile", "match_participant_id"],
["profile", "matchParticipantId"],
["player", "id"],
["player", "user_id"],
["player", "player_id"],
["player", "playerId"],
["player", "member_id"],
["player", "memberId"],
["player", "membership_id"],
["player", "membershipId"],
["player", "participant_id"],
["player", "participantId"],
["player", "match_participant_id"],
["player", "matchParticipantId"],
["member", "id"],
["member", "user_id"],
["member", "player_id"],
["member", "playerId"],
["member", "member_id"],
["member", "memberId"],
["member", "membership_id"],
["member", "membershipId"],
["member", "participant_id"],
["member", "participantId"],
["member", "match_participant_id"],
["member", "matchParticipantId"],
];

const readNestedValue = (subject, path) => {
if (!subject || typeof subject !== "object") return undefined;
if (!Array.isArray(path)) {
return subject[path];
}
return path.reduce((acc, key) => {
if (!acc || typeof acc !== "object") return undefined;
return acc[key];
}, subject);
};

const extractSuggestedPlayerId = (player) => {
if (!player || typeof player !== "object") return null;
const candidates = [
...SUGGESTED_PLAYER_ID_KEYS.map((key) => player[key]),
...nestedPlayerIdKeys.map((path) => readNestedValue(player, path)),
];
for (const candidate of candidates) {
if (candidate === null || candidate === undefined) continue;
const numeric = Number(candidate);
if (Number.isFinite(numeric) && numeric > 0) {
return numeric;
}
}
return null;
};

const MATCH_START_KEYS = [
"start_date_time",
"startDateTime",
"start_time",
"startTime",
"start_at",
"startAt",
];

const extractMatchTimestamp = (match) => {
if (!match || typeof match !== "object") {
return -Infinity;
}

const candidates = [];
MATCH_START_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(match, key)) {
candidates.push(match[key]);
}
});

if (match.match && typeof match.match === "object") {
MATCH_START_KEYS.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(match.match, key)) {
candidates.push(match.match[key]);
}
});
}

for (const value of candidates) {
if (!value) continue;
const date = value instanceof Date ? value : new Date(value);
if (!Number.isNaN(date.getTime())) {
return date.getTime();
}
}

return -Infinity;
};

const InviteScreen = ({
matchId,
currentUser,
Expand Down Expand Up @@ -87,15 +208,27 @@ const InviteScreen = ({
setSuggestionsError("");

try {
const data = await listMatches("my", { perPage: 25, includeHidden: true });
const data = await listMatches("my", {
perPage: 10,
includeHidden: true,
});
if (!aliveCheck()) return;

const matches = Array.isArray(data?.matches) ? data.matches : [];
const sortedMatches = matches
.map((match) => ({ match, ts: extractMatchTimestamp(match) }))
.sort((a, b) => (b.ts ?? -Infinity) - (a.ts ?? -Infinity))
.map(({ match }) => match);
const recentMatches = sortedMatches.slice(0, 3);

const suggestions = buildRecentPartnerSuggestions({
matches,
matches: recentMatches,
currentUser,
memberIdentities,
});

if (!aliveCheck()) return;

setSuggestedPlayers(suggestions);
} catch (error) {
console.error("Failed to load suggested players", error);
Expand All @@ -110,7 +243,11 @@ const InviteScreen = ({
}
}
},
[isPrivateMatch, currentUser, memberIdentities],
[
isPrivateMatch,
currentUser,
memberIdentities,
],
);

// Local state for manual phone invites (isolated from search input)
Expand Down Expand Up @@ -251,8 +388,8 @@ const InviteScreen = ({
? existingPlayerIds
: new Set(existingPlayerIds || []);
return suggestedPlayers.filter((player) => {
const pid = Number(player.user_id);
if (!Number.isFinite(pid) || pid <= 0) return false;
const pid = extractSuggestedPlayerId(player);
if (!pid) return false;
if (blockedIds.has(pid)) return false;
if (selectedPlayers.has(pid)) return false;
return true;
Expand All @@ -266,8 +403,8 @@ const InviteScreen = ({

const handleAddSuggestedPlayer = useCallback(
(player) => {
const pid = Number(player.user_id);
if (!Number.isFinite(pid) || pid <= 0) return;
const pid = extractSuggestedPlayerId(player);
if (!pid) return;
setSelectedPlayers((prev) => {
if (prev.has(pid)) return prev;
const next = new Map(prev);
Expand Down Expand Up @@ -705,8 +842,8 @@ const InviteScreen = ({
<ul className="space-y-3">
{topSuggestions.map((player) => {
const name = player.full_name || "Unknown player";
const pid = Number(player.user_id);
const selected = Number.isFinite(pid) && selectedPlayers.has(pid);
const pid = extractSuggestedPlayerId(player);
const selected = Boolean(pid && selectedPlayers.has(pid));
return (
<li
key={pid}
Expand Down
123 changes: 120 additions & 3 deletions src/utils/inviteSuggestions.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
import { memberMatchesParticipant } from "./memberIdentity";
import { uniqueActiveParticipants } from "./participants";

const candidateIdKeys = [
"player_id",
"playerId",
"user_id",
"userId",
"member_id",
"memberId",
"membership_id",
"membershipId",
"participant_id",
"participantId",
"match_participant_id",
"matchParticipantId",
"invitee_id",
"inviteeId",
"id",
"account_id",
"accountId",
["profile", "player_id"],
["profile", "playerId"],
["profile", "id"],
["profile", "member_id"],
["profile", "memberId"],
["profile", "membership_id"],
["profile", "membershipId"],
["player", "id"],
["player", "player_id"],
["player", "playerId"],
["player", "user_id"],
["player", "userId"],
["player", "member_id"],
["player", "memberId"],
["player", "membership_id"],
["player", "membershipId"],
["user", "id"],
["user", "user_id"],
["user", "userId"],
["user", "player_id"],
["user", "playerId"],
["user", "member_id"],
["user", "memberId"],
["user", "membership_id"],
["user", "membershipId"],
["member", "id"],
["member", "user_id"],
["member", "userId"],
["member", "player_id"],
["member", "playerId"],
["member", "member_id"],
["member", "memberId"],
["member", "membership_id"],
["member", "membershipId"],
];

const readValue = (subject, key) => {
Expand Down Expand Up @@ -141,12 +169,98 @@ const extractMatchMoment = (match) => {
const toArrayOrNull = (value) => {
if (!value) return null;
if (Array.isArray(value)) return value;
if (typeof value[Symbol.iterator] === "function") {
return Array.from(value);
if (typeof value === "object") {
const iterable = value[Symbol.iterator];
if (typeof iterable === "function") {
return Array.from(value);
}
const arrayLikeKeys = ["data", "items", "results", "values", "participants"];
for (const key of arrayLikeKeys) {
if (Array.isArray(value[key])) {
return value[key];
}
}
}
return null;
};

const collectMatchParticipants = (match) => {
if (!match || typeof match !== "object") {
return [];
}

const candidates = [
match.participants,
match.match,
match.players,
match.teammates,
match.members,
match.match_participants,
match.matchParticipants,
match.match_participant_list,
match.matchParticipantList,
match.match?.participants,
match.match?.match_participants,
match.match?.matchParticipants,
match.match?.players,
match.match?.teammates,
match.match?.members,
];

const aggregated = [];

const enqueue = (value) => {
const arr = toArrayOrNull(value);
if (!arr || arr.length === 0) return;
arr.forEach((participant) => {
if (!participant || typeof participant !== "object") return;
aggregated.push(participant);
});
};

const inspectCandidate = (value) => {
if (!value || typeof value !== "object") return;
enqueue(value);
const nestedKeys = [
"participants",
"players",
"teammates",
"members",
"match_participants",
"matchParticipants",
"participant_list",
"participantList",
"player_list",
"playerList",
"match_participant_list",
"matchParticipantList",
];
nestedKeys.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(value, key)) {
enqueue(value[key]);
}
});
};

candidates.forEach((candidate) => {
if (!candidate) return;
if (candidate === match.match) {
inspectCandidate(candidate);
} else {
enqueue(candidate);
if (typeof candidate === "object") {
inspectCandidate(candidate);
}
}
});

if (match.match && typeof match.match === "object" && match.match !== match) {
inspectCandidate(match.match);
}

return aggregated;
};

export const buildRecentPartnerSuggestions = ({
matches = [],
currentUser,
Expand All @@ -165,7 +279,7 @@ export const buildRecentPartnerSuggestions = ({

matches.forEach((match) => {
const { timestamp, iso } = extractMatchMoment(match);
const participants = uniqueActiveParticipants(match?.participants);
const participants = uniqueActiveParticipants(collectMatchParticipants(match));
if (participants.length === 0) return;

const userIsInMatch =
Expand Down Expand Up @@ -197,6 +311,9 @@ export const buildRecentPartnerSuggestions = ({
full_name: name,
lastPlayedAt: iso,
lastPlayedTs: timestamp ?? null,
profile: participant.profile || null,
player: participant.player || null,
member: participant.member || null,
});
}
});
Expand Down