Initial commit
This commit is contained in:
379
claude.md
Normal file
379
claude.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# 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/
|
||||
Reference in New Issue
Block a user