#10 Upload de Imagens no Supabase Storage com Angular em 5 Minutos | [Série] Criando SaaS do Zero

Samuel Rizon shows how to upload and download images with Supabase Storage in an Angular SaaS app. He covers file path generation with UUID, storing avatars in a bucket, removing old files, and a custom Angular pipe that converts storage URLs to base64 with icon fallback. The flow integrates into DynamicFormComponent so uploads update the dashboard in real time.
Chapters
E aí, meus amigos, tudo bem com vocês? Hoje eu vou te ensinar como fazer o upload e usar o Supabase Storage. No vídeo passado eu falei que a gente ia trabalhar com storage, com imagens. Então aqui, ó, eu já coloquei uma imagem e quando a gente vai para página pública a gente carrega essa imagem. E eu vou te mostrar como fazer isso de forma simples, prática e rápida. Eu sou Samuel Rizon e por aqui eu falo sobre tecnologia, carreira SaaS, e esse é o 10º vídeo da nossa série Criando um SaaS do Zero Só com Angular Firebase. Deixa o seu like, se inscreva aqui no canal e ative o sininho para você não perder nenhum dos próximos vídeos. Beleza? Antes de mais nada, deixa eu só repassar com vocês qual que é a ideia do vídeo de hoje. No vídeo passado eu tinha falado, né, que a gente ia fazer o deploy, o dpwa, e depois fazer os últimos ajustes. Só que eu preferi fazer esses ajustes agora, já fazer o upload da imagem, e no próximo vídeo da série a gente já transformar o site em dpwa, fazer o deploy no Firebase e configurar o domínio e DNS. Então Hoje eu vou mostrar alguns ajustes finais que eu fiz, algumas correções de bugs, e vou mostrar para vocês como que eu fiz o upload de imagem bem simples e bem rápido. Bom, o sistema que já tá funcionando, e eu vou só mostrar para vocês os ajustes que eu fiz, tá bom? Eu fiz um commit aqui, eu vou só repassar com vocês. Eu dei um cloud init aqui no projeto para o Cloud ter uma noção do que que é o projeto, tá bom? Configurei um MCP aqui do Playwright pra gente utilizar, eu colocar o Cloud para abrir esse MCP, abrir o Playwright, abrir o navegador e navegar e me ajudar a corrigir algumas coisas. Aqui no package.json eu adicionei esse host aqui, ó. Se você não sabe, com esse host você consegue testar suas alterações no seu celular. Então quando você coloca isso aqui, ele vai te dar o seu IP, você coloca o IP, você vai pelo celular mesmo, coloca o IP da sua máquina que ele vai mostrar no terminal, e depois coloca a porta e você já vai conseguir acessar e testar o site pelo seu celular. Esse aqui é bem simples de fazer no Angular, tá? Aqui algumas configurações, mas vamos lá para as configurações que a gente fez aqui. Então eu removi o CompanyService, eu vou mostrar para vocês depois porque que eu removi isso. Eu deixei o CompanyService load no AuthService, então assim que ele faz o login ele já carrega os dados da empresa do usuário, tá bom? No app routes eu coloquei um guarda de rota no dashboard se o usuário se não tiver logado, ele redirecionar para autenticação. Então foi um guarda que eu criei aqui que pega o Office Service. Se não tiver logado, ele redireciona o usuário para o Auth. No Shell Layout eu coloquei um header, tá? Aqui não tem muito segredo, foi um header bem simples. Coloquei essa opção de Show Header aqui porque se o usuário tiver, né, se for um usuário final, ele não pode ver o header. Então aqui eu já tiro esse header dessa página. Caso o usuário não esteja logado, ele não vai ver esse header. Então aqui o header vai ter o nome da empresa, uma logo fictícia, um ícone de tesouro que eu coloquei aqui por enquanto, mas seria o logo do aplicativo. E aqui um usuário com opção de editar perfil, que não tá funcionando ainda, talvez a gente pode fazer isso aqui depois, e uma opção de sair. Então é assim que a gente faz isso no NZorro. A gente tem um NZ dropdown, o NZ user menu, uma lista aqui, ó, o L e LI. Bem simples, bem prático. Quando a gente, quando ele clica no single out aqui, ó, ele já chama auth_service.push_and_redirect. Então ele desloga o usuário do Supabase, seta as nossas variáveis para nulo e redireciona para o auth. Então aqui foi o shell layout, aqui eu adicionei algumas opções aqui também para parte do menu de CSS. Na página de login eu coloquei o auth_service e logo depois que ele logar eu dou um load no auth_service e redireciono o usuário para dashboard. Então isso aqui foram pequenos ajustes que eu fui testando e fui corrigindo. Corrigindo tanto manualmente quanto com o Claudio me ajudando em algumas coisas. Aqui no offset, olha que legal, eu coloquei o CompanyService e aí eu peguei os dados do usuário, setei usuário, e depois no CompanyService eu dou um load, eu dou um load aqui, eu passo usuário para ele. E aí que eu fiz uma alteração que a gente tinha feito antes, alteração que tava antes, tava carregando só as empresas, era só para teste, né? Mas olha aqui o que que a gente fez: se não tiver um usuário, a gente seta para nulo setCompanies para um array vazio. Mas agora a gente, que que a gente faz? A gente vai na tabela de userCompany, a gente seleciona todas as empresas desse usuário, porque como eu falei, nesse caso um usuário pode ter mais de uma empresa. Então eu vou pegar a empresa que tá relacionada ao meu usuário, que está ativa, e vou dar uma ordenação para essa empresa. Depois que eu pego essa empresa, eu dou só uma tipagem para ela aqui, que o Supabase não vai dar essa tipagem. Filtro por empresas, para as empresas que existe, dou um map nela. Então aqui eu vou ter todas as empresas, eu seto no companies todas as empresas e seleciona a primeira empresa. Então assim que a gente carregar o sistema aqui no config, aqui no app initializer, a gente vai download. Então ele vai carregar os dados do usuário, setar usuário para nossa aplicação e carregar empresa desse usuário. É bem simples, bem prático. E bem fácil. A mesma coisa quando a gente faz um login. Então, o Office Service, eu fiz algumas alterações aqui, por exemplo, redirect. Eu criei aqui também, ó, na dashboard, eu criei um modal. Olha que legal, eu criei um modal para compartilhar. Então aqui, ó, compartilhar a página da barbearia. Eu passo o QR code, eu passo o link direto, né? A pessoa pode tanto selecionar o link direto quanto o QR code. E aí o usuário pode selecionar, o barbeiro pode imprimir esse QR code, colocar na barbearia dele. Então, para isso eu criei esse componente aqui, ó, o modal. Ele vai ter um nz-flex, vai ter um QR code aqui. O ng-zorro já tem um componente de QR code, você só passa o valor para ele, passa o tamanho, ele já gera o QR code para você, você não precisa fazer mais nada. Um divider e aqui um link direto. E uma coisa bem legal que o ng-zorro tem é isso aqui, ó, é um input group. A gente tá até trabalhando, ele tem uma issue até aberta lá no ZardUI para a gente ter esse input group. Isso não tem no ChatCN, mas é uma coisa que o ng-zorro tem que eu acho fantástico. Que você consegue colocar um input e agrupar um input com um botão. Então é bem simples, você coloca um input group aqui, um addon after e um template, né, que é um botão. Esse nz-search aqui eu acho que a gente pode tirar. Deixa eu fazer um teste aqui só para a gente ver. Não, ela não pode não. É uma forma de deixar ele com botão juntinho, tá? Então para ficar junto deixa esse nz-search aqui mesmo. Mas é um input normal, um input normal com template, com um botão no final aqui, tá bom? E aí que tem o share URL, eu pego os dados do modal aqui, do ID do StoryFront, né, para mim colocar o share URL. Copy, caso ele for copiar, eu uso o próprio navigator.clipboard aqui. E o QR code, ele vem do share URL também, ele já cria esse share URL. Como que a gente faz isso? Lá na dashboard eu criei um nz-flex aqui para ele, coloquei um botão de compartilhar, o de ver página publicada já tinha. E aqui, ó, no meu dashboard eu chamo ele, ó, showShareModal. Então eu pego os dados da minha loja e dou um modalService.create, eu passo esse componente que eu tenho. No nz-data eu passo o ID da minha loja e eu dou um footer null aqui para ele não colocar os botões aqui. Gente, NG-Zorro é lindo! Eu falo com vocês, vocês não acreditam, mas é muito mais simples do que vocês estão acostumados. Acho que eu fiz mais algumas alterações aqui de correção de bug, de deadline e coisas que estavam acontecendo. Isso aqui é porque ele não tava atualizando corretamente, né? Tinha esse timeout aqui que não precisava mais. Ele não tava atualizando corretamente algumas coisas e eu corrigi aqui na dashboard. Deixa eu fechar aqui. Aqui no Store Front Page eu adicionei algumas coisas. É que ele tá assim porque eu acabei removendo o NZ Card dele, mas eu coloquei a foto da barbearia aqui em cima. Então se Store Front Data tiver foto, eu coloco um NZ Avatar. E aí que eu vou depois mostrar para vocês isso aqui no Store Front. Eu atualizo aqui, pego algumas coisas, iniciar lá Store Front. Aqui é quando eu pego os dados do StoreFront para eu pegar da minha rota, para eu pegar os dados da minha rota StoreFront API. Aqui eu pego os dados da company, o company service que eu fiz a minha atualização e eu já repassei isso com vocês. Eu adicionei alguns detalhes aqui, ó, no nosso baseless, um border radius. Eu aumentei um pouco o border radius dos botões e dos, de todo o design system do NG-Zorro. E eu adicionei esse código aqui customizado porque o DropDown do NG-Zorro fica muito bonitinho, não fica muito visual. Então com esse código customizado aqui ele já dá uma margem maior e já deixa esse dropdown aqui de uma forma mais visual. Beleza, meus amigos, deixa eu mostrar para vocês então a parte de upload e download dos arquivos. Uma coisa importante, a gente já tinha adicionado alguns arquivos que a gente tem aqui, ó, que é o storage.api deixa eu fechar tudo aqui para mostrar para vocês, e o download storage pipe. Então aqui dá uma olhada, isso aqui a gente já tinha quando a gente criou o nosso formulário dinâmico, que eu precisava adicionar esses arquivos porque o formulário já trabalhava com o upload de imagens. Mas olha que legal, eu tenho storage API, quando eu dou um insert eu pego o meu file path, que é o caminho do arquivo, né, do meu storage lá do Supabase, eu dou um generate path aqui, ó. Aqui no caso eu só gero um path dinâmico. Você pode gerar o UUID ou você pode usar o próprio nome do arquivo, tá bom? Isso aqui é só uma forma da gente gerar um UUID dinâmico para a gente. Nesse caso aqui a gente não precisa salvar o nome do arquivo, mas um file path. E aí eu dou um generate nele. E aí lá no Supabase é só isso aqui, gente: supabase.storage.from_bucket. Esse bucket a gente tem um enum no qual, no nosso caso, no nosso caso a gente só tem um avatar. E deixa eu ver aqui, eu quis from bucket upload, o file path, que é o caminho do arquivo lá no storage, e o file. Esse file aqui ele vai vir do meu DynamicFormComponent. No meu DynamicFormComponent eu tenho aqui um tipo de avatar, exatamente isso aqui. Então aqui eu tenho um tipo de avatar, é um nz-avatar do ng-zorro, e ele tem um nz-upload, e ele tem um before-upload-avatar, que ele vai pegar isso aqui, setar esse valor no meu formulário. Porque quando eu tiver aqui no meu dashboard, deixa eu mostrar para vocês, no meu dashboard, no form-values.foto, ele vai estar o arquivo. Então quando a gente fizer o upload de uma imagem, ele vai setar o meu dynamic form, ele vai setar esse valor desse arquivo, que é o file, que vem do nz-upload-file, ele vai setar isso para o nosso formulário. E aí aqui, ó, gente, olha a coisa mais simples do mundo. Se a foto for diferente da foto do nosso objeto do StoreFront, o que que eu faço? Eu chamo meu Storage API dando um await nele, insert, passando bucket name, o valor formValues foto e o StoreFront photo. No caso aqui ele pede, né, o file path. Esse storefront photo aqui, ó, é para o caso de já existir uma foto. O que que eu vou fazer? Se eu tiver um file path aqui, ó, eu já vou deletar ela. Então eu deleto uma foto que já existe e crio uma nova para esse usuário, tá bom? Então é só isso. Mas se não for diferente, eu vou só pegar do storefront photo aqui e vou atualizar o meu valor. Então, meus amigos, dá uma olhada, com uma linha a gente já tinha, né, essas outras coisas aqui, o API, a gente já tinha o download storage pipe, que esse aqui é a forma de fazer o download que eu vou mostrar para vocês daqui a pouco. Mas com uma só linha eu já tô conseguindo fazer o upload dessa imagem, e isso aqui já vai funcionar e já tá funcionando. Então quando eu venho aqui, por exemplo, e clico aqui seleciono uma imagem, eu selecionei a imagem, e aí eu vou vir aqui mostrar para vocês. Deixa eu mostrar aqui, eu vou no dashboard e eu vou mostrar aqui, ó. Então, salvar, ó, o form_values.foto, ele tem um tipo file, o meu arquivo, tá? E aí que ele vai verificar, é diferente? É. Então vou dar um F9, ele vai vir aqui dentro do insert. Eu vou entrar aqui, ó, eu já tenho um file salvo, então ele vai deletar, ele vai chamar o delete, que é o storage from bucket remove path, vai deletar, vai gerar um novo ID. Depois que ele gerou um novo ID, ele vai chamar lá no bucket de avatar, fazer o upload e retornar o file path desse novo arquivo. Então é esse valor aqui que eu vou salvar na minha coluna foto. E aí ele vai retornar esse valor. Dá um F8 aqui, ó, o valor da foto tem esse exato valor aqui, que é o caminho do meu arquivo lá no storage do Supabase. Eu vou dar um F8 aqui. As alterações foram salvas com sucesso. Se eu vier aqui no storage avatars, eu vou ter o meu avatar aqui. Esse aqui foi um teste que eu fiz aqui, ó, o Barber Kings. Perfeito, tá aqui esse arquivo. Tá aqui. E na minha tabela, quando eu venho aqui na tabela de storefronts, deixa eu achar o nosso aqui, ó, Burger King's foto. Olha lá, é exatamente o caminho. Então é assim que a gente faz o upload de um arquivo, de uma foto, de qualquer coisa desse tipo para o Supabase Storage. E para a gente fazer o download é muito simples, gente. Eu tenho esse downloadStoragePipe aqui, que é um pipe do Angular. O que que ele faz? Ele pega aqui, ó, o image URL, que é o path da minha imagem, do meu arquivo. Eu, o bucket aqui do Avatar, eu já deixo ele como default. Então, ó, se não tiver, retorna. Se for uma instância de arquivo, ele vai dar um blob to base64 aqui. Se a imagem começar com HTTP, ele retorna a imagem. Então, por exemplo, se for uma imagem que é um HTTP, que já é uma imagem fixa, né, então eu já retorno já essa imagem aqui para ele. Já fazer isso. Se for um arquivo mesmo, ele retorna, mas se não, eu vou lá, ó, storageApi.download, e aí eu passo o meu bucket e o meu path. É só isso, pessoal. Depois que eu pego esse data aqui, eu dou um blob to base64. E aí, com isso aqui, esse resolver aqui, ele já vai funcionar. Então, no nosso storefront page, eu já fiz isso. Então, aqui, ó, no nzsrc, eu passo storefront_data_photo, passo nosso pipe download_storage, eu não preciso passar o nosso bucket porque ele já vai pegar como default o avatar, eu passo um pipe async aqui porque é uma promise, né? Aqui eu preciso fazer isso aqui porque ele não aceita o valor nulo, tá bom? Então eu só coloco ele entre parênteses e coloco isso aqui. Don't initialize sem shape circle. E um nz-icon-user, caso ele não consiga carregar, ele vai dar um fallback no ícone de usuário aqui. E é só isso, meus amigos, é assim que a gente já faz o download da nossa foto. A gente só precisa da URL lá do Supabase Storage, a gente passa esse pipe, e quando eu venho aqui no nosso front-end e na nossa página pública, ó, viu que ele carregou o fallback do usuário, do ícone do usuário primeiro? Então ele carrega esse fallback Depois ele carrega essa imagem. Ele carregou aqui um pouquinho mais lento primeiro, mas quando eu dou um F5, o Supabase já traz isso de uma forma muito mais rápida pra gente. Beleza? Olha que simples, olha que fácil. E aqui no nosso Dynamic Form aqui, ó, a gente também já tem isso de forma automática. Então no meu Dynamic Form Component eu já tenho aqui, ó, o NZSRC com avatar, com download storage assim que, que, e aí a única coisa que eu precisei fazer foi aqui dentro do meu objeto, né, do StoreFrontConfig, eu passar o initial value do StoreFrontFoto. E só com isso ele já carregou a foto do usuário aqui quando ele é carregado. Se não tiver nenhuma foto, ele vai ficar com fallback com ícone do usuário. E assim que eu faço o upload da foto, ele já atualiza a foto aqui. E aí quando eu salvo as alterações, ele já faz as alterações aqui para mim em tempo real. E a gente já tá praticamente com o nosso SaaS funcionando aqui, ó, tá sem fila nenhuma no momento. Eu vou adicionar um +10 aqui, mais 30 minutos, e a gente já tem uma horinha de fila. Vou diminuir um pouco aqui na prévia, eu vou visualizar. E aí o que eu adicionei foi isso aqui, ó. Quando a gente vem aqui, copia esse link, eu vou abrir isso aqui numa guia anônima. Então aqui, ó, tá aberto numa guia anônima, por isso que ele tá dessa cor. Mas o reader não tá aparecendo porque meu usuário não tá logado. Show! Agora eu expliquei a tela aqui no meio. A gente já tem aqui tudo funcionando. Se eu vier aqui e atualizar, né, por exemplo, os valores daqui, salvar as configurações, ele já pega em tempo real essas configurações aqui também. E ele pega em tempo real também o nosso timer. Já tá funcionando. Então se eu vier como usuário normal e acessar o ID da minha loja, então o Barbeiro vai compartilhar esse link aqui comigo, vai colocar no WhatsApp dele ou deixar um QR code na loja dele, eu acesso E eu já tenho a minha loja aqui funcionando. Eu já consigo saber se ela tá aberta, se ela tá fechada em tempo real. E o nosso SaaS já tá quase pronto pra gente lançar. E no próximo vídeo a gente vai fazer já o deploy disso aqui pro Firebase e vocês vão ver que a gente já vai ter uma URL pronta pra gente testar e já vai tá funcionando. Vou tentar mostrar pra vocês a parte no celular também. O PWA, a gente vai fazer um PWA, vou mostrar pra vocês como que instala. E aí, o que que você achou? Achou fácil? Fazia parte de upload para o Supabase. E lembrando, se você quiser ter acesso a esse código, entra na comunidade do canal, é o primeiro link aqui na descrição, e eu deixo normalmente no comentário também do vídeo. Entra lá na comunidade, pede o link lá do GitHub que a gente compartilha lá, o link do GitHub com todas as atualizações e com código completo desse projeto. E se você ficou até aqui, deixa um comentário aqui embaixo como que você normalmente fazia isso, ou se você tinha dificuldade. Deixa o seu like, se inscreve no canal, e até o próximo vídeo. Valeu!










