Files
Feeld/web/src/components/layout/Navigation.tsx
Trey T f84786e654 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>
2026-04-16 07:11:21 -05:00

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>
);
}