DEV Community

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

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

Anthony Gonzalez
I'm a software engineer who specializes in web application development with Elixir and the Phoenix Framework.
・15 min read

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

Discussion (0)

Forem Open with the Forem app