Files
Feeld/web/src/pages/Settings.tsx
T
Trey T da2bab21e5 Block banned-country locations and align GraphQL ops
Defense-in-depth banned-country gate covering every entry point that could
set a location Feeld's policy disallows (~60 countries from their support
article):

- New src/config/bannedCountries.ts — single source of truth (ISO codes + aliases)
- New src/utils/reverseGeocode.ts — Nominatim reverse lookup w/ localStorage cache
- New src/api/links/bannedCountryLink.ts — Apollo link chokepoint; intercepts
  every DeviceLocationUpdate mutation and refuses to forward if reverse-geocode
  resolves to a banned country. Catches Settings, Discover, Likes scanner, and
  ApiExplorer raw GraphQL alike.
- useLocation.tsx — setLocation throws BannedCountryError; saveLocation gate;
  sanitize banned entries on localStorage and server hydration
- Settings.tsx — block at search, saved-location pick, and save-current
- Likes.tsx — skip banned saved locations in scanForLikes and "Fuck It" scan
- server/index.js — PUT /api/saved-locations filters; readSavedLocations
  filters legacy banned entries so rotation cron is safe too
- nginx.conf — route additions for new backend endpoints

Plus the broader rc/realign-graphql-ops session work: GraphQL query/mutation
realignment after Feeld API changes, ApiExplorer updates, Profile/Discover/Likes
refinements, useFavorites hook, dataSync extensions, vite proxy adjustments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:30:37 -05:00

2054 lines
85 KiB
TypeScript
Executable File

import { useQuery, useMutation } from '@apollo/client/react';
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';
import { LoadingPage } from '../components/ui/Loading';
import { useLocation, geocodeAddress, BannedCountryError } from '../hooks/useLocation';
import { isCountryBanned, findBannedCountry } from '../config/bannedCountries';
import { useState, useEffect, useCallback } from 'react';
// Inline styles for guaranteed rendering
const styles = {
container: {
maxWidth: '640px',
margin: '0 auto',
paddingBottom: '32px',
},
header: {
marginBottom: '32px',
},
title: {
fontFamily: "'Clash Display', sans-serif",
fontSize: '32px',
fontWeight: 700,
color: '#ffffff',
marginBottom: '8px',
},
subtitle: {
color: '#6b7280',
fontSize: '16px',
},
card: {
background: '#1a1a24',
borderRadius: '20px',
border: '1px solid rgba(255,255,255,0.08)',
marginBottom: '24px',
overflow: 'hidden',
},
cardContent: {
padding: '32px',
},
sectionHeader: {
display: 'flex',
alignItems: 'center',
gap: '16px',
marginBottom: '28px',
},
iconBox: (color: string) => ({
width: '48px',
height: '48px',
borderRadius: '14px',
background: `linear-gradient(135deg, ${color}33 0%, ${color}15 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
sectionTitle: {
fontFamily: "'Clash Display', sans-serif",
fontSize: '22px',
fontWeight: 600,
color: '#ffffff',
},
label: {
fontSize: '11px',
fontWeight: 600,
textTransform: 'uppercase' as const,
letterSpacing: '0.1em',
color: '#6b7280',
marginBottom: '12px',
display: 'block',
},
value: {
fontSize: '18px',
fontWeight: 500,
color: '#ffffff',
},
input: {
flex: 1,
padding: '14px 18px',
background: '#24242f',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '15px',
outline: 'none',
transition: 'border-color 0.2s',
},
button: {
padding: '14px 24px',
background: 'linear-gradient(135deg, #c41e3a 0%, #e91e63 100%)',
border: 'none',
borderRadius: '12px',
color: '#ffffff',
fontSize: '15px',
fontWeight: 600,
cursor: 'pointer',
transition: 'opacity 0.2s',
},
buttonSecondary: {
padding: '10px 18px',
background: '#24242f',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '10px',
color: '#ffffff',
fontSize: '14px',
fontWeight: 500,
cursor: 'pointer',
},
infoBox: {
padding: '18px',
background: '#24242f',
borderRadius: '14px',
marginBottom: '20px',
},
badge: (variant: 'default' | 'primary' | 'success') => {
const colors = {
default: { bg: '#24242f', color: '#9ca3af' },
primary: { bg: 'rgba(196, 30, 58, 0.15)', color: '#e91e63' },
success: { bg: 'rgba(34, 197, 94, 0.15)', color: '#22c55e' },
};
const { bg, color } = colors[variant];
return {
display: 'inline-block',
padding: '8px 14px',
background: bg,
borderRadius: '8px',
color: color,
fontSize: '13px',
fontWeight: 500,
marginRight: '8px',
marginBottom: '8px',
};
},
toggleContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '20px',
background: '#24242f',
borderRadius: '14px',
},
toggle: (isOn: boolean) => ({
width: '56px',
height: '32px',
borderRadius: '16px',
background: isOn ? 'linear-gradient(135deg, #c41e3a 0%, #e91e63 100%)' : '#1a1a24',
position: 'relative' as const,
cursor: 'not-allowed',
transition: 'background 0.3s',
}),
toggleKnob: (isOn: boolean) => ({
position: 'absolute' as const,
top: '4px',
left: isOn ? '28px' : '4px',
width: '24px',
height: '24px',
borderRadius: '12px',
background: '#ffffff',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
transition: 'left 0.3s',
}),
note: {
fontSize: '12px',
color: '#6b7280',
marginTop: '16px',
paddingTop: '16px',
borderTop: '1px solid rgba(255,255,255,0.05)',
},
savedLocation: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 18px',
background: '#24242f',
borderRadius: '12px',
marginBottom: '10px',
cursor: 'pointer',
transition: 'background 0.2s',
},
setLocationButton: {
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 16px',
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
border: 'none',
borderRadius: '10px',
color: '#ffffff',
fontSize: '13px',
fontWeight: 600,
cursor: 'pointer',
transition: 'opacity 0.2s, transform 0.2s',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
},
activeLocationBadge: {
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
padding: '6px 12px',
background: 'rgba(34, 197, 94, 0.15)',
borderRadius: '20px',
color: '#22c55e',
fontSize: '12px',
fontWeight: 600,
},
};
export function SettingsPage() {
const { logout } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
const [searching, setSearching] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
const [saveLocationName, setSaveLocationName] = useState('');
const [showSaveDialog, setShowSaveDialog] = useState(false);
// Desiring For editing state
const [editingDesiringFor, setEditingDesiringFor] = useState(false);
const [tempDesiringFor, setTempDesiringFor] = useState<string[]>([]);
const [savingDesiringFor, setSavingDesiringFor] = useState(false);
// 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);
// Location rotation state
const [rotationState, setRotationState] = useState<any>(null);
const [rotationStatus, setRotationStatus] = useState<any>(null);
const [rotationLoading, setRotationLoading] = useState(true);
const [rotatingNow, setRotatingNow] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const [newGroupLocationIds, setNewGroupLocationIds] = useState<string[]>([]);
const [showCreateGroup, setShowCreateGroup] = useState(false);
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupLocationIds, setEditGroupLocationIds] = useState<string[]>([]);
// Fetch rotation state
const fetchRotationState = useCallback(async () => {
try {
const [stateRes, statusRes] = await Promise.all([
fetch('/api/location-rotation'),
fetch('/api/location-rotation/status'),
]);
if (stateRes.ok) setRotationState(await stateRes.json());
if (statusRes.ok) setRotationStatus(await statusRes.json());
} catch (e) {
console.error('Failed to fetch rotation state:', e);
} finally {
setRotationLoading(false);
}
}, []);
useEffect(() => { fetchRotationState(); }, [fetchRotationState]);
// Refresh rotation status every 30 seconds
useEffect(() => {
const interval = setInterval(async () => {
try {
const res = await fetch('/api/location-rotation/status');
if (res.ok) setRotationStatus(await res.json());
} catch (e) {}
}, 30000);
return () => clearInterval(interval);
}, []);
// Load current profile ID from backend on mount
useEffect(() => {
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 {
// Only send the changed fields — backend merges with file state
const res = await fetch('/api/location-rotation', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (res.ok) {
const data = await res.json();
setRotationState(data.state);
// Refresh status
const statusRes = await fetch('/api/location-rotation/status');
if (statusRes.ok) setRotationStatus(await statusRes.json());
}
} catch (e) {
console.error('Failed to update rotation:', e);
}
};
const handleRotateNow = async () => {
setRotatingNow(true);
setRotationError(null);
try {
const res = await fetch('/api/location-rotation/rotate-now', { method: 'POST' });
const data = await res.json();
if (res.ok) {
setRotationState(data.state);
const statusRes = await fetch('/api/location-rotation/status');
if (statusRes.ok) setRotationStatus(await statusRes.json());
} else {
setRotationError(data.error || 'Rotation failed');
}
} catch (e) {
setRotationError(e instanceof Error ? e.message : 'Rotation failed');
} finally {
setRotatingNow(false);
}
};
const handleCreateGroup = () => {
if (!newGroupName.trim() || newGroupLocationIds.length === 0) return;
const newGroup = {
id: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
name: newGroupName.trim(),
locationIds: newGroupLocationIds,
};
const groups = [...(rotationState?.groups || []), newGroup];
updateRotation({ groups });
setNewGroupName('');
setNewGroupLocationIds([]);
setShowCreateGroup(false);
};
const handleDeleteGroup = (groupId: string) => {
const groups = (rotationState?.groups || []).filter((g: any) => g.id !== groupId);
const updates: Record<string, any> = { groups };
if (rotationState?.activeGroupId === groupId) {
updates.activeGroupId = null;
updates.enabled = false;
}
updateRotation(updates);
};
const handleSaveEditGroup = (groupId: string) => {
const groups = (rotationState?.groups || []).map((g: any) =>
g.id === groupId ? { ...g, locationIds: editGroupLocationIds } : g
);
updateRotation({ groups });
setEditingGroupId(null);
setEditGroupLocationIds([]);
};
// Subscribe to auth status changes
useEffect(() => {
const unsubscribe = authManager.subscribe(() => {
setAuthStatus(authManager.getStatus());
});
return unsubscribe;
}, []);
// Refresh auth status every second while on settings page
useEffect(() => {
const interval = setInterval(() => {
setAuthStatus(authManager.getStatus());
}, 1000);
return () => clearInterval(interval);
}, []);
const handleSaveCredentials = async () => {
setAuthSaving(true);
setAuthMessage(null);
try {
const profileId = profileIdInput.trim();
const refreshToken = refreshTokenInput.trim();
if (!refreshToken) {
setAuthMessage({ type: 'error', text: 'Refresh token is required' });
return;
}
// 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 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 save credentials' });
} finally {
setAuthSaving(false);
}
};
const handleRefreshToken = async () => {
setAuthSaving(true);
setAuthMessage(null);
try {
await authManager.forceRefresh();
setAuthMessage({ type: 'success', text: 'Token refreshed successfully!' });
} catch (err) {
setAuthMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to refresh token' });
} finally {
setAuthSaving(false);
}
};
const {
location,
savedLocations,
setLocation,
saveLocation,
deleteLocation,
clearLocation,
} = useLocation();
const { data: settingsData, loading: settingsLoading } = useQuery(
DISCOVER_SEARCH_SETTINGS_QUERY,
{
variables: { profileId: authManager.getProfileId() },
}
);
const { data: incognitoData, loading: incognitoLoading } = useQuery(
IS_INCOGNITO_QUERY,
{
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..." />;
const settings = settingsData?.profile;
const isIncognito = incognitoData?.profile?.isIncognito;
const handleSearch = async () => {
if (!searchQuery.trim()) return;
setSearching(true);
setSearchError(null);
try {
const result = await geocodeAddress(searchQuery);
if (result) {
if (isCountryBanned(result.country, result.countryCode)) {
const hit = findBannedCountry(result.country, result.countryCode);
setSearchError(
`Feeld is not available in ${hit?.name ?? result.country ?? 'this country'}. Using a location there will get your account flagged. Pick a different location.`
);
return;
}
const newLocation = {
latitude: result.lat,
longitude: result.lng,
name: result.displayName,
country: result.country,
countryCode: result.countryCode,
};
setLocation(newLocation);
await updateLocation({
variables: {
input: {
latitude: result.lat,
longitude: result.lng,
},
},
});
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.');
}
} catch (error) {
setSearchError('Failed to search location. Please try again.');
} finally {
setSearching(false);
}
};
const handleSelectSavedLocation = async (saved: typeof savedLocations[0]) => {
if (isCountryBanned(saved.country, saved.countryCode)) {
const hit = findBannedCountry(saved.country, saved.countryCode);
setLocationStatus({
type: 'error',
text: `Feeld is not available in ${hit?.name ?? saved.country ?? 'that country'}. Remove this saved location and pick a different one.`,
});
setTimeout(() => setLocationStatus(null), 5000);
return;
}
const newLocation = {
latitude: saved.latitude,
longitude: saved.longitude,
name: saved.name,
country: saved.country,
countryCode: saved.countryCode,
};
setLocation(newLocation);
setLocationStatus(null);
try {
await updateLocation({
variables: {
input: {
latitude: saved.latitude,
longitude: saved.longitude,
},
},
});
// 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);
}
};
const handleSaveCurrentLocation = () => {
if (!location || !saveLocationName.trim()) return;
try {
saveLocation(saveLocationName.trim(), location.latitude, location.longitude, {
country: location.country,
countryCode: location.countryCode,
});
setSaveLocationName('');
setShowSaveDialog(false);
} catch (err) {
if (err instanceof BannedCountryError) {
setLocationStatus({
type: 'error',
text: `Can't save — Feeld is not available in ${err.bannedCountry}.`,
});
setTimeout(() => setLocationStatus(null), 5000);
} else {
throw err;
}
}
};
// Desiring For options (from API LocalisedDesireCategory)
const DESIRE_OPTIONS = [
'AFTERCARE', 'BDSM', 'BONDAGE', 'BRAT', 'BRAT_TAMER', 'CASUAL', 'CELIBATE',
'COMMUNICATION', 'CONNECTION', 'COUPLES', 'CUDDLING', 'DATES', 'DOMINANTS',
'EDGING', 'ENM', 'EXPLORATION', 'FF', 'FFF', 'FFFF', 'FFM', 'FLIRTING',
'FOREPLAY', 'FREEDOMME', 'FRIENDSHIPS', 'FUN', 'FWB', 'GGG', 'GROUP',
'INTIMACY', 'KINK', 'KISSING', 'MASSAGE', 'MF', 'MFMF', 'MM', 'MMF', 'MMM',
'MMMM', 'MONOGAMY', 'OPEN_RELATIONSHIP', 'PARTIES', 'POLY', 'RELATIONSHIP',
'ROLE_PLAY', 'ROUGH', 'SENSUAL', 'SINGLES', 'SUBMISSIVES', 'SWITCH',
'TEXTING', 'THREESOME', 'TOYS', 'VANILLA', 'WATCHING'
];
const handleStartEditDesiringFor = () => {
setTempDesiringFor(settings?.desiringFor || []);
setEditingDesiringFor(true);
};
const handleCancelEditDesiringFor = () => {
setEditingDesiringFor(false);
setTempDesiringFor([]);
};
const handleToggleDesiringFor = (option: string) => {
setTempDesiringFor(prev =>
prev.includes(option)
? prev.filter(o => o !== option)
: [...prev, option]
);
};
const handleSaveDesiringFor = async () => {
setSavingDesiringFor(true);
try {
await updateSearchSettings({
variables: {
desiringFor: tempDesiringFor,
},
});
setEditingDesiringFor(false);
// Refetch settings to update the display
window.location.reload();
} catch (error) {
console.error('Failed to update desiring for:', error);
} finally {
setSavingDesiringFor(false);
}
};
return (
<div style={styles.container}>
{/* Header */}
<div style={styles.header}>
<h1 style={styles.title}>Settings</h1>
<p style={styles.subtitle}>Manage your preferences</p>
</div>
{/* Location Settings */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#3b82f6')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Location</h2>
</div>
{/* Current Location */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Current Location</label>
{location ? (
<div style={{ ...styles.infoBox, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<p style={{ color: '#ffffff', fontWeight: 500, marginBottom: '4px' }}>
{location.name || 'Custom Location'}
</p>
<p style={{ fontSize: '12px', color: '#6b7280', fontFamily: 'monospace' }}>
{location.latitude.toFixed(4)}, {location.longitude.toFixed(4)}
</p>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{!showSaveDialog && (
<button style={styles.buttonSecondary} onClick={() => setShowSaveDialog(true)}>
Save
</button>
)}
<button style={styles.buttonSecondary} onClick={clearLocation}>
Clear
</button>
</div>
</div>
) : (
<p style={{ color: '#6b7280' }}>Using default location</p>
)}
</div>
{/* Save Location Dialog */}
{showSaveDialog && location && (
<div style={{ ...styles.infoBox, marginBottom: '24px', border: '1px solid rgba(255,255,255,0.1)' }}>
<label style={styles.label}>Save Location As</label>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={saveLocationName}
onChange={(e) => setSaveLocationName(e.target.value)}
placeholder="e.g., Work, Home, NYC"
style={styles.input}
/>
<button style={styles.button} onClick={handleSaveCurrentLocation}>Save</button>
<button style={styles.buttonSecondary} onClick={() => setShowSaveDialog(false)}>Cancel</button>
</div>
</div>
)}
{/* Search Location */}
<div style={{ marginBottom: '24px' }}>
<label style={styles.label}>Search Location</label>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Enter city, address, or place..."
style={styles.input}
/>
<button
style={{ ...styles.button, opacity: searching ? 0.7 : 1 }}
onClick={handleSearch}
disabled={searching}
>
{searching ? 'Searching...' : 'Search'}
</button>
</div>
{searchError && (
<p style={{ color: '#ef4444', fontSize: '14px', marginTop: '12px' }}>{searchError}</p>
)}
</div>
{/* Saved Locations */}
{savedLocations.length > 0 && (
<div>
<label style={styles.label}>Saved Locations</label>
{savedLocations.map((saved) => {
const isActive = location &&
Math.abs(location.latitude - saved.latitude) < 0.0001 &&
Math.abs(location.longitude - saved.longitude) < 0.0001;
return (
<div
key={saved.id}
style={{
...styles.savedLocation,
border: isActive ? '1px solid rgba(34, 197, 94, 0.3)' : '1px solid transparent',
}}
onMouseEnter={(e) => e.currentTarget.style.background = '#2e2e3a'}
onMouseLeave={(e) => e.currentTarget.style.background = '#24242f'}
>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '4px' }}>
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>{saved.name}</p>
{isActive && (
<span style={styles.activeLocationBadge}>
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: '12px', height: '12px' }}>
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.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>
Active
</span>
)}
</div>
<p style={{ fontSize: '12px', color: '#6b7280', fontFamily: 'monospace', margin: 0 }}>
{saved.latitude.toFixed(4)}, {saved.longitude.toFixed(4)}
</p>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{!isActive && (
<button
style={styles.setLocationButton}
onClick={() => handleSelectSavedLocation(saved)}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '0.9';
e.currentTarget.style.transform = 'scale(1.02)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'scale(1)';
}}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '14px', height: '14px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
Set Location
</button>
)}
<button
style={styles.buttonSecondary}
onClick={() => deleteLocation(saved.id)}
title="Delete saved location"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '16px', height: '16px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
</div>
);
})}
</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>
{/* Location Rotation */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#f59e0b')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M2.985 19.644l3.181-3.183" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Location Rotation</h2>
</div>
{rotationLoading ? (
<p style={{ color: '#6b7280' }}>Loading rotation settings...</p>
) : (
<>
{/* Enable/Disable Toggle */}
<div style={{ ...styles.toggleContainer, marginBottom: '20px', cursor: 'pointer' }}
onClick={() => updateRotation({ enabled: !rotationState?.enabled })}>
<div>
<p style={{ color: '#ffffff', fontWeight: 500, marginBottom: '4px' }}>Auto-Rotate</p>
<p style={{ color: '#6b7280', fontSize: '14px' }}>
Automatically cycle between location groups
</p>
</div>
<div style={{
...styles.toggle(!!rotationState?.enabled),
cursor: 'pointer',
}}>
<div style={styles.toggleKnob(!!rotationState?.enabled)} />
</div>
</div>
{/* Interval Selector */}
<div style={{ marginBottom: '20px' }}>
<label style={styles.label}>Rotation Interval</label>
<div style={{ display: 'flex', gap: '8px' }}>
{[2, 4, 6, 8].map(h => (
<button
key={h}
onClick={() => updateRotation({ intervalHours: h })}
style={{
padding: '10px 18px',
borderRadius: '10px',
fontSize: '14px',
fontWeight: 600,
cursor: 'pointer',
border: rotationState?.intervalHours === h
? '1px solid #f59e0b'
: '1px solid rgba(255,255,255,0.1)',
background: rotationState?.intervalHours === h
? 'rgba(245, 158, 11, 0.2)'
: '#24242f',
color: rotationState?.intervalHours === h ? '#f59e0b' : '#9ca3af',
}}
>
{h}h
</button>
))}
</div>
</div>
{/* Location Groups */}
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<label style={{ ...styles.label, marginBottom: 0 }}>Location Groups</label>
<button
onClick={() => setShowCreateGroup(!showCreateGroup)}
style={{
padding: '6px 14px',
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
border: 'none',
borderRadius: '8px',
color: '#fff',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
}}
>
{showCreateGroup ? 'Cancel' : '+ Create Group'}
</button>
</div>
{/* Create Group Form */}
{showCreateGroup && (
<div style={{ ...styles.infoBox, marginBottom: '16px', border: '1px solid rgba(245, 158, 11, 0.3)' }}>
<div style={{ marginBottom: '12px' }}>
<label style={styles.label}>Group Name</label>
<input
type="text"
value={newGroupName}
onChange={e => setNewGroupName(e.target.value)}
placeholder="e.g., DFW, Austin Area"
style={styles.input}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={styles.label}>Select Locations</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{savedLocations.map(loc => {
const isSelected = newGroupLocationIds.includes(loc.id);
return (
<button
key={loc.id}
onClick={() => setNewGroupLocationIds(prev =>
isSelected ? prev.filter(id => id !== loc.id) : [...prev, loc.id]
)}
style={{
padding: '8px 14px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
cursor: 'pointer',
border: isSelected ? '1px solid #f59e0b' : '1px solid rgba(255,255,255,0.1)',
background: isSelected ? 'rgba(245, 158, 11, 0.2)' : '#1a1a24',
color: isSelected ? '#f59e0b' : '#9ca3af',
}}
>
{loc.name}
</button>
);
})}
</div>
{savedLocations.length === 0 && (
<p style={{ color: '#6b7280', fontSize: '13px' }}>No saved locations. Add locations above first.</p>
)}
</div>
<button
onClick={handleCreateGroup}
disabled={!newGroupName.trim() || newGroupLocationIds.length === 0}
style={{
...styles.button,
opacity: (!newGroupName.trim() || newGroupLocationIds.length === 0) ? 0.5 : 1,
width: '100%',
}}
>
Create Group ({newGroupLocationIds.length} locations)
</button>
</div>
)}
{/* Existing Groups */}
{(rotationState?.groups || []).map((group: any) => {
const isActive = rotationState?.activeGroupId === group.id;
const isEditing = editingGroupId === group.id;
const groupLocations = savedLocations.filter(l => group.locationIds.includes(l.id));
return (
<div
key={group.id}
style={{
...styles.savedLocation,
flexDirection: 'column' as any,
alignItems: 'stretch',
border: isActive ? '1px solid rgba(245, 158, 11, 0.3)' : '1px solid transparent',
marginBottom: '10px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<p style={{ color: '#ffffff', fontWeight: 600, margin: 0, fontSize: '16px' }}>
{group.name}
</p>
{isActive && (
<span style={{
...styles.activeLocationBadge,
background: 'rgba(245, 158, 11, 0.15)',
color: '#f59e0b',
}}>
Active
</span>
)}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{!isActive ? (
<button
onClick={() => updateRotation({ activeGroupId: group.id })}
style={{
...styles.setLocationButton,
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
boxShadow: '0 4px 12px rgba(245, 158, 11, 0.3)',
}}
>
Activate
</button>
) : (
<button
onClick={() => updateRotation({ activeGroupId: null, enabled: false })}
style={styles.buttonSecondary}
>
Deactivate
</button>
)}
<button
onClick={() => {
if (isEditing) {
setEditingGroupId(null);
} else {
setEditingGroupId(group.id);
setEditGroupLocationIds([...group.locationIds]);
}
}}
style={styles.buttonSecondary}
>
{isEditing ? 'Cancel' : 'Edit'}
</button>
<button
onClick={() => handleDeleteGroup(group.id)}
style={styles.buttonSecondary}
title="Delete group"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ width: '16px', height: '16px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
</div>
{/* Location list / edit mode */}
{isEditing ? (
<div style={{ marginTop: '12px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '12px' }}>
{savedLocations.map(loc => {
const isSelected = editGroupLocationIds.includes(loc.id);
return (
<button
key={loc.id}
onClick={() => setEditGroupLocationIds(prev =>
isSelected ? prev.filter(id => id !== loc.id) : [...prev, loc.id]
)}
style={{
padding: '6px 12px',
borderRadius: '8px',
fontSize: '12px',
fontWeight: 500,
cursor: 'pointer',
border: isSelected ? '1px solid #f59e0b' : '1px solid rgba(255,255,255,0.1)',
background: isSelected ? 'rgba(245, 158, 11, 0.2)' : '#1a1a24',
color: isSelected ? '#f59e0b' : '#9ca3af',
}}
>
{loc.name}
</button>
);
})}
</div>
<button
onClick={() => handleSaveEditGroup(group.id)}
style={{ ...styles.button, width: '100%', fontSize: '13px', padding: '10px' }}
>
Save ({editGroupLocationIds.length} locations)
</button>
</div>
) : (
<div style={{ marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{groupLocations.map(loc => (
<span key={loc.id} style={{
padding: '4px 10px',
background: '#1a1a24',
borderRadius: '6px',
color: '#9ca3af',
fontSize: '12px',
}}>
{loc.name}
</span>
))}
{groupLocations.length === 0 && (
<span style={{ color: '#6b7280', fontSize: '12px' }}>
No locations (may need to re-save locations)
</span>
)}
</div>
)}
</div>
);
})}
{(rotationState?.groups || []).length === 0 && !showCreateGroup && (
<p style={{ color: '#6b7280', fontSize: '14px' }}>
No groups yet. Create one to start rotating.
</p>
)}
</div>
{/* Status Display */}
{rotationState?.enabled && rotationStatus && (
<div style={{ ...styles.infoBox, border: '1px solid rgba(245, 158, 11, 0.2)' }}>
<label style={styles.label}>Rotation Status</label>
{rotationStatus.currentLocation && (
<div style={{ marginBottom: '12px' }}>
<p style={{ color: '#9ca3af', fontSize: '12px', margin: '0 0 4px 0' }}>Current Location</p>
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>
{rotationStatus.currentLocation.name}
</p>
</div>
)}
{rotationStatus.nextRotation && (
<div style={{ marginBottom: '12px' }}>
<p style={{ color: '#9ca3af', fontSize: '12px', margin: '0 0 4px 0' }}>Next Rotation</p>
<p style={{ color: '#f59e0b', fontWeight: 500, margin: 0, fontSize: '14px' }}>
{new Date(rotationStatus.nextRotation).toLocaleString()}
</p>
</div>
)}
{rotationStatus.lastResult && (
<div style={{ marginBottom: '12px' }}>
<p style={{ color: '#9ca3af', fontSize: '12px', margin: '0 0 4px 0' }}>Last Result</p>
<p style={{
color: rotationStatus.lastResult.status === 'success' ? '#22c55e' : '#ef4444',
fontWeight: 500,
margin: 0,
fontSize: '14px',
}}>
{rotationStatus.lastResult.status === 'success'
? `${rotationStatus.lastResult.profilesFound} profiles at ${rotationStatus.lastResult.location}`
: `Error: ${rotationStatus.lastResult.error}`
}
</p>
{rotationStatus.lastResult.timestamp && (
<p style={{ color: '#6b7280', fontSize: '11px', margin: '4px 0 0 0' }}>
{new Date(rotationStatus.lastResult.timestamp).toLocaleString()}
</p>
)}
</div>
)}
<button
onClick={handleRotateNow}
disabled={rotatingNow}
style={{
...styles.button,
width: '100%',
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
opacity: rotatingNow ? 0.7 : 1,
}}
>
{rotatingNow ? 'Rotating...' : 'Rotate Now'}
</button>
</div>
)}
{/* Error display */}
{rotationError && (
<div style={{
padding: '12px 16px',
borderRadius: '10px',
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.3)',
color: '#ef4444',
fontSize: '14px',
marginBottom: '16px',
}}>
{rotationError}
</div>
)}
<p style={styles.note}>
Rotates your location between grouped cities on a schedule. Updates your Feeld location and runs a discovery query to appear active. Works even when the browser is closed (runs on the backend).
</p>
</>
)}
</div>
</div>
{/* Discovery Settings */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#c41e3a')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#c41e3a" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<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>
</div>
<h2 style={styles.sectionTitle}>Discovery</h2>
</div>
<div style={{ display: 'grid', gap: '24px' }}>
<div>
<label style={styles.label}>Age Range</label>
<p style={styles.value}>{settings?.ageRange?.[0] || 18} - {settings?.ageRange?.[1] || 99}</p>
</div>
<div>
<label style={styles.label}>Max Distance</label>
<p style={styles.value}>{settings?.distanceMax || 100} miles</p>
</div>
<div>
<label style={styles.label}>Looking For</label>
<div>
{settings?.lookingFor?.map((item: string) => (
<span key={item} style={styles.badge('default')}>
{item.replace(/_/g, ' ')}
</span>
))}
</div>
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<label style={{ ...styles.label, marginBottom: 0 }}>Desiring For (Search Filter)</label>
{!editingDesiringFor && (
<button
onClick={handleStartEditDesiringFor}
style={{
padding: '6px 14px',
background: 'linear-gradient(135deg, #c41e3a 0%, #e91e63 100%)',
border: 'none',
borderRadius: '8px',
color: '#fff',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
}}
>
Edit
</button>
)}
</div>
{editingDesiringFor ? (
<div style={{ background: '#24242f', borderRadius: '14px', padding: '20px' }}>
<p style={{ color: '#9ca3af', fontSize: '13px', marginBottom: '16px' }}>
Select what you're looking for. Profiles matching these desires will appear in discovery.
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '20px', maxHeight: '300px', overflowY: 'auto' }}>
{DESIRE_OPTIONS.map((option) => {
const isSelected = tempDesiringFor.includes(option);
return (
<button
key={option}
onClick={() => handleToggleDesiringFor(option)}
style={{
padding: '8px 14px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
cursor: 'pointer',
border: isSelected ? '1px solid #e91e63' : '1px solid rgba(255,255,255,0.1)',
background: isSelected ? 'rgba(196, 30, 58, 0.2)' : '#1a1a24',
color: isSelected ? '#e91e63' : '#9ca3af',
transition: 'all 0.2s',
}}
>
{option.replace(/_/g, ' ')}
</button>
);
})}
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={handleSaveDesiringFor}
disabled={savingDesiringFor}
style={{
...styles.button,
flex: 1,
opacity: savingDesiringFor ? 0.7 : 1,
}}
>
{savingDesiringFor ? 'Saving...' : `Save (${tempDesiringFor.length} selected)`}
</button>
<button
onClick={handleCancelEditDesiringFor}
style={styles.buttonSecondary}
>
Cancel
</button>
</div>
</div>
) : (
<div>
{settings?.desiringFor && settings.desiringFor.length > 0 ? (
settings.desiringFor.map((item: string) => (
<span key={item} style={styles.badge('primary')}>
{item.replace(/_/g, ' ')}
</span>
))
) : (
<span style={{ color: '#6b7280', fontSize: '14px' }}>No filters set</span>
)}
</div>
)}
</div>
</div>
<p style={styles.note}>Age range, max distance, and looking for are read-only. Desiring For can be edited above.</p>
</div>
</div>
{/* Privacy Settings */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#a855f7')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#a855f7" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Privacy</h2>
</div>
<div style={styles.toggleContainer}>
<div>
<p style={{ color: '#ffffff', fontWeight: 500, marginBottom: '4px' }}>Incognito Mode</p>
<p style={{ color: '#6b7280', fontSize: '14px' }}>Browse without being seen</p>
</div>
<div style={styles.toggle(!!isIncognito)}>
<div style={styles.toggleKnob(!!isIncognito)} />
</div>
</div>
<p style={styles.note}>Privacy toggle is read-only in this version</p>
</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}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#10b981')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
</div>
<h2 style={styles.sectionTitle}>API Credentials</h2>
</div>
{/* Auth Status */}
<div style={{ ...styles.infoBox, marginBottom: '24px', border: authStatus.isAuthenticated ? '1px solid rgba(34, 197, 94, 0.3)' : '1px solid rgba(239, 68, 68, 0.3)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<label style={{ ...styles.label, marginBottom: 0 }}>Token Status</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={handleRefreshToken}
disabled={authSaving}
style={{
padding: '6px 12px',
borderRadius: '8px',
fontSize: '12px',
fontWeight: 600,
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: 'none',
color: '#fff',
cursor: authSaving ? 'wait' : 'pointer',
opacity: authSaving ? 0.7 : 1,
}}
>
{authSaving ? 'Refreshing...' : 'Refresh Now'}
</button>
<span style={{
padding: '4px 10px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 600,
background: authStatus.isAuthenticated ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
color: authStatus.isAuthenticated ? '#22c55e' : '#ef4444',
}}>
{authStatus.isAuthenticated ? 'Valid' : 'Expired / Invalid'}
</span>
</div>
</div>
{/* Current Profile ID */}
<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' }}>
{authManager.getProfileId()}
</p>
</div>
{/* 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' }}>
Managed by backend server
</p>
</div>
{/* Access Token */}
{authStatus.accessToken && (
<div style={{ marginBottom: '12px' }}>
<label style={{ fontSize: '10px', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Access Token</label>
<p style={{ color: '#9ca3af', fontSize: '10px', fontFamily: 'monospace', margin: '4px 0 0 0', wordBreak: 'break-all', maxHeight: '80px', overflow: 'auto', background: '#1a1a24', padding: '8px', borderRadius: '6px' }}>
{authStatus.accessToken}
</p>
</div>
)}
{/* Expiry */}
{authStatus.expiresIn && (
<p style={{ color: '#22c55e', fontSize: '13px', margin: 0 }}>
Expires in: {Math.floor(authStatus.expiresIn / 60)}m {authStatus.expiresIn % 60}s
</p>
)}
{authStatus.lastError && (
<p style={{ color: '#ef4444', fontSize: '13px', margin: 0, marginTop: '8px' }}>
Error: {authStatus.lastError}
</p>
)}
</div>
{/* Profile ID */}
<div style={{ marginBottom: '20px' }}>
<label style={styles.label}>Profile ID</label>
<input
type="text"
value={profileIdInput}
onChange={(e) => setProfileIdInput(e.target.value)}
placeholder="profile#xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
style={{ ...styles.input, width: '100%', fontFamily: 'monospace', fontSize: '13px' }}
/>
</div>
{/* Refresh Token */}
<div style={{ marginBottom: '20px' }}>
<label style={styles.label}>Refresh Token</label>
<textarea
value={refreshTokenInput}
onChange={(e) => setRefreshTokenInput(e.target.value)}
placeholder="AMf-vBx..."
rows={3}
style={{
...styles.input,
width: '100%',
fontFamily: 'monospace',
fontSize: '12px',
resize: 'vertical' as const,
}}
/>
<p style={{ color: '#6b7280', fontSize: '12px', marginTop: '8px' }}>
Get from Proxyman: Find a request to core.api.fldcore.com and copy the refresh token from the token refresh request body.
</p>
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: '10px', marginBottom: '16px' }}>
<button
style={{ ...styles.button, opacity: authSaving ? 0.7 : 1, flex: 1 }}
onClick={handleSaveCredentials}
disabled={authSaving}
>
{authSaving ? 'Saving...' : 'Save & Refresh Token'}
</button>
</div>
{/* Status Message */}
{authMessage && (
<div style={{
padding: '12px 16px',
borderRadius: '10px',
background: authMessage.type === 'success' ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
border: `1px solid ${authMessage.type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
color: authMessage.type === 'success' ? '#22c55e' : '#ef4444',
fontSize: '14px',
}}>
{authMessage.text}
</div>
)}
<p style={styles.note}>
These credentials are stored in localStorage and used for all API requests. Update them when your session expires.
</p>
</div>
</div>
{/* Logout */}
<div style={styles.card}>
<div style={styles.cardContent}>
<div style={styles.sectionHeader}>
<div style={styles.iconBox('#ef4444')}>
<svg viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
</div>
<h2 style={styles.sectionTitle}>Session</h2>
</div>
<button
onClick={logout}
style={{
width: '100%',
padding: '16px 24px',
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '12px',
color: '#ef4444',
fontSize: '16px',
fontWeight: 600,
cursor: 'pointer',
transition: 'background 0.2s',
}}
>
Sign Out
</button>
</div>
</div>
</div>
);
}