DEV Community

Cover image for Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 5]
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 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.

Show-Post Page

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> 
Enter fullscreen mode Exit fullscreen mode

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 */ } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 ... 
Enter fullscreen mode Exit fullscreen mode

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

 ... has_many :likes, InstagramClone.Likes.Like ... 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 %> ... 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 ... 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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

 ... has_many :comments, InstagramClone.Comments.Comment ... 
Enter fullscreen mode Exit fullscreen mode

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 ... 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
hminy572 profile image
hminy572

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

is the file created in post_live folder for like_component's name "like_component" ?

like so

"""
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:
"""

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

That's correct!

Collapse
 
hminy572 profile image
hminy572

thanks for replying! now my code works good!

Collapse
 
hminy572 profile image
hminy572

this is a bit overwhelming for me, but im trying hard to understand everything, i really appreciate that you created this fabulous tutorial!

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

My pleasure, had a lot of fun building the project, take your time trying to understand it, don't rush it, enjoy the process of learning.