Initial commit
This commit is contained in:
903
web/src/components/profile/ProfileDetailModal.tsx
Executable file
903
web/src/components/profile/ProfileDetailModal.tsx
Executable file
@@ -0,0 +1,903 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user