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:
Trey T
2026-04-16 07:11:21 -05:00
parent 0a725508d2
commit f84786e654
176 changed files with 6828 additions and 1177 deletions
+620 -60
View File
@@ -1,8 +1,7 @@
import { useQuery, useMutation } from '@apollo/client/react';
import { DISCOVER_SEARCH_SETTINGS_QUERY, IS_INCOGNITO_QUERY } from '../api/operations/queries';
import { DEVICE_LOCATION_UPDATE_MUTATION, SEARCH_SETTINGS_UPDATE_MUTATION } from '../api/operations/mutations';
import { TEST_CREDENTIALS, getCredentials, setCredentials, clearCredentials } from '../config/constants';
import { saveCredentials as syncCredentialsToServer, saveCustomLocation } from '../api/dataSync';
import { DISCOVER_SEARCH_SETTINGS_QUERY, IS_INCOGNITO_QUERY, POPULAR_LOCATIONS_QUERY, APP_SETTINGS_QUERY, REDEEMED_OFFERS_QUERY, HAS_LINKED_REFLECTION_QUERY } from '../api/operations/queries';
import { DEVICE_LOCATION_UPDATE_MUTATION, SEARCH_SETTINGS_UPDATE_MUTATION, PROFILE_LOCATION_UPDATE_MUTATION, APP_SETTINGS_UPDATE_MUTATION, SYNC_ACCOUNT_MUTATION, ACCOUNT_REDEEM_OFFER_MUTATION, ACCOUNT_DEACTIVATE_MUTATION, ACCOUNT_TERMINATE_MUTATION } from '../api/operations/mutations';
import { saveCustomLocation } from '../api/dataSync';
import { useAuth } from '../hooks/useAuth';
import { authManager } from '../api/auth';
import type { AuthStatus } from '../api/auth';
@@ -220,10 +219,9 @@ export function SettingsPage() {
const [tempDesiringFor, setTempDesiringFor] = useState<string[]>([]);
const [savingDesiringFor, setSavingDesiringFor] = useState(false);
// Auth credentials state
const currentCreds = getCredentials();
const [profileIdInput, setProfileIdInput] = useState(currentCreds.PROFILE_ID);
const [refreshTokenInput, setRefreshTokenInput] = useState(currentCreds.REFRESH_TOKEN);
// Auth credentials state — backend is single source of truth
const [profileIdInput, setProfileIdInput] = useState(authManager.getProfileId());
const [refreshTokenInput, setRefreshTokenInput] = useState('');
const [authStatus, setAuthStatus] = useState<AuthStatus>(authManager.getStatus());
const [authSaving, setAuthSaving] = useState(false);
const [authMessage, setAuthMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
@@ -268,29 +266,160 @@ export function SettingsPage() {
return () => clearInterval(interval);
}, []);
// Seed token to backend on load
// Load current profile ID from backend on mount
useEffect(() => {
const seedToken = async () => {
const creds = getCredentials();
if (creds.REFRESH_TOKEN && creds.PROFILE_ID) {
try {
await fetch('/api/location-rotation/seed-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: creds.REFRESH_TOKEN,
profileId: creds.PROFILE_ID,
analyticsId: creds.EVENT_ANALYTICS_ID,
}),
});
} catch (e) {}
}
};
seedToken();
setProfileIdInput(authManager.getProfileId());
}, []);
const [rotationError, setRotationError] = useState<string | null>(null);
// Teleport state
const [teleportLoading, setTeleportLoading] = useState(false);
const [teleportStatus, setTeleportStatus] = useState<string | null>(null);
const [teleportCity, setTeleportCity] = useState<string | null>(null);
// Notifications/Preferences state
const [syncingAccount, setSyncingAccount] = useState(false);
const [syncResult, setSyncResult] = useState<string | null>(null);
// Account management state
const [offerCode, setOfferCode] = useState('');
const [redeemingOffer, setRedeemingOffer] = useState(false);
const [offerResult, setOfferResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [pauseConfirm, setPauseConfirm] = useState(false);
const [pausingAccount, setPausingAccount] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState(0); // 0=none, 1=first confirm, 2=second confirm
const [deletingAccount, setDeletingAccount] = useState(false);
const [accountActionResult, setAccountActionResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
// Teleport queries/mutations
const { data: popularLocationsData } = useQuery(POPULAR_LOCATIONS_QUERY);
const [profileLocationUpdate] = useMutation(PROFILE_LOCATION_UPDATE_MUTATION);
// App settings queries/mutations
const { data: appSettingsData, refetch: refetchAppSettings } = useQuery(APP_SETTINGS_QUERY);
const [appSettingsUpdate] = useMutation(APP_SETTINGS_UPDATE_MUTATION);
const [syncAccount] = useMutation(SYNC_ACCOUNT_MUTATION);
// Account queries/mutations
const { data: redeemedOffersData, refetch: refetchOffers } = useQuery(REDEEMED_OFFERS_QUERY);
const { data: linkedReflectionData } = useQuery(HAS_LINKED_REFLECTION_QUERY);
const [redeemOffer] = useMutation(ACCOUNT_REDEEM_OFFER_MUTATION);
const [deactivateAccount] = useMutation(ACCOUNT_DEACTIVATE_MUTATION);
const [terminateAccount] = useMutation(ACCOUNT_TERMINATE_MUTATION);
const handleTeleport = async (loc: any) => {
setTeleportLoading(true);
setTeleportStatus(null);
try {
await profileLocationUpdate({
variables: {
input: {
teleportLocation: {
latitude: loc.latitude,
longitude: loc.longitude,
city: loc.geocode?.city || '',
country: loc.geocode?.country || '',
},
},
},
});
setTeleportCity(`${loc.geocode?.city || 'Unknown'}, ${loc.geocode?.country || ''}`);
setTeleportStatus('success');
} catch (err) {
setTeleportStatus('error');
} finally {
setTeleportLoading(false);
}
};
const handleResetTeleport = async () => {
setTeleportLoading(true);
setTeleportStatus(null);
try {
await profileLocationUpdate({
variables: {
input: {
deviceLocation: { latitude: 0, longitude: 0 },
},
},
});
setTeleportCity(null);
setTeleportStatus('reset');
} catch (err) {
setTeleportStatus('error');
} finally {
setTeleportLoading(false);
}
};
const handleToggleNotification = async (field: string, currentValue: boolean) => {
try {
await appSettingsUpdate({ variables: { [field]: !currentValue } });
refetchAppSettings();
} catch (err) {
console.error('Failed to update setting:', err);
}
};
const handleSyncAccount = async () => {
setSyncingAccount(true);
setSyncResult(null);
try {
const result = await syncAccount();
const data = result.data?.syncAccount;
setSyncResult(`Synced: Majestic=${data?.isMajestic ? 'Yes' : 'No'}, Uplift=${data?.isUplift ? 'Yes' : 'No'}, Pings=${data?.availablePings ?? 'N/A'}`);
} catch (err) {
setSyncResult('Sync failed: ' + (err instanceof Error ? err.message : 'Unknown error'));
} finally {
setSyncingAccount(false);
}
};
const handleRedeemOffer = async () => {
if (!offerCode.trim()) return;
setRedeemingOffer(true);
setOfferResult(null);
try {
await redeemOffer({ variables: { input: { offerName: offerCode.trim() } } });
setOfferResult({ type: 'success', text: `Offer "${offerCode.trim()}" redeemed!` });
setOfferCode('');
refetchOffers();
} catch (err) {
setOfferResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to redeem offer' });
} finally {
setRedeemingOffer(false);
}
};
const handlePauseAccount = async () => {
setPausingAccount(true);
setAccountActionResult(null);
try {
await deactivateAccount();
setAccountActionResult({ type: 'success', text: 'Account paused (deactivated).' });
setPauseConfirm(false);
} catch (err) {
setAccountActionResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to pause account' });
} finally {
setPausingAccount(false);
}
};
const handleDeleteAccount = async () => {
setDeletingAccount(true);
setAccountActionResult(null);
try {
await terminateAccount();
setAccountActionResult({ type: 'success', text: 'Account terminated. You will be logged out.' });
setDeleteConfirm(0);
} catch (err) {
setAccountActionResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to delete account' });
} finally {
setDeletingAccount(false);
}
};
const updateRotation = async (updates: Record<string, any>) => {
setRotationError(null);
try {
@@ -388,31 +517,34 @@ export function SettingsPage() {
const profileId = profileIdInput.trim();
const refreshToken = refreshTokenInput.trim();
// Save to localStorage
setCredentials({ profileId, refreshToken });
if (!refreshToken) {
setAuthMessage({ type: 'error', text: 'Refresh token is required' });
return;
}
// Sync to server for cross-browser persistence
await syncCredentialsToServer(profileId, refreshToken);
// Seed directly to backend — single source of truth
const resp = await fetch('/api/auth/seed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken, profileId }),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${resp.status}`);
}
await authManager.forceRefresh();
setAuthMessage({ type: 'success', text: 'Credentials saved and synced to server!' });
// Reload page to apply new credentials everywhere
setAuthMessage({ type: 'success', text: 'Credentials saved to server! All devices will use the new token.' });
setRefreshTokenInput('');
setTimeout(() => window.location.reload(), 1500);
} catch (err) {
setAuthMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to refresh token' });
setAuthMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to save credentials' });
} finally {
setAuthSaving(false);
}
};
const handleResetCredentials = () => {
clearCredentials();
const defaultCreds = getCredentials();
setProfileIdInput(defaultCreds.PROFILE_ID);
setRefreshTokenInput(defaultCreds.REFRESH_TOKEN);
setAuthMessage({ type: 'success', text: 'Credentials reset to defaults. Reload to apply.' });
};
const handleRefreshToken = async () => {
setAuthSaving(true);
setAuthMessage(null);
@@ -438,19 +570,20 @@ export function SettingsPage() {
const { data: settingsData, loading: settingsLoading } = useQuery(
DISCOVER_SEARCH_SETTINGS_QUERY,
{
variables: { profileId: TEST_CREDENTIALS.PROFILE_ID },
variables: { profileId: authManager.getProfileId() },
}
);
const { data: incognitoData, loading: incognitoLoading } = useQuery(
IS_INCOGNITO_QUERY,
{
variables: { profileId: TEST_CREDENTIALS.PROFILE_ID },
variables: { profileId: authManager.getProfileId() },
}
);
const [updateLocation] = useMutation(DEVICE_LOCATION_UPDATE_MUTATION);
const [updateSearchSettings] = useMutation(SEARCH_SETTINGS_UPDATE_MUTATION);
const [locationStatus, setLocationStatus] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
if (settingsLoading || incognitoLoading) return <LoadingPage message="Loading settings..." />;
@@ -483,6 +616,8 @@ export function SettingsPage() {
},
});
setLocationStatus({ type: 'success', text: `Location set to ${result.displayName} — you'll appear to users in this area` });
setTimeout(() => setLocationStatus(null), 4000);
setSearchQuery('');
} else {
setSearchError('Location not found. Try a different search.');
@@ -501,9 +636,10 @@ export function SettingsPage() {
name: saved.name,
};
setLocation(newLocation);
setLocationStatus(null);
try {
const result = await updateLocation({
await updateLocation({
variables: {
input: {
latitude: saved.latitude,
@@ -511,15 +647,13 @@ export function SettingsPage() {
},
},
});
console.log('DeviceLocationUpdate response:', result.data);
// Check if API actually updated the location
const deviceLocation = result.data?.deviceLocationUpdate?.location?.device;
if (deviceLocation && (deviceLocation.latitude === 0 && deviceLocation.longitude === 0)) {
console.warn('API returned 0,0 - location update may require premium/Majestic membership');
}
// API returns 0,0 as privacy masking — location IS set server-side
setLocationStatus({ type: 'success', text: `Location set to ${saved.name} — you'll appear to users in this area` });
setTimeout(() => setLocationStatus(null), 4000);
} catch (error) {
console.error('Failed to update location:', error);
setLocationStatus({ type: 'error', text: 'Failed to update location' });
setTimeout(() => setLocationStatus(null), 4000);
}
};
@@ -742,6 +876,22 @@ export function SettingsPage() {
})}
</div>
)}
{/* Location status toast */}
{locationStatus && (
<div style={{
marginTop: '12px',
padding: '10px 16px',
borderRadius: '10px',
fontSize: '13px',
fontFamily: "'Satoshi', sans-serif",
background: locationStatus.type === 'success' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
color: locationStatus.type === 'success' ? '#86efac' : '#f87171',
border: `1px solid ${locationStatus.type === 'success' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
}}>
{locationStatus.text}
</div>
)}
</div>
</div>
@@ -1263,6 +1413,422 @@ export function SettingsPage() {
</div>
</div>
{/* Teleport */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#8b5cf6')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5a17.92 17.92 0 01-8.716-2.247m0 0A8.966 8.966 0 013 12c0-1.97.633-3.794 1.706-5.277" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Teleport</h2>
</div>
{/* Teleport Status Badge */}
<div style={{ marginBottom: '20px' }}>
<label style={styles.label}>Current Status</label>
{teleportCity ? (
<span style={{ ...styles.badge('primary'), fontSize: '14px' }}>
Teleporting to {teleportCity}
</span>
) : (
<span style={{ ...styles.badge('success'), fontSize: '14px' }}>
Device Location
</span>
)}
{teleportStatus === 'error' && (
<p style={{ color: '#ef4444', fontSize: '13px', marginTop: '8px' }}>Failed to update teleport location.</p>
)}
</div>
{/* Popular Locations Grid */}
<div style={{ marginBottom: '20px' }}>
<label style={styles.label}>Popular Locations</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
{(popularLocationsData?.popularLocations || []).map((loc: any, i: number) => (
<button
key={i}
onClick={() => handleTeleport(loc)}
disabled={teleportLoading}
style={{
padding: '12px 10px',
background: '#24242f',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '10px',
color: '#ffffff',
fontSize: '13px',
fontWeight: 500,
cursor: teleportLoading ? 'wait' : 'pointer',
opacity: teleportLoading ? 0.6 : 1,
transition: 'all 0.2s',
textAlign: 'center' as const,
}}
onMouseEnter={(e) => { if (!teleportLoading) { e.currentTarget.style.background = '#2e2e3a'; e.currentTarget.style.borderColor = 'rgba(139, 92, 246, 0.4)'; } }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#24242f'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; }}
>
<div style={{ fontWeight: 600, marginBottom: '2px' }}>{loc.geocode?.city || 'Unknown'}</div>
<div style={{ fontSize: '11px', color: '#6b7280' }}>{loc.geocode?.country || ''}</div>
</button>
))}
</div>
{teleportLoading && (
<p style={{ color: '#8b5cf6', fontSize: '13px', marginTop: '12px' }}>Teleporting...</p>
)}
</div>
{/* Reset to Device Location */}
<button
onClick={handleResetTeleport}
disabled={teleportLoading}
style={{
width: '100%',
padding: '14px 24px',
background: '#24242f',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '15px',
fontWeight: 600,
cursor: teleportLoading ? 'wait' : 'pointer',
opacity: teleportLoading ? 0.6 : 1,
transition: 'opacity 0.2s',
}}
>
{teleportLoading ? 'Resetting...' : 'Reset to Device Location'}
</button>
<p style={styles.note}>
Teleport your profile to appear in a different city. Popular locations are provided by Feeld. Reset to return to your device location.
</p>
</div>
</div>
{/* Notifications & Preferences */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#f97316')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#f97316" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Notifications & Preferences</h2>
</div>
{(() => {
const acct = appSettingsData?.account;
const appSettings = acct?.appSettings;
if (!appSettings) return <p style={{ color: '#6b7280' }}>Loading notification settings...</p>;
const toggles: Array<{ label: string; field: string; value: boolean }> = [
{ label: 'New Connection', field: 'receiveNewConnectionPushNotifications', value: !!appSettings.receiveNewConnectionPushNotifications },
{ label: 'New Ping', field: 'receiveNewPingPushNotifications', value: !!appSettings.receiveNewPingPushNotifications },
{ label: 'New Message', field: 'receiveNewMessagePushNotifications', value: !!appSettings.receiveNewMessagePushNotifications },
{ label: 'New Like', field: 'receiveNewLikePushNotifications', value: !!appSettings.receiveNewLikePushNotifications },
{ label: 'Marketing', field: 'receiveMarketingNotifications', value: !!appSettings.receiveMarketingNotifications },
{ label: 'News Email', field: 'receiveNewsEmailNotifications', value: !!appSettings.receiveNewsEmailNotifications },
{ label: 'Promotions Email', field: 'receivePromotionsEmailNotifications', value: !!appSettings.receivePromotionsEmailNotifications },
{ label: 'News Push', field: 'receiveNewsPushNotifications', value: !!appSettings.receiveNewsPushNotifications },
{ label: 'Promotions Push', field: 'receivePromotionsPushNotifications', value: !!appSettings.receivePromotionsPushNotifications },
];
return (
<>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
{toggles.map(({ label, field, value }) => (
<div
key={field}
style={{ ...styles.toggleContainer, cursor: 'pointer' }}
onClick={() => handleToggleNotification(field, value)}
>
<div>
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>{label}</p>
</div>
<div style={{ ...styles.toggle(value), cursor: 'pointer' }}>
<div style={styles.toggleKnob(value)} />
</div>
</div>
))}
</div>
{/* Distance Units */}
<div
style={{ ...styles.toggleContainer, cursor: 'pointer', marginBottom: '24px' }}
onClick={() => handleToggleNotification('isDistanceInMiles', !!acct?.isDistanceInMiles)}
>
<div>
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>Distance in Miles</p>
<p style={{ color: '#6b7280', fontSize: '13px', margin: '4px 0 0 0' }}>
{acct?.isDistanceInMiles ? 'Using miles' : 'Using kilometers'}
</p>
</div>
<div style={{ ...styles.toggle(!!acct?.isDistanceInMiles), cursor: 'pointer' }}>
<div style={styles.toggleKnob(!!acct?.isDistanceInMiles)} />
</div>
</div>
{/* Sync Account */}
<button
onClick={handleSyncAccount}
disabled={syncingAccount}
style={{
...styles.button,
width: '100%',
background: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)',
opacity: syncingAccount ? 0.7 : 1,
}}
>
{syncingAccount ? 'Syncing...' : 'Sync Account'}
</button>
{syncResult && (
<p style={{ color: '#f97316', fontSize: '13px', marginTop: '12px' }}>{syncResult}</p>
)}
</>
);
})()}
<p style={styles.note}>
Toggle notification preferences for your account. Changes are saved immediately. Sync Account refreshes your subscription status from the server.
</p>
</div>
</div>
{/* Account Management */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#06b6d4')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Account</h2>
</div>
{/* Subscription Status */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Subscription</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
<span style={styles.badge(appSettingsData?.account?.isMajestic ? 'success' : 'default')}>
{appSettingsData?.account?.isMajestic ? 'Majestic' : 'Free'}
</span>
{appSettingsData?.account?.isUplift && (
<span style={styles.badge('primary')}>Uplift Active</span>
)}
<span style={styles.badge('default')}>
{appSettingsData?.account?.availablePings ?? '?'} Pings Available
</span>
</div>
</div>
{/* Reflect Link Status */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Reflect Link</label>
<span style={styles.badge(linkedReflectionData?.hasLinkedReflection ? 'success' : 'default')}>
{linkedReflectionData?.hasLinkedReflection ? 'Linked' : 'Not Linked'}
</span>
</div>
{/* Redeemed Offers */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Redeemed Offers</label>
{(redeemedOffersData?.account?.redeemedOffers || []).length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{(redeemedOffersData?.account?.redeemedOffers || []).map((offer: any, i: number) => (
<div key={i} style={styles.infoBox}>
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>{offer.offerName}</p>
{offer.redeemedAt && (
<p style={{ color: '#6b7280', fontSize: '12px', margin: '4px 0 0 0' }}>
Redeemed: {new Date(offer.redeemedAt).toLocaleDateString()}
</p>
)}
</div>
))}
</div>
) : (
<p style={{ color: '#6b7280', fontSize: '14px' }}>No offers redeemed</p>
)}
</div>
{/* Redeem Offer */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Redeem Offer Code</label>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={offerCode}
onChange={(e) => setOfferCode(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleRedeemOffer()}
placeholder="Enter offer code..."
style={styles.input}
/>
<button
onClick={handleRedeemOffer}
disabled={redeemingOffer || !offerCode.trim()}
style={{
...styles.button,
opacity: (redeemingOffer || !offerCode.trim()) ? 0.6 : 1,
}}
>
{redeemingOffer ? 'Redeeming...' : 'Redeem'}
</button>
</div>
{offerResult && (
<p style={{
color: offerResult.type === 'success' ? '#22c55e' : '#ef4444',
fontSize: '13px',
marginTop: '8px',
}}>
{offerResult.text}
</p>
)}
</div>
{/* Pause Account */}
<div style={{ marginBottom: '16px' }}>
{!pauseConfirm ? (
<button
onClick={() => setPauseConfirm(true)}
style={{
width: '100%',
padding: '14px 24px',
background: 'rgba(245, 158, 11, 0.15)',
border: '1px solid rgba(245, 158, 11, 0.3)',
borderRadius: '12px',
color: '#f59e0b',
fontSize: '15px',
fontWeight: 600,
cursor: 'pointer',
transition: 'background 0.2s',
}}
>
Pause Account
</button>
) : (
<div style={{ ...styles.infoBox, border: '1px solid rgba(245, 158, 11, 0.3)' }}>
<p style={{ color: '#f59e0b', fontWeight: 500, marginBottom: '12px' }}>
Are you sure you want to pause your account? Your profile will be hidden.
</p>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={handlePauseAccount}
disabled={pausingAccount}
style={{
...styles.button,
flex: 1,
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
opacity: pausingAccount ? 0.7 : 1,
}}
>
{pausingAccount ? 'Pausing...' : 'Yes, Pause'}
</button>
<button
onClick={() => setPauseConfirm(false)}
style={{ ...styles.buttonSecondary, flex: 1 }}
>
Cancel
</button>
</div>
</div>
)}
</div>
{/* Delete Account */}
<div style={{ marginBottom: '16px' }}>
{deleteConfirm === 0 && (
<button
onClick={() => setDeleteConfirm(1)}
style={{
width: '100%',
padding: '14px 24px',
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '12px',
color: '#ef4444',
fontSize: '15px',
fontWeight: 600,
cursor: 'pointer',
transition: 'background 0.2s',
}}
>
Delete Account
</button>
)}
{deleteConfirm === 1 && (
<div style={{ ...styles.infoBox, border: '1px solid rgba(239, 68, 68, 0.3)' }}>
<p style={{ color: '#ef4444', fontWeight: 500, marginBottom: '12px' }}>
This will permanently delete your account. This action cannot be undone. Are you sure?
</p>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => setDeleteConfirm(2)}
style={{
...styles.button,
flex: 1,
background: 'rgba(239, 68, 68, 0.3)',
}}
>
Yes, I'm sure
</button>
<button
onClick={() => setDeleteConfirm(0)}
style={{ ...styles.buttonSecondary, flex: 1 }}
>
Cancel
</button>
</div>
</div>
)}
{deleteConfirm === 2 && (
<div style={{ ...styles.infoBox, border: '2px solid #ef4444' }}>
<p style={{ color: '#ef4444', fontWeight: 700, marginBottom: '12px', fontSize: '16px' }}>
FINAL WARNING: This is irreversible. Your account, matches, and messages will be permanently deleted.
</p>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={handleDeleteAccount}
disabled={deletingAccount}
style={{
...styles.button,
flex: 1,
background: 'linear-gradient(135deg, #dc2626 0%, #ef4444 100%)',
opacity: deletingAccount ? 0.7 : 1,
}}
>
{deletingAccount ? 'Deleting...' : 'DELETE PERMANENTLY'}
</button>
<button
onClick={() => setDeleteConfirm(0)}
style={{ ...styles.buttonSecondary, flex: 1 }}
>
Cancel
</button>
</div>
</div>
)}
</div>
{/* Account Action Result */}
{accountActionResult && (
<div style={{
padding: '12px 16px',
borderRadius: '10px',
background: accountActionResult.type === 'success' ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
border: `1px solid ${accountActionResult.type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
color: accountActionResult.type === 'success' ? '#22c55e' : '#ef4444',
fontSize: '14px',
}}>
{accountActionResult.text}
</div>
)}
<p style={styles.note}>
Pausing hides your profile temporarily. Deleting permanently removes your account, all data, matches, and messages.
</p>
</div>
</div>
{/* Auth Credentials */}
<div style={styles.card}>
<div style={styles.cardContent}>
@@ -1314,15 +1880,15 @@ export function SettingsPage() {
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '10px', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Profile ID</label>
<p style={{ color: '#9ca3af', fontSize: '11px', fontFamily: 'monospace', margin: '4px 0 0 0', wordBreak: 'break-all' }}>
{currentCreds.PROFILE_ID}
{authManager.getProfileId()}
</p>
</div>
{/* Current Refresh Token */}
{/* Refresh Token (managed by backend) */}
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '10px', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Refresh Token</label>
<p style={{ color: '#9ca3af', fontSize: '10px', fontFamily: 'monospace', margin: '4px 0 0 0', wordBreak: 'break-all', maxHeight: '60px', overflow: 'auto' }}>
{currentCreds.REFRESH_TOKEN}
Managed by backend server
</p>
</div>
@@ -1391,12 +1957,6 @@ export function SettingsPage() {
>
{authSaving ? 'Saving...' : 'Save & Refresh Token'}
</button>
<button
style={styles.buttonSecondary}
onClick={handleResetCredentials}
>
Reset to Defaults
</button>
</div>
{/* Status Message */}