904 lines
32 KiB
TypeScript
Executable File
904 lines
32 KiB
TypeScript
Executable File
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 { addLikedProfileToStorage } from '../../hooks/useLikedProfiles';
|
|
import { addSentPing, addDislikedProfile } from '../../api/dataSync';
|
|
import { ProxiedImage } from '../ui/ProxiedImage';
|
|
import { PingModal } from './PingModal';
|
|
|
|
interface ProfileDetailModalProps {
|
|
profileId: string;
|
|
onClose: () => void;
|
|
onMatch?: () => void;
|
|
}
|
|
|
|
const styles = {
|
|
// Overlay
|
|
overlay: (isClosing: boolean) => ({
|
|
position: 'fixed' as const,
|
|
inset: 0,
|
|
zIndex: 50,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '16px',
|
|
opacity: isClosing ? 0 : 1,
|
|
transition: 'opacity 200ms ease-out',
|
|
}),
|
|
backdrop: {
|
|
position: 'absolute' as const,
|
|
inset: 0,
|
|
background: 'rgba(0,0,0,0.85)',
|
|
backdropFilter: 'blur(12px)',
|
|
},
|
|
|
|
// Modal container
|
|
modal: (isClosing: boolean) => ({
|
|
position: 'relative' as const,
|
|
width: '100%',
|
|
maxWidth: '560px',
|
|
maxHeight: '90vh',
|
|
background: '#0f0f13',
|
|
borderRadius: '24px',
|
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
overflow: 'hidden',
|
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.7)',
|
|
transform: isClosing ? 'scale(0.95)' : 'scale(1)',
|
|
opacity: isClosing ? 0 : 1,
|
|
transition: 'transform 200ms ease-out, opacity 200ms ease-out',
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
}),
|
|
|
|
// Scrollable wrapper for photo + content
|
|
scrollableContent: {
|
|
flex: 1,
|
|
overflowY: 'auto' as const,
|
|
overflowX: 'hidden' as const,
|
|
},
|
|
|
|
// Loading state
|
|
loadingContainer: {
|
|
padding: '80px',
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
loadingSpinner: {
|
|
width: '40px',
|
|
height: '40px',
|
|
borderRadius: '50%',
|
|
border: '3px solid rgba(190,49,68,0.2)',
|
|
borderTopColor: '#be3144',
|
|
animation: 'spin 1s linear infinite',
|
|
},
|
|
|
|
// Photo section
|
|
photoSection: {
|
|
position: 'relative' as const,
|
|
aspectRatio: '3/4',
|
|
background: '#000',
|
|
overflow: 'hidden',
|
|
flexShrink: 0,
|
|
},
|
|
photo: {
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'cover' as const,
|
|
transition: 'opacity 300ms ease',
|
|
},
|
|
photoGradient: {
|
|
position: 'absolute' as const,
|
|
inset: 0,
|
|
background: 'linear-gradient(180deg, rgba(0,0,0,0.3) 0%, transparent 20%, transparent 50%, rgba(15,15,19,0.8) 80%, rgba(15,15,19,1) 100%)',
|
|
pointerEvents: 'none' as const,
|
|
},
|
|
|
|
// Photo navigation
|
|
navButton: (side: 'left' | 'right') => ({
|
|
position: 'absolute' as const,
|
|
top: '50%',
|
|
[side]: '16px',
|
|
transform: 'translateY(-50%)',
|
|
width: '44px',
|
|
height: '44px',
|
|
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',
|
|
}),
|
|
photoIndicators: {
|
|
position: 'absolute' as const,
|
|
top: '16px',
|
|
left: '16px',
|
|
right: '16px',
|
|
display: 'flex',
|
|
gap: '4px',
|
|
},
|
|
photoIndicator: (isActive: boolean) => ({
|
|
flex: 1,
|
|
height: '3px',
|
|
borderRadius: '2px',
|
|
background: isActive ? '#fff' : 'rgba(255,255,255,0.3)',
|
|
transition: 'background 200ms ease',
|
|
cursor: 'pointer',
|
|
}),
|
|
photoCounter: {
|
|
position: 'absolute' as const,
|
|
bottom: '100px',
|
|
right: '16px',
|
|
padding: '6px 12px',
|
|
borderRadius: '20px',
|
|
background: 'rgba(0,0,0,0.6)',
|
|
backdropFilter: 'blur(8px)',
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
color: 'rgba(255,255,255,0.8)',
|
|
},
|
|
|
|
// Close button
|
|
closeButton: {
|
|
position: 'absolute' as const,
|
|
top: '16px',
|
|
right: '16px',
|
|
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,
|
|
},
|
|
|
|
// Header overlay on photo
|
|
headerOverlay: {
|
|
position: 'absolute' as const,
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
padding: '24px',
|
|
zIndex: 5,
|
|
},
|
|
nameRow: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '12px',
|
|
marginBottom: '8px',
|
|
},
|
|
name: {
|
|
fontFamily: "'Clash Display', sans-serif",
|
|
fontSize: '32px',
|
|
fontWeight: 700,
|
|
color: '#fff',
|
|
margin: 0,
|
|
letterSpacing: '-0.02em',
|
|
},
|
|
age: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '24px',
|
|
fontWeight: 500,
|
|
color: 'rgba(255,255,255,0.7)',
|
|
},
|
|
badge: (variant: 'majestic' | 'verified') => ({
|
|
width: '32px',
|
|
height: '32px',
|
|
borderRadius: '50%',
|
|
background: variant === 'majestic'
|
|
? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
|
|
: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
boxShadow: variant === 'majestic'
|
|
? '0 4px 12px rgba(245,158,11,0.4)'
|
|
: '0 4px 12px rgba(59,130,246,0.4)',
|
|
}),
|
|
detailsRow: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '15px',
|
|
color: 'rgba(255,255,255,0.6)',
|
|
},
|
|
detailDot: {
|
|
width: '4px',
|
|
height: '4px',
|
|
borderRadius: '50%',
|
|
background: 'rgba(255,255,255,0.3)',
|
|
},
|
|
interactionBadge: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
padding: '6px 12px',
|
|
borderRadius: '20px',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
background: 'rgba(34,197,94,0.2)',
|
|
color: '#86efac',
|
|
marginTop: '12px',
|
|
},
|
|
scrollHint: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '6px',
|
|
marginTop: '16px',
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '13px',
|
|
color: 'rgba(255,255,255,0.5)',
|
|
animation: 'bounce 2s infinite',
|
|
},
|
|
|
|
// Content area
|
|
content: {
|
|
padding: '24px',
|
|
},
|
|
|
|
// Bio section
|
|
bioSection: {
|
|
marginBottom: '24px',
|
|
},
|
|
bioText: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '15px',
|
|
lineHeight: 1.7,
|
|
color: 'rgba(255,255,255,0.85)',
|
|
whiteSpace: 'pre-wrap' as const,
|
|
margin: 0,
|
|
},
|
|
|
|
// Section
|
|
section: {
|
|
marginBottom: '20px',
|
|
},
|
|
sectionLabel: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
textTransform: 'uppercase' as const,
|
|
letterSpacing: '0.1em',
|
|
color: 'rgba(255,255,255,0.4)',
|
|
marginBottom: '12px',
|
|
},
|
|
tagsContainer: {
|
|
display: 'flex',
|
|
flexWrap: 'wrap' as const,
|
|
gap: '8px',
|
|
},
|
|
tag: (variant: 'primary' | 'default' | 'success') => {
|
|
const colors = {
|
|
primary: { bg: 'rgba(190,49,68,0.2)', color: '#f4a5b0', border: 'rgba(190,49,68,0.3)' },
|
|
default: { bg: 'rgba(255,255,255,0.05)', color: 'rgba(255,255,255,0.7)', border: 'rgba(255,255,255,0.1)' },
|
|
success: { bg: 'rgba(34,197,94,0.15)', color: '#86efac', border: 'rgba(34,197,94,0.25)' },
|
|
};
|
|
const c = colors[variant];
|
|
return {
|
|
padding: '8px 14px',
|
|
borderRadius: '20px',
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
background: c.bg,
|
|
color: c.color,
|
|
border: `1px solid ${c.border}`,
|
|
};
|
|
},
|
|
|
|
// Partner section
|
|
partnerCard: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '14px',
|
|
padding: '14px',
|
|
background: 'rgba(255,255,255,0.03)',
|
|
borderRadius: '16px',
|
|
border: '1px solid rgba(255,255,255,0.06)',
|
|
cursor: 'pointer',
|
|
transition: 'all 200ms ease',
|
|
},
|
|
partnerCardHover: {
|
|
background: 'rgba(255,255,255,0.06)',
|
|
border: '1px solid rgba(255,255,255,0.12)',
|
|
},
|
|
partnerChevron: {
|
|
marginLeft: 'auto',
|
|
color: 'rgba(255,255,255,0.4)',
|
|
},
|
|
|
|
// Back button
|
|
backButton: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
padding: '8px 16px',
|
|
marginBottom: '16px',
|
|
background: 'rgba(255,255,255,0.05)',
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: '12px',
|
|
color: 'rgba(255,255,255,0.7)',
|
|
fontSize: '14px',
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
cursor: 'pointer',
|
|
transition: 'all 200ms ease',
|
|
},
|
|
partnerPhoto: {
|
|
width: '48px',
|
|
height: '48px',
|
|
borderRadius: '50%',
|
|
objectFit: 'cover' as const,
|
|
border: '2px solid rgba(190,49,68,0.4)',
|
|
},
|
|
partnerName: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '15px',
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
margin: 0,
|
|
},
|
|
partnerLabel: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '12px',
|
|
color: 'rgba(255,255,255,0.5)',
|
|
margin: 0,
|
|
},
|
|
|
|
// Action bar
|
|
actionBar: {
|
|
padding: '20px 24px',
|
|
borderTop: '1px solid rgba(255,255,255,0.06)',
|
|
display: 'flex',
|
|
gap: '12px',
|
|
},
|
|
actionButton: (variant: 'primary' | 'secondary' | 'ping') => ({
|
|
flex: variant === 'ping' ? 'none' : 1,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '10px',
|
|
padding: variant === 'ping' ? '16px' : '16px 24px',
|
|
borderRadius: '16px',
|
|
fontSize: '15px',
|
|
fontWeight: 600,
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
border: variant === 'primary' ? 'none' : variant === 'ping' ? '1px solid rgba(245,158,11,0.3)' : '1px solid rgba(255,255,255,0.1)',
|
|
background: variant === 'primary'
|
|
? 'linear-gradient(135deg, #be3144 0%, #c41e3a 100%)'
|
|
: variant === 'ping'
|
|
? 'rgba(245,158,11,0.15)'
|
|
: 'rgba(255,255,255,0.05)',
|
|
color: variant === 'ping' ? '#fbbf24' : '#fff',
|
|
cursor: 'pointer',
|
|
transition: 'all 200ms ease',
|
|
boxShadow: variant === 'primary' ? '0 8px 24px rgba(190,49,68,0.4)' : 'none',
|
|
}),
|
|
|
|
// Error state
|
|
errorContainer: {
|
|
padding: '60px 40px',
|
|
textAlign: 'center' as const,
|
|
},
|
|
errorIcon: {
|
|
width: '64px',
|
|
height: '64px',
|
|
borderRadius: '50%',
|
|
background: 'rgba(239,68,68,0.1)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
margin: '0 auto 20px',
|
|
},
|
|
errorTitle: {
|
|
fontFamily: "'Clash Display', sans-serif",
|
|
fontSize: '20px',
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
margin: '0 0 8px 0',
|
|
},
|
|
errorText: {
|
|
fontFamily: "'Satoshi', sans-serif",
|
|
fontSize: '14px',
|
|
color: 'rgba(255,255,255,0.5)',
|
|
margin: '0 0 24px 0',
|
|
},
|
|
};
|
|
|
|
export function ProfileDetailModal({ profileId, onClose, onMatch }: ProfileDetailModalProps) {
|
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
const [showPingModal, setShowPingModal] = useState(false);
|
|
const [viewingProfileId, setViewingProfileId] = useState(profileId);
|
|
const [profileHistory, setProfileHistory] = useState<string[]>([]);
|
|
|
|
const handleViewPartner = (partnerId: string) => {
|
|
setProfileHistory(prev => [...prev, viewingProfileId]);
|
|
setViewingProfileId(partnerId);
|
|
setCurrentPhotoIndex(0);
|
|
};
|
|
|
|
const handleGoBack = () => {
|
|
const previousId = profileHistory[profileHistory.length - 1];
|
|
if (previousId) {
|
|
setProfileHistory(prev => prev.slice(0, -1));
|
|
setViewingProfileId(previousId);
|
|
setCurrentPhotoIndex(0);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
if (showPingModal) {
|
|
setShowPingModal(false);
|
|
} else {
|
|
handleClose();
|
|
}
|
|
} else if (e.key === 'ArrowRight') nextPhoto();
|
|
else if (e.key === 'ArrowLeft') prevPhoto();
|
|
};
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [showPingModal]);
|
|
|
|
const handleClose = () => {
|
|
setIsClosing(true);
|
|
setTimeout(onClose, 200);
|
|
};
|
|
|
|
const { data, loading, error } = useQuery(PROFILE_QUERY, {
|
|
variables: { profileId: viewingProfileId },
|
|
});
|
|
|
|
// Query for available pings
|
|
const { data: accountData, refetch: refetchAccount } = useQuery(ACCOUNT_STATUS_QUERY, {
|
|
fetchPolicy: 'cache-and-network',
|
|
});
|
|
// Always report 2 pings available — bypass Feeld's client-side limit
|
|
const availablePings = Math.max(accountData?.account?.availablePings ?? 2, 2);
|
|
|
|
const [likeProfile, { loading: liking }] = useMutation(PROFILE_LIKE_MUTATION, {
|
|
variables: { targetProfileId: viewingProfileId },
|
|
onCompleted: (data) => {
|
|
// Store liked profile locally for "You Liked" feature
|
|
addLikedProfileToStorage(viewingProfileId, profile?.imaginaryName);
|
|
|
|
if (data.profileLike.status === 'MATCHED') {
|
|
onMatch?.();
|
|
}
|
|
handleClose();
|
|
},
|
|
});
|
|
|
|
const [sendPingMutation, { loading: sendingPing }] = useMutation(PROFILE_PING_MUTATION, {
|
|
onCompleted: async (data) => {
|
|
if (data.profilePing?.status === 'SENT') {
|
|
// Store sent ping locally
|
|
await addSentPing(viewingProfileId, profile?.imaginaryName);
|
|
refetchAccount();
|
|
setShowPingModal(false);
|
|
handleClose();
|
|
}
|
|
},
|
|
});
|
|
|
|
const [dislikeProfile, { loading: disliking }] = useMutation(PROFILE_DISLIKE_MUTATION, {
|
|
variables: { targetProfileId: viewingProfileId },
|
|
onCompleted: async (data) => {
|
|
if (data.profileDislike === 'SENT') {
|
|
// Store disliked profile locally
|
|
await addDislikedProfile({
|
|
id: viewingProfileId,
|
|
imaginaryName: profile?.imaginaryName,
|
|
age: profile?.age,
|
|
gender: profile?.gender,
|
|
sexuality: profile?.sexuality,
|
|
photos: profile?.photos,
|
|
});
|
|
console.log('Disliked profile:', profile?.imaginaryName);
|
|
}
|
|
handleClose();
|
|
},
|
|
});
|
|
|
|
const handleSendPing = (message: string) => {
|
|
sendPingMutation({
|
|
variables: {
|
|
targetProfileId: viewingProfileId,
|
|
message,
|
|
overrideInappropriate: false,
|
|
},
|
|
});
|
|
};
|
|
|
|
const profile = data?.profile;
|
|
const photos = profile?.photos || [];
|
|
const theyLikedMe = profile?.interactionStatus?.theirs === 'LIKED';
|
|
|
|
const nextPhoto = () => {
|
|
if (photos.length > 1) {
|
|
setCurrentPhotoIndex((prev) => (prev + 1) % photos.length);
|
|
}
|
|
};
|
|
|
|
const prevPhoto = () => {
|
|
if (photos.length > 1) {
|
|
setCurrentPhotoIndex((prev) => (prev - 1 + photos.length) % photos.length);
|
|
}
|
|
};
|
|
|
|
const currentPhotoUrl = photos[currentPhotoIndex]?.pictureUrls?.large ||
|
|
photos[currentPhotoIndex]?.pictureUrls?.medium;
|
|
|
|
return (
|
|
<div style={styles.overlay(isClosing)} onClick={handleClose}>
|
|
<div style={styles.backdrop} />
|
|
|
|
<div style={styles.modal(isClosing)} onClick={(e) => e.stopPropagation()}>
|
|
{loading ? (
|
|
<div style={styles.loadingContainer}>
|
|
<div style={styles.loadingSpinner} />
|
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
|
</div>
|
|
) : error ? (
|
|
<div style={styles.errorContainer}>
|
|
<div style={styles.errorIcon}>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="1.5" style={{ width: '32px', height: '32px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<h3 style={styles.errorTitle}>Failed to load profile</h3>
|
|
<p style={styles.errorText}>{error.message}</p>
|
|
<button
|
|
style={styles.actionButton('secondary')}
|
|
onClick={handleClose}
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
) : profile ? (
|
|
<>
|
|
{/* Scrollable wrapper */}
|
|
<div style={styles.scrollableContent}>
|
|
{/* Photo Section */}
|
|
<div style={styles.photoSection}>
|
|
{currentPhotoUrl ? (
|
|
<ProxiedImage
|
|
src={currentPhotoUrl}
|
|
alt={profile.imaginaryName}
|
|
style={styles.photo}
|
|
/>
|
|
) : (
|
|
<div style={{ ...styles.photo, background: '#1a1a1f', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.2)" strokeWidth="1" style={{ width: '64px', height: '64px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
|
|
<div style={styles.photoGradient} />
|
|
|
|
{/* Close button */}
|
|
<button
|
|
style={styles.closeButton}
|
|
onClick={handleClose}
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.7)'}
|
|
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.5)'}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '20px', height: '20px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Photo navigation */}
|
|
{photos.length > 1 && (
|
|
<>
|
|
<div style={styles.photoIndicators}>
|
|
{photos.map((_: any, idx: number) => (
|
|
<div
|
|
key={idx}
|
|
style={styles.photoIndicator(idx === currentPhotoIndex)}
|
|
onClick={() => setCurrentPhotoIndex(idx)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
style={styles.navButton('left')}
|
|
onClick={prevPhoto}
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.7)'}
|
|
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.5)'}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ width: '20px', height: '20px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
style={styles.navButton('right')}
|
|
onClick={nextPhoto}
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.7)'}
|
|
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(0,0,0,0.5)'}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ width: '20px', height: '20px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div style={styles.photoCounter}>
|
|
{currentPhotoIndex + 1} / {photos.length}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Header overlay */}
|
|
<div style={styles.headerOverlay}>
|
|
<div style={styles.nameRow}>
|
|
<h2 style={styles.name}>{profile.imaginaryName}</h2>
|
|
<span style={styles.age}>{profile.age}</span>
|
|
{profile.isMajestic && (
|
|
<div style={styles.badge('majestic')} title="Majestic Member">
|
|
<svg viewBox="0 0 24 24" fill="#fff" style={{ width: '18px', height: '18px' }}>
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
{profile.verificationStatus && (
|
|
<div style={styles.badge('verified')} title="Verified">
|
|
<svg viewBox="0 0 24 24" fill="#fff" style={{ width: '18px', height: '18px' }}>
|
|
<path fillRule="evenodd" d="M8.603 3.799A4.49 4.49 0 0112 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 013.498 1.307 4.491 4.491 0 011.307 3.497A4.49 4.49 0 0121.75 12a4.49 4.49 0 01-1.549 3.397 4.491 4.491 0 01-1.307 3.497 4.491 4.491 0 01-3.497 1.307A4.49 4.49 0 0112 21.75a4.49 4.49 0 01-3.397-1.549 4.49 4.49 0 01-3.498-1.306 4.491 4.491 0 01-1.307-3.498A4.49 4.49 0 012.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 011.307-3.497 4.49 4.49 0 013.497-1.307zm7.007 6.387a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div style={styles.detailsRow}>
|
|
<span>{typeof profile.gender === 'string' ? profile.gender : ''}</span>
|
|
<div style={styles.detailDot} />
|
|
<span>{typeof profile.sexuality === 'string' ? profile.sexuality : ''}</span>
|
|
{profile.distance && (
|
|
<>
|
|
<div style={styles.detailDot} />
|
|
<span>{Math.round(profile.distance.mi)} mi away</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
{/* Likes you badge */}
|
|
{theyLikedMe && (
|
|
<div style={styles.interactionBadge}>
|
|
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: '14px', height: '14px' }}>
|
|
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
|
</svg>
|
|
Likes you
|
|
</div>
|
|
)}
|
|
{/* Scroll hint */}
|
|
<div style={styles.scrollHint}>
|
|
<span>Scroll for more</span>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '16px', height: '16px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
|
</svg>
|
|
</div>
|
|
<style>{`@keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(5px); } 60% { transform: translateY(3px); } }`}</style>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div style={styles.content}>
|
|
{/* Back button when viewing partner */}
|
|
{profileHistory.length > 0 && (
|
|
<button
|
|
style={styles.backButton}
|
|
onClick={handleGoBack}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.15)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
|
|
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)';
|
|
}}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '16px', height: '16px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
|
</svg>
|
|
Back to previous profile
|
|
</button>
|
|
)}
|
|
|
|
{/* Bio */}
|
|
{profile.bio && (
|
|
<div style={styles.bioSection}>
|
|
<p style={styles.bioText}>{profile.bio}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hidden Bio */}
|
|
{profile.hasHiddenBio && profile.hiddenBio && (
|
|
<div style={{ ...styles.section, padding: '16px', background: 'rgba(190,49,68,0.1)', borderRadius: '16px', border: '1px solid rgba(190,49,68,0.2)' }}>
|
|
<p style={styles.sectionLabel}>Private Note</p>
|
|
<p style={{ ...styles.bioText, fontSize: '14px' }}>{profile.hiddenBio}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Connection Goals */}
|
|
{profile.connectionGoals?.length > 0 && (
|
|
<div style={styles.section}>
|
|
<p style={styles.sectionLabel}>Looking For</p>
|
|
<div style={styles.tagsContainer}>
|
|
{profile.connectionGoals.filter((g: any) => typeof g === 'string').map((goal: string) => (
|
|
<span key={goal} style={styles.tag('primary')}>
|
|
{goal.replace(/_/g, ' ')}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Desires */}
|
|
{profile.desires?.length > 0 && (
|
|
<div style={styles.section}>
|
|
<p style={styles.sectionLabel}>Desires</p>
|
|
<div style={styles.tagsContainer}>
|
|
{profile.desires.filter((d: any) => typeof d === 'string').map((desire: string) => (
|
|
<span key={desire} style={styles.tag('default')}>
|
|
{desire.replace(/_/g, ' ')}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Interests */}
|
|
{profile.interests?.length > 0 && (
|
|
<div style={styles.section}>
|
|
<p style={styles.sectionLabel}>Interests</p>
|
|
<div style={styles.tagsContainer}>
|
|
{profile.interests.filter((i: any) => typeof i === 'string').map((interest: string) => (
|
|
<span key={interest} style={styles.tag('success')}>
|
|
{interest}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Partner */}
|
|
{profile.constellation?.length > 0 && (
|
|
<div style={styles.section}>
|
|
<p style={styles.sectionLabel}>Partner</p>
|
|
{profile.constellation.map((partner: any) => (
|
|
<div
|
|
key={partner.partnerId}
|
|
style={styles.partnerCard}
|
|
onClick={() => partner.partnerProfile?.id && handleViewPartner(partner.partnerProfile.id)}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.06)';
|
|
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.03)';
|
|
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.06)';
|
|
}}
|
|
>
|
|
{partner.partnerProfile?.photos?.[0]?.pictureUrls && (
|
|
<ProxiedImage
|
|
src={partner.partnerProfile.photos[0].pictureUrls.medium || partner.partnerProfile.photos[0].pictureUrls.small}
|
|
alt={partner.partnerProfile.imaginaryName}
|
|
style={styles.partnerPhoto}
|
|
/>
|
|
)}
|
|
<div>
|
|
<p style={styles.partnerName}>{partner.partnerProfile?.imaginaryName}</p>
|
|
{partner.partnerLabel && typeof partner.partnerLabel === 'string' && (
|
|
<p style={styles.partnerLabel}>{partner.partnerLabel}</p>
|
|
)}
|
|
</div>
|
|
<div style={styles.partnerChevron}>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '20px', height: '20px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Last seen */}
|
|
{profile.lastSeen && (
|
|
<p style={{ ...styles.sectionLabel, marginTop: '16px', marginBottom: 0 }}>
|
|
Last active {new Date(profile.lastSeen).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Bar */}
|
|
<div style={styles.actionBar}>
|
|
<button
|
|
style={{
|
|
...styles.actionButton('secondary'),
|
|
opacity: disliking ? 0.7 : 1,
|
|
}}
|
|
onClick={() => dislikeProfile()}
|
|
disabled={disliking}
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
|
|
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '20px', height: '20px' }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
{disliking ? 'Passing...' : 'Pass'}
|
|
</button>
|
|
{/* Ping button */}
|
|
<button
|
|
style={{
|
|
...styles.actionButton('ping'),
|
|
opacity: availablePings > 0 ? 1 : 0.5,
|
|
}}
|
|
onClick={() => availablePings > 0 && setShowPingModal(true)}
|
|
title={availablePings > 0 ? `Send a ping (${availablePings} available)` : 'No pings available'}
|
|
onMouseEnter={(e) => {
|
|
if (availablePings > 0) e.currentTarget.style.background = 'rgba(245,158,11,0.25)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'rgba(245,158,11,0.15)';
|
|
}}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: '20px', height: '20px' }}>
|
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
style={{
|
|
...styles.actionButton('primary'),
|
|
opacity: liking ? 0.7 : 1,
|
|
}}
|
|
onClick={() => likeProfile()}
|
|
disabled={liking}
|
|
onMouseEnter={(e) => {
|
|
if (!liking) e.currentTarget.style.transform = 'scale(1.02)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.transform = 'scale(1)';
|
|
}}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: '20px', height: '20px' }}>
|
|
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
|
</svg>
|
|
{liking ? 'Liking...' : 'Like'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Ping Modal */}
|
|
{showPingModal && (
|
|
<PingModal
|
|
profileName={profile.imaginaryName}
|
|
availablePings={availablePings}
|
|
sending={sendingPing}
|
|
onSend={handleSendPing}
|
|
onClose={() => setShowPingModal(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|