DEV Community

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

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

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

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

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

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

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

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

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

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

 has_many :posts_bookmarks, InstagramClone.Posts.Bookmarks 
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

 live "/:username/saved", UserLive.Profile, :saved 
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

Top comments (5)

Collapse
 
volanar profile image
volanar

Thank you for the lessons! Are you planning to release lessons on developing a freelance platform for PETAL, like github.com/elixirprogrammer/lancer ?
I am interested in the topic of creating a freelance platform, since I am switching from PHP to Elixir and I want to write such a platform on Elixir with the PETAL stack. I think many people will be interested

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

Yes, rewriting my old projects is on my to-do list. I have to figure out how to find the time to do it.

Collapse
 
volanar profile image
volanar

Cool. There are very few interesting projects on this technology stack. I only know one github.com/bonfire-networks

Collapse
 
dasdaad profile image
dasdaad

Rapid increase in the number of subscribers on social networks! Our service gives you the opportunity to buy followers to increase your influence on instagram reels views. We guarantee the security and confidentiality of your data. Promote your account with our help and start getting more likes, comments and attention!

Collapse
 
kriss23132 profile image
kriss23132

where can I buy followers for Instagram?