Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 5]
Anthony Gonzalez
Posted on May 13, 2021
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>
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 */
}
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.
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
Posted on May 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.