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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user