OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Roteiros de viagem em torno de jogos de futebol (groundhopping). A pessoa escolhe países, datas e times favoritos; a tripsz cruza com o calendário de jogos reais e devolve a melhor sequência de cidades, em três opções de roteiro (A, B e C). Quem assina o Passaporte de Estádios também registra os jogos a que foi, ganha XP, conquistas e aparece no ranking de torcedores.
avatars e team-logos)app/manifest.js, ícones e tela de carregamento estática em app/layout.jsapp/
page.js, [...slug]/page.js renderizam o mesmo app (rota coringa evita 404 no F5)
u/[slug]/ perfil público de um torcedor
admin/page.js painel administrativo
termos/, privacidade/, redefinir-senha/
api/
trip/plan monta o roteiro com jogos reais
games/search Buscar Jogos por cidade (data + raio)
games/browse Buscar Jogos por país, liga ou clube (lê só do fixtures_calendar, sem gastar API)
venues/photo Foto real do estádio (Wikipedia) quando a API-Football não tem
attended-games/* Registrar Jogo (busca por estádio/time)
teams/*, cities/suggest escudos e sugestões de times e cidades
ranking, profile/*, public-profile/[slug]
subscribe/* assinar, cancelar, trocar cartão, faturas
checkout, webhook pagamento avulso e avisos do Mercado Pago
consultoria/availability
admin/* usuários, assinaturas, finanças, ligas etc.
components/
TripszApp.jsx só o App: estado geral e troca de telas
routes.js mapa tela ⇄ URL (SCREEN_TO_PATH / PATH_TO_SCREEN)
ui/ peças visuais: Button, Badge, Pill, Icon, Wordmark, Loading...
nav/ menus: TopNavPublic, AuthedNav, AuthedFooter, WizardChrome
landing/ página inicial
auth/ criar conta, login, redefinir senha
wizard/ passos do questionário e tela de cálculo
roteiro/ resultado A/B/C, detalhe e Meus Roteiros
checkout/ pagamento avulso da consultoria
conta/ conquistas, ranking, nível, perfil, assinatura, paywall
jogos/ Buscar Jogos, Meu Calendário, Meus Jogos, Registrar Jogo
jogos/detalhes/ modal "Detalhes da experiência" (Meus Jogos): uma aba por arquivo
TeamBadge.jsx escudo do time, com várias fontes e fallback
SocialLinks.jsx redes sociais do rodapé (o endereço do Instagram fica aqui)
lib/
tripPlanner.js motor de roteiros (lógica pura, sem rede/banco)
tripOptions.js monta as opções A / B / C a partir do plano
fixturesCalendar.js calendário de jogos reais (servidor)
calendarCore.js, calendarUtils.js, fixtureLocation.js, gamesSearch*.js, gamesBrowse.js
footballApi.js cliente da API-Football
supabase.js clientes do Supabase (navegador e admin)
tokens.js cores e fontes
tripPlanClient.js chama /api/trip/plan e converte o plano para a tela
passportAccess.js quem tem acesso ao Passaporte de Estádios (limite de 20 jogos grátis)
gameSeason.js, seasons.js temporada de um jogo (europeia ou ano-calendário)
searchKey.js normaliza nomes de busca (acento, til, hífen, apóstrofo)
gameDetailsCore.js detalhes de um jogo (escalação, gols, estatísticas): lógica pura
gameDetails.js busca na API-Football + cache desses detalhes (só servidor)
xpRules.js regra de XP: total e XP gerado por cada jogo
fanLevels.js categorias de torcedor (nomes, faixas de XP e textos)
data/ cidades, clássicos (rivalries), geoExtras
O app é uma página só com roteamento próprio: o mapa SCREEN_TO_PATH em
components/routes.js liga cada tela a uma URL (/roteiro/destino, /conta/jogos, ...).
lib/teamLogoIds.js) ou pelo
nome (os escolhidos na busca /api/teams/suggest, que devolve o nome como a API escreve).lib/fixturesCalendar.js guarda os jogos de cada liga na tabela fixtures_calendar,
já com cidade, coordenada e país. Antes de cada consulta só as ligas dos países
pedidos são conferidas; se foram atualizadas há menos de 12 h, nem vai à API.
O horizonte guardado é de 270 dias, mas a busca de jogos e as datas do roteiro aceitam só até 6 meses à frente (maxSearchDate); sem jogo na data pedida, a busca mostra os mais próximos (±10 dias) no mesmo raio. A busca de uma pessoa lê da tabela e não gasta cota.
XP = jogos registrados × 50 + estádios × 100 + países × 200 + conquistas × 150
(lib/xpRules.js). Só jogos registrados contam; gerar um roteiro não dá XP.
Em Meus Jogos, cada jogo mostra o XP que gerou (jogo + estádio novo + país novo + conquista
que ele desbloqueou); a soma dos jogos sempre fecha com o total.
Os textos das categorias de torcedor ficam em lib/fanLevels.js.
"Ver detalhes da experiência" abre um modal com 5 abas: Escalação (campo com os 11 de cada time, reservas e técnico), Gols (linha do tempo com placar corrido; no desktop, resumo ao lado), Estádio (capacidade, cidade, país e visitas da pessoa), Estatísticas (barras mandante x visitante) e XP (o que o jogo rendeu, próxima categoria e compartilhar). No celular abre como folha de baixo pra cima; no desktop, centralizado (Figma 516:*).
GET /api/games/details?gameId=... (exige login; só devolve jogo da própria
pessoa). Jogo registrado à mão não tem api_fixture_id: só as abas Estádio e XP têm dados.venues_cache) e fica em fixture_details_cache. Se a
tabela não existir, funciona igual (cache em memória), só gasta mais cota.media.api-sports.io/football/venues/<id>.png; se falhar, usa a foto de
reserva STADIUM_FALLBACK_IMAGE (components/jogos/detalhes/AbaEstadio.jsx).Depois de registrar um jogo, a pessoa marca quem foi com ela. Quem já tem conta NÃO recebe o jogo sozinho: recebe um aviso ("Fulano te marcou em [jogo]") com "Adicionar ao meu Passaporte" ou "Ignorar", então ninguém grava dado em conta alheia nem infla o XP/ranking de outra pessoa. Quem não tem conta entra por convite (seção acima).
components/jogos/acompanhantes/*): QuemFoiComVoce (etapa depois do registro), ConvidarModal, JogoSalvo
(confirmação + "Seus convites"), DetalhesJogoModal (abre pela linha "Com Ana e Bruno" em Meus Jogos; o modal de 5 abas
continua sendo o "Ver detalhes da experiência") e MarcacoesPendentes (os avisos, no topo de Meus Jogos).lib/companion*.js e app/api/{friends,games/companions,games/[gameId]/companions,companion-requests}. Tabela
game_companions (supabase-acompanhantes.sql, RLS ligado e sem política).@ EXATO (profiles.username), mínimo de 3 caracteres, devolve no máximo uma pessoa (nunca lista
usuários), 15 buscas/min por pessoa. "Amigo" (sugestões) = quem você já marcou ou convidou e que tem conta.Quem registrou um jogo pode convidar quem estava com ele. Quem recebe o link cria a conta (ou entra) e o jogo cai no Passaporte dela. Fase 1 (feita): o lado de quem RECEBE e todo o servidor. A tela que CRIA o convite ("Convidar alguém novo") e a marcação de amigos que já têm conta (com aceite) são as fases seguintes.
/convite/<código> (components/convite/*, estado e ações em useConvite.js). Estados: válido
(cadastro), expirado, já usado (login), link inexistente, dono do convite, pessoa já logada. Não é indexada./conta/bem-vindo (components/conta/BoasVindasConvite.jsx), com os dados do resgate guardados no navegador.lib/inviteServer.js (criar, listar, reenviar), lib/inviteRedeem.js (página pública e resgate) e
lib/inviteShared.js; rotas em app/api/invites/*. Tabela game_invites (supabase-convites.sql): RLS ligado e sem política.lib/rateLimit.js);
o convite guarda uma cópia do jogo; resgate idempotente, sem duplicar, e o dono não resgata o próprio.
Guarda só o nome digitado de quem foi convidado (nunca telefone ou e-mail).localStorage (tripsz_pending_invite, 14 dias) e o
app (TripszApp.jsx) resgata na primeira sessão (SIGNED_IN/INITIAL_SESSION), pelo CÓDIGO, nunca pelo e-mail digitado.@ (profiles.first_name e profiles.username); sobrenome nunca sai.
Configure na Vercel (Settings → Environment Variables) e, para rodar local, copie .env.example para .env.local e preencha.| Variável | Para quê |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | URL do projeto Supabase |
NEXT_PUBLIC_SUPABASE_ANON_KEY | chave pública do Supabase |
SUPABASE_SERVICE_ROLE_KEY | chave de servidor do Supabase (nunca expor no navegador) |
MERCADOPAGO_ACCESS_TOKEN | token do Mercado Pago (teste ou produção) |
NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY | chave pública (troca de cartão) |
FOOTBALL_API_KEY | chave da API-Football |
ADMIN_EMAILS | e-mails com acesso ao /admin, separados por vírgula |
NEXT_PUBLIC_FREE_ACCESS_EMAILS | e-mails com Passaporte de Estádios liberado sem pagar |
NEXT_PUBLIC_SITE_URL | URL do site (ex.: https://tripsz.vercel.app) |
supabase-schema.sql é o schema real de produção (exportado do Supabase em 09/10/2026):
tabelas, índices e políticas de RLS. Rodando esse arquivo num projeto novo e criando os
buckets públicos avatars e team-logos em Storage, o ambiente volta do zero.
trip_answers, orders,
subscriptions, attended_games, saved_games, public_profiles.service_role acessa): fixtures_cache, fixtures_fetch_log, fixtures_calendar,
calendar_sync_log, team_home_venues, venues_cache e team_logos.newsletter_subscribers: qualquer pessoa pode se inscrever.supabase-fixture-details-cache.sql cria fixture_details_cache (cache só do
servidor, como os de cima). Depois de rodar, incorpore o trecho ao supabase-schema.sql.As tabelas antigas leagues e fixtures foram apagadas; uma cópia ficou no schema
backup do Supabase e pode ser removida quando você quiser.
npm install
cp .env.example .env.local # e preencha as variáveis
npm run dev
Abre em http://localhost:3000
/api/webhook e confira as URLs de
retorno com o domínio real. Comece pelas credenciais de teste.As rotas de assinatura, checkout, perfil e do painel admin não confiam em userId/e-mail enviados pelo navegador. O navegador manda o token de login (lib/authFetch.js) e o servidor o confere com o Supabase (lib/serverAuth.js): o usuário vem do token, e o admin é quem está logado e com e-mail em ADMIN_EMAILS. Ao criar uma rota nova que depende de quem é a pessoa, use authUser(request) ou requireAdmin(request).
usuário edita suas respostas em trip_answers e ligar a proteção contra senhas vazadas (Authentication → Providers → Email).backup do Supabase pode ser removido depois de conferido.lib/gameImportance.js pontua cada jogo (competição: Champions/Libertadores > Europa/Sul-Americana > 1ª divisão > copas; fase; clubes grandes). O planner soma essa importância ao peso, então mesmo sem clubes favoritos as 3 opções (A, B, C) preferem os jogos principais. Favoritos continuam acima de tudo. A lista de clubes é estática (grafias da API).
/api/subscribe com cardTokenId)./api/subscribe/pix, sem renovação automática; acesso até subscriptions.current_period_end)./api/checkout (API de Pagamentos); o Pix é confirmado por /api/checkout/status e pelo webhook.components/pagamento/PagamentoModal.jsx (Card Payment Brick + QR Code Pix). Precisa de NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY e MERCADOPAGO_ACCESS_TOKEN.subscriptions.mercadopago_payment_id.components/auth/NomeUsuarioFields.jsx); user_metadata.name continua sendo "Nome Sobrenome".lib/username.js (3–20 caracteres, a–z, 0–9, . e _, sem repetição) e conferência em GET /api/username/check.supabase-profiles-username.sql (rodar uma vez no SQL Editor) cria public.profiles (índice único no @), as funções que geram o @ e um gatilho em auth.users. Quem entra com Google recebe nome.sobrenome (com número se já existir).