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]

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

Keep up to date with any new courses we’re working on and receive exclusive offers and discounts when they launch.

Share on:
comments powered by Disqus