Easy-to-manage touch screens
Free, open-source touch screens. Guided USB setup, a drag-and-drop editor and local control. Made for Home Assistant.
Check-ins, desafios e ranking das aulas de forró do Espaço Livre. Forró de segunda a segunda!
Translation pending
LifestyleTypeScript · PLpgSQLGitHub @FelipeMDROnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
App de check-ins, desafios e ranking para as aulas de forró do Espaço Livre, inspirado no GymRats. Projeto social, custo de infraestrutura R$ 0. As aulas acontecem a semana toda — forró de segunda a segunda! 🎶 Foi à aula → tira a foto na hora → interage com a turma → sobe no ranking. 💃🕺Read the full README ↗
App de check-ins, desafios e ranking para as aulas de forró do Espaço Livre, inspirado no GymRats. Projeto social, custo de infraestrutura R$ 0. As aulas acontecem a semana toda — forró de segunda a segunda! 🎶
Foi à aula → tira a foto na hora → interage com a turma → sobe no ranking. 💃🕺
nome;telefone;turma;papel)Sem configurar nada, o app roda em modo demo: dados de exemplo no
localStorage, login fictício, tudo funciona (inclusive como organizador).
Dica: cadastre-se com o telefone 11 97777-1234 para ver o vínculo
automático de turma pela lista de chamada.
npm install
npm run dev # http://localhost:5173
A câmera exige HTTPS (ou localhost) e permissão do navegador.
supabase/schema.sql
e execute. Isso cria as tabelas, RLS, triggers (cadastro por telefone,
turma automática), o bucket de fotos e o realtime.supabase/emails/README.md — SMTP próprio,
URLs de retorno, limite de envio e os textos personalizados. Sem isso o
"esqueci minha senha" diz que enviou e o e-mail não chega a ninguém: o
servidor embutido do Supabase só entrega para membros da organização, e
no máximo umas duas mensagens por hora..env:cp .env.example .env
# preencha VITE_SUPABASE_URL e VITE_SUPABASE_ANON_KEY
insert into public.alunos_cadastrados (nome, telefone, turma, papel_danca)
values ('Seu Nome', '11 91234-5678', 'Avançado', 'Condutor(a)');
update public.roles set papel = 'organizador'
where user_id = (
select id from auth.users where email = '[email protected]'
);
nome;telefone;turma;papel
Ana Xote;11 98888-0003;Avançado;Condutor
Ana Xote;11 98888-0003;Intermediário;Conduzido
Pedro Baião;11 98888-0004;Iniciante 02;Conduzido
Esqueci a senha: o aluno resolve sozinho pelo link na tela de entrada, desde que o passo 3 esteja feito. A exceção são as contas anteriores à migração 013, que nasceram sem e-mail: elas precisam cadastrar um no perfil primeiro. Enquanto não cadastram, só o organizador destrava (Authentication
Users > usuário > Reset password).
vercel.json já cuida das rotas do SPA).VITE_SUPABASE_URL e
VITE_SUPABASE_ANON_KEY.O free tier pausa o projeto após 7 dias sem atividade. O workflow
.github/workflows/keepalive.yml faz um
ping semanal — basta criar os secrets SUPABASE_URL e SUPABASE_ANON_KEY
no repositório do GitHub (Settings > Secrets and variables > Actions).
npx web-push generate-vapid-keysVITE_VAPID_PUBLIC_KEY (no .env e na Vercel) —
com ela definida, aparece o botão "Ativar lembretes" no perfil.supabase/functions/send-push:supabase functions deploy send-push
supabase secrets set VAPID_PUBLIC_KEY=... VAPID_PRIVATE_KEY=...
iPhone: push e câmera no iOS funcionam melhor com o PWA instalado na tela inicial (Compartilhar > Adicionar à Tela de Início). Mantenha o grupo do WhatsApp como canal paralelo de lembretes.
| Limite | Estratégia já implementada |
|---|---|
| 1 GB de storage | Compressão no cliente com teto de bytes — 90 KB por foto, 20 KB por avatar (src/lib/image.ts), o que dá ~12 mil fotos. Avatar antigo e foto de check-in excluído são apagados do bucket na hora. Limpeza automática toda semana (Edge Function limpar-fotos + pg_cron): arquiva fotos de +4 meses (mantém a presença, pula os favoritos) e recolhe órfãos. A 60 check-ins/dia isso segura o acervo em ~550 MB. Conferir: supabase/retencao.sql |
| 5 GB egress/mês | Fotos cacheadas no service worker (CacheFirst, 30 dias) |
| 500 MB de banco | Check-ins são linhas pequenas — irrelevante nessa escala |
| Pausa por inatividade | Workflow de keep-alive semanal |
| 50k usuários ativos | Folga enorme para 200–1.000 alunos |
src/
lib/ tipos, datas/streak/janelas, telefone, imagem, CSV
badges.ts distintivos (derivados dos dados)
api.ts interface única da camada de dados
supabaseApi.ts produção (auth, Postgres, storage, realtime)
demoApi.ts modo demonstração (localStorage, dados de exemplo)
context/ AuthContext (sessão/perfil/papel), ToastContext
components/ Layout, CameraCapture, CheckinCard, ChallengeForm…
pages/ Login, Feed, Checkin, Desafios, Ranking, Perfil, Painel
sw.ts service worker (precache, cache de fotos, push)
supabase/
schema.sql esquema completo: tabelas + RLS + triggers + storage
migracoes/ mudanças para bancos JÁ no ar (`npm run migrar`)
retencao.sql limpeza de fotos antigas (rodar semestralmente)
functions/send-push edge function do lembrete
Banco já em produção? Não rode o
schema.sqlde novo — ele recria políticas que já existem e dá erro. Use os arquivos desupabase/migracoes/na ordem numérica:npm run migrarroda os que faltam (veja o DEPLOY.md).
diaDaNoite/limitesDaNoite, HORA_VIRADA_NOITE) — em
espaço livre a galera fica de madrugada, e quem chegou às 23h e quem
chegou à 1h estavam juntos. Com o corte na meia-noite eles caíam em
datas diferentes e não podiam se marcar. No banco, noite_do_checkin
faz o mesmo (migração 018), e converte para o fuso local ANTES de
tirar a data: criado_em::date usava UTC, então em Itajubá qualquer
check-in a partir das 21h locais já virava "amanhã" e a marcação
falhava para quase toda a aula. Só as duplas usam essa regra — aula e
agenda continuam em dia de calendário, e desafios já tinham a própria
noção de janela que cruza a meia-noite.parceirosPossiveis(data) lista quem posso marcar num dia — e devolve
lista vazia se EU não tiver check-in nesse dia, mesmo que a outra
pessoa tenha. marcarDupla/marcar_dupla (SQL) já recusavam a
gravação nesse caso, mas o botão continuava aparecendo em três lugares
(grade pós-check-in, perfil, feed) — convidando para uma ação que
ia falhar ao confirmar em vez de nunca ser oferecida. Um bug de UX, não
de integridade: nenhuma dupla inválida chegava a ser gravada.challenge_id: um check-in vale automaticamente para
todos os desafios em que o aluno está inscrito cuja janela (período +
dia da semana + horário) bate com o momento da foto. Uma foto por aula,
sem burocracia.challenge_janelas —
no máximo uma janela por dia (dia_semana, hora_inicio, hora_fim).
O projeto funciona a semana toda, com espaços diferentes tendo horários
de início diferentes, então segunda pode abrir às 18h e quarta às 20h
dentro do mesmo desafio.a<10 dígitos>@alunos.forrodesegunda.app) que não recebe mensagem —
então o "esqueci minha senha" do Supabase, que envia para o e-mail da
conta, não tinha para onde ir. Agora quem se cadastra informa um
e-mail real, que vira o e-mail da conta e portanto o identificador do
login. O telefone continua sendo a chave da lista de chamada (é ele
que libera o cadastro e traz nome e turmas), só deixou de identificar
o login.
Contas anteriores seguem no e-mail sintético e por isso continuam
entrando pelo número: aquele endereço é calculado a partir dele, sem
consulta nenhuma. Essa é a razão de a rampa ser barata — e de não
existir uma função , que teria de ser pública (quem
vai entrar não tem sessão) e viraria um jeito de descobrir o e-mail de
qualquer aluno pelo número.| Comando | O quê |
|---|---|
npm run dev | servidor de desenvolvimento |
npm run build | typecheck + build de produção (gera o PWA em dist/) |
npm run preview | serve o build localmente (para testar o service worker) |
npm run icons | regenera os ícones do PWA a partir do logo |
Conquistas/medalhas, bônus por trazer amigo (validado pelo organizador), importação da lista de chamada por CSV, galeria de vídeos de passos, avisos do organizador, modo offline completo.
telefone → e-mailalunos_cadastrados só é lida por handle_new_user, na criação da
conta — daí a lista não controlar acesso e não fazer sentido guardar
nela quem já entrou. A importação de CSV é a matrícula do semestre:
para quem já tem conta ela troca profile_turmas direto (antes o CSV
só escrevia na chamada, e re-importar um veterano não fazia nada); para
quem não tem, troca a linha da chamada. As turmas do arquivo
substituem as atuais pessoa a pessoa — quem não está no arquivo não é
tocado, então dá para importar uma turma por vez.encerrarSemestre() esvazia
profile_turmas de todo mundo; depois as planilhas repovoam. Sem esse
passo, quem terminou o curso e não volta em planilha nenhuma ficaria
na turma do semestre passado para sempre, já que a substituição é
pessoa a pessoa. Ficar sem turma é um estado normal e suportado: o
veterano continua com conta, pontos, check-ins e distintivos, só não
pertence a uma turma.suspende_desafios tira o dia da conta de todos os desafios — sem
isso, bastava aparecer no local numa noite sem forró para marcar
presença. Não é automático porque nem todo feriado fecha o espaço (pode
ser justo a noite do Forró na Rep), e a decisão vive no cancelamento,
não no desafio. Como a suspensão é checada sobre o dia em que a janela
abriu, uma janela que vira a noite cai inteira.PAGINA_FEED), com cursor por criado_em — não por
deslocamento, que repetiria ou pularia itens quando alguém postasse
no meio. O gargalo não era o tamanho: era o realtime, que refazia a
consulta inteira a cada curtida de qualquer pessoa, para todo mundo
com o app aberto. Agora a atualização busca só a primeira página e
funde com o que está na tela, o intervalo é de 3 s e nada é buscado
com o app em segundo plano. As fotos já eram loading="lazy" e ficam
em cache-first no service worker por 30 dias.IntersectionObserver, não botão. Uma <div>
vazia (sentinela) fica logo depois do último cartão só enquanto
temMais é verdadeiro; quando ela entra na tela, carregarMais()
busca a próxima página sozinho. rootMargin: '600px' busca ANTES de
chegar no fim de verdade, para a página seguinte já estar pronta
quando a rolagem chegar lá. O observer é recriado (useEffect com
[temMais, carregarMais] nas dependências) sempre que carregarMais
muda de identidade — ele é um useCallback que depende de feed,
então cada nova página resulta num observer novo com o cursor certo.<Link to="/publicacao/:id"> com miniatura — sem
isso, "Fulana reagiu à sua foto" não dizia qual. A rota busca o
check-in avulso (getCheckin, fora da paginação do feed) e reaproveita
o CheckinCard. Como o corpo da notificação já usa um <Link> para
a foto, o avatar por dentro NÃO é outro <Link> para o perfil — <a>
dentro de <a> quebra o HTML; quem quiser o perfil clica no autor já
dentro da publicação. "Dancei com" não referencia uma foto, então
continua sem link, só a resposta de confirmar/recusar.FavoritosGrid também
virou uma grade de <Link to="/publicacao/:id">, no lugar do
visualizador escuro que reimplementava reações, comentários e "tirar
dos favoritos" por conta própria (o CheckinCard já faz as três
coisas — o botão de estrela só aparece quando item.user_id é quem
está logado, então a leitura no perfil de outra pessoa continua sem
ele). Como a página é remontada ao voltar para o perfil, o onMudou
que recarregava a lista manualmente deixou de ter função — favoritosDe
parou de buscar reacoes/comentarios, que também não tinham mais
quem os lesse.criado_em), o aluno não consegue burlar pelo relógio.profile_turmas (aluno ↔ turma + papel na
dança), porque um aluno pode fazer várias turmas com papéis diferentes.
Só a organização escreve nessa tabela (RLS) — é isso que impede alguém de
se colocar no Avançado. Na lista de chamada, o mesmo telefone aparece uma
vez por turma. O vínculo usa o telefone normalizado (últimos 10 dígitos),
então +55 (11) 91234-5678 e 11912345678 casam.src/lib/badges.ts) são
puramente derivados de turmas/check-ins/eventos, sem armazenar nada. Os
personalizados são o oposto — um catálogo (distintivos) que a
organização cria e concede manualmente (distintivos_concedidos), sem
autoconcessão possível (RLS só deixa organizador escrever). Existiu um
distintivo automático de "Campeão(ã) de desafio"; foi substituído pelo
sistema personalizado, que dá mais controle (top 3, top 5, qualquer
motivo) sem exigir 1º lugar exato.