应用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序

<!--more-->

在第 7 局部中,咱们在顶部题目导航菜单中增加了搜寻性能,在这部分中,咱们将钻研书签性能,并在以下内容向咱们的主页增加新帖子时告诉用户。您能够赶上Instagram 克隆 GitHub Repo。

当咱们尝试创立未抉择图像的新帖子时,让咱们处理错误,为此,咱们须要在外部的保留句柄函数中正确进行模式匹配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

因为咱们用来Ecto.Multi更新用户的帖子计数并创立帖子,所以在后果中咱们必须进行相应的模式匹配。

当初,在lib/instagram_clone_web/live/post_live/new.html.leex第 23 行中增加一个 div 来显示谬误:photo_url

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

每次创立新帖子时,咱们都会应用 phoenix pubsub 向主页实时视图发送音讯,这样咱们就能够显示一个 div,单击该 div 将从新加载实时视图。外面lib/instagram_clone/posts.ex增加以下内容:

 @pubsub_topic "new_posts_added"  def pubsub_topic, do: @pubsub_topic  def subscribe do    InstagramCloneWeb.Endpoint.subscribe(@pubsub_topic)  end

在外面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)                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

在外面lib/instagram_clone_web/live/page_live.ex让咱们解决将要发送的音讯:

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

在咱们的 mount 函数中,咱们订阅 pubsub 主题并调配页面题目,并new_posts_added确定是否必须在模板中显示 div。在咱们的例子中handle_info,咱们接管用户的音讯和模式匹配以获取用户 ID,而后查看该用户 ID 是否在调配给套接字的以后用户的以下列表中,如果是,则设为new_posts_addedtrue在咱们上面的列表中。

在第 2 行中lib/instagram_clone_web/live/page_live.html.leex增加以下内容:

 <%= 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 %>

当初,当咱们关注的用户在咱们的主页上增加新帖子时,咱们会收到告诉。

帖子书签

转到终端,让咱们创立一个架构来解决帖子书签:

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

在生成的迁徙中:

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])  endend

外面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()  endend

外面lib/instagram_clone/accounts/user.ex和lib/instagram_clone/posts/post.ex

  has_many :posts_bookmarks, InstagramClone.Posts.Bookmarks

更新lib/instagram_clone/posts.ex如下:

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  endend

增加了以下性能:

  • list_saved_profile_posts/3获取所有已分页的已保留帖子。
  • bookmarked?/2查看书签是否存在。
  • create_bookmark/2创立书签。
  • unbookmark/1删除书签。
  • count_user_saved/1获取给定用户的已保留帖子总数。

另外,对于所有获取帖子性能,咱们正在预加载帖子书签列表,因而咱们能够将该列表发送到书签组件以设置咱们要用于该性能的按钮。

在外面lib/instagram_clone_web/live/post_live/创立一个名为的文件bookmark_component.ex并增加以下内容:

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

在第 94即将lib/instagram_clone_web/live/post_live/show.html.leex带有书签图标的 div 更改为以下内容:

        <%= 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 %>

在第 41 行内lib/instagram_clone_web/live/page_post_feed_component.html.leex,将蕴含书签图标的 div 更改为以下内容:

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

在第 72 行外部lib/instagram_clone_web/router.ex增加以下路由:

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

第 102 行外部lib/instagram_clone_web/live/header_nav_component.html.leex

              <%= 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 %>

更新lib/instagram_clone_web/live/user_live/profile.ex如下:

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  endend

增加了以下性能:

  • assign_posts/1获取并调配个人资料保留的帖子。
  • apply_action(socket, :saved)在保留路线页面时调配保留的帖子,并live_action调配:edit_profile以显示编辑个人资料按钮。
  • redirect_when_not_my_saved/1当尝试间接转到不属于以后用户的已保留配置文件时重定向。
  • show_saved_profile_link?/1指定my_saved?以后用户是否领有配置文件。
  • get_total_posts_count/1以确定咱们必须取得的帖子总数。
  • get_posts/1以确定要获取哪些帖子。

咱们不再在挂载函数中调配帖子,而是在索引和保留的操作中实现。此外,在这些函数中,咱们调配链接款式,并saved_page?确定当页脚中的钩子被触发时咱们必须加载更多的帖子。

更新lib/instagram_clone_web/live/user_live/profile.html.leex如下:

<%= 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>

增加了帖子和保留的链接,仅当以后用户领有该配置文件时才会显示保留的链接,并且咱们在加载更多页脚中增加了一个加载图标。

转自:Elixirprogrammer