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:
Trey T
2026-04-16 07:11:21 -05:00
parent 0a725508d2
commit f84786e654
176 changed files with 6828 additions and 1177 deletions
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@apollo/client/react';
import { PROFILE_QUERY, ACCOUNT_STATUS_QUERY } from '../../api/operations/queries';
import { PROFILE_LIKE_MUTATION, PROFILE_PING_MUTATION, PROFILE_DISLIKE_MUTATION } from '../../api/operations/mutations';
import { PROFILE_LIKE_MUTATION, PROFILE_PING_MUTATION, PROFILE_DISLIKE_MUTATION, PROFILE_BLOCK_MUTATION, PROFILE_REPORT_MUTATION } from '../../api/operations/mutations';
import { addLikedProfileToStorage } from '../../hooks/useLikedProfiles';
import { addSentPing, addDislikedProfile } from '../../api/dataSync';
import { authManager } from '../../api/auth';
import { ProxiedImage } from '../ui/ProxiedImage';
import { PingModal } from './PingModal';
@@ -390,6 +391,106 @@ const styles = {
boxShadow: variant === 'primary' ? '0 8px 24px rgba(190,49,68,0.4)' : 'none',
}),
// More options button
moreButton: {
position: 'absolute' as const,
top: '16px',
right: '64px',
width: '40px',
height: '40px',
borderRadius: '50%',
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(8px)',
border: '1px solid rgba(255,255,255,0.15)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
cursor: 'pointer',
transition: 'all 200ms ease',
zIndex: 10,
fontSize: '20px',
fontWeight: 700,
fontFamily: "'Satoshi', sans-serif",
letterSpacing: '1px',
},
// Block/Report dropdown
blockReportOverlay: {
position: 'absolute' as const,
inset: 0,
zIndex: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.7)',
backdropFilter: 'blur(4px)',
},
blockReportMenu: {
width: '280px',
background: 'rgba(15,15,20,0.95)',
borderRadius: '20px',
border: '1px solid rgba(255,255,255,0.1)',
padding: '8px',
boxShadow: '0 20px 40px rgba(0,0,0,0.5)',
},
blockReportTitle: {
fontFamily: "'Clash Display', sans-serif",
fontSize: '16px',
fontWeight: 600,
color: '#fff',
textAlign: 'center' as const,
padding: '12px 16px 8px',
margin: 0,
},
blockReportItem: (variant: 'danger' | 'warning' | 'default') => ({
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
padding: '14px 16px',
borderRadius: '12px',
border: 'none',
background: 'transparent',
color: variant === 'danger' ? '#ef4444' : variant === 'warning' ? '#f59e0b' : 'rgba(255,255,255,0.8)',
fontSize: '14px',
fontWeight: 500,
fontFamily: "'Satoshi', sans-serif",
cursor: 'pointer',
transition: 'background 150ms ease',
textAlign: 'left' as const,
}),
blockReportDivider: {
height: '1px',
background: 'rgba(255,255,255,0.06)',
margin: '4px 8px',
},
blockReportCancel: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
padding: '14px 16px',
borderRadius: '12px',
border: 'none',
background: 'rgba(255,255,255,0.05)',
color: 'rgba(255,255,255,0.6)',
fontSize: '14px',
fontWeight: 500,
fontFamily: "'Satoshi', sans-serif",
cursor: 'pointer',
transition: 'background 150ms ease',
marginTop: '4px',
},
blockReportError: {
fontFamily: "'Satoshi', sans-serif",
fontSize: '13px',
color: '#ef4444',
textAlign: 'center' as const,
padding: '8px 16px',
margin: 0,
},
// Error state
errorContainer: {
padding: '60px 40px',
@@ -426,6 +527,10 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
const [showPingModal, setShowPingModal] = useState(false);
const [viewingProfileId, setViewingProfileId] = useState(profileId);
const [profileHistory, setProfileHistory] = useState<string[]>([]);
const [showBlockReport, setShowBlockReport] = useState(false);
const [blockReportMode, setBlockReportMode] = useState<'block' | 'report' | null>(null);
const [blockReportLoading, setBlockReportLoading] = useState(false);
const [blockReportError, setBlockReportError] = useState<string | null>(null);
const handleViewPartner = (partnerId: string) => {
setProfileHistory(prev => [...prev, viewingProfileId]);
@@ -445,7 +550,11 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (showPingModal) {
if (showBlockReport) {
setShowBlockReport(false);
setBlockReportMode(null);
setBlockReportError(null);
} else if (showPingModal) {
setShowPingModal(false);
} else {
handleClose();
@@ -455,7 +564,7 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [showPingModal]);
}, [showPingModal, showBlockReport]);
const handleClose = () => {
setIsClosing(true);
@@ -466,6 +575,21 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
variables: { profileId: viewingProfileId },
});
// Fetch our cached discoveredLocation (the saved-location label that was
// active when we last saw this profile).
const [discoveredLocation, setDiscoveredLocation] = useState<string | null>(null);
useEffect(() => {
if (!viewingProfileId) return;
setDiscoveredLocation(null);
fetch(`/api/discovered-profiles/lookup/${encodeURIComponent(viewingProfileId)}`)
.then(r => r.ok ? r.json() : null)
.then(d => {
const loc = d?.profile?.discoveredLocation;
if (typeof loc === 'string' && loc) setDiscoveredLocation(loc);
})
.catch(() => {});
}, [viewingProfileId]);
// Query for available pings
const { data: accountData, refetch: refetchAccount } = useQuery(ACCOUNT_STATUS_QUERY, {
fetchPolicy: 'cache-and-network',
@@ -517,6 +641,62 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
},
});
const [blockProfile] = useMutation(PROFILE_BLOCK_MUTATION);
const [reportProfile] = useMutation(PROFILE_REPORT_MUTATION);
const handleBlock = async (blockCategory: string) => {
setBlockReportLoading(true);
setBlockReportError(null);
try {
await blockProfile({
variables: {
input: {
targetProfileId: viewingProfileId,
blockCategory,
},
},
});
setShowBlockReport(false);
setBlockReportMode(null);
handleClose();
} catch (err: any) {
setBlockReportError(err.message || 'Failed to block profile');
} finally {
setBlockReportLoading(false);
}
};
const handleReport = async (reportCategory: string) => {
setBlockReportLoading(true);
setBlockReportError(null);
try {
await reportProfile({
variables: {
input: {
targetProfileId: viewingProfileId,
sourceProfileId: authManager.getProfileId(),
reportCategory,
},
},
});
setBlockReportMode(null);
setBlockReportError(null);
// Show brief confirmation then close
setBlockReportLoading(false);
setTimeout(() => {
setShowBlockReport(false);
handleClose();
}, 1200);
// Temporarily reuse error field for success message
setBlockReportError('Report submitted successfully.');
return;
} catch (err: any) {
setBlockReportError(err.message || 'Failed to report profile');
} finally {
setBlockReportLoading(false);
}
};
const handleSendPing = (message: string) => {
sendPingMutation({
variables: {
@@ -594,6 +774,21 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
<div style={styles.photoGradient} />
{/* More options button */}
<button
style={styles.moreButton}
onClick={() => { setShowBlockReport(true); setBlockReportMode(null); setBlockReportError(null); }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.7)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.5)'}
title="More options"
>
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: '20px', height: '20px' }}>
<circle cx="12" cy="5" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="12" cy="19" r="2" />
</svg>
</button>
{/* Close button */}
<button
style={styles.closeButton}
@@ -677,6 +872,12 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
<span>{Math.round(profile.distance.mi)} mi away</span>
</>
)}
{discoveredLocation && (
<>
<div style={styles.detailDot} />
<span>{discoveredLocation}</span>
</>
)}
</div>
{/* Likes you badge */}
{theyLikedMe && (
@@ -885,6 +1086,115 @@ export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetai
</button>
</div>
{/* Block/Report Overlay */}
{showBlockReport && (
<div style={styles.blockReportOverlay} onClick={() => { if (!blockReportLoading) { setShowBlockReport(false); setBlockReportMode(null); setBlockReportError(null); } }}>
<div style={styles.blockReportMenu} onClick={(e) => e.stopPropagation()}>
{!blockReportMode ? (
<>
<p style={styles.blockReportTitle}>Options</p>
<button
style={styles.blockReportItem('danger')}
onClick={() => setBlockReportMode('block')}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(239,68,68,0.1)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '18px', height: '18px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
Block
</button>
<div style={styles.blockReportDivider} />
<button
style={styles.blockReportItem('warning')}
onClick={() => setBlockReportMode('report')}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(245,158,11,0.1)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '18px', height: '18px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3v1.5M3 21v-6m0 0l2.77-.693a9 9 0 016.208.682l.108.054a9 9 0 006.086.71l3.114-.732a48.524 48.524 0 01-.005-10.499l-3.11.732a9 9 0 01-6.085-.711l-.108-.054a9 9 0 00-6.208-.682L3 4.5M3 15V4.5" />
</svg>
Report
</button>
<button
style={styles.blockReportCancel}
onClick={() => { setShowBlockReport(false); setBlockReportError(null); }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
>
Cancel
</button>
</>
) : blockReportMode === 'block' ? (
<>
<p style={styles.blockReportTitle}>Block Reason</p>
{['NOT_INTERESTED', 'SOMETHING_ELSE'].map((category) => (
<button
key={category}
style={{
...styles.blockReportItem('default'),
opacity: blockReportLoading ? 0.5 : 1,
cursor: blockReportLoading ? 'not-allowed' : 'pointer',
}}
onClick={() => !blockReportLoading && handleBlock(category)}
disabled={blockReportLoading}
onMouseEnter={(e) => { if (!blockReportLoading) e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
>
{blockReportLoading ? 'Blocking...' : category.replace(/_/g, ' ')}
</button>
))}
{blockReportError && <p style={styles.blockReportError}>{blockReportError}</p>}
<button
style={{ ...styles.blockReportCancel, opacity: blockReportLoading ? 0.5 : 1 }}
onClick={() => { if (!blockReportLoading) { setBlockReportMode(null); setBlockReportError(null); } }}
disabled={blockReportLoading}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
>
Back
</button>
</>
) : (
<>
<p style={styles.blockReportTitle}>Report Reason</p>
{['INAPPROPRIATE', 'UNDERAGE', 'OFFENSIVE', 'OTHER'].map((category) => (
<button
key={category}
style={{
...styles.blockReportItem('default'),
opacity: blockReportLoading ? 0.5 : 1,
cursor: blockReportLoading ? 'not-allowed' : 'pointer',
}}
onClick={() => !blockReportLoading && handleReport(category)}
disabled={blockReportLoading}
onMouseEnter={(e) => { if (!blockReportLoading) e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
>
{blockReportLoading ? 'Reporting...' : category.replace(/_/g, ' ')}
</button>
))}
{blockReportError && (
<p style={{
...styles.blockReportError,
color: blockReportError.includes('successfully') ? '#22c55e' : '#ef4444',
}}>{blockReportError}</p>
)}
<button
style={{ ...styles.blockReportCancel, opacity: blockReportLoading ? 0.5 : 1 }}
onClick={() => { if (!blockReportLoading) { setBlockReportMode(null); setBlockReportError(null); } }}
disabled={blockReportLoading}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
>
Back
</button>
</>
)}
</div>
</div>
)}
{/* Ping Modal */}
{showPingModal && (
<PingModal