DEV Community

Cover image for Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 6]
Anthony Gonzalez
Anthony Gonzalez

Posted on • Edited on • Originally published at elixirprogrammer.com

Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 6]

In part 5 we added the show-post page, in this part, we will work on the homepage. You can catch up with the Instagram Clone GitHub Repo.

Let's start by adding a function to our posts context to get the feed and another one to get the total number of the feed, open lib/instagram_clone/posts.ex:

 @doc """ Returns the list of paginated posts of a given user id And posts of following list of given user id With user and likes preloaded With 2 most recent comments preloaded with user and likes User, page, and per_page are given with the socket assigns ## Examples iex> get_accounts_feed(following_list, assigns) [%{photo_url: "", url_id: ""}, ...] """ def get_accounts_feed(following_list, assigns) do user = assigns.current_user page = assigns.page per_page = assigns.per_page query = from c in Comment, select: %{id: c.id, row_number: over(row_number(), :posts_partition)}, windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]] comments_query = from c in Comment, join: r in subquery(query), on: c.id == r.id and r.row_number <= 2 Post |> where([p], p.user_id in ^following_list) |> or_where([p], p.user_id == ^user.id) |> limit(^per_page) |> offset(^((page - 1) * per_page)) |> order_by(desc: :id) |> preload([:user, :likes, comments: ^{comments_query, [:user, :likes]}]) |> Repo.all() end def get_accounts_feed_total(following_list, assigns) do user = assigns.current_user Post |> where([p], p.user_id in ^following_list) |> or_where([p], p.user_id == ^user.id) |> select([p], count(p.id)) |> Repo.one() end 
Enter fullscreen mode Exit fullscreen mode

We need the list of following, inside lib/instagram_clone/accounts.ex add the following function:

 @doc """ Returns the list of following user ids ## Examples iex> get_following_list(user) [3, 2, 1] """ def get_following_list(user) do Follows |> select([f], f.followed_id) |> where(follower_id: ^user.id) |> Repo.all() end 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/page_live.ex let's assign the feed:

 alias InstagramClone.Uploaders.Avatar alias InstagramClone.Accounts alias InstagramCloneWeb.UserLive.FollowComponent alias InstagramClone.Posts alias InstagramCloneWeb.Live.LikeComponent @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) {:ok, socket |> assign(page: 1, per_page: 15), temporary_assigns: [user_feed: []]} end @impl true def handle_params(_params, _uri, socket) do {:noreply, socket |> assign(live_action: apply_action(socket.assigns.current_user)) |> assign_posts()} end defp apply_action(current_user) do if !current_user, do: :root_path end defp assign_posts(socket) do if socket.assigns.current_user do current_user = socket.assigns.current_user following_list = Accounts.get_following_list(current_user) accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns) socket |> assign(following_list: following_list) |> assign(accounts_feed_total: accounts_feed_total) |> assign_user_feed() else socket end end defp assign_user_feed(socket, following_list) do user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns) socket |> assign(user_feed: user_feed) end 
Enter fullscreen mode Exit fullscreen mode

Page and per page were assigned to the socket in our mount function. We are checking if a user is logged in to get the following list and pass it to the assign feed function to return the socket with the feed assigned, we do that in our handle params function.

Now let's create a component for posts feed, inside our live folder add the following files:

lib/instagram_clone_web/live/page_post_feed_component.ex
lib/instagram_clone_web/live/page_post_feed_component.html.leex

Inside lib/instagram_clone_web/live/page_live.html.leex:

<%= if @current_user do %> <section class="flex"> <div id="user-feed" class="w-3/5" phx-update="append"> <%= for post <- @user_feed do %> <%= live_component @socket, InstagramCloneWeb.Live.PagePostFeedComponent, post: post, id: post.id, current_user: @current_user %> <% end %> </div> </section> <div id="profile-posts-footer" class="flex justify-center" phx-hook="ProfilePostsScroll"> </div> <% else %> <%= live_component @socket, InstagramCloneWeb.PageLiveComponent, id: 1 %> <% end %> 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/page_post_feed_component.ex:

defmodule InstagramCloneWeb.Live.PagePostFeedComponent do use InstagramCloneWeb, :live_component alias InstagramClone.Uploaders.Avatar alias InstagramClone.Comments alias InstagramClone.Comments.Comment @impl true def mount(socket) do {:ok, socket |> assign(changeset: Comments.change_comment(%Comment{})), temporary_assigns: [comments: []]} end @impl true def handle_event("save", %{"comment" => comment_param}, socket) do %{"body" => body} = comment_param current_user = socket.assigns.current_user post = socket.assigns.post if body == "" do {:noreply, socket} else comment = Comments.create_comment(current_user, post, comment_param) {:noreply, socket |> update(:comments, fn comments -> [comment | comments] end) |> assign(changeset: Comments.change_comment(%Comment{}))} end end end 
Enter fullscreen mode Exit fullscreen mode

We are setting the form changeset and temporary comments that we will use to append new comments. The save handle function is the same one that we used on our show page.

Inside lib/instagram_clone_web/live/page_post_feed_component.html.leex:

<div class="mb-16 shadow" id="post-<%= @post.id %>"> <div class="flex p-4 items-center"> <!-- Post header section --> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %> <%= img_tag Avatar.get_thumb(@post.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %> <% end %> <div class="ml-3"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> </div> <!-- End post header section --> </div> <!-- Post Image section --> <%= img_tag @post.photo_url, class: "w-full object-contain h-full shadow-sm" %> <!-- End Post Image section --> <div class="w-full"> <!-- Action icons section --> <div class="flex pl-4 pr-2 pt-2"> <%= live_component @socket, InstagramCloneWeb.Live.LikeComponent, id: @post.id, liked: @post, w_h: "w-8 h-8", current_user: @current_user %> <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %> <div class="ml-4 w-8 h-8"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> </svg> </div> <% end %> <div class="ml-4 w-8 h-8 cursor-pointer"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /> </svg> </div> <div class="w-8 h-8 ml-auto cursor-pointer"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /> </svg> </div> </div> <!-- End Action icons section --> <!-- Description section --> <button class="px-5 text-xs text-gray-500 font-bold focus:outline-none"><%= @post.total_likes %> likes</button> <!-- End Description Section --> </div> <%= if @post.description do %> <!-- Description section --> <div class="flex mt-2"> <div class="px-4 w-11/12"> <%= live_redirect @post.user.username, to: Routes.user_profile_path(@socket, :index, @post.user.username), class: "font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= @post.description %></p></span> </span> </div> </div> <!-- End Description Section --> <% end %> <%= if @post.total_comments > 2 do %> <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %> <h6 class="px-5 text-sm text-gray-400"> View all <%= @post.total_comments %> comments </h6> <% end %> <% end %> <section id="comments" phx-update="append"> <%= for comment <- @post.comments do %> <div class="flex" id="comment-<%= comment.id %>"> <div class="px-4 w-11/12"> <%= live_redirect comment.user.username, to: Routes.user_profile_path(@socket, :index, comment.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= comment.body %></p> </span> </div> <%= live_component @socket, InstagramCloneWeb.Live.LikeComponent, id: comment.id, liked: comment, w_h: "w-5 h-5", current_user: @current_user %> </div> <% end %> <%= for comment <- @comments do %> <div class="flex" id="comment-<%= comment.id %>"> <div class="px-4 w-11/12"> <%= live_redirect comment.user.username, to: Routes.user_profile_path(@socket, :index, comment.user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> <span class="text-sm text-gray-700"> <p class="inline"><%= comment.body %></p> </span> </div> <%= live_component @socket, InstagramCloneWeb.Live.LikeComponent, id: comment.id, liked: comment, w_h: "w-5 h-5", current_user: @current_user %> </div> <% end %> </section> <h6 class="px-5 py-2 text-xs text-gray-400"><%= Timex.from_now(@post.inserted_at) %></h6> <!-- Comment input section --> <%= f = form_for @changeset, "#", id: @id, phx_submit: "save", phx_target: @myself, class: "p-2 flex items-center mt-3 border-t-2 border-gray-100", x_data: "{ disableSubmit: true, inputText: null, displayCommentBtn: (refs) => { refs.cbtn.classList.remove('opacity-30') refs.cbtn.classList.remove('cursor-not-allowed') }, disableCommentBtn: (refs) => { refs.cbtn.classList.add('opacity-30') refs.cbtn.classList.add('cursor-not-allowed') } }" %> <div class="w-full"> <%= textarea f, :body, class: "w-full border-0 focus:ring-transparent resize-none", rows: 1, placeholder: "Add a comment...", aria_label: "Add a comment...", autocorrect: "off", autocomplete: "off", x_model: "inputText", "@input": "[ (inputText.length != 0) ? [disableSubmit = false, displayCommentBtn($refs)] : [disableSubmit = true, disableCommentBtn($refs)] ]" %> </div> <div> <%= submit "Post", phx_disable_with: "Posting...", class: "text-light-blue-500 opacity-30 cursor-not-allowed font-bold pb-2 text-sm focus:outline-none", x_ref: "cbtn", "@click": "inputText = null", "x_bind:disabled": "disableSubmit" %> </div> </form> </div> 
Enter fullscreen mode Exit fullscreen mode

We are using the same form to add new comments that we used on our show page, and we are looping through the post comments and the temporary comments to be able to update the comments when a new one is added.

We need to handle the messages sent from the like component when we like a post or a comment, also we have to handle the event triggered with a hook to load more posts, update lib/instagram_clone_web/live/page_live.ex to the following:

defmodule InstagramCloneWeb.PageLive do use InstagramCloneWeb, :live_view alias InstagramClone.Uploaders.Avatar alias InstagramClone.Accounts alias InstagramCloneWeb.UserLive.FollowComponent alias InstagramClone.Posts alias InstagramCloneWeb.Live.LikeComponent @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) {:ok, socket |> assign(page: 1, per_page: 15), temporary_assigns: [user_feed: []]} end @impl true def handle_params(_params, _uri, socket) do {:noreply, socket |> assign(live_action: apply_action(socket.assigns.current_user)) |> assign_posts()} end @impl true def handle_event("load-more-profile-posts", _, socket) do {:noreply, socket |> load_posts} end defp load_posts(socket) do total_posts = socket.assigns.accounts_feed_total page = socket.assigns.page per_page = socket.assigns.per_page total_pages = ceil(total_posts / per_page) if page == total_pages do socket else socket |> update(:page, &(&1 + 1)) |> assign_user_feed() end end @impl true def handle_info({LikeComponent, :update_comment_likes, _}, socket) do {:noreply, socket} end @impl true def handle_info({LikeComponent, :update_post_likes, post}, socket) do post_feed = Posts.get_post_feed!(post.id) {:noreply, socket |> update(:user_feed, fn user_feed -> [post_feed | user_feed] end)} end defp apply_action(current_user) do if !current_user, do: :root_path end defp assign_posts(socket) do if socket.assigns.current_user do current_user = socket.assigns.current_user following_list = Accounts.get_following_list(current_user) accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns) socket |> assign(following_list: following_list) |> assign(accounts_feed_total: accounts_feed_total) |> assign_user_feed() else socket end end defp assign_user_feed(socket) do user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns) socket |> assign(user_feed: user_feed) end end 
Enter fullscreen mode Exit fullscreen mode

Let's make some changes to our like component, because we are sharing it between posts and comments, move the file to the live folder outside the post_live folder and rename the module to the following:

lib/instagram_clone_web/live/like_component.ex

defmodule InstagramCloneWeb.Live.LikeComponent do 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/post_live/show.html.leex on line 70 rename the component:

 ... InstagramCloneWeb.Live.LikeComponent, ... 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/post_live/comment_component.html.leex on line 24 also rename the component:

 ... InstagramCloneWeb.PostLive.LikeComponent, ... 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/like_component.ex, let's update send_msg() to send the liked as variable instead of just the id:

 ... defp send_msg(liked) do msg = get_struct_msg_atom(liked) send(self(), {__MODULE__, msg, liked}) end ... 
Enter fullscreen mode Exit fullscreen mode

Also inside lib/instagram_clone_web/live/like_component.ex, let's delete the liked?() function and instead let's check if the user id is inside of a list of user ids on line 61:

 ... if assigns.current_user.id in assigns.liked.likes do # LINE 61 # DELETE THIS FUNCTION WE WON"T NEED ANYMORE # Enum.any?(likes, fn l -> # l.user_id == user_id # end) ... 
Enter fullscreen mode Exit fullscreen mode

And on line 30 let's update to check the database:

 ... if Likes.liked?(current_user.id, liked.id) do ... 
Enter fullscreen mode Exit fullscreen mode

Our new updated file should look like the following:

defmodule InstagramCloneWeb.Live.LikeComponent do use InstagramCloneWeb, :live_component alias InstagramClone.Likes @impl true def update(assigns, socket) do get_btn_status(socket, assigns) end @impl true def render(assigns) do ~L""" <button phx-target="<%= @myself %>" phx-click="toggle-status" class="<%= @w_h %> focus:outline-none"> <%= @icon %> </button> """ end @impl true def handle_event("toggle-status", _params, socket) do current_user = socket.assigns.current_user liked = socket.assigns.liked if Likes.liked?(current_user.id, liked.id) do unlike(socket, current_user.id, liked) else like(socket, current_user, liked) end end defp like(socket, current_user, liked) do Likes.create_like(current_user, liked) send_msg(liked) {:noreply, socket |> assign(icon: unlike_icon(socket.assigns))} end defp unlike(socket, current_user_id, liked) do Likes.unlike(current_user_id, liked) send_msg(liked) {:noreply, socket |> assign(icon: like_icon(socket.assigns))} end defp send_msg(liked) do msg = get_struct_msg_atom(liked) send(self(), {__MODULE__, msg, liked}) end defp get_btn_status(socket, assigns) do if assigns.current_user.id in assigns.liked.likes do get_socket_assigns(socket, assigns, unlike_icon(assigns)) else get_socket_assigns(socket, assigns, like_icon(assigns)) end end defp get_socket_assigns(socket, assigns, icon) do {:ok, socket |> assign(assigns) |> assign(icon: icon)} end defp get_struct_name(struct) do struct.__struct__ |> Module.split() |> List.last() |> String.downcase() end defp get_struct_msg_atom(struct) do name = get_struct_name(struct) update_struct_likes = "update_#{name}_likes" String.to_atom(update_struct_likes) end defp like_icon(assigns) do ~L""" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" /> </svg> """ end defp unlike_icon(assigns) do ~L""" <svg class="text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd" /> </svg> """ end end 
Enter fullscreen mode Exit fullscreen mode

Now when we preload the likes we have to only sent the list of ids, open lib/instagram_clone/posts.ex and on every function that we are getting posts, we have to update how we preload the likes:

defmodule InstagramClone.Posts do @moduledoc """ The Posts context. """ import Ecto.Query, warn: false alias InstagramClone.Repo alias InstagramClone.Posts.Post alias InstagramClone.Accounts.User alias InstagramClone.Comments.Comment alias InstagramClone.Likes.Like @doc """ Returns the list of posts. ## Examples iex> list_posts() [%Post{}, ...] """ def list_posts do Repo.all(Post) end @doc """ Returns the list of paginated posts of a given user id. ## Examples iex> list_user_posts(page: 1, per_page: 10, user_id: 1) [%{photo_url: "", url_id: ""}, ...] """ def list_profile_posts(page: page, per_page: per_page, user_id: user_id) do Post |> select([p], map(p, [:url_id, :photo_url])) |> where(user_id: ^user_id) |> limit(^per_page) |> offset(^((page - 1) * per_page)) |> order_by(desc: :id) |> Repo.all end @doc """ Returns the list of paginated posts of a given user id And posts of following list of given user id With user and likes preloaded With 2 most recent comments preloaded with user and likes User, page, and per_page are given with the socket assigns ## Examples iex> get_accounts_feed(following_list, assigns) [%{photo_url: "", url_id: ""}, ...] """ def get_accounts_feed(following_list, assigns) do user = assigns.current_user page = assigns.page per_page = assigns.per_page query = from c in Comment, select: %{id: c.id, row_number: over(row_number(), :posts_partition)}, windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]] comments_query = from c in Comment, join: r in subquery(query), on: c.id == r.id and r.row_number <= 2 likes_query = Like |> select([l], l.user_id) Post |> where([p], p.user_id in ^following_list) |> or_where([p], p.user_id == ^user.id) |> limit(^per_page) |> offset(^((page - 1) * per_page)) |> order_by(desc: :id) |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}]) |> Repo.all() end @doc """ Gets a single post. Raises `Ecto.NoResultsError` if the Post does not exist. ## Examples iex> get_post!(123) %Post{} iex> get_post!(456) ** (Ecto.NoResultsError) """ def get_post!(id) do likes_query = Like |> select([l], l.user_id) Repo.get!(Post, id) |> Repo.preload([:user, likes: likes_query]) end def get_post_feed!(id) do query = from c in Comment, select: %{id: c.id, row_number: over(row_number(), :posts_partition)}, windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]] comments_query = from c in Comment, join: r in subquery(query), on: c.id == r.id and r.row_number <= 2 likes_query = Like |> select([l], l.user_id) Post |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}]) |> Repo.get!(id) end def get_post_by_url!(id) do likes_query = Like |> select([l], l.user_id) Repo.get_by!(Post, url_id: id) |> Repo.preload([:user, likes: likes_query]) end @doc """ Creates a post. ## Examples iex> create_post(%{field: value}) {:ok, %Post{}} iex> create_post(%{field: bad_value}) {:error, %Ecto.Changeset{}} """ def create_post(%Post{} = post, attrs \\ %{}, user) do post = Ecto.build_assoc(user, :posts, put_url_id(post)) changeset = Post.changeset(post, attrs) update_posts_count = from(u in User, where: u.id == ^user.id) Ecto.Multi.new() |> Ecto.Multi.update_all(:update_posts_count, update_posts_count, inc: [posts_count: 1]) |> Ecto.Multi.insert(:post, changeset) |> Repo.transaction() end # Generates a base64-encoding 8 bytes defp put_url_id(post) do url_id = Base.encode64(:crypto.strong_rand_bytes(8), padding: false) %Post{post | url_id: url_id} end @doc """ Updates a post. ## Examples iex> update_post(post, %{field: new_value}) {:ok, %Post{}} iex> update_post(post, %{field: bad_value}) {:error, %Ecto.Changeset{}} """ def update_post(%Post{} = post, attrs) do post |> Post.changeset(attrs) |> Repo.update() end @doc """ Deletes a post. ## Examples iex> delete_post(post) {:ok, %Post{}} iex> delete_post(post) {:error, %Ecto.Changeset{}} """ def delete_post(%Post{} = post) do Repo.delete(post) end @doc """ Returns an `%Ecto.Changeset{}` for tracking post changes. ## Examples iex> change_post(post) %Ecto.Changeset{data: %Post{}} """ def change_post(%Post{} = post, attrs \\ %{}) do Post.changeset(post, attrs) end end 
Enter fullscreen mode Exit fullscreen mode

We also have to do the same for comments, open lib/instagram_clone/comments.ex and update the file to the following:

defmodule InstagramClone.Comments do @moduledoc """ The Comments context. """ import Ecto.Query, warn: false alias InstagramClone.Repo alias InstagramClone.Likes.Like alias InstagramClone.Comments.Comment @doc """ Returns the list of comments. ## Examples iex> list_comments() [%Comment{}, ...] """ def list_comments do Repo.all(Comment) end def list_post_comments(assigns, public: public) do user = assigns.current_user post_id = assigns.post.id per_page = assigns.per_page page = assigns.page likes_query = Like |> select([l], l.user_id) Comment |> where(post_id: ^post_id) |> get_post_comments_sorting(public, user) |> limit(^per_page) |> offset(^((page - 1) * per_page)) |> preload([:user, likes: ^likes_query]) |> Repo.all end defp get_post_comments_sorting(module, public, user) do if public do order_by(module, asc: :id) else order_by(module, fragment("(CASE WHEN user_id = ? then 1 else 2 end)", ^user.id)) end end @doc """ Gets a single comment. Raises `Ecto.NoResultsError` if the Comment does not exist. ## Examples iex> get_comment!(123) %Comment{} iex> get_comment!(456) ** (Ecto.NoResultsError) """ def get_comment!(id) do likes_query = Like |> select([l], l.user_id) Repo.get!(Comment, id) |> Repo.preload([:user, likes: likes_query]) end @doc """ Creates a comment. ## Examples iex> create_comment(%{field: value}) {:ok, %Comment{}} iex> create_comment(%{field: bad_value}) {:error, %Ecto.Changeset{}} """ def create_comment(user, post, attrs \\ %{}) do update_total_comments = post.__struct__ |> where(id: ^post.id) comment_attrs = %Comment{} |> Comment.changeset(attrs) comment = comment_attrs |> Ecto.Changeset.put_assoc(:user, user) |> Ecto.Changeset.put_assoc(:post, post) Ecto.Multi.new() |> Ecto.Multi.update_all(:update_total_comments, update_total_comments, inc: [total_comments: 1]) |> Ecto.Multi.insert(:comment, comment) |> Repo.transaction() |> case do {:ok, %{comment: comment}} -> likes_query = Like |> select([l], l.user_id) comment |> Repo.preload(likes: likes_query) end end @doc """ Updates a comment. ## Examples iex> update_comment(comment, %{field: new_value}) {:ok, %Comment{}} iex> update_comment(comment, %{field: bad_value}) {:error, %Ecto.Changeset{}} """ def update_comment(%Comment{} = comment, attrs) do comment |> Comment.changeset(attrs) |> Repo.update() end @doc """ Deletes a comment. ## Examples iex> delete_comment(comment) {:ok, %Comment{}} iex> delete_comment(comment) {:error, %Ecto.Changeset{}} """ def delete_comment(%Comment{} = comment) do Repo.delete(comment) end @doc """ Returns an `%Ecto.Changeset{}` for tracking comment changes. ## Examples iex> change_comment(comment) %Ecto.Changeset{data: %Comment{}} """ def change_comment(%Comment{} = comment, attrs \\ %{}) do Comment.changeset(comment, attrs) end end 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/post_live/show.ex update line 6:

 ... alias InstagramCloneWeb.Live.LikeComponent ... 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/post_live/show.html.leex update line 70 and line:

 ... InstagramCloneWeb.Live.LikeComponent, ... 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/post_live/comment_component.html.leex update line 24:

 ... InstagramCloneWeb.Live.LikeComponent, ... 
Enter fullscreen mode Exit fullscreen mode

Update lib/instagram_clone/likes.ex to the following:

defmodule InstagramClone.Likes do import Ecto.Query, warn: false alias InstagramClone.Repo alias InstagramClone.Likes.Like def create_like(user, liked) do user = Ecto.build_assoc(user, :likes) like = Ecto.build_assoc(liked, :likes, user) update_total_likes = liked.__struct__ |> where(id: ^liked.id) Ecto.Multi.new() |> Ecto.Multi.insert(:like, like) |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: 1]) |> Repo.transaction() end def unlike(user_id, liked) do like = liked?(user_id, liked.id) update_total_likes = liked.__struct__ |> where(id: ^liked.id) Ecto.Multi.new() |> Ecto.Multi.delete(:like, like) |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: -1]) |> Repo.transaction() end # Returns nil if not found def liked?(user_id, liked_id) do Repo.get_by(Like, [user_id: user_id, liked_id: liked_id]) end end 
Enter fullscreen mode Exit fullscreen mode

Let's add a sidebar with 5 random users suggestion, inside lib/instagram_clone/accounts.ex add the following function:

 def random_5(user) do following_list = get_following_list(user) User |> where([u], u.id not in ^following_list) |> where([u], u.id != ^user.id) |> order_by(desc: fragment("Random()")) |> limit(5) |> Repo.all() end 
Enter fullscreen mode Exit fullscreen mode

Inside lib/instagram_clone_web/live/page_live.ex add a handle_info() and update the private assign_posts() function to the following:

 ... @impl true def handle_info({FollowComponent, :update_totals, _}, socket) do {:noreply, socket} end defp assign_posts(socket) do if socket.assigns.current_user do current_user = socket.assigns.current_user random_5_users = Accounts.random_5(current_user) socket |> assign(users: random_5_users) |> assign_user_feed() else socket end end 
Enter fullscreen mode Exit fullscreen mode

Now to display the sidebar with the random users update Inside lib/instagram_clone_web/live/page_live.html.leex to the following:

<%= if @current_user do %> <section class="flex"> <div id="user-feed" class="w-3/5" phx-update="append"> <%= for post <- @user_feed do %> <%= live_component @socket, InstagramCloneWeb.Live.PagePostFeedComponent, post: post, id: post.id, current_user: @current_user %> <% end %> </div> <div> <sidebar class="fixed w-1/4"> <section class=" ml-auto pl-8"> <div class="flex items-center"> <!-- Post header section --> <%= live_redirect to: Routes.user_profile_path(@socket, :index, @current_user.username) do %> <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "w-14 h-14 rounded-full object-cover object-center" %> <% end %> <div class="ml-3"> <%= live_redirect @current_user.username, to: Routes.user_profile_path(@socket, :index, @current_user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> <h2 class="text-sm text-gray-500"><%= @current_user.full_name %></h2> </div> <!-- End post header section --> </div> <h1 class="text-gray-500 mt-5">Suggestions For You</h1> <%= for user <- @users do %> <div class="flex items-center p-3"> <!-- Post header section --> <%= live_redirect to: Routes.user_profile_path(@socket, :index, user.username) do %> <%= img_tag Avatar.get_thumb(user.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %> <% end %> <div class="ml-3"> <%= live_redirect user.username, to: Routes.user_profile_path(@socket, :index, user.username), class: "truncate font-bold text-sm text-gray-500 hover:underline" %> <h2 class="text-xs text-gray-500">Suggested for you</h2> </div> <span class="ml-auto"> <%= live_component @socket, InstagramCloneWeb.UserLive.FollowComponent, id: user.id, user: user, current_user: @current_user %> </span> <!-- End post header section --> </div> <% end %> </section> </sidebar> </div> </section> <div id="profile-posts-footer" class="flex justify-center" phx-hook="ProfilePostsScroll"> </div> <% else %> <%= live_component @socket, InstagramCloneWeb.PageLiveComponent, id: 1 %> <% end %> 
Enter fullscreen mode Exit fullscreen mode

 
Alt Text
 

That's it for now, you could improve the code by making it more efficient by sending the following list to the follow component, to set the button without having to go to the database.

I really appreciate your time, thank you so much for reading.

 

CHECK OUT THE INSTAGRAM CLONE GITHUB REPO

 
 
 

Join The Elixir Army

Top comments (8)

Collapse
 
udemeetim profile image
Itan

I highly recommend exploring the official Phoenix documentation on Channels and LiveView for a deep dive into real-time functionalities. Tools like Phoenix Presence can also enhance your app by showing who's online in real-time. Remember, experimentation is key. And since we're on the topic of exploring new territories, if you're curious about diversifying your knowledge and exploring content platforms, perhaps you might wonder, what is fansly? It's an interesting study in building community-centric platforms, which could inspire unique features for our clone.

Collapse
 
dazzlingdcruz11 profile image
Socialcaptainau53

That was the tremendous post i have ever seen in my life, Visit the best site to buy Australian instagram followers.

Collapse
 
newsstorms profile image
News Storms • Edited

Yes Cool. There are very few interesting projects on this technology stack. I only know one buy youtube views at buyyoutubviews.com

Collapse
 
beghammm profile image
beghammm

thanks for this nice content.
i really want to clone Instagram by Android java, and add this feature to my mobile app beside my website pages.
i create my website by wordpress.
digi-follower.com/

Collapse
 
nytimesgoal profile image
Brice Solari

As many people already mentioned that Instagram clone but I think its good idea to learn that how Instagram works. Even as its making a lot of people life easier specially on social media. Now a days every wants to be a star on social media and specially on Instagram. There are many ways to be a star buy my personally suggestion is to buy Instagram followers from any of the website which is mentioned by a very famous news site TheReporter.com

Collapse
 
jesdelins profile image
Jesdelins • Edited

Social networks in our time is one of the most basic pastime of any person. It's nice to be popular in them. I asked this question and found Buzzoid Instagram followers. Good service allows Intagram to become popular. They will help you have a lot of followers and likes. Convenient service.

Collapse
 
danielburbe profile image
Daneil Burbe

There are 100s of social media networking sites and 1000s of services to provide best service for better social media results. Specially, buzziod, likes.io, goread.io, qubeviews.com
Its highly recommended to use above mentioned API to solve any issue related to instagram.

Collapse
 
ahmad1998dev profile image
Ahmad1998dev

This was great. Well done doing it. We're trying to make instagram clones at inbo.ir too.