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
96 changes: 94 additions & 2 deletions src/TennisMatchApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3052,6 +3052,10 @@ const TennisMatchApp = () => {
}
const styles = notificationTypeMap[notification.canonicalType] || notificationTypeMap.general;
const meta = [];
const playerName =
typeof notification.playerName === "string" ? notification.playerName.trim() : "";
const matchLabel =
typeof notification.matchLabel === "string" ? notification.matchLabel.trim() : "";
if (notification.matchLabel) {
meta.push({ icon: Calendar, label: notification.matchLabel });
}
Expand All @@ -3066,6 +3070,9 @@ const TennisMatchApp = () => {
meta.push({ icon: null, label: tag });
});
}
if (playerName) {
meta.unshift({ icon: User, label: playerName });
}

const actions = [];
if (notification.matchId) {
Expand All @@ -3076,13 +3083,98 @@ const TennisMatchApp = () => {
});
}

const joinedMatchLabel = matchLabel ? ` ${matchLabel}` : "";
const inviteMatchLabel = matchLabel ? ` for ${matchLabel}` : "";
const ensureSentence = (value) => {
if (!value) return "";
const trimmed = value.trim();
if (!trimmed) return "";
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
};
let title = typeof notification.title === "string" ? notification.title.trim() : "";
let description = typeof notification.body === "string" ? notification.body.trim() : "";

if (!title) {
switch (notification.canonicalType) {
case "player_joined":
title = playerName
? `${playerName} joined${joinedMatchLabel}`
: `${styles.statusLabel}${joinedMatchLabel ? ` ·${joinedMatchLabel}` : ""}`;
break;
case "player_left":
title = playerName
? `${playerName} left${joinedMatchLabel}`
: `${styles.statusLabel}${joinedMatchLabel ? ` ·${joinedMatchLabel}` : ""}`;
break;
case "invite_accepted":
title = playerName
? `${playerName} accepted the invite${inviteMatchLabel}`
: styles.statusLabel;
break;
case "invite_declined":
title = playerName
? `${playerName} declined the invite${inviteMatchLabel}`
: styles.statusLabel;
break;
case "invite_sent":
title = playerName
? `Invite sent to ${playerName}${inviteMatchLabel}`
: styles.statusLabel;
break;
default:
title = styles.statusLabel;
}
}

if (!description) {
switch (notification.canonicalType) {
case "player_joined":
description = ensureSentence(
playerName
? `${playerName} is confirmed to play${joinedMatchLabel}`
: `A player joined${joinedMatchLabel}`,
);
break;
case "player_left":
description = ensureSentence(
playerName
? `${playerName} is no longer participating${joinedMatchLabel}`
: `A player left${joinedMatchLabel}`,
);
break;
case "invite_accepted":
description = ensureSentence(
playerName
? `${playerName} will be joining${joinedMatchLabel || " this match"}`
: `An invite was accepted${inviteMatchLabel}`,
);
break;
case "invite_declined":
description = ensureSentence(
playerName
? `${playerName} passed on${joinedMatchLabel || " this match"}`
: `An invite was declined${inviteMatchLabel}`,
);
break;
case "invite_sent":
description = ensureSentence(
playerName
? `An invite was delivered to ${playerName}${inviteMatchLabel}`
: `An invite was sent${inviteMatchLabel}`,
);
break;
default:
description = "";
}
}

items.push({
id: `notification-${notification.id}`,
statusLabel: styles.statusLabel,
tone: styles.tone,
icon: styles.icon,
title: notification.title || styles.statusLabel,
description: notification.body || "",
title,
description,
meta,
timestamp: notification.createdAt || null,
timestampLabel: notification.createdAtLabel || "",
Expand Down
138 changes: 125 additions & 13 deletions src/components/NotificationsFeed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,128 @@ const cleanString = (value) => {
};

const formatPersonName = (subject, fallback = "") => {
if (!subject) return fallback;
if (typeof subject === "string") return subject.trim() || fallback;
if (typeof subject === "object") {
const seen = new Set();

const extractFromObject = (value, depth = 0) => {
if (!value || depth > 4) return "";
if (typeof value === "string") return cleanString(value);
if (typeof value === "number" && Number.isFinite(value)) {
return cleanString(String(value));
}
if (typeof value !== "object") return "";
if (seen.has(value)) return "";
seen.add(value);

if (Array.isArray(value)) {
for (const entry of value) {
const arrayResult = extractFromObject(entry, depth + 1);
if (arrayResult) return arrayResult;
}
return "";
}

const {
name,
full_name,
fullName,
first_name,
firstName,
last_name,
lastName,
preferred_name,
preferredName,
display_name,
displayName,
nickname,
nick_name,
nickName,
given_name,
givenName,
family_name,
familyName,
surname,
player_name,
playerName,
member_name,
memberName,
} = subject;
participant_name,
participantName,
subject_name,
subjectName,
player_full_name,
playerFullName,
member_full_name,
memberFullName,
} = value;

const direct =
cleanString(name) ||
cleanString(full_name) ||
cleanString(fullName) ||
cleanString(preferred_name) ||
cleanString(preferredName) ||
cleanString(display_name) ||
cleanString(displayName) ||
cleanString(nickname) ||
cleanString(nick_name) ||
cleanString(nickName) ||
cleanString(player_full_name) ||
cleanString(playerFullName) ||
cleanString(member_full_name) ||
cleanString(memberFullName) ||
cleanString(player_name) ||
cleanString(playerName) ||
cleanString(member_name) ||
cleanString(memberName);
cleanString(memberName) ||
cleanString(participant_name) ||
cleanString(participantName) ||
cleanString(subject_name) ||
cleanString(subjectName);
if (direct) return direct;
const first = cleanString(first_name) || cleanString(firstName);
const last = cleanString(last_name) || cleanString(lastName);

const first =
cleanString(preferred_name) ||
cleanString(preferredName) ||
cleanString(given_name) ||
cleanString(givenName) ||
cleanString(name) ||
cleanString(value.first_name) ||
cleanString(value.firstName);
const last =
cleanString(family_name) ||
cleanString(familyName) ||
cleanString(surname) ||
cleanString(value.last_name) ||
cleanString(value.lastName);
if (first || last) {
return [first, last].filter(Boolean).join(" ");
}
}
return fallback;

const nestedCandidates = [
value.player,
value.member,
value.participant,
value.invitee,
value.user,
value.person,
value.subject,
value.profile,
value.owner,
value.contact,
value.context,
value.meta,
value.details,
value.data,
value.attributes,
value.info,
];

for (const candidate of nestedCandidates) {
const nestedResult = extractFromObject(candidate, depth + 1);
if (nestedResult) return nestedResult;
}

return "";
};

const result = extractFromObject(subject);
return result || fallback;
};

const humanizeKey = (value = "") => {
Expand Down Expand Up @@ -229,6 +315,32 @@ const resolveMatchFromNotification = (notification) => {

const resolvePlayerFromNotification = (notification) => {
if (!notification) return null;
const directName =
cleanString(notification.player_full_name) ||
cleanString(notification.playerFullName) ||
cleanString(notification.member_full_name) ||
cleanString(notification.memberFullName) ||
cleanString(notification.subject_name) ||
cleanString(notification.subjectName) ||
cleanString(notification.context?.player_full_name) ||
cleanString(notification.context?.playerFullName) ||
cleanString(notification.context?.member_full_name) ||
cleanString(notification.context?.memberFullName) ||
cleanString(notification.context?.subject_name) ||
cleanString(notification.context?.subjectName) ||
cleanString(notification.meta?.player_full_name) ||
cleanString(notification.meta?.playerFullName) ||
cleanString(notification.meta?.member_full_name) ||
cleanString(notification.meta?.memberFullName) ||
cleanString(notification.meta?.subject_name) ||
cleanString(notification.meta?.subjectName) ||
cleanString(notification.data?.player_full_name) ||
cleanString(notification.data?.playerFullName) ||
cleanString(notification.data?.member_full_name) ||
cleanString(notification.data?.memberFullName) ||
cleanString(notification.data?.subject_name) ||
cleanString(notification.data?.subjectName);
if (directName) return directName;
return (
notification.player ||
notification.member ||
Expand Down