Pular para o conteúdo principal

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.tsx loader 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/toggle server-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étodoEndpointDescrição
GET/bff/favorite-gamesLista jogos favoritos do usuário (com game data)
PUT/bff/favorite-games/toggleToggle 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

CamadaArquivo (front-web-base)Responsabilidade
Serviceapp/services/favorites.server.tsWrapper do BFF sobre o ApiClient (list, toggle) + transforms snake_casecamelCase
Cacheapp/services/favorites.cache.server.tsFavoritesCacheService (platform-cache por userId) + helper loadFavoriteSlugsForRequest
Layout loaderapp/routes/_layout.tsxHidrata favoriteSlugs no store, uma vez por user
Home loaderapp/routes/_index.tsxResolve game details dos favoritos fora das home rows (cap 30)
API routeapp/routes/api/favorites.tsProxy interno autenticado: GET lê, POST/DELETE toggle
API helperapp/routes/api/games/by-slugs.tsFallback client-side: resolve slugs em batch (cap 50)
Storeapp/store/games.ts (slice favorites)favoriteSlugs: Set<string>, _isFavoritesHydrated, pendingFavoriteWrites
Hookapp/hooks/useFavorites.tsuseFavorites().toggle, useIsFavorite(slug)
Botãoapp/components/games/FavoriteButton.tsxCoração (variant overlay/inline); abre auth modal se deslogado
Rowapp/components/home/FavoritesRow.tsxCarrossel + animações (enter/exit + FLIP reflow) + empty state
Feature flagapp/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 (createFavoritesCacheServicenull).
  • 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:

  1. O loadFavoriteSlugsForRequest a efetivamente buscar (caso contrário retorna []).
  2. A row "Jogos Favoritos" na home (desde que também registrada nas home rows).
  3. O FavoriteButton nos 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

  1. Feature flag em overrides/<brand>/app/config/features/features.ts:

    favoriteGames: true,
  2. 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 WidgetRenderer não monta a row mesmo com a flag ligada.

  3. 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

SintomaOnde 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 em slug_url (não slug). Normalizado em games.cache.server.ts (getDetail) e em api/games/by-slugs.ts. Se usar getDetail por slug em outro lugar, replique a normalização.
  • Hidratação uma vez por user: o _layout.tsx hidrata 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.
  • useIsFavorite usa _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.