380 lines
16 KiB
Markdown
380 lines
16 KiB
Markdown
# 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/
|