An open API service indexing awesome lists of open source software.

https://github.com/eduardorerick/letmeask

Aplicação que me garantiu uma bolsa de estudos no Ignite! Desenvolvida durante a NLW com outras funcionalidades adicionadas por mim.
https://github.com/eduardorerick/letmeask

firebase react typescript

Last synced: about 1 year ago
JSON representation

Aplicação que me garantiu uma bolsa de estudos no Ignite! Desenvolvida durante a NLW com outras funcionalidades adicionadas por mim.

Awesome Lists containing this project

README

          

![Letmeasklogo](.github/Logo.png)

![Letmeask](.github/letmeaskhome.png)


Visite o site aqui

Tecnologias   |   
Projeto

Dia 1   |   
Dia 2   |   
Dia 3   |   
Dia 4   |   
Dia 5


Minhas alterações no projeto

## ✨ Tecnologias

Esse projeto foi desenvolvido com as seguintes tecnologias:

- [React](https://reactjs.org)
- [TypeScript](https://www.typescriptlang.org/)
- [Firebase](https://firebase.google.com/)

## 💻 Projeto

O letmeask é um app desenvolvido durante a NLW que permite que alguém realizando lives crie uma sala para receber perguntas, tendo maior interação com o usuário.

## Dia 1

Configuração de ambiente


Foi dado inicio ao aplicativo React com yarn create react-app letmeask --template typescript


Tópicos que eu considerei importantes no dia:



  • Introdução ao Typescript

  • Introdução ao Firebase

  • Resumo sobre SPA

  • Benefícios na utilizando de functions no React

  • Introdução a Hooks

## Dia 2

Páginas iniciais e autenticação


Uma bomba de conteúdo! Começamos com uma simples página com HTML e SCSS e então partimos para o início do método de autenticação.


Primeiro fizemos a integração do react-router-dom para navegar pelas páginas, então fizemos o método de login pelo google utilizando o firebase. Com uma função assíncrona async function signInWithGoogle() definimos o provedor como const provider = firebase.auth.GoogleAuthProvider() e definimos o resultado como const result = await auth.signInWithPopup(provider)


Nesse ponto, se o cliente concluir a autenticação já temos um result com várias informações, que permite a gente a criar uma estrutura condicional para o nosso código, então podemos checar se o cliente tem foto, nome.



if (result.user) {
const { displayName, photoURL, uid} = result.user;

if(!displayName || !photoURL) {
throw new Error('Missing information from Google Account.')
};

setUser({
id: uid,
name: displayName,
avatar: photoURL
});
};

Se o usuário não possuir um nome e foto de perfil, a função retornará um erro com uma string, se ele tem todos os dados, a função vai "setar" o estado com os dados novos do usuário.


Para manter os dados do usuário caso ele atualize a página foi utilizado o useEffect, foi usado dentro da função um observador para garantir que o objeto Auth não esteja em um estado intermediário (como inicialização) ao identificar o usuário atual.

Por fim, foi feito uma refatoração do código, todo o AuthContext foi passado para um arquivo TSX próprio, e foi criado também o arquivo UseAuth.js para simplificar o uso de hooks

import { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';

export function useAuth() {
const value = useContext(AuthContext)
return value
};

## Dia 3

Criando novas salas e novas perguntas

Para criar uma nova sala no database do firebase precisamos da função
firebase.database().ref() que retorna uma referência, que é uma localização dentro da database do Firebase.
assim podemos escrever :

const roomRef = database.ref('rooms');

const firebaseRoom = await roomRef.push({
title: newRoom,
authorId: user?.id,
})

Dessa maneira, estamos passando para a database na localização "rooms" um objeto contendo title e authorId que foi passado pelo usuário.

Depois de feito o scss da Sala, foi criado um component RoomCode para apenas pegar o código.


Para ter acesso ao código utilizamos o useParams do react-router-dom e para poder guardar os valores precisamos definir uma constante:



const params = useParams();

porém, precisamos definir quais são os parametros que queremos receber nessa rota.


então definimos:

type RoomParams = {
id: string;
}

então fica:

const params = useParams();

agora a função sabe quais parametros vai receber.

e agora usamos o componente criado e passamos esse parametro como uma prop:

Criamos uma pequena função para o botão de copiar o numero da sala

function copyRoomCodeToClipboard() {
navigator.clipboard.writeText(props.code)
}

O próximo passo é fazer o botão de enviar questões funcionar.


Para isso definidos um novo estado chamado newQuestion

const [newQuestion, setNewQuestion] = useState('')

e importamos também o user que guardamos com a função signInWithGoogle()

agora é só checar se a perguntava enviada tem mesmo algum conteúdo.

if (newQuestion.trim() === "") {
return
}

e verificar se o usuário está logado.

if(!user) {
throw new Error('You must be logged in');
}

se passar por essas condições, definimos um objeto com os dados da nova pergunta e os dados do usuário.

const question = {
content: newQuestion,
author: {
name: user?.name,
avatar: user.avatar,
},
isHighlighted: false,
isAnswered: false
}

isHighlighted e isAnswered com valores booleanos para no futuro termos um controle da interface de acordo com seus valores.

então passamos esse objeto para a database com

await database.ref(`rooms/${roomId}/questions`).push(question)

E para consumir questões da database do Firebase vamos utilizar o hook useEffect(() => {}, []) para buscar no firebase os dados das perguntas.

useEffect(() => {
const roomRef = database.ref(`rooms/${roomId}`);

roomRef.on('value', room => {
console.log(room.val());
})
}, [])

Este evento .on irá disparar uma vez com os dados iniciais armazenados neste local e, em seguida, disparar novamente cada vez que os dados forem alterados.

O console.log(room.val()) vai nos devolver um objeto com authorId:string, questions:object, title:string.

então definimos o tipo de objeto

//Record para tipar objetos, e dentro de <> fica o tipo da chave
type FirebaseQuestions = Record

então definimos a constante:

const firebaseQuestions: FirebaseQuestions = databaseRoom.questions;

e transformamos esse objeto em um vetor com Object.entries();

const parsedQuestions = Object.entries(firebaseQuestions)

dessa forma o objeto {"name": "Eduardo", "cidade": "belém"} vai retornar [["name", "eduardo"], ["cidade","belém"]]

então podemos utilizar o .map(value => {}) tratando o value como um vetor, fazendo uma desestruturação sabendo que o primeiro valor é a chave e o segundo valor é o valor dessa chave. [key, value]

const parsedQuestions = Object.entries(firebaseQuestions).map(([key, value]) => {
return {
id: key,
content: value.content,
author: value.author,
isHighlighted: value.isHighlighted,
isAnswered: value.isAnswered,
}
})

agora que temos um [] que contém um object com as perguntas, precisamos salvar isso em algum estado.

Criamos um para as perguntas e um para o titulo.

const [questions,setQuestions] = useState([])
const [title, setTitle] = useState('')

e definimos o tipo do estado das perguntas:

type Question = {
id:string;
author: {
name: string;
avatar: string;
}
content: string;
isAnswered: boolean;
isHighlighted: boolean;
}

e passamos os valores para os estados ainda dentro de

setTitle(databaseRoom.title)
setQuestions(parsedQuestions)

Agora basta usarmos essas informações na interface.

## Dia 4

Estrutura das perguntas HTML e CSS

Foi feito um componente Question com HTML e CSS para servir como a div que vai conter as perguntas.
Esse componente foi importado para Room.tsx onde foi feito um .map() nele.

{questions.map(question => {
return (

)
})}

Agora todo item contido em questions vai retornar como um componente Question.

Criando o hook useRoom

Criamos uma função chamada useRoom() e agora temos que trazer todas as funcionalidades que vão ser utilizadas tanto na página do usuário quanto na página do admin .
Então pegamos a parte de carregamento das questões

Passamos as funções de useEffect() do arquivo Room.tsx, suas tipagens, os estados:questions e title, e então exportamos dessa função useRoom() apenas as perguntas e os titulos, para que possamos importar de volta no Room.tsx.


return { questions, title }.

Mas para que o firebase consiga localizar aonde queremos fazer a referência no banco de dados é necessário do roomId, que é os pedaços dinâmicos do URL da página que colocamos como placeholder no path, precisamos passar essa rota para o useRoom(), então : useRoom(roomId: string)

Agora quando usarmos o hook na page Room.tsx passamos o roomId, que é o id da pagina no Route que foi inserido pelo handleCreateRoom na page NewRoom.tsx

const { questions, title } = useRoom(roomId)

Feito isso, o código na page Room.tsx já parece muito mais limpo e podemos aproveitar essa funcionalidade na página do admin!

Criamos a page AdminRoom.tsx copiando toda a page Room.tsx, retiramos todo o form e adicionamos o componente Button no header.

No componente Button foi passado um type { isOutlined?: boolean } e nas props da function agora podemos passar ({isOutlined = false, ...props})

Então colocamos uma condicional no className:


className={`button ${isOutlined? 'outlined' : ''}`}


E agora caso isOutlined seja true a classe outlined também é aplicada.

Criando funcionalidade de Like

Depois de feito o CSS do botão do like, é criado na page Room.tsx uma função assíncrona que recebe a questionId e a informação se já foi dado o like ou não.

handleLikeQuestion(questionId:string, likeId: string | undefined) {}

essa função vai fazer o push para a database com o authorId.

Primeiro fazemos uma condição para saber se o usuário já deu o like ou não.

if (likeId) {
await database.ref(`rooms/${roomId}/questions/${questionId}/likes/${likeId}`).remove()
}

Caso retorne false então selecionamos a localização na database.

await database.ref(`rooms/${roomId}/questions/${questionId}/likes`)

e enviamos os dados nessa localização

await database.ref(`rooms/${roomId}/questions/${questionId}/likes`).push({
authorId: user?.id,
})

Para contarmos os números de likes é necessário voltarmos no nosso hook useRoom()

Adicionamos a linha likeCount: Object.values(value.likes ?? {}).length para que a gente receba a quantidade de objetos com o authorId que foi passado anteriormente e o ?? {} serve para caso não tenha nenhum.

E para acompanhar se o usuário deu like ou não precisamos pegar seus dados de autenticação com useAuth()

const { user } = useAuth()

Agora que temos o user.id adicionamos a linha

likeId: Object.entries(value.likes ?? {}).find(([ key , like ]) => like.authorId === user?.id)?.[0]

.find() percorre o array até encontrar uma condição que satisfaça o que passamos para ele, retornando seu conteúdo.

?.[0] retorna nulo caso ele não ache nada na posição 0.

Então pegamos cada um dos like e verificamos se o authorId é igual ao user?.id.

Agora adicionamos cada um no QuestionType informando seus tipos.

type QuestionType = {
id:string;
author: {
name: string;
avatar: string;
}
content: string;
isAnswered: boolean;
isHighlighted: boolean;
likeCount: number;
likeId: string | undefined;
}

E atualizamos também a tipagem no FirebaseQuestions

type FirebaseQuestions = Record
}>

para remover todos os event listener utilizamos

return () => {
roomRef.off('value')
}

E no final adicionamos user?.id no array de dependências, pois essa variável não está sendo definida dentro do useEffect()

Então fica:

useEffect(() => {
const roomRef = database.ref(`rooms/${roomId}`);

roomRef.on('value', room => {
const databaseRoom = room.val();
const firebaseQuestions: FirebaseQuestions = databaseRoom.questions ?? {};

const parsedQuestions = Object.entries(firebaseQuestions).map(([key, value]) => {
return {
id: key,
content: value.content,
author: value.author,
isHighlighted: value.isHighlighted,
isAnswered: value.isAnswered,
likeCount: Object.values(value.likes ?? {}).length,
likeId: Object.entries(value.likes ?? {}).find(([key, like]) => like.authorId === user?.id)?.[0],
}
})

setTitle(databaseRoom.title)
setQuestions(parsedQuestions)
})

return () => {
roomRef.off('value')
}
}, [roomId, user?.id])

Então agora no botão adicionamos uma classe para caso likeId retorne o Id do usuário.

className={`like-button ${question.likeId ? 'liked' : ''}`}

E a função onClick:

onClick={() => handleLikeQuestion(question.id, question.likeId)}

Pronto! a funcionalidade de dar like está completa.

Remoção de pergunta sem o modal

Precisamos criar um botão dentro de que recebe a função handleDeleteQuestion(question.id)
E essa função assíncrona que recebe uma string:

async function handleDeleteQuestion(questionId: string) {
if (window.confirm('Tem certeza que deseja excluir essa pergunta?')) {
await database.ref(`rooms/${roomId}/questions/${questionId}`).remove();
}
}

Se window.confirm() retornar true, ele acha a pergunta com a questionId na .ref() passada e remove a pergunta com .remove()

Para encerrar a sala criamos uma função para fazer o update do objeto no banco de dados para conter a data que a sala foi encerrada e enviamos o usuário para a tela inicial do app, então:

const history = useHistory()

async function handleEndRoom () {
database.ref(`rooms/${roomId}`).update({
endedAt: new Date()
})
history.push('/')
}

E para evitar que pessoas entrem na sala colocamos no handleJoinRoom() do Home.tsx a seguinte condicional :

if (roomRef.val().endedAt) {
alert('Room already closed');
return;
}

Fim do dia 4! Ufa!

## Dia 5

Criação dos botões

Muito HTML e CSS para as criações dos botões handleCheckQuestionAsAnswered e handleHighlightQuestion

Foi atualizado as tipagens do component Questions

type QuestionProps = {
content: string;
author: {
name: string;
avatar: string;
}
children?: ReactNode;
isAnswered?: boolean;
isHighlighted?: boolean;
}

E então exporta esse componente recebendo false como a prop default


export function Question({
content,
author,
isAnswered = false,
isHighlighted = false,
children,
}

E adicionado suas respectivas classes de acordo com o valor desses estados]

className={cx(
'question',
{ answered: isAnswered},
{ highlighted: isHighlighted && !isAnswered}
)}

Hospedando o projeto

O hosting é feito com o próprio hosting do Firebase.

O primeiro passo é instalar o Firebase Tools

npm install -g firebase-tools

E então fazer o login no google

firebase login

Ir para a pasta do projeto e executar este comando no diretório raiz do seu app:

firebase init

E precisamos dizer quais features estamos usando do Firebase, no nosso caso: Realtime Database e Hosting.
Escolhemos usar um projeto já existente e selecionamos o public diretory : build, que é o arquivo que o create-react-app gera os arquivos para produção.
Perguntam se é uma SPA e respondemos que sim.

Agora que temos o firebase.json e os outros arquivos na nossa aplicação estamos prontos para por em produção.

Rodamos a build do projeto

yarn build

e iniciamos o deploy.

firebase deploy

E a aplicação já está funcionando online.


## Minhas alterações no projeto

Criar a página de lista de salas




Primeiro criei um estado para armazenar esses dados.

const [rooms, setRooms] = useState([])


para criar a página de lista de salas, criei o arquivo RoomList.tsx e usei o hook useEffect() para carregar os dados necessários para renderizar a sala.

Peguei a referência do meu banco de dados.

const dbRef = database.ref(`rooms`);

Então li todos os dados e retornei eles em um array contendo vários objetos.

dbRef.once('value', rooms => {
const dbRoom: object = rooms.val() ?? {}
const parsedRooms = Object.entries(dbRoom).map(([key,value]) => {
return {
roomId: key,
title: value.title,
roomIsOpen: value.roomIsOpen
}
})
setRooms(parsedRooms)
})

Fiz as devidas tipagens de como eu queria esse objeto dentro do array.

type RoomType = {
roomId:string;
title: string;
roomIsOpen?: boolean;
}[]

Agora só preciso utilizar .map() para me retornar as salas no formato que eu quero, porém, também quero mostrar algo caso não tenha nenhuma sala disponível.

Então crio a seguinte condicional:

{rooms.length !== 0 ?
rooms.map((item: any) => {
return(

handleGoToRoom(item.roomId, item?.roomIsOpen)}
key={item.roomId} >
{item.title}
)})
: (

Não temos salas no momento


Empty Room
)}

Agora, se rooms contém algum resultado vai aparecer:

![Room List](.github/roomlist.png)




E se não tiver resultado:


![Empty Room List](.github/emptyroomlist.png)


Adicionando animações CSS

Foi adicionado utilizando o site [Animista](https://animista.net/)
![screen-capture](https://user-images.githubusercontent.com/82004348/123522184-2dfc2280-d692-11eb-90b9-81c42fa115a2.gif)