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

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

Show-Post Page

In part 4 we added the profile posts section and post page, in this part, we will work on the show-post page. You can catch up with the Instagram Clone GitHub Repo.

Let's start by adding our base template for our show page, open lib/instagram_clone_web/live/post_live/show.html.leex and add the following:

<section class="flex">
  <!-- Post Image section -->
  <%= img_tag @post.photo_url,
          class: "w-3/5 object-contain h-full" %>
  <!-- End Post Image section -->

  <div class="w-2/5 border-2 h-full">
    <div class="flex p-4 items-center border-b-2">
      <!-- Post header section -->
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %>
        <%= img_tag @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>

    <div class="no-scrollbar h-96 overflow-y-scroll p-4 flex flex-col">
      <%= if @post.description do %>
        <!-- Description section -->
        <div class="flex mt-2">
          <%= 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="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 class="flex mt-3">
              <div class="text-gray-400 text-xs"><%= Timex.from_now @post.inserted_at %></div>
            </div>
          </div>

        </div>
      <!-- End Description Section -->
      <% end %>

    </div>

    <div class="w-full border-t-2">
      <!-- Action icons section -->
      <div class="flex pl-4 pr-2 pt-2">
        <div class="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="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>
          <svg class="hidden 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>
        </div>
        <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 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>
        <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>
      <h6 class="px-5 text-xs text-gray-400"><%= Timex.format!(@post.inserted_at, "{Mfull} {D}, {YYYY}") %></h6>
      <!-- End Description Section -->

      <!-- Comment input section -->
      <div class="p-2 flex items-center mt-3 border-t-2 border-gray-100">
        <div class="w-full">
          <textarea
            aria-label="Add a comment..."
            placeholder="Add a comment..."
            class="w-full border-0 focus:ring-transparent resize-none"
            autocomplete="off"
            autocorrect="off"
            rows="1"></textarea>
        </div>
        <div><button class="text-light-blue-500 font-bold pb-2 text-sm">Post</button></div>
      </div>
    <!-- End Comment input section -->
    </div>
  </div>

</section>

Open assets/css/app.scss and add the following styles to the bottom of the file to not show the scrollbar on the comments section of the page:

/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
  display: none;
}

.no-scrollbar {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}

Show Post Page

Likes

Let's create the likes context, in our terminal:

$ mix phx.gen.context Likes Like likes user_id:references:users liked_id:integer

Inside the migration that was generated:

defmodule InstagramClone.Repo.Migrations.CreateLikes do
  use Ecto.Migration

  def change do
    create table(:likes) do
      add :liked_id, :integer
      add :user_id, references(:users, on_delete: :nothing)

      timestamps()
    end

    create index(:likes, [:user_id, :liked_id])
  end
end

Back in our terminal: $ mix ecto.migrate

Inside lib/instagram_clone/likes/like.ex:

defmodule InstagramClone.Likes.Like do
  use Ecto.Schema

  schema "posts_likes" do
    field :liked_id, :integer
    belongs_to :user, InstagramClone.Accounts.User

    timestamps()
  end
end

Add the likes relationship to the post schema, open lib/instagram_clone/posts/post.ex:


...

has_many :likes, InstagramClone.Likes.Like, foreign_key: :liked_id

...

Add the likes relationship to the user schema, open lib/instagram_clone/accounts/user.ex:


...

has_many :likes, InstagramClone.Likes.Like

...

Inside lib/instagram_clone/likes.ex:

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 = get_like(user_id, liked)
    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
  defp get_like(user_id, liked) do
    Enum.find(liked.likes, fn l ->
      l.user_id == user_id
    end)
  end
end

Let's create a component to handle likes, under lib/instagram_clone_web/live/post_live add a file named like_component.ex and add the following:

defmodule InstagramCloneWeb.PostLive.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 liked?(current_user.id, liked.likes) 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.id})
  end

  defp get_btn_status(socket, assigns) do
    if liked?(assigns.current_user.id, 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

  # Returns true if id found in list
  defp liked?(user_id, likes) do
    Enum.any?(likes, fn l ->
      l.user_id == user_id
    end)
  end

end

Inside lib/instagram_clone_web/live/post_live/show.html.leex on line 50, replace the div containing the heart icon with the following:


...

        <%= if @current_user do %>
          <%= live_component @socket,
              InstagramCloneWeb.PostLive.LikeComponent,
              id: @post.id,
              liked: @post,
              w_h: "w-8 h-8",
              current_user: @current_user %>
        <% else %>
          <%= link to: Routes.user_session_path(@socket, :new) do %>
            <button class="w-8 h-8 focus:outline-none">
              <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>
            </button>
          <% end %>
        <% end %>

...

Inside lib/instagram_clone_web/live/post_live/show.ex we need to handle the message sent from the component to update the likes count:

...

  alias InstagramCloneWeb.PostLive.LikeComponent

  @impl true
  def handle_info({LikeComponent, :update_post_likes, post_id}, socket) do
    {:noreply, 
      socket 
      |> assign(post: Posts.get_post!(post_id))}
  end

Open lib/instagram_clone/posts.ex and let's update get_post!() and get_post_by_url() functions to preload the user that belongs_to and the likes:


...
  def get_post!(id) do
    Repo.get!(Post, id)
    |> Repo.preload([:user, :likes])
  end

  def get_post_by_url!(id) do
    Repo.get_by!(Post, url_id: id)
    |> Repo.preload([:user, :likes])
  end

...

Post Comments

Let's create a comments context for comments, in our terminal type in the following command:

$ mix phx.gen.context Comments Comment comments post_id:references:posts user_id:references:users body:text total_likes:integer

Inside the migration that was generated:

defmodule InstagramClone.Repo.Migrations.CreateComments do
  use Ecto.Migration

  def change do
    create table(:comments) do
      add :body, :text
      add :total_likes, :integer, default: 0
      add :post_id, references(:posts, on_delete: :nothing)
      add :user_id, references(:users, on_delete: :nothing)

      timestamps()
    end

    create index(:comments, [:post_id])
    create index(:comments, [:user_id])
  end
end

Back in our terminal: $ mix ecto.migrate

Inside lib/instagram_clone/comments/comment.ex:

defmodule InstagramClone.Comments.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string
    field :total_likes, :integer, default: 0
    belongs_to :post, InstagramClone.Posts.Post
    belongs_to :user, InstagramClone.Accounts.User
    has_many :likes, InstagramClone.Likes.Like, foreign_key: :liked_id

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:body])
    |> validate_required([:body])
  end
end

Add the following inside lib/instagram_clone/accounts/user.ex and lib/instagram_clone/posts/post.ex:


...

  has_many :comments, InstagramClone.Comments.Comment

...

Inside lib/instagram_clone/comments.ex add the followings functions:


...
  @doc  """
  Returns paginated comments sorted by current user id or by id if public
  """
  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

    Comment
    |> where(post_id: ^post_id)
    |> get_post_comments_sorting(public, user)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> preload([:user, :likes])
    |> 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
    Repo.get!(Comment, id)
    |> Repo.preload([:user, :likes])
  end

  @doc """
  Creates a comment and updates total comments count in post
  Returns the comment created with likes preloaded
  """
  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}} ->
        comment |> Repo.preload(:likes)
    end
  end

...

Let's update lib/instagram_clone_web/live/post_live/show.ex to the following:

defmodule InstagramCloneWeb.PostLive.Show do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Posts
  alias InstagramClone.Uploaders.Avatar
  alias InstagramCloneWeb.PostLive.LikeComponent
  alias InstagramClone.Comments
  alias InstagramClone.Comments.Comment

  @impl true
  def mount(%{"id" => id}, session, socket) do
    socket = assign_defaults(session, socket)
    post = Posts.get_post_by_url!(URI.decode(id))

    {:ok,
      socket
      |> assign(changeset: Comments.change_comment(%Comment{}))
      |> assign(comments_section_update: "prepend")
      |> assign(post: post)
      |> assign(page: 1, per_page: 15)
      |> assign_comments()
      |> set_load_more_comments_btn(),
      temporary_assigns: [comments: []]}
  end

  defp assign_comments(socket) do
    current_user = socket.assigns.current_user

    if current_user do
      comments = Comments.list_post_comments(socket.assigns, public: false)
      socket |> assign(comments: comments)
    else
      comments = Comments.list_post_comments(socket.assigns, public: true)
      socket |> assign(comments: comments)
    end
  end

  defp set_load_more_comments_btn(socket) do
    post_total_comments = socket.assigns.post.total_comments
    per_page = socket.assigns.per_page

    if post_total_comments > per_page do
      socket |> assign(load_more_comments_btn: "flex")
    else
      socket |> assign(load_more_comments_btn: "hidden")
    end
  end

  @impl true
  def handle_info({LikeComponent, :update_comment_likes, comment_id}, socket) do
    comment = Comments.get_comment!(comment_id)
    {:noreply,
      socket
      |> update(:comments, fn comments -> [comment | comments] end)}
  end

  @impl true
  def handle_info({LikeComponent, :update_post_likes, post_id}, socket) do
    {:noreply,
      socket
      |> assign(post: Posts.get_post!(post_id))}
  end

  @impl true
  def handle_event("load-more-comments", _, socket) do
    {:noreply,
      socket
      |> assign(comments_section_update: "append")
      |> load_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(comments_section_update: "prepend")
        |> assign(changeset: Comments.change_comment(%Comment{}))}
    end
  end

  defp load_comments(socket) do
    total_comments = socket.assigns.post.total_comments
    page = socket.assigns.page
    per_page = socket.assigns.per_page
    total_pages = ceil(total_comments / per_page)

    socket
    |> hide_btn?(page, total_pages)
    |> update(:page, &(&1 + 1))
    |> assign_comments()
  end

  defp hide_btn?(socket, page, total_pages) do
    if (page + 1) == total_pages do
      socket |> assign(load_more_comments_btn: "hidden")
    else
      socket
    end
  end
end

Under lib/instagram_clone_web/live/post_live create the comment component comment_component.ex:

defmodule InstagramCloneWeb.PostLive.CommentComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
end

The comment component template under lib/instagram_clone_web/live/post_live/comment_component.html.leex:

<div class="flex py-2" id="comment-<%= @comment.id %>">
  <div class="w-1/12 pt-1">
    <%= live_redirect to: Routes.user_profile_path(@socket, :index, @comment.user.username) do %>
      <%= img_tag Avatar.get_thumb(@comment.user.avatar_url),
        class: "w-8 h-8 rounded-full object-cover object-center" %>
    <% end %>
  </div>
  <div class="px-4 w-10/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>
    </span>
    <div class="flex mt-3">
      <div class="text-gray-400 text-xs"><%= Timex.from_now @comment.inserted_at %></div>
      <button class="px-3 text-xs text-gray-700 focus:outline-none"><%= @comment.total_likes %> likes</button>
      <button class="text-xs text-gray-700 focus:outline-none">Reply</button>
    </div>
  </div>

  <%= if @current_user do %>
    <%= live_component @socket,
        InstagramCloneWeb.PostLive.LikeComponent,
        id: @comment.id,
        liked: @comment,
        w_h: "w-6 h-6",
        current_user: @current_user %>
  <% else %>
    <%= link to: Routes.user_session_path(@socket, :new) do %>
      <button class="w-6 h-6 focus:outline-none">
        <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>
      </button>
    <% end %>
  <% end %>
</div>

Lastly let's update lib/instagram_clone_web/live/post_live/show.html.leex :

<section class="flex">
  <!-- Post Image section -->
  <%= img_tag @post.photo_url,
          class: "w-3/5 object-contain h-full" %>
  <!-- End Post Image section -->

  <div class="w-2/5 border-2 h-full">
    <div class="flex p-4 items-center border-b-2">
      <!-- Post header section -->
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %>
        <%= img_tag @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>

    <div class="no-scrollbar h-96 overflow-y-scroll p-4 flex flex-col">
      <%= if @post.description do %>
        <!-- Description section -->
        <div class="flex mt-2">
          <%= 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="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 class="flex mt-3">
              <div class="text-gray-400 text-xs"><%= Timex.from_now @post.inserted_at %></div>
            </div>
          </div>

        </div>
      <!-- End Description Section -->
      <% end %>

      <section id="comments" phx-update="<%= @comments_section_update %>">
        <%= for comment <- @comments do %>
          <%= live_component @socket,
            InstagramCloneWeb.PostLive.CommentComponent,
            id: comment.id,
            current_user: @current_user,
            comment: comment %>
        <% end %>
      </section>

      <button
        class="w-full <%= @load_more_comments_btn %> justify-center pt-2 focus:outline-none"
        phx-click="load-more-comments">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </button>

    </div>

    <div class="w-full border-t-2">
      <!-- Action icons section -->
      <div class="flex pl-4 pr-2 pt-2">
        <%= if @current_user do %>
          <%= live_component @socket,
              InstagramCloneWeb.PostLive.LikeComponent,
              id: @post.id,
              liked: @post,
              w_h: "w-8 h-8",
              current_user: @current_user %>
        <% else %>
          <%= link to: Routes.user_session_path(@socket, :new) do %>
            <button class="w-8 h-8 focus:outline-none">
              <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>
            </button>
          <% end %>
        <% 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 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>
        <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>
      <h6 class="px-5 text-xs text-gray-400"><%= Timex.format!(@post.inserted_at, "{Mfull} {D}, {YYYY}") %></h6>
      <!-- End Description Section -->

      <!-- Comment input section -->
      <%= if @current_user do %>
        <%= f = form_for @changeset, "#",
          phx_submit: "save",
          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>
      <% else %>
        <div class="p-4 flex items-center mt-3 border-t-2 border-gray-100">
          <%= link "Log in to comment",
            to: Routes.user_session_path(@socket, :new),
            class: "text-light-blue-600" %>
        </div>
      <% end %>
      <!-- End Comment input section -->
    </div>
  </div>

</section>

We added a couple of AlpineJS directives to disable the submit button for comments when the textarea is empty.  

Show Post Page GIF

 

That's it for this part, we have learned a lot throughout this series, still a lot of work to do, development never ends.

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

 

CHECK OUT THE INSTAGRAM CLONE GITHUB REPO

Join The Elixir Army