- New Matches page with match scoring system - New OkCupid page and API integration - Enhanced Likes page with scanner improvements and enrichment - Updated Settings, Discover, Messages, and Chat pages - Improved auth, GraphQL client, and Stream Chat setup - Added new backend endpoints (matchScoring.js) - Removed old Proxyman capture logs - Updated nginx config and Vite proxy settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
// Match scoring engine for discovered profiles
|
|
|
|
const DEFAULT_WEIGHTS = {
|
|
verification: 15,
|
|
photoBase: 2, // per photo (max 6)
|
|
photoVerified: 3, // per verified photo
|
|
bioLong: 15, // >200 chars
|
|
bioMedium: 10, // >100 chars
|
|
bioShort: 5, // >30 chars
|
|
desiresMany: 8, // >=5 desires
|
|
desiresSome: 5, // >=3 desires
|
|
connectionGoals: 5, // has any
|
|
distanceClose: 15, // <=15mi
|
|
distanceMedium: 10, // <=30mi
|
|
distanceFar: 5, // <=50mi
|
|
ageSweetSpot: 15, // 24-40 (preferred)
|
|
ageOk: 5, // 21-45
|
|
ageOutOfRange: -10, // outside 21-45 penalty
|
|
theyLikedYou: 30, // interactionStatus.theirs === 'LIKED'
|
|
connectionDesires: 5, // >=2 of CONNECTION/COMMUNICATION/FWB/INTIMACY/RELATIONSHIP
|
|
};
|
|
|
|
const CONNECTION_DESIRE_KEYWORDS = [
|
|
'CONNECTION', 'COMMUNICATION', 'FWB', 'INTIMACY', 'RELATIONSHIP',
|
|
'connection', 'communication', 'fwb', 'intimacy', 'relationship',
|
|
'Friends with benefits', 'Long-term relationship', 'Short-term relationship',
|
|
];
|
|
|
|
function safeStr(v) {
|
|
return typeof v === 'string' ? v : '';
|
|
}
|
|
|
|
function scoreProfile(profile, weights = DEFAULT_WEIGHTS) {
|
|
const breakdown = {};
|
|
let total = 0;
|
|
|
|
// Verification
|
|
const verStatus = safeStr(profile.verificationStatus);
|
|
if (verStatus === 'VERIFIED' || verStatus === 'verified') {
|
|
breakdown.verification = weights.verification;
|
|
total += weights.verification;
|
|
}
|
|
|
|
// Photos
|
|
const photos = Array.isArray(profile.photos) ? profile.photos : [];
|
|
const photoCount = Math.min(photos.length, 6);
|
|
if (photoCount > 0) {
|
|
const photoScore = photoCount * weights.photoBase;
|
|
breakdown.photos = photoScore;
|
|
total += photoScore;
|
|
}
|
|
|
|
// Verified photos (pictureType === 'VERIFIED' or similar)
|
|
const verifiedPhotos = photos.filter(p =>
|
|
p.pictureType === 'VERIFIED' || p.pictureType === 'verified'
|
|
).length;
|
|
if (verifiedPhotos > 0) {
|
|
const vpScore = verifiedPhotos * weights.photoVerified;
|
|
breakdown.verifiedPhotos = vpScore;
|
|
total += vpScore;
|
|
}
|
|
|
|
// Bio quality
|
|
const bio = safeStr(profile.bio);
|
|
if (bio.length > 200) {
|
|
breakdown.bio = weights.bioLong;
|
|
total += weights.bioLong;
|
|
} else if (bio.length > 100) {
|
|
breakdown.bio = weights.bioMedium;
|
|
total += weights.bioMedium;
|
|
} else if (bio.length > 30) {
|
|
breakdown.bio = weights.bioShort;
|
|
total += weights.bioShort;
|
|
}
|
|
|
|
// Desires
|
|
const desires = Array.isArray(profile.desires) ? profile.desires : [];
|
|
if (desires.length >= 5) {
|
|
breakdown.desires = weights.desiresMany;
|
|
total += weights.desiresMany;
|
|
} else if (desires.length >= 3) {
|
|
breakdown.desires = weights.desiresSome;
|
|
total += weights.desiresSome;
|
|
}
|
|
|
|
// Connection goals
|
|
const goals = Array.isArray(profile.connectionGoals) ? profile.connectionGoals : [];
|
|
if (goals.length > 0) {
|
|
breakdown.connectionGoals = weights.connectionGoals;
|
|
total += weights.connectionGoals;
|
|
}
|
|
|
|
// Distance
|
|
const distMi = profile.distance?.mi;
|
|
if (typeof distMi === 'number') {
|
|
if (distMi <= 15) {
|
|
breakdown.distance = weights.distanceClose;
|
|
total += weights.distanceClose;
|
|
} else if (distMi <= 30) {
|
|
breakdown.distance = weights.distanceMedium;
|
|
total += weights.distanceMedium;
|
|
} else if (distMi <= 50) {
|
|
breakdown.distance = weights.distanceFar;
|
|
total += weights.distanceFar;
|
|
}
|
|
}
|
|
|
|
// Age preference: 24-40 sweet spot, 21-45 ok, outside penalized
|
|
const age = profile.age;
|
|
if (typeof age === 'number') {
|
|
if (age >= 24 && age <= 40) {
|
|
breakdown.age = weights.ageSweetSpot;
|
|
total += weights.ageSweetSpot;
|
|
} else if (age >= 21 && age <= 45) {
|
|
breakdown.age = weights.ageOk;
|
|
total += weights.ageOk;
|
|
} else {
|
|
breakdown.age = weights.ageOutOfRange;
|
|
total += weights.ageOutOfRange;
|
|
}
|
|
}
|
|
|
|
// They liked you
|
|
if (profile.interactionStatus?.theirs === 'LIKED') {
|
|
breakdown.theyLikedYou = weights.theyLikedYou;
|
|
total += weights.theyLikedYou;
|
|
}
|
|
|
|
// Connection desires (check desires array for relationship-oriented ones)
|
|
const desireStrings = desires.map(d => typeof d === 'string' ? d : '');
|
|
const matchingDesires = desireStrings.filter(d =>
|
|
CONNECTION_DESIRE_KEYWORDS.some(kw => d.toUpperCase().includes(kw.toUpperCase()))
|
|
);
|
|
if (matchingDesires.length >= 2) {
|
|
breakdown.connectionDesires = weights.connectionDesires;
|
|
total += weights.connectionDesires;
|
|
}
|
|
|
|
return { total, breakdown };
|
|
}
|
|
|
|
export { DEFAULT_WEIGHTS, scoreProfile, safeStr };
|