Project Templates
Social Network
Feed e Interações para Sua Rede Social
39 min
introdução neste tutorial, você aprenderá como construir um feed de rede social com recursos interativos, como postagens, comentários e curtidas, usando o back4app como seu serviço de backend esses recursos formam o núcleo de qualquer aplicativo de rede social, permitindo que os usuários compartilhem conteúdo e interajam com outros usuários o back4app é uma plataforma de backend as a service (baas) construída sobre o parse server que fornece aos desenvolvedores uma infraestrutura robusta para criar aplicativos escaláveis sem gerenciar código do lado do servidor suas capacidades de banco de dados em tempo real e gerenciamento de usuários integrado fazem dele uma excelente escolha para desenvolver aplicativos de redes sociais ao final deste tutorial, você terá implementado um feed social totalmente funcional que permite aos usuários criar postagens com texto e imagens ver postagens de outros usuários em um feed curtir postagens comentar em postagens ver detalhes da postagem com comentários você aprenderá como estruturar eficientemente suas classes de banco de dados, implementar atualizações em tempo real e criar uma interface de usuário responsiva que proporciona uma experiência contínua em diferentes dispositivos pré requisitos para completar este tutorial, você precisará de uma conta back4app você pode se inscrever para uma conta gratuita em back4app com https //www back4app com um projeto back4app configurado você pode aprender a criar um novo projeto seguindo nosso guia de introdução ao back4app https //www back4app com/docs/get started/welcome node js instalado em sua máquina local conhecimento básico de javascript e react js um sistema de autenticação funcional se você ainda não configurou um, siga nosso sistema de autenticação para redes sociais tutorial primeiro familiaridade com conceitos modernos de desenvolvimento web (componentes, gerenciamento de estado, etc ) projeto back4gram encontre aqui o código completo para um projeto de amostra de rede social construído com back4app passo 1 – compreendendo o modelo de dados antes de escrever qualquer código, é importante entender o modelo de dados para nossa rede social precisaremos de várias classes parse para construir a funcionalidade do nosso feed classe post a classe post armazenará todas as informações relacionadas ao post autor um ponteiro para o usuário que criou o post conteúdo o conteúdo de texto do post imagem um arquivo de imagem opcional curtidas um número representando quantas curtidas o post tem comentários um array de ponteiros para objetos de comentário (opcional, pode ser consultado separadamente) criadoem gerenciado automaticamente pelo back4app atualizadoem gerenciado automaticamente pelo back4app classe de comentário o comentário classe armazenará comentários em postagens autor um ponteiro para o usuário que escreveu o comentário postagem um ponteiro para a postagem que está sendo comentada conteúdo o conteúdo de texto do comentário criadoem gerenciado automaticamente pelo back4app atualizadoem gerenciado automaticamente pelo back4app rastreamento de curtidas temos duas opções para rastrear curtidas contador simples armazenar uma contagem de curtidas em cada postagem (mais fácil, mas menos detalhado) registros de curtidas criar uma classe de curtida , que rastreia cada curtida individual (mais detalhado) para este tutorial, implementaremos ambas as abordagens, começando com o contador mais simples e depois mostrando como implementar registros de curtidas individuais para recursos mais avançados vamos começar configurando essas classes no back4app passo 2 – configurando o modelo de dados no back4app neste passo, você criará as classes necessárias no seu banco de dados back4app criando a classe post faça login no seu painel do back4app e navegue até o seu projeto na barra lateral esquerda, clique em "banco de dados" para abrir o navegador de banco de dados clique no botão "criar uma classe" no topo da página na janela modal que aparece, insira "post" como o nome da classe e selecione "personalizado" como o tipo em seguida, clique em "criar classe" \[imagem modal criar classe back4app com "post" inserido como nome da classe] agora, adicione as seguintes colunas à sua classe post clique em "adicionar coluna" crie as seguintes colunas autor (tipo pointer, classe alvo user) conteúdo (tipo string) imagem (tipo file) curtidas (tipo número, valor padrão 0) criando a classe comentário clique em "criar uma classe" novamente digite "comentário" como o nome da classe e selecione "personalizado" como o tipo adicione as seguintes colunas autor (tipo pointer, classe alvo user) post (tipo pointer, classe alvo post) conteúdo (tipo string) configurando permissões em nível de classe para segurança, vamos configurar permissões apropriadas para nossas classes no navegador de banco de dados, selecione a classe post clique no botão "segurança" configure as permissões leitura pública sim (qualquer um pode ver os posts) escrita pública não (apenas usuários autenticados podem criar posts) adicionar campo público não encontrar, obter, criar, atualizar, deletar requer autenticação do usuário repita configurações semelhantes para a classe comment agora que temos nosso modelo de dados configurado, vamos implementar os componentes de front end para o feed da nossa rede social a página de feed é o coração de qualquer rede social ela exibe postagens de usuários e permite interação vamos criar um componente feed que busca e exibe postagens crie um arquivo chamado src/pages/feedpage js import react, { usestate, useeffect, useref } from 'react'; import { usenavigate } from 'react router dom'; import { box, flex, vstack, heading, text, button, input, simplegrid, avatar, hstack, iconbutton, textarea, spinner, center, image, } from '@chakra ui/react'; import { link as routerlink } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function feedpage() { const \[searchquery, setsearchquery] = usestate(''); const \[newpostcontent, setnewpostcontent] = usestate(''); const \[posts, setposts] = usestate(\[]); const \[isloading, setisloading] = usestate(true); const \[isposting, setisposting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // new state variables for image upload const \[selectedimage, setselectedimage] = usestate(null); const \[imagepreview, setimagepreview] = usestate(null); const fileinputref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { // redirect to login if not authenticated navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // handle image selection const handleimagechange = (e) => { if (e target files && e target files\[0]) { const file = e target files\[0]; setselectedimage(file); // create a preview url const reader = new filereader(); reader onloadend = () => { setimagepreview(reader result); }; reader readasdataurl(file); } }; // clear selected image const handleclearimage = () => { setselectedimage(null); setimagepreview(null); if (fileinputref current) { fileinputref current value = ''; } }; // fetch posts useeffect(() => { const fetchposts = async () => { if (!currentuser) { console log('no current user, skipping post fetch'); return; } setisloading(true); console log('fetching posts for user ', currentuser id); try { // create a query for the post class const query = new parse query('post'); console log('created post query'); // include the user who created the post query include('author'); console log('including author in query'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query console log('executing query '); const results = await query find(); console log('query results received ', results length, 'posts found'); // convert parse objects to plain objects const fetchedposts = \[]; for (let i = 0; i < results length; i++) { const post = results\[i]; try { const author = post get('author'); console log(`processing post ${i+1}/${results length}, author `, author ? author id 'null'); if (!author) { console warn(`post ${post id} has no author, skipping`); continue; } const postobj = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; fetchedposts push(postobj); } catch (posterror) { console error(`error processing post ${i+1} `, posterror); console error('post data ', post tojson()); } } console log('successfully processed', fetchedposts length, 'posts'); setposts(fetchedposts); } catch (error) { console error('error fetching posts ', error); console error('error details ', error code, error message); toaster create({ title 'error loading posts', description error message, type 'error', }); } finally { setisloading(false); console log('post loading completed'); } }; fetchposts(); }, \[currentuser, navigate]); // function to create a new post with image const handlecreatepost = async () => { if (!newpostcontent trim() && !selectedimage) return; setisposting(true); console log('creating new post '); try { // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { console log('uploading image '); const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); console log('image uploaded successfully'); } console log('post object created, saving to database '); // save the post const savedpost = await post save(); console log('post saved successfully with id ', savedpost id); // add the new post to the state const newpost = { id savedpost id, content savedpost get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, image savedpost get('image') ? savedpost get('image') url() null, likes 0, createdat savedpost get('createdat') }; console log('adding new post to state'); setposts(\[newpost, posts]); setnewpostcontent(''); setselectedimage(null); setimagepreview(null); toaster create({ title 'post created', description 'your post has been published successfully!', type 'success', }); } catch (error) { console error('error creating post ', error); console error('error details ', error code, error message); toaster create({ title 'error creating post', description error message, type 'error', }); } finally { setisposting(false); console log('post creation completed'); } }; // function to like a post const handlelikepost = async (postid) => { try { // get the post const query = new parse query('post'); const post = await query get(postid); // increment likes post increment('likes'); await post save(); // update the post in the state setposts(posts map(p => { if (p id === postid) { return { p, likes p likes + 1 }; } return p; })); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // function to logout const handlelogout = async () => { try { await parse user logout(); navigate('/login'); } catch (error) { console error('error logging out ', error); } }; const handlesearchsubmit = (e) => { e preventdefault(); if (searchquery trim()) { navigate(`/search?q=${encodeuricomponent(searchquery)}`); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; return ( \<flex direction="row" h="100vh"> {/ left sidebar (navigation) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderright="1px solid" bordercolor="gray 600" \> \<vstack align="stretch" spacing={4}> \<heading size="md">social network\</heading> \<button as={routerlink} to="/feed" variant="ghost" justifycontent="flex start"> home \</button> \<button as={routerlink} to="/search" variant="ghost" justifycontent="flex start"> search \</button> \<button as={routerlink} to="/messages" variant="ghost" justifycontent="flex start"> messages \</button> \<button as={routerlink} to="/profile" variant="ghost" justifycontent="flex start"> profile \</button> \<button onclick={handlelogout} variant="ghost" colorscheme="red" justifycontent="flex start"> logout \</button> \</vstack> \</box> {/ main content (feed) /} \<box flex="1" p={4} overflowy="auto"> \<form onsubmit={handlesearchsubmit}> \<input placeholder="search " value={searchquery} onchange={(e) => setsearchquery(e target value)} mb={4} /> \</form> {/ create post with image upload /} \<box border="1px solid" bordercolor="gray 600" p={4} borderradius="md" mb={6}> \<textarea placeholder="what's on your mind?" value={newpostcontent} onchange={(e) => setnewpostcontent(e target value)} mb={2} resize="none" /> {/ image preview /} {imagepreview && ( \<box position="relative" mb={2}> \<image src={imagepreview} alt="preview" maxh="200px" borderradius="md" /> \<button position="absolute" top="2" right="2" size="sm" colorscheme="red" onclick={handleclearimage} \> remove \</button> \</box> )} \<box> \<input type="file" accept="image/ " onchange={handleimagechange} style={{ display 'none' }} ref={fileinputref} id="image upload" /> \<button as="label" htmlfor="image upload" cursor="pointer" variant="outline" size="sm" mr={2} mb={0} \> 📷 add photo \</button> \</box> \<button colorscheme="blue" onclick={handlecreatepost} isloading={isposting} disabled={(!newpostcontent trim() && !selectedimage) || isposting} \> post \</button> \</box> {/ posts feed with images /} {isloading ? ( \<center py={10}> \<spinner size="xl" /> \</center> ) posts length > 0 ? ( \<vstack align="stretch" spacing={4}> {posts map(post => ( \<box key={post id} border="1px solid" bordercolor="gray 600" p={4} borderradius="md" \> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(post createdat)}\</text> \</hstack> \<text mb={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box mb={4}> \<image src={post image} alt="post image" borderradius="md" maxh="400px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" size="sm" onclick={() => handlelikepost(post id)} \> ❤️ {post likes} \</button> \<button variant="ghost" size="sm" as={routerlink} to={`/post/${post id}`} \> 💬 comment \</button> \<button variant="ghost" size="sm"> 🔄 share \</button> \</hstack> \</box> ))} \</vstack> ) ( \<text>no posts yet follow users or create your first post!\</text> )} \</box> {/ right sidebar (trending hashtags) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderleft="1px solid" bordercolor="gray 600" \> \<heading size="md" mb={4}> trending today \</heading> \<simplegrid columns={1} spacing={2}> \<button variant="outline" size="sm" colorscheme="whitealpha">#travel\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#tech\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#foodie\</button> \</simplegrid> \</box> \</flex> ); } export default feedpage; este componente feed inclui várias características principais verificação de autenticação garante que apenas usuários logados possam visualizar o feed criação de postagens permite que os usuários criem novas postagens com texto e imagens opcionais upload de imagem gerencia a seleção, visualização e upload de imagens usando parse file exibição do feed mostra postagens de todos os usuários em ordem cronológica reversa funcionalidade de curtir permite que os usuários curtam postagens usando uma abordagem simples de contagem navegação fornece links para outras partes da aplicação vamos examinar como o back4app está nos ajudando a implementar essas funcionalidades criação de postagens com parse o back4app torna a criação de postagens com imagens simples // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); } // save the post const savedpost = await post save(); parse file gerencia automaticamente uploads de arquivos, armazenamento e geração de url, facilitando a adição de imagens às postagens buscando postagens com parse query o sistema de consulta do back4app facilita a busca e exibição de postagens // create a query for the post class const query = new parse query('post'); // include the user who created the post query include('author'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query const results = await query find(); o include('author') método é particularmente poderoso, pois inclui automaticamente o objeto user referenciado com cada postagem, reduzindo a necessidade de múltiplas consultas projeto back4gram encontre aqui o código completo para um projeto de exemplo de rede social construído com back4app passo 4 – implementando a visualização de detalhes do post com comentários agora, vamos criar uma página de detalhes do post que exibe um único post com seus comentários e permite que os usuários adicionem novos comentários crie um arquivo chamado src/pages/postdetailspage js import react, { usestate, useeffect } from 'react'; import { useparams, usenavigate, link as routerlink } from 'react router dom'; import { box, flex, vstack, heading, text, button, avatar, hstack, textarea, spinner, center, image } from '@chakra ui/react'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function postdetailspage() { const { id } = useparams(); const navigate = usenavigate(); const \[post, setpost] = usestate(null); const \[comments, setcomments] = usestate(\[]); const \[newcomment, setnewcomment] = usestate(''); const \[isloading, setisloading] = usestate(true); const \[iscommenting, setiscommenting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // fetch post and comments useeffect(() => { const fetchpostdetails = async () => { if (!currentuser) return; setisloading(true); try { // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); // convert post to plain object const fetchedpost = { id postobject id, content postobject get('content'), author { id postobject get('author') id, username postobject get('author') get('username'), avatar postobject get('author') get('avatar') ? postobject get('author') get('avatar') url() null }, likes postobject get('likes') || 0, createdat postobject get('createdat'), image postobject get('image') ? postobject get('image') url() null }; // convert comments to plain objects const fetchedcomments = commentresults map(comment => ({ id comment id, content comment get('content'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null }, createdat comment get('createdat') })); setpost(fetchedpost); setcomments(fetchedcomments); } catch (error) { console error('error fetching post details ', error); toaster create({ title 'error loading post', description error message, type 'error', }); navigate('/feed'); } finally { setisloading(false); } }; if (id) { fetchpostdetails(); } }, \[id, currentuser, navigate]); // function to add a comment const handleaddcomment = async () => { if (!newcomment trim()) return; setiscommenting(true); try { // get the post object const postquery = new parse query('post'); const postobject = await postquery get(id); // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); // add the new comment to the state const newcommentobj = { id comment id, content comment get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, createdat comment get('createdat') }; setcomments(\[ comments, newcommentobj]); setnewcomment(''); toaster create({ title 'comment added', description 'your comment has been posted successfully!', type 'success', }); } catch (error) { console error('error adding comment ', error); toaster create({ title 'error adding comment', description error message, type 'error', }); } finally { setiscommenting(false); } }; // function to like the post const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // increment likes postobject increment('likes'); await postobject save(); // update the post in the state setpost({ post, likes post likes + 1 }); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; if (isloading) { return ( \<center h="100vh"> \<spinner size="xl" /> \</center> ); } if (!post) { return ( \<center h="100vh"> \<vstack> \<text>post not found\</text> \<button as={routerlink} to="/feed" colorscheme="blue"> back to feed \</button> \</vstack> \</center> ); } return ( \<box maxw="800px" mx="auto" p={4}> \<button as={routerlink} to="/feed" mb={4} variant="ghost"> ← back to feed \</button> {/ post /} \<box border="1px solid" bordercolor="gray 600" p={6} borderradius="md" mb={6}> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<vstack align="start" spacing={0}> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">{formatdate(post createdat)}\</text> \</vstack> \</hstack> \<text fontsize="lg" my={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box my={4}> \<image src={post image} alt="post image" borderradius="md" maxh="500px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" onclick={handlelikepost} \> ❤️ {post likes} \</button> \<button variant="ghost"> 🔄 share \</button> \</hstack> \</box> {/ comments section /} \<box> \<heading size="md" mb={4}> comments ({comments length}) \</heading> {/ add comment /} \<box mb={6}> \<textarea placeholder="write a comment " value={newcomment} onchange={(e) => setnewcomment(e target value)} mb={2} /> \<button colorscheme="blue" onclick={handleaddcomment} isloading={iscommenting} disabled={!newcomment trim()} \> post comment \</button> \</box> \<hr style={{ marginbottom "1rem" }} /> {/ comments list /} {comments length > 0 ? ( \<vstack align="stretch" spacing={4}> {comments map(comment => ( \<box key={comment id} p={4} bg="gray 700" borderradius="md"> \<hstack mb={2}> \<avatar src={comment author avatar} name={comment author username} size="sm" /> \<text fontweight="bold">{comment author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(comment createdat)}\</text> \</hstack> \<text>{comment content}\</text> \</box> ))} \</vstack> ) ( \<text color="gray 400">no comments yet be the first to comment!\</text> )} \</box> \</box> ); } export default postdetailspage; this post detail page provides several important features 1\ post display shows the complete post with author information, content, image, and likes 2\ comment creation allows users to add new comments to the post 3\ comment listing displays all comments in chronological order 4\ like functionality enables users to like the post let's examine the key aspects of how back4app is helping us implement these features \### fetching post details with related objects back4app makes it easy to fetch a post with related information ```javascript // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); o include('author') método recupera automaticamente o objeto user relacionado junto com a postagem, eliminando a necessidade de consultas separadas consultando comentários relacionados o sistema de consulta do back4app nos permite encontrar todos os comentários relacionados a uma postagem específica // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); o equalto('post', postobject) método filtra os comentários para apenas aqueles relacionados à postagem atual criando comentários com ponteiros criar um comentário com um relacionamento a uma postagem é simples // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); o equalto('post', postobject) método filtra os comentários para apenas aqueles relacionados à postagem atual o post campo é definido como um ponteiro para o objeto post, estabelecendo a relação entre o comentário e o post projeto back4gram encontre aqui o código completo para um projeto de exemplo de rede social construído com back4app passo 5 – implementando funcionalidade avançada de curtir com parse o contador de curtidas simples que implementamos até agora funciona bem para funcionalidades básicas, mas tem limitações os usuários podem curtir um post várias vezes um usuário não pode "descurtir" um post não podemos mostrar quem curtiu um post vamos criar um sistema de curtidas mais avançado usando uma classe like para rastrear curtidas individuais configurando a classe like primeiro, crie uma nova classe no seu painel do back4app clique em "criar uma classe" no navegador de banco de dados digite "curtir" como o nome da classe e selecione "personalizado" como o tipo adicione as seguintes colunas usuário (tipo ponteiro, classe alvo user) postagem (tipo ponteiro, classe alvo post) implementando a gestão de curtidas em nosso código agora, vamos atualizar nossa funcionalidade de curtir postagens no postdetailspage js arquivo // add this state at the beginning of the component const \[hasliked, sethasliked] = usestate(false); // add this to the useeffect that fetches post details // check if current user has liked this post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const userlike = await likequery first(); sethasliked(!!userlike); // replace the handlelikepost function with this new version const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // check if user has already liked the post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const existinglike = await likequery first(); if (existinglike) { // user already liked the post, so remove the like await existinglike destroy(); // decrement likes count on the post postobject increment('likes', 1); await postobject save(); // update ui sethasliked(false); setpost({ post, likes post likes 1 }); } else { // user hasn't liked the post yet, so add a like const like = parse object extend('like'); const like = new like(); like set('user', currentuser); like set('post', postobject); await like save(); // increment likes count on the post postobject increment('likes'); await postobject save(); // update ui sethasliked(true); setpost({ post, likes post likes + 1 }); } } catch (error) { console error('error toggling like ', error); toaster create({ title 'error', description 'could not process your like please try again ', type 'error', }); } }; em seguida, atualize o botão de curtir para refletir o estado atual \<button variant="ghost" onclick={handlelikepost} color={hasliked ? "red 500" "inherit"} \> {hasliked ? "❤️" "🤍"} {post likes} \</button> esta implementação rastreia curtidas individuais usando uma curtir classe permite que os usuários alternem curtidas (curtir e descurtir) indica visualmente se o usuário atual curtiu o post mantém a contagem de curtidas no post para consultas eficientes mostrando quem curtir um post você também pode adicionar funcionalidade para mostrar quem curtiu um post // add this state const \[likeusers, setlikeusers] = usestate(\[]); const \[showlikes, setshowlikes] = usestate(false); // add this function const fetchlikes = async () => { try { const postquery = new parse query('post'); const postobject = await postquery get(id); const likequery = new parse query('like'); likequery equalto('post', postobject); likequery include('user'); const likes = await likequery find(); const users = likes map(like => { const user = like get('user'); return { id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null }; }); setlikeusers(users); setshowlikes(true); } catch (error) { console error('error fetching likes ', error); } }; em seguida, adicione um elemento de ui para exibir os usuários que curtiram o post \<text color="blue 400" cursor="pointer" onclick={fetchlikes} fontsize="sm" \> see who liked this post \</text> {showlikes && ( \<box mt={2} p={2} bg="gray 700" borderradius="md"> \<heading size="xs" mb={2}>liked by \</heading> {likeusers length > 0 ? ( \<vstack align="stretch"> {likeusers map(user => ( \<hstack key={user id}> \<avatar size="xs" src={user avatar} name={user username} /> \<text fontsize="sm">{user username}\</text> \</hstack> ))} \</vstack> ) ( \<text fontsize="sm">no likes yet\</text> )} \</box> )} projeto back4gram encontre aqui o código completo para um projeto de rede social construído com back4app passo 6 – implementando atualizações em tempo real com parse live query uma das características mais poderosas do back4app é a capacidade de receber atualizações em tempo real com live query isso permite que seu aplicativo seja atualizado automaticamente quando novos posts ou comentários são criados, sem a necessidade de atualização manual configurando live query no back4app primeiro, você precisa habilitar live query para sua aplicação no back4app vá para o seu painel do back4app navegue até configurações do app > configurações do servidor role para baixo até "nomes de classes do parse live query" adicione "post" e "comment" à lista de classes salve suas alterações implementando live query para postagens em tempo real vamos atualizar nosso feedpage js para incluir live query para atualizações de post em tempo real // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser) return; try { console log('setting up live query for posts'); // create a query that will be used for the subscription const query = new parse query('post'); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new posts livequerysubscription current on('create', (post) => { console log('new post received via live query ', post id); // skip posts from the current user as they're already in the ui if (post get('author') id === currentuser id) { console log('skipping post from current user'); return; } // format the new post const author = post get('author'); const newpost = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; // add the new post to the posts state setposts(prevposts => \[newpost, prevposts]); }); // handle post updates livequerysubscription current on('update', (post) => { console log('post updated via live query ', post id); // format the updated post const newlikes = post get('likes') || 0; // update the post in the state setposts(prevposts => prevposts map(p => p id === post id ? { p, likes newlikes } p ) ); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser]); implementando live query para comentários em tempo real da mesma forma, vamos atualizar nosso postdetailspage js para incluir live query para comentários em tempo real // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser || !id) return; try { console log('setting up live query for comments'); // get the post object to use in the query const postquery = new parse query('post'); const postobject = await postquery get(id); // create a query that will be used for the subscription const query = new parse query('comment'); query equalto('post', postobject); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query for comments'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new comments livequerysubscription current on('create', (comment) => { console log('new comment received via live query ', comment id); // skip comments from the current user as they're already in the ui if (comment get('author') id === currentuser id) { console log('skipping comment from current user'); return; } // format the new comment const author = comment get('author'); const newcomment = { id comment id, content comment get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, createdat comment get('createdat') }; // add the new comment to the comments state setcomments(prevcomments => \[ prevcomments, newcomment]); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query for comments ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser, id]); com essas implementações de live query, sua rede social agora será atualizada em tempo real novas postagens de outros usuários aparecerão automaticamente no feed contagens de curtidas serão atualizadas em tempo real quando outros usuários curtirem uma postagem novos comentários aparecerão automaticamente na página de detalhes da postagem projeto back4gram encontre aqui o código completo para um projeto de amostra de rede social construído com back4app passo 7 – consultas avançadas e otimização de desempenho à medida que sua rede social cresce, o desempenho se torna cada vez mais importante vamos explorar algumas técnicas avançadas para otimizar seu aplicativo usando as poderosas capacidades de consulta do back4app implementando paginação em vez de carregar todas as postagens de uma vez, implemente a paginação para carregá las em lotes // add these state variables const \[page, setpage] = usestate(0); const \[hasmoreposts, sethasmoreposts] = usestate(true); const postsperpage = 10; // update your fetchposts function const fetchposts = async (loadmore = false) => { if (!currentuser) return; if (!loadmore) { setisloading(true); } try { const query = new parse query('post'); query include('author'); query descending('createdat'); query limit(postsperpage); query skip(page postsperpage); const results = await query find(); // process and format posts as before const fetchedposts = \[ ]; // update the hasmoreposts state sethasmoreposts(results length === postsperpage); // update the posts state if (loadmore) { setposts(prevposts => \[ prevposts, fetchedposts]); } else { setposts(fetchedposts); } // update the page if this was a "load more" request if (loadmore) { setpage(prevpage => prevpage + 1); } } catch (error) { // handle errors } finally { setisloading(false); } }; // add a load more button at the bottom of your posts list {hasmoreposts && ( \<button onclick={() => fetchposts(true)} variant="outline" isloading={isloading} mt={4} \> load more posts \</button> )} implementando um feed específico para o usuário para uma experiência mais personalizada, você pode querer mostrar posts apenas de usuários que o usuário atual segue // assuming you have a "follow" class with "follower" and "following" pointers const fetchfollowingposts = async () => { try { // first, find all users that the current user follows const followquery = new parse query('follow'); followquery equalto('follower', currentuser); const followings = await followquery find(); // extract the users being followed const followedusers = followings map(follow => follow\ get('following')); // add the current user to show their posts too followedusers push(currentuser); // create a query for posts from these users const query = new parse query('post'); query containedin('author', followedusers); query include('author'); query descending('createdat'); query limit(20); const results = await query find(); // process results } catch (error) { // handle errors } }; usando funções em nuvem para operações complexas para operações complexas que exigiriam várias consultas, considere usar funções em nuvem do back4app vá para o seu painel do back4app navegue até código em nuvem > funções em nuvem crie uma nova função // example function to get a feed with posts, likes, and comment counts parse cloud define("getfeed", async (request) => { try { const user = request user; const page = request params page || 0; const limit = request params limit || 10; if (!user) { throw new error("user must be logged in"); } // query for posts const query = new parse query("post"); query include("author"); query descending("createdat"); query limit(limit); query skip(page limit); const posts = await query find({ usemasterkey true }); // format posts and get additional info const formattedposts = await promise all(posts map(async (post) => { // check if user has liked this post const likequery = new parse query("like"); likequery equalto("post", post); likequery equalto("user", user); const hasliked = await likequery count({ usemasterkey true }) > 0; // get comment count const commentquery = new parse query("comment"); commentquery equalto("post", post); const commentcount = await commentquery count({ usemasterkey true }); // format post for response return { id post id, content post get("content"), image post get("image") ? post get("image") url() null, author { id post get("author") id, username post get("author") get("username"), avatar post get("author") get("avatar") ? post get("author") get("avatar") url() null }, likes post get("likes") || 0, hasliked hasliked, commentcount commentcount, createdat post get("createdat") }; })); return { posts formattedposts }; } catch (error) { throw new error(`error fetching feed ${error message}`); } }); então chame esta função em nuvem do seu aplicativo const fetchfeed = async () => { try { setisloading(true); const result = await parse cloud run('getfeed', { page 0, limit 10 }); setposts(result posts); } catch (error) { console error('error fetching feed ', error); toaster create({ title 'error loading feed', description error message, type 'error', }); } finally { setisloading(false); } }; essa abordagem pode reduzir significativamente o número de consultas ao banco de dados e melhorar o desempenho projeto back4gram encontre aqui o código completo para um projeto de amostra de rede social construído com back4app conclusão neste tutorial, você construiu um feed de rede social abrangente com recursos interativos usando o back4app vamos recapitular o que você realizou design do modelo de dados criado um modelo de dados estruturado para postagens, comentários e curtidas implementação do feed criada uma página de feed que exibe postagens de todos os usuários criação de postagens implementada a funcionalidade para os usuários criarem postagens com texto e imagens sistema de comentários criado um sistema de comentários que permite aos usuários interagir com as postagens funcionalidade de curtidas implementados sistemas de curtidas simples e avançados atualizações em tempo real adicionada live query para atualizações em tempo real de postagens e comentários otimização de desempenho exploradas técnicas para melhorar o desempenho à medida que seu aplicativo escala o back4app forneceu um banco de dados seguro e escalável para armazenar conteúdo gerado pelo usuário armazenamento de arquivos para imagens de postagens autenticação de usuário embutida para segurança consulta ao vivo para atualizações em tempo real funções em nuvem para operações complexas de backend com esses blocos de construção em vigor, sua rede social tem uma base sólida que pode escalar à medida que sua base de usuários cresce próximos passos para aprimorar ainda mais sua rede social, considere implementar esses recursos notificações alerta os usuários quando suas postagens recebem curtidas ou comentários sistema de seguimento de usuários permita que os usuários se sigam e personalizem seus feeds hashtags e pesquisa implemente um sistema de hashtags e funcionalidade de pesquisa aprimorada postagens ricas em mídia suporte a vídeos, múltiplas imagens e outros tipos de mídia análises acompanhe o engajamento e o comportamento do usuário para melhorar seu aplicativo para o código completo da aplicação de rede social back4gram, você pode conferir o repositório do github https //github com/templates back4app/back4gram ao aproveitar os poderosos serviços de backend do back4app, você pode se concentrar em criar uma ótima experiência do usuário enquanto a plataforma lida com a infraestrutura complexa necessária para uma aplicação de rede social