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>
This commit is contained in:
142
web/server/matchScoring.js
Normal file
142
web/server/matchScoring.js
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user