# Feeld Web Client

A React/TypeScript web application that provides browser-based access to the Feeld dating platform. Connects to the same GraphQL backend as the mobile app using Firebase authentication.

## Quick Start

```bash
cd web
npm install
npm run dev:all    # Starts both Vite dev server and Express backend
```

- Frontend: http://localhost:3000
- Backend API: http://localhost:3001

## Project Structure

```
/Feeld
├── web/                          # Main React application
│   ├── src/
│   │   ├── api/                  # API layer
│   │   │   ├── auth.ts          # Firebase token management (AuthManager class)
│   │   │   ├── client.ts        # Apollo GraphQL client setup
│   │   │   ├── dataSync.ts      # localStorage + server sync service
│   │   │   └── operations/      # GraphQL queries and mutations
│   │   │       ├── queries.ts
│   │   │       ├── mutations.ts
│   │   │       └── experimental.ts
│   │   ├── components/
│   │   │   ├── ui/              # Shared UI primitives
│   │   │   │   ├── Avatar.tsx       # Profile avatar with fallback
│   │   │   │   ├── Badge.tsx        # Status/tag badges
│   │   │   │   ├── Button.tsx       # Styled button component
│   │   │   │   ├── Card.tsx         # Card container
│   │   │   │   ├── Loading.tsx      # Loading spinner/skeleton
│   │   │   │   └── ProxiedImage.tsx # Image component with proxy URL handling
│   │   │   ├── layout/          # Layout and Navigation
│   │   │   │   ├── Layout.tsx       # Main layout wrapper with nav
│   │   │   │   └── Navigation.tsx   # Side rail (desktop) / bottom bar (mobile)
│   │   │   ├── profile/         # Profile display components
│   │   │   │   ├── ProfileCard.tsx        # Profile card (used in Likes, Discover, etc.)
│   │   │   │   ├── ProfileDetailModal.tsx # Full profile modal with partner nav
│   │   │   │   └── PingModal.tsx          # Send ping modal with message
│   │   │   ├── chat/            # Chat components
│   │   │   │   └── ChatListItem.tsx
│   │   │   └── LoginPage.tsx
│   │   ├── pages/               # Route pages
│   │   │   ├── Discover.tsx     # Profile discovery feed with scanner
│   │   │   ├── Likes.tsx        # Likes/Pings/You Liked/Passed (4 tabs)
│   │   │   ├── SentPings.tsx    # Outbound pings tracker
│   │   │   ├── Messages.tsx     # Chat list
│   │   │   ├── Chat.tsx         # Individual conversation
│   │   │   ├── Profile.tsx      # Own profile view
│   │   │   ├── Settings.tsx     # App settings
│   │   │   └── ApiExplorer.tsx  # Debug tool for testing GraphQL
│   │   ├── hooks/               # Custom React hooks
│   │   │   ├── useAuth.tsx      # Authentication context/provider
│   │   │   ├── useLocation.tsx  # Location context with geocoding
│   │   │   ├── useLikedProfiles.ts
│   │   │   ├── useDislikedProfiles.ts  # Track passed/disliked profiles
│   │   │   └── useSentPings.ts         # Track outbound pings
│   │   ├── context/
│   │   │   └── StreamChatContext.tsx  # Stream Chat SDK provider
│   │   ├── config/
│   │   │   └── constants.ts     # API URLs, headers, credentials
│   │   ├── utils/
│   │   │   └── images.ts        # Image URL proxying
│   │   ├── App.tsx              # Root component with providers
│   │   └── main.tsx             # Entry point
│   ├── server/
│   │   └── index.js             # Express backend for data persistence
│   ├── vite.config.ts           # Vite bundler config with API proxies
│   ├── package.json
│   ├── docker-compose.yml       # Docker stack for Unraid production
│   └── docker-compose.local.yml # Docker stack for local development
├── proxyman_extracted/          # Captured API requests/responses
├── proxyman_chat/               # Captured Stream Chat API data
├── stream_extracted/            # Stream Chat SDK captures
└── API_DOCUMENTATION.md         # Comprehensive API docs (4780 lines)
```

## Architecture

### Stack
- **React 19** with TypeScript
- **Apollo Client 4** for GraphQL
- **Stream Chat SDK** for real-time messaging
- **Tailwind CSS 4** for styling
- **Vite 7** for bundling
- **Express 5** for local backend

### State Management
- **React Context** for global state (Auth, StreamChat, Location)
- **Apollo Client cache** for GraphQL data
- **localStorage** for offline persistence with server sync

### Provider Hierarchy
```tsx
<AuthProvider>
  <ApolloProvider>
    <StreamChatProvider>
      <LocationProvider>
        <BrowserRouter>
          <Routes />
        </BrowserRouter>
      </LocationProvider>
    </StreamChatProvider>
  </ApolloProvider>
</AuthProvider>
```

## Key Files

| File | Purpose |
|------|---------|
| `web/src/App.tsx` | Root component with all providers |
| `web/src/api/client.ts` | Apollo GraphQL client with auth headers |
| `web/src/api/auth.ts` | Firebase token refresh (AuthManager singleton) |
| `web/src/api/dataSync.ts` | Dual storage: localStorage + backend server |
| `web/src/config/constants.ts` | API endpoints, credentials, headers |
| `web/src/hooks/useAuth.tsx` | Auth context and login/logout |
| `web/src/context/StreamChatContext.tsx` | Stream Chat initialization |
| `web/src/components/profile/ProfileCard.tsx` | Profile card with `safeText()` rendering |
| `web/src/pages/Likes.tsx` | Likes page with enrichment, scanner, auto-refresh |
| `web/server/index.js` | Express backend for data persistence |
| `API_DOCUMENTATION.md` | Full API reference from reverse engineering |

## API Integration

### GraphQL Backend
- **Endpoint**: `https://core.api.fldcore.com/graphql` (proxied via Vite)
- **Client**: Apollo Client with auth link middleware
- **Key queries**: `ProfileQuery`, `DiscoverProfiles`, `WhoLikesMe`, `ListSummaries`
- **Key mutations**: `ProfileLike`, `ProfileDislike`, `DeviceLocationUpdate`, `SearchSettingsUpdate`

### Required Headers
```typescript
{
  'Authorization': 'Bearer <firebase_jwt>',
  'x-profile-id': 'profile#<uuid>',
  'x-app-version': '8.8.3',
  'x-device-os': 'ios',
  'x-os-version': '18.6.2',
  'x-transaction-id': '<uuid>',
  'x-event-analytics-id': '<uuid>'
}
```

### Firebase Authentication
- Token refresh via `https://securetoken.googleapis.com/v1/token`
- AuthManager class handles automatic refresh with 1-minute buffer
- Tokens stored in memory, refresh token in localStorage

### Stream Chat
- Real-time messaging via Stream Chat SDK
- Credentials fetched via `StreamCredentialsQuery` GraphQL
- API Key: `y4tp4akjeb49`

### Local Backend Endpoints
```
GET/PUT  /api/data/:userId             # Full user data
GET/PUT  /api/data/:userId/:key        # Specific key
DELETE   /api/data/:userId/:key        # Delete key
POST     /api/data/:userId/liked-profiles
DELETE   /api/data/:userId/liked-profiles/:profileId
GET/POST /api/disliked-profiles        # Passed/disliked profiles cache
DELETE   /api/disliked-profiles/:id    # Remove from passed
GET/POST /api/who-liked-you            # Cache profiles that liked user
GET/POST /api/sent-pings               # Track outbound pings
PUT      /api/sent-pings/:targetProfileId  # Update ping status
DELETE   /api/sent-pings/:targetProfileId  # Remove sent ping
POST     /api/auth/login
GET      /api/auth/verify
POST     /api/auth/logout
GET      /api/health
```

## Data Persistence

Hybrid localStorage + server approach in `dataSync.ts`:
1. **Write**: localStorage immediately → sync to server
2. **Read**: Try server first → fallback to localStorage
3. **Health check**: Server ping every 30 seconds

Keys synced: `liked_profiles`, `disliked_profiles`, `who_liked_you_profiles`, `locations`, `credentials`, `current_location`, `analytics_id`

## Routes

| Path | Page | Description |
|------|------|-------------|
| `/discover` | DiscoverPage | Profile discovery feed with scanner |
| `/likes` | LikesPage | Likes / Pings / You Liked / Passed (4 tabs) |
| `/sent-pings` | SentPingsPage | Outbound pings tracker |
| `/messages` | MessagesPage | Chat conversation list |
| `/chat/:channelId` | ChatPage | Individual chat |
| `/profile` | ProfilePage | Own profile view |
| `/settings` | SettingsPage | Location, search settings, credentials |
| `/api-explorer` | ApiExplorerPage | Debug tool for testing GraphQL queries |

## Development

### Scripts
```bash
npm run dev              # Vite dev server only
npm run server           # Express backend only
npm run dev:all          # Both in parallel (recommended)
npm run build            # Production build
npm run lint             # ESLint check
npm run docker:local     # Start local Docker stack
npm run docker:local:down   # Stop local Docker stack
npm run docker:local:logs   # View local Docker logs
```

### Vite Proxy Configuration
The Vite dev server proxies requests to bypass CORS:

**External APIs:**
- `/api/graphql` → `core.api.fldcore.com` (GraphQL backend)
- `/api/firebase` → `securetoken.googleapis.com` (auth tokens)
- `/api/images` → `res.cloudinary.com` (profile images)
- `/api/fldcdn` → `prod.fldcdn.com` (CDN assets)

**Local backend:**
- `/api/who-liked-you` → `localhost:3001`
- `/api/sent-pings` → `localhost:3001`
- `/api/disliked-profiles` → `localhost:3001`
- `/api/data` → `localhost:3001`
- `/api/auth` → `localhost:3001`
- `/api/health` → `localhost:3001`

Mobile app User-Agent is injected on external proxies to bypass hotlink protection.

### Docker

Two compose files:
- **`docker-compose.yml`** - Production config for Unraid server (absolute paths to `/mnt/user/appdata/FeeldWeb`)
- **`docker-compose.local.yml`** - Local dev config (relative paths)

```bash
# Local development
docker compose -f docker-compose.local.yml up -d --build

# Production (on Unraid server)
cd /mnt/user/appdata/FeeldWeb && docker compose up -d --build
```
- Frontend: port 3000 (Vite dev server)
- Backend: port 3001 (Express)
- Nginx: port 7743 (reverse proxy)

## Testing

No automated tests currently. Manual testing via:
- `/api-explorer` route for GraphQL query testing
- Console logging throughout codebase
- `/api/health` endpoint for backend status

## Credentials & Configuration

Credentials are stored in `constants.ts` and localStorage:
- `feeld_profile_id` - Current profile UUID
- `feeld_refresh_token` - Firebase refresh token
- `feeld_auth_token` - Current session token
- `feeld_analytics_id` - Event tracking ID

## Features

### Likes Page (4 Tabs)
- **Likes**: Profiles that have liked you, enriched with cached data from `/api/who-liked-you`
- **Pings**: Profiles that pinged you (liked with a message)
- **You Liked**: Profiles you've liked
- **Passed**: Profiles you've disliked/passed on, stored via `/api/disliked-profiles`

**Enrichment**: WhoLikesMe API returns limited data (null photos for non-Majestic users). Cached profile data from the scanner (Discover API) is merged in via `useMemo`. Older likes (further down the list) get matched first to preserve chronological accuracy when multiple people share the same name.

**Auto-refresh**: On page load, profiles with missing/expired photos are automatically refreshed via individual `ProfileQuery` calls. Uses `useRef` + `sessionStorage` to prevent crash-remount loops (React error unmounts/remounts the component, resetting `useState` but not `useRef`/`sessionStorage`).

**Scanner**: Scans multiple saved locations via the Discover API to find real profile data. "Fuck It" mode scans all saved locations. Profiles that have liked you are auto-saved to `/api/who-liked-you`.

### Profile Detail Modal
- View full profile details including photos, bio, desires, interests
- **Partner navigation**: Click on linked partner profiles to view their details
- Back button to return to previous profile when viewing partners
- Like/Dislike actions with API mutations
- Ping modal for sending messages with likes

### Discover Page
- Profile browsing with like/dislike/ping actions
- Auto-saves profiles that have liked you (detected via `interactionStatus.theirs === 'LIKED'`)
- Profiles cached to `/api/who-liked-you` for enriching Likes page data

### Sent Pings Page
- Tracks outbound pings (likes with messages) sent to other profiles
- Shows ping message, target profile, and status
- Stored via `/api/sent-pings` backend endpoint

## Gotchas

### GraphQL `__typename` Objects
The Feeld GraphQL API can return profile fields (gender, sexuality, location, goals, distance) as objects with `{__typename: "..."}` instead of plain strings. **Always use `safeText()` or type checks when rendering any profile field.** Never render `profile.gender` or similar fields directly in JSX.

```tsx
// ProfileCard.tsx has a safeText() helper:
const safeText = (v: any): string => {
  if (v == null) return '';
  if (typeof v === 'string') return v;
  if (typeof v === 'number' || typeof v === 'boolean') return String(v);
  return '';
};

// Usage: <span>{safeText(profile.gender)}</span>
// NOT:   <span>{profile.gender}</span>  // Can crash with "Objects are not valid as React child"
```

Also sanitize profile data before saving to backend cache with `safeStr()`:
```tsx
const safeStr = (v: any) => (typeof v === 'string' ? v : '');
```

### Signed Image URLs
Cloudinary/fldcdn signed image URLs expire after a few hours. Cached profiles need periodic refresh to get new URLs. The auto-refresh on the Likes page handles this.

### API Version Enforcement
Feeld API enforces `x-app-version` — old versions get `UNSUPPORTED_APP_VERSION` 400 errors. Check the App Store for the current version and update `constants.ts` and `vite.config.ts` headers.

### WhoLikesMe Photo Limitations
WhoLikesMe API returns null photo URLs for non-Majestic (non-paying) users. The scanner (Discover API) returns real photos, which is why we cache scanner results and enrich WhoLikesMe data.

### Render-time Side Effects
Never call functions with side effects during React render. Use `useMemo` for data transformation and separate `useEffect` for side effects like API calls or cache updates. Calling side-effect functions during render causes infinite re-render loops.

## Common Tasks

### Add a new page
1. Create component in `web/src/pages/`
2. Add route in `web/src/App.tsx`
3. Add navigation link in `web/src/components/layout/Navigation.tsx`

### Add a new GraphQL query/mutation
1. Define in `web/src/api/operations/queries.ts` or `mutations.ts`
2. Use fragments from existing definitions for consistency
3. Call via Apollo's `useQuery`/`useMutation` hooks

### Add a new context/hook
1. Create in `web/src/hooks/` or `web/src/context/`
2. Export provider and hook
3. Add provider to hierarchy in `App.tsx`

### Debug API issues
1. Use `/api-explorer` route to test queries directly
2. Check Network tab for request/response
3. Verify headers in `constants.ts`
4. Check `API_DOCUMENTATION.md` for endpoint details

## Remote Deployment (Unraid)

Production runs on an Unraid server at `10.3.3.11`. Code lives at `/mnt/user/appdata/FeeldWeb/`, persistent data at `/mnt/user/downloads/feeldWeb/`.

```bash
# SSH in
sshpass -p 'Intel22' ssh -o StrictHostKeyChecking=no root@10.3.3.11

# Sync a file
sshpass -p 'Intel22' scp -o StrictHostKeyChecking=no \
  "web/FILE_PATH" "root@10.3.3.11:/mnt/user/appdata/FeeldWeb/FILE_PATH"

# Restart containers
sshpass -p 'Intel22' ssh -o StrictHostKeyChecking=no root@10.3.3.11 \
  'docker restart feeld-web-frontend feeld-web-backend feeld-web-nginx'
```

External URL: https://feeld.treytartt.com (proxied via Nginx Proxy Manager on port 7743)

## External Resources

- **API Documentation**: `API_DOCUMENTATION.md` (comprehensive reverse-engineered docs)
- **Captured Requests**: `proxyman_extracted/`, `proxyman_chat/`, `stream_extracted/`
- **Stream Chat Docs**: https://getstream.io/chat/docs/
- **Apollo Client Docs**: https://www.apollographql.com/docs/react/
