BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
축구 경기 일정 검색, 클럽·리그·도시·국가별 경기 찾기, 본 경기 기록 및 경기장 여권을 모아 스테이디움을 획득합니다
Monte roteiros de viagem para jogos de futebol, encontre partidas por clube, liga, cidade ou país, registre os jogos que você viu e conquiste estádios no seu Passaporte de Estádios.
스포츠·피트니스JavaScript · PLpgSQLGitHub @lauro-guedes온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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).