Favorite Games
Feature de "Jogos Favoritos" do template: o usuário marca um jogo com 💗 e o slug é persistido no BFF Cactus (/bff/favorite-games). A home renderiza uma row "Jogos Favoritos" e o coração aparece em todos os GameCards via o componente único FavoriteButton.
Gated por brand via featuresConfig.favoriteGames. Não requer env vars dedicadas — usa o ApiClient padrão (API_BASE_URL/ORIGIN_DOMAIN/CF_WORKER_KEY), igual ao resto das chamadas BFF.
:::info Histórico
A feature foi originalmente implementada contra uma API externa (fav-games.d7k.io, com header x-api-key e as envs FAV_GAMES_API_URL/FAV_GAMES_API_KEY). Em 2026-05-02 foi migrada para o BFF padrão e essas envs foram removidas. Se encontrar referências a fav-games.d7k.io ou FAV_GAMES_*, são resíduos a limpar.
:::
Visao geral
Leitura (SSR) Escrita (toggle)
│ │
▼ ▼
_layout.tsx loader FavoriteButton click
loadFavoriteSlugsForRequest() │
│ ▼
▼ useFavorites().toggle(slug)
FavoritesCacheService.getSlugs() │ (otimista no store)
│ ▼
▼ POST/DELETE /api/favorites { slug }
platform-cache (por userId) │
│ ▼
▼ FavoritesCacheService.toggle()
GET /bff/favorite-games ├──► PUT /bff/favorite-games/toggle
└──► engine.purge(userId)
- Leitura: o
_layout.tsxloader hidrata os slugs no store uma vez por identidade de usuário. O_index.tsx(home) resolve game details para favoritos que não estão nas home rows. - Escrita: toggle otimista no client +
PUT /bff/favorite-games/toggleserver-side, com rollback em erro.
Backend — BFF
Servido pelo BFF da brand via ApiClient de @cactus-agents/api-client. A identidade do usuário vem do JWT (não há userId/brand no payload); a brand é resolvida pelo header tenant (derivado do ORIGIN_DOMAIN).
| Método | Endpoint | Descrição |
|---|---|---|
GET | /bff/favorite-games | Lista jogos favoritos do usuário (com game data) |
PUT | /bff/favorite-games/toggle | Toggle idempotente, body { slug_url } → retorna { message, is_favorite } |
O toggle é idempotente do ponto de vista de input: o mesmo slug adiciona ou remove, e o backend devolve is_favorite com o estado final. As rotas antigas por id (/bff/favorite-games/<id>) e por slug no path foram removidas — não há fallback. Um 404 aqui é regressão de contrato com o BFF.
Arquivos principais
| Camada | Arquivo (front-web-base) | Responsabilidade |
|---|---|---|
| Service | app/services/favorites.server.ts | Wrapper do BFF sobre o ApiClient (list, toggle) + transforms snake_case→camelCase |
| Cache | app/services/favorites.cache.server.ts | FavoritesCacheService (platform-cache por userId) + helper loadFavoriteSlugsForRequest |
| Layout loader | app/routes/_layout.tsx | Hidrata favoriteSlugs no store, uma vez por user |
| Home loader | app/routes/_index.tsx | Resolve game details dos favoritos fora das home rows (cap 30) |
| API route | app/routes/api/favorites.ts | Proxy interno autenticado: GET lê, POST/DELETE toggle |
| API helper | app/routes/api/games/by-slugs.ts | Fallback client-side: resolve slugs em batch (cap 50) |
| Store | app/store/games.ts (slice favorites) | favoriteSlugs: Set<string>, _isFavoritesHydrated, pendingFavoriteWrites |
| Hook | app/hooks/useFavorites.ts | useFavorites().toggle, useIsFavorite(slug) |
| Botão | app/components/games/FavoriteButton.tsx | Coração (variant overlay/inline); abre auth modal se deslogado |
| Row | app/components/home/FavoritesRow.tsx | Carrossel + animações (enter/exit + FLIP reflow) + empty state |
| Feature flag | app/config/features/features.ts (override por brand) | Habilita por brand |
Camada de cache
FavoritesCacheService espelha o GamesCacheService: envolve o FavoritesService com o engine de @cactus-agents/platform-cache, com chave por userId (resource userFavorites). Mutações chamam engine.purge(RESOURCE, userId) para invalidar a entrada do usuário, então a próxima leitura vai à origem.
TTL/SWR são configurados via PLATFORM_CACHE_POLICY_JSON (gerenciado pelo front-ops). Sem entrada, o engine cai em bypass — funciona, só não cacheia.
createFavoritesCacheService retorna null quando a request não tem token, deixando os callers degradarem graciosamente (loader/action não quebram).
Helper de loader
loadFavoriteSlugsForRequest(request, env, waitUntil?, context?): Promise<string[]>
Helper único para descobrir os favoritos do user no server. Nunca lança — retorna [] quando:
- A feature flag está desligada (
!featuresConfig.favoriteGames). - O usuário não está autenticado (sem
profile.user.id). - A request não tem token (
createFavoritesCacheService→null). - A chamada ao BFF falha (
catch→[]).
Dedup via cache scoped por request (getRequestCache) — chamadas múltiplas no mesmo request reaproveitam a Promise. Usado por _layout.tsx e _index.tsx.
Feature flag
AppFeatureFlags.favoriteGames?: boolean (default false). Quando ligada, habilita:
- O
loadFavoriteSlugsForRequesta efetivamente buscar (caso contrário retorna[]). - A row "Jogos Favoritos" na home (desde que também registrada nas home rows).
- O
FavoriteButtonnos game cards.
:::caution Brand overrides
O brandOverridesPlugin faz substituição de arquivo inteiro (não deep-merge). Ao habilitar/desabilitar a flag, garanta que favoriteGames está presente em todos os overrides/<brand>/app/config/features/features.ts — senão a brand recebe undefined. Ver Forking — Override Files.
:::
Habilitando para um brand novo
-
Feature flag em
overrides/<brand>/app/config/features/features.ts:favoriteGames: true, -
Row do carrossel em
overrides/<brand>/app/config/sections/home-rows.legacy.ts:{slug: "favorites-row",type: "widget",i18nKey: "casino:favorites.title",icon: "⭐",},Sem essa entrada, o
WidgetRenderernão monta a row mesmo com a flag ligada. -
Sanity check: reload da home logado → a row aparece (ou o empty state).
Não há env vars a configurar — a feature usa o BFF padrão (
API_BASE_URL).
Como debugar
| Sintoma | Onde olhar |
|---|---|
| "Favorito aparece e some" | Network → /api/favorites (status do POST/DELETE). 200 + card some = store reescrito por rehidratação stale (_isFavoritesHydrated). |
| "Slug favoritado mas card não aparece na row" | Network → /api/games/by-slugs. {"games":[]} = BFF não resolveu o slug (catálogo dessincronizado ou regressão de slug_url). |
| "POST falha com 401" | JWT cookie expirou/inválido → relogar. |
| "POST falha com 404 no toggle" | Regressão de contrato no BFF (/bff/favorite-games/toggle removido/alterado). |
Testar o BFF direto (com vars do .dev.vars):
curl -H 'tenant: <ORIGIN_DOMAIN>' \
-H 'cf-worker-key: <CF_WORKER_KEY>' \
'<API_BASE_URL>/casino-games?slug=pgsoft/fortune-snake'
Pontos de atencao
- Gotcha
slug_url:GET /casino-games?slug=...retorna o slug emslug_url(nãoslug). Normalizado emgames.cache.server.ts(getDetail) e emapi/games/by-slugs.ts. Se usargetDetailpor slug em outro lugar, replique a normalização. - Hidratação uma vez por user: o
_layout.tsxhidrata o store por identidade (lastHydratedUserIdRef), evitando que loaders em paralelo com um POST otimista reescrevam o store com dado stale. Trade-off: mudanças cross-tab/device só aparecem após reload. useIsFavoriteusa_isFavoritesHydrated(flag dedicada), não_isHomeHydrated— senão toggles fora da home (game detail, cassino) seriam ignorados.- Documentação detalhada da UI/animações (FLIP, enter/exit, empty state) vive em
front-web-base/docs/features/favorites/overview.md.