尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Lugares que combinam com você. PWA de recomendação de restaurantes por ocasião em Campo Grande (MS).
A pessoa escolhe a ocasião ("A dois, pra conversar", "Pagode e porção"...) e o tipo de comida, recebe os lugares que mais combinam, com o índice de compatibilidade e o "Por que combina com você?", e manda as 3 melhores opções para o grupo votar pelo WhatsApp, arrastando cards.
Precisa de Node 20+ e Docker (o Supabase local roda em containers).
npm install
npx supabase start # sobe Postgres, Auth, Storage e um caixa de e-mail local
cp .env.example .env.local # cole as chaves que o comando acima imprimiu
npm run dev
NEXT_PUBLIC_SUPABASE_URL = API URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY = Publishable key, SUPABASE_SECRET_KEY = Secret key.ADMIN_EMAILS para virar admin.supabase/seed.sql, fotos de teste do Unsplash) para testar. Em produção ele começa vazio e se enche pelo botão de importação do /admin.npx supabase db reset apaga tudo e recria o banco com o seed.| Quem | Onde | O que faz |
|---|---|---|
| Qualquer pessoa, sem conta | /, /o/..., /l/..., /v/... | Escolhe ocasião e comida, vê lugares, vota em link de amigo, avalia em 3 toques |
| Cliente com conta | /perfil | Listas "Quero ir" e "Já fui", selo Explorador (5 avaliações), nome |
| Dono de restaurante | /painel | Pede acesso ao lugar; depois de aprovado vê aparições por ocasião, cliques no WhatsApp e rotas, quem salvou; edita WhatsApp, Instagram, cardápio, tipo de comida e foto de capa |
| Time Kombin (admin) | /admin | Importa restaurantes do Google com um botão, revisa lugares, marca destaques, cadastra lugar à mão, aprova donos, vê cliques |
Login: e-mail e senha (com "Criar conta" e "Esqueci a senha") e, opcionalmente, Google (Supabase Auth). Admin é quem tem o e-mail em ADMIN_EMAILS ou role = 'admin' na tabela profiles.
https://kombin-seven.vercel.app) e, em Redirect URLs, adicione https://kombin-seven.vercel.app/auth/callback. Sem isso, o link de "esqueci a senha" volta para o lugar errado.https://<seu-projeto>.supabase.co/auth/v1/callback.AUTH_GOOGLE=1 e faça um Redeploy. O botão "Continuar com Google" só aparece com ela, para nunca mostrar um botão que dá erro.| Parte | Onde está |
|---|---|
| Descobrir: saudação do momento, ocasiões em carrossel, sua lista, "Escolhas para o seu momento" | src/app/page.tsx, src/components/OccasionCard.tsx, src/components/RestaurantCard.tsx |
| Filtros (momento, com quem, orçamento por pessoa, sem couvert, dá pra conversar, espaço kids, bairro) e contagem ao vivo | src/app/buscar |
Ranking por ocasião ou por comida (/o/tudo?c=pizza) + chips rápidos | src/app/o/[slug]/page.tsx, src/components/QuickPrefs.tsx |
| Tipos de comida, etiquetas de ambiente e mapeamento dos tipos do Google | src/lib/cuisines.ts, src/lib/tags.ts |
| Índice de compatibilidade (regras e pesos, sem IA em tempo real) | src/lib/match.ts, pesos em src/lib/occasions.ts |
| "Decidir com o mozão/galera": link de votação sem login, cards para arrastar, resultado ao vivo | src/app/v/[code], src/components/VoteBoard.tsx |
| Página do lugar: galeria, "Por que combina com você?", informações, sinais da galera, salvar | src/app/l/[id]/page.tsx |
| Botão "Reservar via WhatsApp" com mensagem pronta e clique rastreado | src/app/api/go/[kind]/route.ts |
| Avaliação relâmpago em 3 toques (gasto, couvert, barulho) | src/components/MicroReview.tsx, src/app/api/review/route.ts |
| Login com senha, cadastro, nova senha, perfil, painel do restaurante, admin | src/app/entrar, redefinir-senha, perfil, painel, admin |
Importação do Google pelo /admin (ou terminal), notas por regra e vibe-check opcional por IA de visão | src/app/admin/ImportPanel.tsx, src/app/admin/import-actions.ts, src/lib/ingest/ |
| Animações: transição entre telas e foto que "voa" para a página do lugar (View Transitions), abas, coração, confete | src/app/globals.css, src/components/ui/ |
| Banco | supabase/migrations/ |
Cada ocasião dá pesos aos critérios: vibe (nota por regra, da IA ou do admin para aquela ocasião), barulho, couvert, orçamento, distância, etiquetas que a ocasião pede (ex.: aniversário quer "bom pra grupos" e "aceita reserva") e tamanho do grupo. Cada critério tem um encaixe de 0 a 1 e uma confiança, que cresce com as micro-reviews:
compat = 100 × (Σ peso·confiança·encaixe + 0,5) / (Σ peso·confiança + 1)
Lugar sem dados fica em 50%. Filtros duros (tipo de comida, bairro, etiquetas marcadas, "sem couvert", orçamento muito abaixo) tiram o lugar da lista. O resultado nunca passa de 99% e sempre vem com até 3 motivos e 1 alerta. Para mudar o comportamento, mexa nos pesos em src/lib/occasions.ts. Os testes estão em tests/match.test.ts (npm test).
Três coisas deixam o índice mais esperto, todas de graça (testes em tests/context.test.ts):
src/lib/weather.ts, src/lib/context.ts): a cada 15 minutos o servidor consulta a Open-Meteo (sem chave, sem cadastro) para o centro de Campo Grande. Chovendo ou abaixo de 18 °C, lugar com a etiqueta "Coberto / climatizado" sobe e lugar só com "Área externa" desce, e a lista mostra "Chovendo agora em CG · Priorizando lugares cobertos". Em noite quente (24 °C ou mais, sem chuva) a área externa ganha um empurrão. Lugar sem nenhuma das duas etiquetas não muda. A importação marca "Coberto" quando o Google diz que o lugar não tem mesa ao ar livre; o resto se ajusta no /admin.src/components/MicroReview.tsx, src/app/api/review/route.ts): avaliação vale em dobro se a pessoa fez check-in (o navegador calcula a distância até o lugar com Haversine e manda só "sim"; a localização nunca sai do celular) ou clicou em "Como chegar" entre 30 minutos e 3 dias antes de avaliar. A view place_signals faz as médias ponderadas.src/lib/taste.ts): cada busca fica na tabela search_log (sem localização) e, junto com os lugares que a pessoa escolhe (Como chegar, WhatsApp, salvos, avaliados), vira um gosto: etiquetas, comidas, gasto típico e se prefere lugar calmo. O que foi feito há um mês vale metade. Depois de umas 3 buscas, o gosto entra como mais um critério e a lista avisa "No seu estilo".supabase/migrations/0001_init.sql, 0002_contas_paineis_culinaria.sql, 0003_tags_destaque.sql, 0004_seguranca.sql, 0005_cardapio_dono.sql e 0006_indice_contexto.sql. O editor de consultas da Vercel não serve: ele não roda vários comandos de uma vez.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY e SUPABASE_SECRET_KEY. Nos dois casos, cadastre ADMIN_EMAILS na Vercel.GOOGLE_MAPS_API_KEY na Vercel (Settings > Environment Variables), depois faça um Redeploy. Antes, crie um alerta de orçamento de US$ 1 e limite a cota diária da API./admin > Importar do Google > Começar importação. Primeiro entram, pelo nome, as casas essenciais de Campo Grande (lista ESSENTIALS em src/lib/ingest/google.ts: Enoteca Decanter, Turista Sushi, Mr. VIP...). Depois vêm 16 buscas (pizzarias, japoneses, bares com música...); entram só lugares abertos, com nota 4,2+ e 150+ avaliações, já aprovados e com notas por ocasião calculadas a partir dos atributos do Google (música ao vivo, bom pra grupos, cardápio infantil...). Dá para rodar de novo: só entram lugares novos. Lugar específico que faltou: campo "Adicionar um lugar específico" na mesma tela.GEMINI_API_KEY (ou ANTHROPIC_API_KEY). Em /admin > Importar aparece "Refinar com IA de visão": a IA olha 3 fotos de cada lugar e ajusta as notas pelo ambiente; ambiente feio volta para "Para revisar"./admin > Aprovados, confira o tipo de comida, marque os destaques (aparecem primeiro no Descobrir) e cadastre o WhatsApp de cada lugar.npm run refresh (renova as coordenadas, regra dos 30 dias do Google).Pelo terminal, a mesma importação: npm run ingest -- --dry-run (não grava, salva scripts/out/), --limit 40, --vision, --min-rating 4.3, --min-reviews 200, --provider anthropic. Precisa das chaves no .env.local (npx vercel env pull .env.local baixa as da Vercel).
| Etapa | Cota grátis | Depois |
|---|---|---|
| Importação: Text Search com atributos (Enterprise + Atmosphere) | 1.000 chamadas | US$ 40 por mil |
| Fotos exibidas no app (Place Photos) | 1.000 fotos | US$ 7 por mil |
| Vibe-check no Gemini Flash (opcional, 3 fotos por lugar) | plano gratuito (com limite por minuto) | ver preços do Google AI |
| Página do lugar: nota e horário ao vivo (Enterprise) | 1.000 | US$ 20 por mil |
Uma importação completa gasta 16 buscas. As fotos passam pelo nosso servidor (/api/photo/...) e ficam no cache da Vercel por 1 dia; a nota e o horário ao vivo, por 30 minutos. Assim cada foto é cobrada no máximo uma vez por dia, e não a cada visita. No plano gratuito do Gemini, o Google pode usar as imagens enviadas para melhorar os modelos.
place_id (permitido para sempre) e as coordenadas por até 30 dias.src/lib/validation.ts, zod) e conferir o limite de requisições (src/lib/rate-limit.ts). A chave pública vai para o navegador só para o login; a migração 0004_seguranca.sql liga RLS em todas as tabelas, libera apenas leitura do que é público ou da própria pessoa e revoga qualquer escrita pela chave pública.GOOGLE_DAILY_LIMIT, padrão 500 chamadas). Mesmo assim, coloque cota diária na Places API e alerta de orçamento no Google Cloud: é o único freio que vale fora do código.ADMIN_EMAILS e ainda não tem conta. Em produção prefira update profiles set role = 'admin' where id = (select id from auth.users where email = '[email protected]'); e deixe ADMIN_EMAILS vazio, ou mantenha a confirmação de e-mail ligada.Suba o repositório no GitHub e importe na Vercel (plano Hobby). Cadastre as mesmas variáveis do .env.local no painel da Vercel. O plano Hobby é para uso não comercial: quando começarem a cobrar restaurantes, migrem para o Pro ou para outro provedor.
Direção "Tela cheia": a foto do lugar ocupa a tela e o resto é noite.
#2A1B12, superfícies #36251A e #423024, texto #F6F0E6, secundário #B9B0A6. Um destaque só, o amarelo do ipê #FFC81E: botão principal, "% combina" e o sublinhado da aba ativa. Tokens em src/app/globals.css.title (semibold, letra apertada); o tamanho do nome do lugar vem de posterNameSize em src/lib/screen.ts.btn() e BTN_* em src/components/ui/styles.ts), foto não tem canto arredondado, botão por cima de foto é um círculo claro (ROUND_LIGHT). Sem vidro, sem sombra, sem gradiente de enfeite; o véu escuro só existe embaixo de texto em cima de foto (scrim-bottom, scrim-top).src/components/Discover.tsx), passa pra cima para o próximo; a última tela da ocasião manda os 3 primeiros pra votar ou abre a lista com filtros. O servidor já manda o ranking das 6 ocasiões, então trocar de aba é instantâneo.HIDDEN em src/components/ui/TabBar.tsx).kb_did). Conta serve para listas, selo e painéis.src/lib/catalog.ts) e é renovado na hora quando o admin ou um dono muda algo. O login só é verificado quando há cookie de sessão. Dados do Google ao vivo carregam depois da página. Cada tela tem esqueleto de carregamento.<ViewTransition>); gestos (arrastar card, folha de avaliação) usam motion. Tudo respeita "reduzir movimento" do sistema.| Comando | O que faz |
|---|---|
npm run dev | servidor de desenvolvimento |
npm test | testes do índice e da importação |
npx supabase start / npx supabase db reset | banco local / recriar com dados fictícios |
npx tsx scripts/gen-seed.ts | regenera supabase/seed.sql a partir de tests/fixtures.ts |
npm run lint / npm run typecheck | qualidade |
npm run ingest -- [--dry-run] [--limit N] [--vision] [--provider gemini|anthropic] | importação pelo terminal |
npm run refresh | renova coordenadas do Google |