# 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 ``` ## 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 ', 'x-profile-id': 'profile#', 'x-app-version': '8.8.3', 'x-device-os': 'ios', 'x-os-version': '18.6.2', 'x-transaction-id': '', 'x-event-analytics-id': '' } ``` ### 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: {safeText(profile.gender)} // NOT: {profile.gender} // 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/