Architecture Overview
Two independent frontends sharing one Firebase Realtime Database.
EJ Website (ej/) |
Mobile App (app/) |
|
|---|---|---|
| Style | Desktop editorial, sidebar list | Mobile social, feed waterfall |
| Navigation | Panel stack (nested modals) | Tab switching (flat) |
| Discovery | Search + sidebar filter | Feed + nearby GPS |
| Map default | 3D globe (MapLibre/globe.gl) | Flat MapLibre |
| JS style | ES6 (const, arrow, hooks) |
ES5 (var, function) |
Firebase Schema
/foods/{foodId}
name, englishName, emoji, type ('food'|'want')
lat, lng, country, region, province (+ English variants)
description, englishDescription
tags[], restaurants[{name, address, url, note}]
recipe, recipeLink, origin, originLink
createdBy, createdAt, isMinefield
source{type, collection} ← curated foods only
/images/{foodId}
[url, url, ...] ← stored SEPARATELY from /foods to keep reads fast
/users/{username}
avatar ← base64 string
bio, createdAt
followers/{username: true}
following/{username: true}
collects/{foodId: true}
collections/{id} { name, createdAt, items/{foodId: true} }
favFolders/{id} { name, createdAt, items/{foodId: true} }
hiddenFoods/{foodId: true}
blockedUsers/{username: true}
trips/{tripId}
name, date, description, visibility ('public'|'followers'|'private')
coverImage ← base64, compressed 300×300 @50%
routeMode ('foot'|'bicycle'|'car')
stops[{foodId?, note} | {placeName, lat, lng, note}] ← food stop OR custom place stop
routeGeometry ← GeoJSON LineString from OSRM
routeDistance (m), routeDuration (s)
collaborators/{username: true}
shareRequests/{username: timestamp} ← pending join requests; cleared on accept/decline
createdAt, updatedAt, sharedFrom
/notifications/{username}/{notifId}
action 'like'|'collect'|'follow'|'comment'|'trip-invite'|'trip-request'
fromUser, timestamp ← timestamp required for unread query
foodId, foodName ← for like/collect/comment
tripId, tripOwner, tripName ← for trip-invite/trip-request
/stats
userCount, pageViews
IndexedDB Cache
Both apps cache the full /foods list in IndexedDB.
- TTL: 30 min (EJ), configurable (App)
- On load: show cached data immediately → fetch Firebase → overwrite cache
- Gotcha: Firebase data has no
imagesfield. Saving from Firebase overwrites the cache and strips any images previously stored inline. This is expected — images are always fetched from/images/{id}.
Debugging Checklist
| Symptom | Likely cause | Where to look |
|---|---|---|
| Food has no photos in detail panel | /images/{foodId} empty in Firebase |
Firebase console → images node |
| Profile thumbnails all blank | thumbImages state not populated yet |
profile.js useEffect([userFoods.length]) |
| Notification badge stuck at 0 | Notification missing timestamp |
Check push call in app.js |
| Notifications not appearing in messages | timestamp is null → sorts before existing |
Same as above |
| Nearby sort not working | noShuffle not passed / if(noShuffle) return after quality sort |
feed.js |
| Route not showing on map | route-line source not set / fitBounds not called |
globe.js route-line source, route.js useEffect |
| Trip detail route stays after close | useEffect cleanup missing setData(empty) |
route.js TripDetailView cleanup return |
| App white screen | React error in a component | Browser devtools console |