- 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>
281 lines
10 KiB
TypeScript
Executable File
281 lines
10 KiB
TypeScript
Executable File
import { NavLink, useLocation } from 'react-router-dom';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
const navItems = [
|
|
{
|
|
to: '/discover',
|
|
label: 'Discover',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
to: '/likes',
|
|
label: 'Likes',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
to: '/messages',
|
|
label: 'Chat',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
to: '/sent-pings',
|
|
label: 'Pings',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
to: '/okcupid',
|
|
label: 'OKC',
|
|
icon: (
|
|
<span style={{ width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 800, fontSize: '11px', color: 'currentColor', fontFamily: 'var(--font-display, system-ui)' }}>OK</span>
|
|
),
|
|
},
|
|
{
|
|
to: '/matches',
|
|
label: 'Matches',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
to: '/profile',
|
|
label: 'Profile',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<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>
|
|
),
|
|
},
|
|
{
|
|
to: '/settings',
|
|
label: 'Settings',
|
|
icon: (
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: 24, height: 24 }}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
const s = {
|
|
// Desktop side rail
|
|
desktop: {
|
|
position: 'fixed' as const,
|
|
left: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: '80px',
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
alignItems: 'center',
|
|
paddingTop: '24px',
|
|
paddingBottom: '24px',
|
|
zIndex: 50,
|
|
background: 'var(--color-void)',
|
|
borderRight: '1px solid rgba(255,255,255,0.04)',
|
|
},
|
|
desktopLogo: {
|
|
width: '40px',
|
|
height: '40px',
|
|
borderRadius: '12px',
|
|
background: 'var(--gradient-desire)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: '32px',
|
|
boxShadow: '0 4px 16px rgba(124,58,237,0.3)',
|
|
},
|
|
desktopLogoText: {
|
|
color: '#fff',
|
|
fontFamily: "var(--font-display)",
|
|
fontWeight: 700,
|
|
fontSize: '18px',
|
|
},
|
|
desktopNavList: {
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
gap: '4px',
|
|
flex: 1,
|
|
},
|
|
desktopLink: (active: boolean) => ({
|
|
position: 'relative' as const,
|
|
width: '48px',
|
|
height: '48px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: '14px',
|
|
color: active ? '#fff' : 'rgba(255,255,255,0.38)',
|
|
background: active ? 'rgba(124,58,237,0.12)' : 'transparent',
|
|
border: active ? '1px solid rgba(124,58,237,0.20)' : '1px solid transparent',
|
|
transition: 'all 250ms cubic-bezier(0.22,1,0.36,1)',
|
|
cursor: 'pointer',
|
|
textDecoration: 'none',
|
|
}),
|
|
desktopTooltip: {
|
|
position: 'absolute' as const,
|
|
left: '100%',
|
|
marginLeft: '12px',
|
|
padding: '6px 12px',
|
|
borderRadius: '8px',
|
|
background: 'var(--color-surface-elevated)',
|
|
border: '1px solid rgba(255,255,255,0.06)',
|
|
fontSize: '13px',
|
|
fontWeight: 500,
|
|
color: 'rgba(255,255,255,0.87)',
|
|
whiteSpace: 'nowrap' as const,
|
|
pointerEvents: 'none' as const,
|
|
opacity: 0,
|
|
transform: 'translateX(4px)',
|
|
transition: 'all 200ms cubic-bezier(0.22,1,0.36,1)',
|
|
},
|
|
desktopActiveDot: {
|
|
position: 'absolute' as const,
|
|
right: '-4px',
|
|
width: '6px',
|
|
height: '6px',
|
|
borderRadius: '50%',
|
|
background: 'var(--color-desire)',
|
|
boxShadow: '0 0 8px rgba(124,58,237,0.5)',
|
|
},
|
|
|
|
// Mobile bottom bar
|
|
mobile: {
|
|
position: 'fixed' as const,
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 50,
|
|
background: 'rgba(18,18,18,0.82)',
|
|
backdropFilter: 'blur(24px) saturate(180%)',
|
|
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
|
|
borderTop: '1px solid rgba(255,255,255,0.06)',
|
|
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
},
|
|
mobileInner: {
|
|
display: 'flex',
|
|
justifyContent: 'space-around',
|
|
alignItems: 'center',
|
|
height: '56px',
|
|
padding: '0 4px',
|
|
},
|
|
mobileLink: (active: boolean) => ({
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '2px',
|
|
width: '64px',
|
|
height: '48px',
|
|
borderRadius: '12px',
|
|
color: active ? 'var(--color-desire)' : 'rgba(255,255,255,0.38)',
|
|
background: active ? 'rgba(124,58,237,0.10)' : 'transparent',
|
|
transition: 'all 200ms cubic-bezier(0.22,1,0.36,1)',
|
|
textDecoration: 'none',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
userSelect: 'none' as const,
|
|
WebkitUserSelect: 'none' as const,
|
|
}),
|
|
mobileLabel: (active: boolean) => ({
|
|
fontSize: '10px',
|
|
fontWeight: active ? 600 : 500,
|
|
fontFamily: 'var(--font-body)',
|
|
lineHeight: 1,
|
|
letterSpacing: '0.02em',
|
|
}),
|
|
mobileIconWrap: (active: boolean) => ({
|
|
transition: 'transform 200ms cubic-bezier(0.22,1,0.36,1)',
|
|
transform: active ? 'scale(1.1)' : 'scale(1)',
|
|
display: 'flex',
|
|
}),
|
|
};
|
|
|
|
export function Navigation() {
|
|
const location = useLocation();
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
|
const [hovered, setHovered] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const onResize = () => setIsMobile(window.innerWidth < 768);
|
|
window.addEventListener('resize', onResize);
|
|
return () => window.removeEventListener('resize', onResize);
|
|
}, []);
|
|
|
|
const isActive = (to: string) =>
|
|
location.pathname === to || (to === '/discover' && location.pathname === '/');
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<nav style={s.mobile}>
|
|
<div style={s.mobileInner}>
|
|
{navItems.map((item) => {
|
|
const active = isActive(item.to);
|
|
return (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
style={s.mobileLink(active)}
|
|
>
|
|
<span style={s.mobileIconWrap(active)}>{item.icon}</span>
|
|
<span style={s.mobileLabel(active)}>{item.label}</span>
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<nav style={s.desktop}>
|
|
<div style={s.desktopLogo}>
|
|
<span style={s.desktopLogoText}>F</span>
|
|
</div>
|
|
|
|
<div style={s.desktopNavList}>
|
|
{navItems.map((item) => {
|
|
const active = isActive(item.to);
|
|
return (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
style={s.desktopLink(active)}
|
|
onMouseEnter={() => setHovered(item.to)}
|
|
onMouseLeave={() => setHovered(null)}
|
|
>
|
|
{item.icon}
|
|
|
|
{active && <div style={s.desktopActiveDot} />}
|
|
|
|
<div style={{
|
|
...s.desktopTooltip,
|
|
...(hovered === item.to ? { opacity: 1, transform: 'translateX(0)' } : {}),
|
|
}}>
|
|
{item.label}
|
|
</div>
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|