const EMPTY_BOARD = { to_me: [], by_me: [], together: [], hidden: [], no_go: [] }; export function sortedBoardItems(items) { return [...items].sort( (left, right) => (right.popularity || 0) - (left.popularity || 0) || left.name.localeCompare(right.name), ); } export function optimisticBoard(previousBoard, kinkId, interestState, directions, qc, auth) { const kid = String(kinkId); const nextBoard = Object.fromEntries( Object.entries(previousBoard || EMPTY_BOARD).map(([key, items]) => [ key, (items || []).filter((item) => String(item.id) !== kid), ]), ); const sourceItem = findBoardItemInData(previousBoard, kid) || (qc ? findBoardItemFromQuery(qc, auth, kid) : null); /** If play-board has not loaded yet (or kink not in recs cache), still place a stub so we never strip a row and return empty buckets. */ const itemBase = sourceItem || { id: kid, name: kid, summary: "", popularity: 0 }; const item = { ...itemBase, interest_state: interestState, directions }; if (interestState === "hard_no") { nextBoard.no_go.push(item); } else if (interestState === "not_interested") { nextBoard.hidden.push(item); } else { const targetDirections = directions.length ? directions : ["together"]; for (const direction of targetDirections) { if (nextBoard[direction]) nextBoard[direction].push({ ...item }); } } for (const key of Object.keys(nextBoard)) nextBoard[key] = sortedBoardItems(nextBoard[key]); return nextBoard; } export function findBoardItemInData(board, kinkId) { const kid = String(kinkId); for (const items of Object.values(board || EMPTY_BOARD)) { const match = (items || []).find((item) => String(item.id) === kid); if (match) return match; } return null; } function normalizeKinkItem(raw) { if (!raw?.id || !raw?.name) return null; return { id: raw.id, name: raw.name, summary: raw.summary || raw.definition || raw.detail_summary || "", popularity: raw.popularity || 0, }; } function findInItemsPayload(payload, kinkId) { const kid = String(kinkId); const match = payload?.items?.find((item) => String(item?.kink?.id) === kid); return normalizeKinkItem(match?.kink || null); } function findBoardItemFromQuery(qc, auth, kinkId) { const detail = qc.getQueryData(["kink", kinkId]); const fromDetail = normalizeKinkItem(detail); if (fromDetail) return fromDetail; const scopedKeys = [ ["recs", auth?.userId, ""], ["prompts", auth?.userId, ""], ]; for (const key of scopedKeys) { const fromScoped = findInItemsPayload(qc.getQueryData(key), kinkId); if (fromScoped) return fromScoped; } const queryCache = qc.getQueryCache(); const itemQueries = queryCache.findAll({ predicate: (query) => { const key = query.queryKey || []; return key[0] === "recs" || key[0] === "prompts" || key[0] === "search"; }}); for (const query of itemQueries) { const fromItems = findInItemsPayload(query.state?.data, kinkId); if (fromItems) return fromItems; } const overlapQueries = queryCache.findAll({ predicate: (query) => (query.queryKey || [])[0] === "group-overlap" }); for (const query of overlapQueries) { const data = query.state?.data || {}; const direct = (data.direct_matches || []).find((item) => item?.id === kinkId); if (direct) return normalizeKinkItem(direct); const similar = (data.similar_matches || []).find((item) => item?.kink?.id === kinkId)?.kink; const fromSimilar = normalizeKinkItem(similar); if (fromSimilar) return fromSimilar; } return null; } export function removeKinkFromItemsPayload(payload, kinkId) { if (!payload?.items) return payload; const kid = String(kinkId); return { ...payload, items: payload.items.filter((item) => String(item?.kink?.id) !== kid), }; } export function playCommitKey(kinkId, interestState, directions = []) { return `${kinkId}::${interestState}::${[...(directions || [])].sort().join("|")}`; }