Visite o site aqui
Tecnologias |
Projeto
Dia 1 |
Dia 2 |
Dia 3 |
Dia 4 |
Dia 5
Minhas alterações no projeto
Esse projeto foi desenvolvido com as seguintes tecnologias:
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.
Foi dado inicio ao aplicativo React com create-react-app
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
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
};
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<RoomParams>();
agora a função sabe quais parametros vai receber.
e agora usamos o componente criado e passamos esse parametro como uma prop:
<RoomCode code={params.id} />
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<string, {
author: {
name: string;
avatar: string;
}
content: string;
isAnswered: boolean;
isHighlighted: boolean;
}>
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<Question[]>([])
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 <codeuseEffect()
setTitle(databaseRoom.title)
setQuestions(parsedQuestions)
Agora basta usarmos essas informações na interface.
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 (
<Question
key={question.id}
content={question.content}
author={question.author}
/>
)
})}
Agora todo item contido em questions vai retornar como um componente Question
.
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.
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<string, {
author: {
name: string;
avatar: string;
}
content: string;
isAnswered: boolean;
isHighlighted: boolean;
likes: Record<string, {
authorId:string;
}>
}>
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.
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!
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}
)}
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.
Primeiro criei um estado para armazenar esses dados.
const [rooms, setRooms] = useState<RoomType>([])
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(
<div
className={`room-item-div ${item.roomIsOpen? '': 'closed'}`}
onClick={() => handleGoToRoom(item.roomId, item?.roomIsOpen)}
key={item.roomId} >
{item.title}
</div>)})
: (
<div className="empty-list">
<h1>Não temos salas no momento</h1>
<img src={emptyImg} alt="Empty Room" />
</div>)}
Agora, se rooms contém algum resultado vai aparecer:
E se não tiver resultado:
Foi adicionado utilizando o site Animista