SOS Telha
Pro-bono

SOS Telha

Plataforma comunitária que liga quem tem telhas (oferta) a quem precisa (procura) após a tempestade Kristin em Leiria e Marinha Grande, Portugal.

Abrir a app
Next.jsSupabaseTailwindshadcn/uiLeafletVercel

16 de fevereiro de 2025

SOS Telha: Rede de Apoio à Reconstrução

O Que É

O SOS Telha é uma rede de apoio à reconstrução desenvolvida pro-bono pela KORVO em resposta à devastação causada pela Tempestade Kristin no distrito de Leiria e Marinha Grande, Portugal.

Os utilizadores publicam Oferta (tenho telhas) ou Procura (preciso de telhas), com opção de mão de obra (preciso de ajuda para colocar telhas). Tudo é mostrado num mapa interativo para que as pessoas vejam o que está perto e evitem deslocações desnecessárias.

Mapa e lista do SOS TelhaMapa e lista do SOS Telha

Vista mobile de criar pedido

Vista mobile de criar pedido — formulárioVista mobile de criar pedido — formulário

Parte 2 — seleção de localização estilo UberParte 2 — seleção de localização estilo Uber

Para Quem

Residentes e empresas na área de Leiria/Marinha Grande afetados pela Tempestade Kristin que tenham telhas sobradas para dar ou precisem de telhas ou mão de obra para reconstruir.

Funcionalidades

  • Mapa interativo com clustering — ver oferta e procura perto de si
  • Vista em lista com filtros: tipo de telha, logística, "só os meus posts"
  • Ordenação: mais recentes primeiro ou mais perto de mim
  • Banner de segurança a avisar que trabalhos em telhado são perigosos e não devem ser feitos sozinhos
  • Área de administração para moderar posts e feedback
  • Alertas por Telegram e Discord quando utilizadores reportam posts suspeitos — agir imediatamente a partir do alerta (ex.: apagar o post) ou ir ao painel de administração

Por Que Importa

Após uma calamidade, fazer corresponder oferta e procura localmente acelera a reconstrução e reduz desperdício. O mapa e os filtros facilitam encontrar ou oferecer ajuda perto de si.

Como Foi Construído

  • Next.js (App Router), Supabase (auth + dados), Tailwind CSS, shadcn/ui
  • Leaflet para o mapa com clustering
  • React Hook Form + Zod para formulários
  • Modo escuro opcional
  • Deploy na Vercel com analytics

Por dentro

Os posts de telha são carregados através de uma server action do Next.js: "use server" faz correr a função no servidor, consultamos o Supabase pelos registos mais recentes e mapeamo-los para o tipo TilePost. Um ficheiro que mostra o fluxo completo da base de dados à UI.

// app/actions/tile-posts.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { rowToTilePost } from "@/lib/tile-posts";
import type { TilePost } from "@/lib/types";

export async function getTilePosts(): Promise<TilePost[]> {
  const supabase = await createClient();
  const { data, error } = await supabase
    .from("tile_posts")
    .select(
      "id, tile_type, quantity, tile_type_other, tile_items, lat, lng, " +
      "post_type, contact, status, logistics, created_at, description, needs_mao_de_obra"
    )
    .order("created_at", { ascending: false });

  if (error) {
    console.error("getTilePosts error:", error);
    return [];
  }

  return (data ?? []).map(rowToTilePost);
}

Publicar sem conta

Não quisemos obrigar ninguém a criar conta para publicar ou editar. Por isso usamos um sistema de creator token em cookie: sem login, mas sabemos quais posts pertencem a qual browser.

Quando alguém publica um post, geramos um segredo aleatório (ex. um UUID), guardamo-lo na linha na base de dados e guardamos o mapeamento id do post → token num cookie httpOnly. Só o servidor o consegue ler, por isso o browser não pode ser enganado para o enviar para outro site. Sem emails nem passwords—apenas “estes post IDs foram criados neste browser.”

Quando voltam para marcar um post como concluído, atualizar quantidade ou apagar, o servidor lê o cookie, procura o token desse post e compara com creator_token na base de dados. Se coincidir, permitimos a atualização (usando o client service-role, porque o RLS bloqueia updates anónimos). Assim as pessoas podem publicar e gerir os seus anúncios sem conta; só o browser que criou o post pode alterá-lo ou apagá-lo.

Guardar o token ao criar:

// Ao criar: um token secreto por post, guardado na DB e num cookie httpOnly
const token = crypto.randomUUID();
await supabase.from("tile_posts").insert({
  // ...tile_type, quantity, lat, lng, etc.
  creator_token: token,
}).select("id").single();
const existing = await getCreatorTokensCookie();
await setCreatorTokensCookie({
  ...existing,
  [id]: token, // postId → token para este browser poder editar este post depois
});

Verificar antes de permitir editar ou apagar:

// Antes de qualquer update/delete: token no cookie para este post === creator_token na DB
const cookieTokens = await getCreatorTokensCookie();
const token = cookieTokens[postId];
if (!token) {
  return { success: false, error: "Só o autor pode marcar como concluído." };
}
const supabase = await createClient();
const { data: post } = await supabase
  .from("tile_posts")
  .select("creator_token")
  .eq("id", postId)
  .single();
if (!post || post.creator_token !== token) {
  return { success: false, error: "Só o autor pode marcar como concluído." };
}
// Verificado: mesmo browser que criou o post. Usar client admin para mutar (RLS bloqueia updates anónimos).
const admin = createAdminClient();
await admin.from("tile_posts").update({ status: "claimed" }).eq("id", postId);

Desenvolvido pela KORVO como projeto de paixão para ligar comunidades e apoiar a reconstrução após a Tempestade Kristin.

Abrir a app
                                                                                                      
                                                                        ░░▒▒▓▓▓▓▓▓▒▒                  
                                                                  ▒▒▓▓▒▒▓▓▓▓▓▓▒▒▒▒▓▓▓▓▓▓              
                                                    ░░▒▒▓▓▒▒▒▒▒▒▓▓▓▓▓▓████████▓▓▒▒▓▓████░░            
                                                ░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓██████▓▓▓▓██▓▓▒▒▓▓▓▓████░░          
                                                ░░░░░░▒▒▒▒▒▒▓▓▓▓▓▓████████▓▓██▓▓▒▒▓▓▓▓██████▒▒        
                                          ░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒▒▓▓████▓▓▓▓▓▓▓▓▓▓▒▒▓▓████████        
                                          ░░▒▒▒▒▓▓▓▓▓▓▒▒▒▒▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓████▓▓▒▒▓▓▓▓██████▓▓      
                                              ░░▒▒▓▓▓▓▓▓▒▒▒▒▒▒▓▓▓▓▓▓▓▓████████████▒▒▓▓▒▒▓▓██████      
                                                            ▒▒▓▓▓▓▓▓████▓▓██████▓▓▒▒▒▒▓▓▓▓██████▒▒    
                                                              ▓▓████████▓▓██████▓▓▓▓▒▒▓▓▓▓████████    
                                                              ▓▓████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████░░  
                                                              ▓▓▓▓████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████▒▒  
                                                            ▓▓▓▓▓▓▓▓██▓▓████████▓▓▓▓▓▓▒▒▓▓▓▓▓▓████▓▓  
                                                        ▒▒▓▓▓▓▓▓▒▒▓▓▓▓▓▓████▓▓████▓▓▓▓▓▓▒▒▓▓▓▓▓▓██▓▓  
                                                      ▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓██▓▓▓▓██▓▓▓▓▓▓▓▓▓▓██████  
                                                    ██████▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████  
                                                ▓▓▓▓██████▓▓▓▓▒▒▓▓▒▒▒▒▓▓▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████  
                                            ░░████▓▓▓▓████▓▓▓▓▓▓▒▒▒▒▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓██████░░
                                          ░░██▓▓▓▓▓▓▒▒▓▓██▓▓▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓██████▓▓
                                          ██████▓▓▓▓▓▓▒▒▓▓▒▒▓▓▓▓▒▒▓▓▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▓▓▒▒▒▒▓▓▓▓▓▓██████
                                        ████▓▓████▓▓▓▓▓▓▓▓▓▓██▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▒▒▒▒▓▓██████
                                      ░░██▓▓████▓▓▓▓████▓▓██▓▓████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████
                                      ██▓▓▓▓▓▓▓▓▓▓██████████▓▓████████████████████▓▓████████▓▓████████
                                    ░░▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████
                                    ▓▓▓▓▓▓▓▓██████████████████████████████████████████████████████████
                                    ▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████▒▒
                                  ▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████▓▓  
                                  ██▓▓▓▓████████████████████████████████████████████████████████████░░
                                ▒▒▓▓▓▓██████████████████████████████████████████████████████████████  
                                ██▓▓▓▓██▓▓██████████████████████████████████████████████████████████  
                              ▒▒▓▓████████████████████████████████████████████████████████████████▒▒  
                              ▓▓▓▓▓▓▓▓▓▓██████████████████████████████████████████████████████████    
                            ▓▓▓▓▓▓██▓▓▓▓██▓▓██████████████████████████████████████████████████████    
                          ▒▒▓▓▓▓▓▓▓▓████▓▓██████████████████████████████████████████████████████      
                          ▓▓▓▓▓▓▓▓██████▓▓██████████████████████████████████████████████████████      
                        ▓▓▓▓▓▓▓▓▓▓██████▓▓██████████████████████████████████████████████████▓▓██      
                      ░░▓▓▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████████▒▒      
                      ░░▓▓▓▓██▓▓██████████████████████████████████████████████████████████▓▓██        
                      ▒▒▓▓██▓▓████████████████████████████████████████████████████████████████        
                      ▓▓██▓▓▓▓▓▓████████████████████████████████████████████████████████▓▓████        
                    ██▓▓▓▓████████████████▓▓▓▓██████████████████████████████████████████████▓▓        
                  ░░▓▓▓▓▓▓▓▓██████████████▓▓██████████████████████████████████████████▓▓████░░        
                  ██▓▓████████████████████████████████████████████████████████████████▓▓████          
                ▒▒▓▓██████████████████████████████████████████████████████████████████████▓▓          
                ██▓▓████████████████████████████████████████████████████████████████▓▓▓▓▓▓▒▒          
              ▒▒▓▓██████████████████████████████████████████████████████████████▓▓▒▒  ▓▓▓▓░░          
              ▓▓██████████████████████████████████████████████████████████████░░  ▓▓  ██▓▓            
            ░░██████████████████████████████████████████████████████████▓▓        ▒▒  ▓▓██            
            ▓▓████████████████████████████████████████████▓▓▓▓  ▓▓██                  ▓▓░░            
          ░░██████████████████████████████████████▓▓▓▓▓▓░░      ▒▒██                  ▓▓              
          ████████████████████████████████████▓▓▓▓▓▓▒▒░░          ██░░                                
        ▓▓████████████████████████████████▓▓▓▓▒▒▒▒▓▓▓▓            ▓▓░░                                
      ▓▓██████████████████████████████████░░░░░░  ▓▓░░      ▒▒    ░░▓▓░░                              
    ████████▓▓██████████████████████████          ▓▓██        ▓▓    ▓▓▒▒                              
  ▒▒██████████████████████████████████            ░░██░░      ██░░  ▓▓▒▒                              
  ████████████████████████████  ████          ▒▒  ░░▓▓▓▓      ▒▒▓▓░░▒▒▓▓▓▓                            
▓▓██████▓▓██████████████████  ░░▓▓░░            ▓▓▓▓▓▓▒▒▒▒    ░░░░▒▒▒▒▒▒▓▓                            
  ▓▓▓▓██████████████████▒▒                      ░░▓▓▒▒▒▒▓▓▓▓░░  ░░░░░░▓▓▒▒▓▓                          
          ░░▒▒██████▒▒                              ▓▓▒▒▒▒▓▓▒▒▒▒░░░░░░▓▓▓▓▓▓▒▒                        
              ▓▓▓▓                                  ░░░░░░▒▒▒▒▒▒░░▒▒░░░░▒▒▓▓▓▓▒▒░░                    
                                                            ▒▒▒▒░░░░░░  ▒▒▒▒▒▒▒▒░░▒▒                  
                                                              ▒▒░░░░▒▒        ░░▒▒▒▒░░                

Pronto para começar o próximo sprint?

Contacto

pedro@korvo.tech

+351 914 808 798

Leiria, Portugal

Social

  • LinkedIn
  • Instagram
  • X

Legal

  • Política de Privacidade
  • Termos e Condições