Files
Feeld/web/server/matchScoring.js
Trey T f84786e654 Add Matches page, OkCupid integration, and major UI/feature updates
- 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>
2026-04-16 07:11:21 -05:00

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 };