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

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

Bookmarks Functionality

In part 7 we added the search functionality in our top header navigation menu, in this part we will work on the bookmarks functionality and notify users when a following adds new posts to our homepage. You can catch up with the Instagram Clone GitHub Repo.

Let's handle errors when we try to create a new post with no image selected, to do that we need to pattern match correctly in our save handle function inside lib/instagram_clone_web/live/post_live/new.ex:

  def handle_event("save", %{"post" => post_params}, socket) do
    post = PostUploader.put_image_url(socket, %Post{})

    case Posts.create_post(post, post_params, socket.assigns.current_user) do
      {:ok, %{post: post}} -> # <- THIS LINE WAS UPDATED
        PostUploader.save(socket)

        {:noreply,
         socket
         |> put_flash(:info, "Post created successfully")
         |> push_redirect(to: Routes.user_profile_path(socket, :index, socket.assigns.current_user.username))}
         |> push_redirect(to: Routes.live_path(socket, InstagramCloneWeb.PostLive.Show, post.url_id))}

      {:error, :post, %Ecto.Changeset{} = changeset, %{}} -> # <- THIS LINE WAS UPDATED
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

Because we are using Ecto.Multi to update posts count to users and creating the post, in our result we have to pattern match accordingly.

Now inside lib/instagram_clone_web/live/post_live/new.html.leex on line 23 let's add a div to display the :photo_url error:

    <div class="flex justify-center">
      <%= error_tag f, :photo_url, class: "text-red-700 block" %>
    </div>

Every time that a new post is created we are going to use phoenix pubsub to send a message to the homepage liveview, so we can display a div that when clicked will reload the liveview. Inside lib/instagram_clone/posts.ex add the following:

  @pubsub_topic "new_posts_added"

  def pubsub_topic, do: @pubsub_topic

  def subscribe do
    InstagramCloneWeb.Endpoint.subscribe(@pubsub_topic)
  end

Inside lib/instagram_clone_web/live/post_live/new.ex let's send the message:

  def handle_event("save", %{"post" => post_params}, socket) do
    post = PostUploader.put_image_url(socket, %Post{})

    case Posts.create_post(post, post_params, socket.assigns.current_user) do
      {:ok, %{post: post}} -> # <- THIS LINE WAS UPDATED
        PostUploader.save(socket)

        send_msg(post)

        {:noreply,
         socket
         |> put_flash(:info, "Post created successfully")
         |> push_redirect(to: Routes.user_profile_path(socket, :index, socket.assigns.current_user.username))}
         |> push_redirect(to: Routes.live_path(socket, InstagramCloneWeb.PostLive.Show, post.url_id))}

      {:error, :post, %Ecto.Changeset{} = changeset, %{}} -> # <- THIS LINE WAS UPDATED
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

  defp send_msg(post) do
    # Broadcast that new post was added
    InstagramCloneWeb.Endpoint.broadcast_from(
      self(),
      Posts.pubsub_topic,
      "new_post",
      %{
        post: post
      }
    )
  end

Inside lib/instagram_clone_web/live/page_live.ex let's handle the message that's going to be sent:

  alias  InstagramClone.Posts.Post

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    if connected?(socket), do: Posts.subscribe

    {:ok,
      socket
      |> assign(page_title: "InstagraClone")
      |> assign(new_posts_added: false)
      |> assign(page: 1, per_page: 15),
      temporary_assigns: [user_feed: []]}
  end

  @impl true
  def handle_info(%{event: "new_post", payload: %{post: %Post{user_id: post_user_id}}}, socket) do
    if post_user_id in socket.assigns.following_list do
      {:noreply, socket |> assign(new_posts_added: true)}
    else
      {:noreply, socket}
    end
  end

In our mount function, we subscribe to the pubsub topic and assign a page title and new_posts_added to determine if we have to display the div in our template. In our handle_info we are receiving the message and pattern matching on the user to just get the user ID, then we check if that user ID is in our following list of the current user that is assigned to the socket, and making the new_posts_added true if it is in our following list.

Inside lib/instagram_clone_web/live/page_live.html.leex on line 2 add the following:

  <%= if @new_posts_added do %>
    <div class="flex justify-center w-3/5 sticky top-14">
      <%= live_redirect to: Routes.page_path(@socket, :index), class: "user-profile-follow-btn" do %>
        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
        </svg>
        Load New Posts
      <% end %>
    </div>
  <% end %>

  Alt Text  

Now when a user that we are following adds a new post, while we are on our homepage, we get notified.

Posts Bookmarks

Go to the terminal and let's create a schema to handle the posts bookmarks:

mix phx.gen.schema Posts.Bookmarks posts_bookmarks user_id:references:users post_id:references:posts

Inside the migration that was generated:

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

  def change do
    create table(:posts_bookmarks) do
      add :user_id, references(:users, on_delete: :delete_all)
      add :post_id, references(:posts, on_delete: :delete_all)

      timestamps()
    end

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

Inside lib/instagram_clone/posts/bookmarks.ex:

defmodule InstagramClone.Posts.Bookmarks do
  use Ecto.Schema

  schema "posts_bookmarks" do
    belongs_to :user, InstagramClone.Accounts.User
    belongs_to :post, InstagramClone.Posts.Post

    timestamps()
  end

end

Inside lib/instagram_clone/accounts/user.ex and lib/instagram_clone/posts/post.ex:

  has_many :posts_bookmarks, InstagramClone.Posts.Bookmarks

Update lib/instagram_clone/posts.ex to the following:

defmodule InstagramClone.Posts do
  @moduledoc """
  The Posts context.
  """

  import Ecto.Query, warn: false
  alias InstagramClone.Repo

  alias InstagramClone.Posts.Post
  alias InstagramClone.Accounts.User
  alias InstagramClone.Comments.Comment
  alias InstagramClone.Likes.Like
  alias InstagramClone.Posts.Bookmarks

  @pubsub_topic "new_posts_added"

  def pubsub_topic, do: @pubsub_topic

  def subscribe do
    InstagramCloneWeb.Endpoint.subscribe(@pubsub_topic)
  end
  @doc """
  Returns the list of posts.

  ## Examples

      iex> list_posts()
      [%Post{}, ...]

  """
  def list_posts do
    Repo.all(Post)
  end

  @doc """
  Returns the list of paginated posts of a given user id.

  ## Examples

      iex> list_user_posts(page: 1, per_page: 10, user_id: 1)
      [%{photo_url: "", url_id: ""}, ...]

  """
  def list_profile_posts(page: page, per_page: per_page, user_id: user_id) do
    Post
    |> select([p], map(p, [:url_id, :photo_url]))
    |> where(user_id: ^user_id)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> Repo.all
  end

  def list_saved_profile_posts(page: page, per_page: per_page, user_id: user_id) do
    Bookmarks
    |> where(user_id: ^user_id)
    |> join(:inner, [b], p in assoc(b, :post))
    |> select([b, p], %{url_id: p.url_id, photo_url: p.photo_url})
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> Repo.all
  end
  @doc """
  Returns the list of paginated posts of a given user id
  And posts of following list of given user id
  With user and likes preloaded
  With 2 most recent comments preloaded with user and likes
  User, page, and per_page are given with the socket assigns

  ## Examples

      iex> get_accounts_feed(following_list, assigns)
      [%{photo_url: "", url_id: ""}, ...]

  """
  def get_accounts_feed(following_list, assigns) do
    user = assigns.current_user
    page = assigns.page
    per_page = assigns.per_page
    query =
      from c in Comment,
      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]
    comments_query =
      from c in Comment,
      join: r in subquery(query),
      on: c.id == r.id and r.row_number <= 2
    likes_query = Like |> select([l], l.user_id)
    bookmarks_query = Bookmarks |> select([b], b.user_id)

    Post
    |> where([p], p.user_id in ^following_list)
    |> or_where([p], p.user_id == ^user.id)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> preload([:user, posts_bookmarks: ^bookmarks_query, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])
    |> Repo.all()
  end

  def get_accounts_feed_total(following_list, assigns) do
    user = assigns.current_user

    Post
    |> where([p], p.user_id in ^following_list)
    |> or_where([p], p.user_id == ^user.id)
    |> select([p], count(p.id))
    |> Repo.one()
  end

  @doc """
  Gets a single post.

  Raises `Ecto.NoResultsError` if the Post does not exist.

  ## Examples

      iex> get_post!(123)
      %Post{}

      iex> get_post!(456)
      ** (Ecto.NoResultsError)

  """
  def get_post!(id) do
    likes_query = Like |> select([l], l.user_id)
    bookmarks_query = Bookmarks |> select([b], b.user_id)

    Repo.get!(Post, id)
    |> Repo.preload([:user, posts_bookmarks: bookmarks_query, likes: likes_query])
  end

  def get_post_feed!(id) do
    query =
      from c in Comment,
      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]
    comments_query =
      from c in Comment,
      join: r in subquery(query),
      on: c.id == r.id and r.row_number <= 2
    likes_query = Like |> select([l], l.user_id)
    bookmarks_query = Bookmarks |> select([b], b.user_id)

    Post
    |> preload([:user, posts_bookmarks: ^bookmarks_query, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])
    |> Repo.get!(id)
  end

  def get_post_by_url!(id) do
    likes_query = Like |> select([l], l.user_id)
    bookmarks_query = Bookmarks |> select([b], b.user_id)

    Repo.get_by!(Post, url_id: id)
    |> Repo.preload([:user, posts_bookmarks: bookmarks_query, likes: likes_query])
  end

  @doc """
  Creates a post.

  ## Examples

      iex> create_post(%{field: value})
      {:ok, %Post{}}

      iex> create_post(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_post(%Post{} = post, attrs \\ %{}, user) do
    post = Ecto.build_assoc(user, :posts, put_url_id(post))
    changeset = Post.changeset(post, attrs)
    update_posts_count = from(u in User, where: u.id == ^user.id)

    Ecto.Multi.new()
    |> Ecto.Multi.update_all(:update_posts_count, update_posts_count, inc: [posts_count: 1])
    |> Ecto.Multi.insert(:post, changeset)
    |> Repo.transaction()
  end

  # Generates a base64-encoding 8 bytes
  defp put_url_id(post) do
    url_id = Base.encode64(:crypto.strong_rand_bytes(8), padding: false)

    %Post{post | url_id: url_id}
  end

  @doc """
  Updates a post.

  ## Examples

      iex> update_post(post, %{field: new_value})
      {:ok, %Post{}}

      iex> update_post(post, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a post.

  ## Examples

      iex> delete_post(post)
      {:ok, %Post{}}

      iex> delete_post(post)
      {:error, %Ecto.Changeset{}}

  """
  def delete_post(%Post{} = post) do
    Repo.delete(post)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking post changes.

  ## Examples

      iex> change_post(post)
      %Ecto.Changeset{data: %Post{}}

  """
  def change_post(%Post{} = post, attrs \\ %{}) do
    Post.changeset(post, attrs)
  end

  # Returns nil if not found
  def bookmarked?(user_id, post_id) do
    Repo.get_by(Bookmarks, [user_id: user_id, post_id: post_id])
  end

  def create_bookmark(user, post) do
    user = Ecto.build_assoc(user, :posts_bookmarks)
    post = Ecto.build_assoc(post, :posts_bookmarks, user)

    Repo.insert(post)
  end

  def unbookmark(bookmarked?) do
    Repo.delete(bookmarked?)
  end

  def count_user_saved(user) do
    Bookmarks
    |> where(user_id: ^user.id)
    |> select([b], count(b.id))
    |> Repo.one
  end
end

The following functions were added:

  • list_saved_profile_posts/3 to get all the paginated saved posts.

  • bookmarked?/2 to check if a bookmark exists.

  • create_bookmark/2 to create a bookmark.

  • unbookmark/1 to delete a bookmark.

  • count_user_saved/1 to get total saved posts for given user.

Also to all the get posts functions, we are preloading posts bookmarks list, so we can send that list to the bookmarks component to set the button that we are going to use for the functionality.

Inside lib/instagram_clone_web/live/post_live/ create a file named bookmark_component.ex and add the following:

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

  alias InstagramClone.Posts

  @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="h-8 w-8 ml-auto focus:outline-none">

      <%= @icon %>

    </button>
    """
  end

  @impl true
  def handle_event("toggle-status", _params, socket) do
    current_user = socket.assigns.current_user
    post = socket.assigns.post
    bookmarked? = Posts.bookmarked?(current_user.id, post.id)

    if bookmarked? do
      unbookmark(socket, bookmarked?)
    else
      bookmark(socket, current_user, post)
    end
  end

  defp unbookmark(socket, bookmarked?) do
    Posts.unbookmark(bookmarked?)

    {:noreply,
      socket
      |> assign(icon: bookmark_icon(socket.assigns))}
  end

  defp bookmark(socket, current_user, post) do
    Posts.create_bookmark(current_user, post)

    {:noreply,
      socket
      |> assign(icon: bookmarked_icon(socket.assigns))}
  end

  defp get_btn_status(socket, assigns) do
    if assigns.current_user.id in assigns.post.posts_bookmarks do
      get_socket_assigns(socket, assigns, bookmarked_icon(assigns))
    else
      get_socket_assigns(socket, assigns, bookmark_icon(assigns))
    end
  end

  defp get_socket_assigns(socket, assigns, icon) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign(icon: icon)}
  end

  defp bookmark_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="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
    </svg>
    """
  end

  defp bookmarked_icon(assigns) do
    ~L"""
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
      <path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
    </svg>
    """
  end

end

Inside lib/instagram_clone_web/live/post_live/show.html.leex on line 94 change the div with the bookmark icon to the following:

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

Inside lib/instagram_clone_web/live/page_post_feed_component.html.leex on line 41, change the div containing the bookmark icon to the following:

      <%= live_component @socket,
              InstagramCloneWeb.PostLive.BookmarkComponent,
              id: @post.id,
              post: @post,
              current_user: @current_user %>

Inside lib/instagram_clone_web/router.ex on line 72 add the following route:

     live "/:username/saved", UserLive.Profile, :saved

Inside lib/instagram_clone_web/live/header_nav_component.html.leex on line 102:

              <%= live_redirect to: Routes.user_profile_path(@socket, :saved, @current_user.username) do %>
                <li class="py-2 px-4 hover:bg-gray-50">Saved</li>
              <% end %>

Update lib/instagram_clone_web/live/user_live/profile.ex to the following:

defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Accounts
  alias InstagramCloneWeb.UserLive.FollowComponent
  alias InstagramClone.Posts

  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)

    {:ok,
      socket
      |> assign(page: 1, per_page: 15)
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})"),
      temporary_assigns: [posts: []]}
  end

  defp assign_posts(socket) do
    socket
    |> assign(posts:
      Posts.list_profile_posts(
        page: socket.assigns.page,
        per_page: socket.assigns.per_page,
        user_id: socket.assigns.user.id
      )
    )
  end

  defp assign_saved_posts(socket) do
    socket
    |> assign(posts:
      Posts.list_saved_profile_posts(
        page: socket.assigns.page,
        per_page: socket.assigns.per_page,
        user_id: socket.assigns.user.id
      )
    )
  end

  @impl true
  def handle_event("load-more-profile-posts", _, socket) do
    {:noreply, socket |> load_posts}
  end

  defp load_posts(socket) do
    total_posts = get_total_posts_count(socket)
    page = socket.assigns.page
    per_page = socket.assigns.per_page
    total_pages = ceil(total_posts / per_page)

    if page == total_pages do
      socket
    else
      socket
      |> update(:page, &(&1 + 1))
      |> get_posts()
    end
  end

  defp get_total_posts_count(socket) do
    if socket.assigns.saved_page? do
      Posts.count_user_saved(socket.assigns.user)
    else
      socket.assigns.user.posts_count
    end
  end

  defp get_posts(socket) do
    if socket.assigns.saved_page? do
      assign_saved_posts(socket)
    else
      assign_posts(socket)
    end
  end

  @impl true
  def handle_params(_params, _uri, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action)}
  end

  @impl true
  def handle_info({FollowComponent, :update_totals, updated_user}, socket) do
    {:noreply, apply_msg_action(socket, socket.assigns.live_action, updated_user)}
  end

  defp apply_msg_action(socket, :follow_component, updated_user) do
    socket |> assign(user: updated_user)
  end

  defp apply_msg_action(socket, _, _updated_user) do
    socket
  end

  defp apply_action(socket, :index) do
    selected_link_styles = "text-gray-600 border-t-2 border-black -mt-0.5"
    live_action = get_live_action(socket.assigns.user, socket.assigns.current_user)

    socket
    |> assign(selected_index: selected_link_styles)
    |> assign(selected_saved: "text-gray-400")
    |> assign(saved_page?: false)
    |> assign(live_action: live_action)
    |> show_saved_profile_link?()
    |> assign_posts()
  end

  defp apply_action(socket, :saved) do
    selected_link_styles = "text-gray-600 border-t-2 border-black -mt-0.5"

    socket
    |> assign(selected_index: "text-gray-400")
    |> assign(selected_saved: selected_link_styles)
    |> assign(live_action: :edit_profile)
    |> assign(saved_page?: true)
    |> show_saved_profile_link?()
    |> redirect_when_not_my_saved()
    |> assign_saved_posts()
  end

  defp apply_action(socket, :following) do
    following = Accounts.list_following(socket.assigns.user)
    socket |> assign(following: following)
  end

  defp apply_action(socket, :followers) do
    followers = Accounts.list_followers(socket.assigns.user)
    socket |> assign(followers: followers)
  end

  defp redirect_when_not_my_saved(socket) do
    username = socket.assigns.current_user.username

    if socket.assigns.my_saved? do
      socket
    else
      socket
      |> push_redirect(to: Routes.user_profile_path(socket, :index, username))
    end
  end

  defp show_saved_profile_link?(socket) do
    user = socket.assigns.user
    current_user = socket.assigns.current_user

    if current_user && current_user.id == user.id do
      socket |> assign(my_saved?: true)
    else
      socket |> assign(my_saved?: false)
    end
  end

  defp get_live_action(user, current_user) do
    cond do
      current_user && current_user.id == user.id -> :edit_profile
      current_user -> :follow_component
      true -> :login_btn
    end
  end

end

The following functions were added:

  • assign_posts/1 to get and assigned profile saved posts.

  • apply_action(socket, :saved) to assign saved posts when saved route page, and live_action is assigned to :edit_profile to display the edit profile button.

  • redirect_when_not_my_saved/1 redirects when trying to go directly to a profile saved that doesn't belong to current user.

  • show_saved_profile_link?/1 assigns my_saved? if current user owns profile.

  • get_total_posts_count/1 to determine which total posts count we have to get.

  • get_posts/1 to determine which posts to get.

We are no longer assigning posts in our mount function, it is done in our index and saved actions. Also, in those functions we are assigning the links styles and saved_page? to determine which posts we have to load more when the hook in our footer gets triggered.

Update lib/instagram_clone_web/live/user_live/profile.html.leex to the following:

<%= if @live_action == :following do %>
  <%= live_modal @socket, InstagramCloneWeb.UserLive.Profile.FollowingComponent,
    width: "w-1/4",
    current_user: @current_user,
    following: @following,
    return_to: Routes.user_profile_path(@socket, :index, @user.username) %>
<% end %>

<%= if @live_action == :followers do %>
  <%= live_modal @socket, InstagramCloneWeb.UserLive.Profile.FollowersComponent,
    width: "w-1/4",
    current_user: @current_user,
    followers: @followers,
    return_to: Routes.user_profile_path(@socket, :index, @user.username) %>
<% end %>

<header class="flex justify-center px-10">
  <!-- Profile Picture Section -->
  <section class="w-1/4">
      <%= img_tag @user.avatar_url,
          class: "w-40 h-40 rounded-full object-cover object-center" %>
  </section>
  <!-- END Profile Picture Section -->

  <!-- Profile Details Section -->
  <section class="w-3/4">
    <div class="flex px-3 pt-3">
        <h1 class="truncate md:overflow-clip text-2xl md:text-2xl text-gray-500 mb-3"><%= @user.username %></h1>
        <span class="ml-11">
          <%= if @live_action == :edit_profile do %>
            <%= live_patch "Edit Profile",
                to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings),
                class: "py-1 px-2 border-2 rounded font-semibold hover:bg-gray-50" %>
          <% end %>

          <%= if @live_action == :follow_component do %>
            <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                user: @user,
                current_user: @current_user %>
          <% end %>

          <%= if @live_action == :login_btn do %>
            <%= link "Follow", to: Routes.user_session_path(@socket, :new), class: "user-profile-follow-btn" %>
          <% end %>
        </span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b><%= @user.posts_count %></b> Posts</li>
          <%= live_patch to: Routes.user_profile_path(@socket, :followers, @user.username) do %>
            <li class="ml-11"><b><%= @user.followers_count %></b> Followers</li>
          <% end %>
          <%= live_patch to: Routes.user_profile_path(@socket, :following, @user.username) do %>
            <li class="ml-11"><b><%= @user.following_count %></b> Following</li>
          <% end %>
      </ul>
    </div>

    <div class="p-3">
      <h2 class="text-md text-gray-600 font-bold"><%= @user.full_name %></h2>
      <%= if @user.bio do %>
        <p class="max-w-full break-words"><%= @user.bio %></p>
      <% end %>
      <%= if @user.website do %>
        <%= link display_website_uri(@user.website),
          to: @user.website,
          target: "_blank", rel: "noreferrer",
          class: "text-blue-700" %>
      <% end %>
    </div>
  </section>
  <!-- END Profile Details Section -->
</header>

<section class="border-t-2 mt-5">
  <ul class="flex justify-center text-center space-x-20">
    <%= live_redirect to: Routes.user_profile_path(@socket, :index, @user.username) do %>
      <li class="pt-4 px-1 text-sm <%= @selected_index %>">
        POSTS
      </li>
    <% end %>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <%= if @my_saved? do %>
      <%= live_redirect to: Routes.user_profile_path(@socket, :saved, @user.username) do %>
        <li class="pt-4 px-1 text-sm <%= @selected_saved %>">
          SAVED
        </li>
      <% end %>
    <% end %>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>

<!-- Gallery Grid -->
<div id="posts" phx-update="append" class="mt-9 grid gap-8 grid-cols-3">
  <%= for post <- @posts do %>
    <%= live_redirect img_tag(post.photo_url, class: "object-cover h-80 w-full"),
      id: post.url_id,
      to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, post.url_id) %>
  <% end %>
</div>

<div
  id="profile-posts-footer"
  class="flex justify-center"
  phx-hook="ProfilePostsScroll">

  <svg class="animate-spin mr-3 h-8 w-8 text-gray-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  </svg>
  Loading...

</div>

Posts and saved links were added, saved will only be displayed if the current user owns the profile, and we added a loading icon to our load more footer.

  Alt Text  

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

 

CHECK OUT THE INSTAGRAM CLONE GITHUB REPO

Join The Elixir Army