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

在第 2 部分中,我们添加了编辑帐户和上传用户头像的功能,在这部分中,我们将处理用户的个人资料。您可以赶上Instagram 克隆 GitHub Repo。

lib/instagram_clone_web/router.ex:browser
scope "/", InstagramCloneWeb do
  pipe_through :browser
  
  live "/", PageLive,  :index
  live "/:username", UserLive.Profile # THIS LINE WAS ADDED
end
lib/instagram_clone_web/live/user_live
lib/instagram_clone_web/live/user_live/profile.exlib/instagram_clone_web/live/user_live/profile.html.leex
lib/instagram_clone_web/live/user_live/profile.ex
defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view
  
  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok,
      socket
      |> assign(username: username)}
  end

end
lib/instagram_clone_web/live/user_live/profile.html.leex
<h1 class="text-5xl"><%= @username %></h1>
lib/instagram_clone_web/live/header_nav_component.html.leex
<%= live_patch to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Profile, @current_user.username)  do  %>
lib/instagram_clone/accounts.exprofile()
...

  @doc """
  Gets the user with the given username param.
  """
  def profile(param) do
    Repo.get_by!(User, username: param)
  end

...
lib/instagram_clone_web/live/user_live/profile.ex
defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view
  
  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok, socket}
  end

end
lib/instagram_clone_web.exlive_view()
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {InstagramCloneWeb.LayoutView, "live.html"}

      unquote(view_helpers())
      import InstagramCloneWeb.LiveHelpers

      alias InstagramClone.Accounts.User
      alias InstagramClone.Accounts # <-- THIS LINE WAS ADDED
      @impl true
      def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do
        with %User{id: ^id} <- socket.assigns.current_user do
          {:noreply,
            socket
            |> redirect(to: "/")
            |> put_flash(:info, "Logged out successfully.")}
        else
          _any -> {:noreply, socket}
        end
      end
	  @doc """
	    Because we are calling this function in each liveview, 
	    and we needed access to the username param in our profile liveview, 
	    we updated this function for when the username param is present,
	    get the user and assign it along with page title to the socket
	  """
      @impl true
      def handle_params(params, uri, socket) do
        if Map.has_key?(params, "username") do
          %{"username" => username} = params
          user = Accounts.profile(username)
          {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)
          |> assign(user: user, page_title: "#{user.full_name} (@#{user.username})")}
        else
          {:noreply,
            socket
            |> assign(current_uri_path: URI.parse(uri).path)}
        end
      end
    end
  end

Repo.get_by!(可查询、子句、选项)

从查询中获取单个结果。Ecto.NoResultsError如果未找到记录或有多个条目,则引发。在生产中,当找不到记录时会出现 404 错误。

lib/instagram_clone_web/live/header_nav_component.htm.leex
<li  @click="open = false"  class="py-2 px-4 hover:bg-gray-50">Profile</li>
lib/instagram_clone_web/live/user_live/profile.html.leex
<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"><button class="py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500">Follow</button></span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <li class="ml-11"><b>0</b> Followers</li>
          <li class="ml-11"><b>0</b> Following</li>
      </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">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>
lib/instagram_clone_web/live/render_helpers.ex
  def display_website_uri(website) do
    website = website
    |> String.replace_leading("https://", "")
    |> String.replace_leading("http://", "")
    website
  end
lib/instagram_clone_web/live/user_live
lib/instagram_clone_web/live/user_live/follow_component.ex
defmodule InstagramCloneWeb.UserLive.FollowComponent do
  use InstagramCloneWeb, :live_component

  def render(assigns) do
    ~L"""
    <button
      phx-target="<%= @myself %>"
      phx-click="toggle-status"
      class="<%= @follow_btn_styles? %>"><%= @follow_btn_name? %></button>
    """
  end

  def handle_event("toggle-status", _params, socket) do
    follow_btn_name? = get_follow_btn_name?(socket.assigns.follow_btn_name?)
    follow_btn_styles? = get_follow_btn_styles?(socket.assigns.follow_btn_name?)

    :timer.sleep(200)
    {:noreply,
      socket
      |> assign(follow_btn_name?: follow_btn_name?)
      |> assign(follow_btn_styles?: follow_btn_styles?)}
  end

  defp get_follow_btn_name?(name) when name == "Follow" do
    "Unfollow"
  end
  defp get_follow_btn_name?(name) when name == "Unfollow" do
    "Follow"
  end

  defp get_follow_btn_styles?(name) when name == "Follow"  do
    "py-1 px-2 text-red-500 border-2 rounded font-semibold hover:bg-gray-50 focus:outline-none"
  end
  defp get_follow_btn_styles?(name) when name == "Unfollow" do
    "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 focus:outline-none"
  end
end

在我们的渲染函数中,我们只有按钮和一个将在组件内部获取句柄的单击函数。它有 2 个分配,一个用于按钮名称,另一个用于样式,这些将在我们的配置文件 LiveView 中设置,然后在我们的事件函数中,我们将它们分配回套接字。

lib/instagram_clone_web/live/user_live/profile.html.leex
<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">
	      <!-- THE BUTTON WAS REPLACED FOR THE COMPONENT -->
          <%= cond do %>
            <% @current_user && @current_user == @user -> %>
              <%= 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" %>

            <% @current_user -> %>
              <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                follow_btn_name?: @follow_btn_name?,
                follow_btn_styles?: @follow_btn_styles? %>

            <% true -> %>
              <%= link "Follow", to: Routes.user_session_path(@socket, :new), class: "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500" %>
          <% end %>
          <!-- ALL THIS UNTIL HERE WAS ADDED -->
        </span>
    </div>

    <div>
      <ul class="flex p-3">
          <li><b>0</b> Posts</li>
          <li class="ml-11"><b>0</b> Followers</li>
          <li class="ml-11"><b>0</b> Following</li>
      </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">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>
lib/instagram_clone_web/live/user_live/profile.ex
defmodule InstagramCloneWeb.UserLive.Profile do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Accounts

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

    get_assigns(socket, current_user, user)
  end

  defp get_follow_btn_name? do
    "Follow"
  end

  defp get_follow_btn_styles? do
    "py-1 px-5 border-none shadow rounded text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 focus:outline-none"
  end

  defp get_assigns(socket, current_user, user) do
    if current_user && current_user !== user do
      follow_btn_name? = get_follow_btn_name?()
      follow_btn_styles? = get_follow_btn_styles?()

      {:ok,
        socket
        |> assign(follow_btn_name?: follow_btn_name?)
        |> assign(follow_btn_styles?: follow_btn_styles?)}
    else
      {:ok, socket}
    end
  end

end

让我们创建一个关注模式来处理终端中的关注者:

$ mix phx.gen.schema Accounts.Follows accounts_follows follower_id:references:users followed_id:references:users

打开生成的迁移并添加以下内容:

defmodule  InstagramClone.Repo.Migrations.CreateAccountsFollows  do
  use Ecto.Migration
  
  def  change  do
    create table(:accounts_follows)  do
      add :follower_id,  references(:users,  on_delete:  :delete_all)
      add :followed_id,  references(:users,  on_delete:  :delete_all)

      timestamps()
    end
	
    create index(:accounts_follows,  [:follower_id])
    create index(:accounts_follows,  [:followed_id])
  end
end

另外,让我们在用户表中添加 2 个新字段,以跟踪总关注者和关注者,回到我们的终端:

$ mix ecto.gen.migration adds_follower_followings_count_to_users_table

打开生成的迁移并添加以下内容:

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

  def change do
    alter table(:users) do
      add :followers_count, :integer, default: 0
      add :following_count, :integer, default: 0
    end
  end
end
$ mix ecto.migrate
lib/instagram_clone/accounts/follows.ex
defmodule InstagramClone.Accounts.Follows do
  use Ecto.Schema

  alias InstagramClone.Accounts.User

  schema "accounts_follows" do
    belongs_to :follower, User
    belongs_to :followed, User

    timestamps()
  end

end
lib/instagram_clone/accounts/user.ex
  alias InstagramClone.Accounts.Follows

  @derive {Inspect,  except:  [:password]}
  schema "users"  do
    field :email,  :string
    field :password,  :string,  virtual:  true
    field :hashed_password,  :string
    field :confirmed_at,  :naive_datetime
    field :username,  :string
    field :full_name,  :string
    field :avatar_url,  :string,  default:  "/images/default-avatar.png"
    field :bio,  :string
    field :website,  :string
    field :followers_count, :integer, default: 0
    field :following_count, :integer, default: 0
    has_many :following, Follows,  foreign_key:  :follower_id
    has_many :followers, Follows,  foreign_key:  :followed_id
    timestamps()
  end

  def registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email, :password, :username, :full_name, :avatar_url, :bio, :website])
    |> validate_required([:username, :full_name])
    |> validate_length(:username, min: 5, max: 30)
    |> validate_format(:username, ~r/^[a-zA-Z0-9_.-]*$/, message: "Please use letters and numbers without space(only characters allowed _ . -)")
    |> unique_constraint(:username)
    |> unsafe_validate_unique(:username, InstagramClone.Repo)
    |> validate_length(:full_name, min: 4, max: 255)
    |> validate_format(:full_name,  ~r/^[a-zA-Z0-9 ]*$/,  message:  "Please use letters and numbers")
    |> validate_website_schemes()
    |> validate_website_authority()
    |> validate_email()
    |> validate_password(opts)
  end

  defp validate_website_schemes(changeset) do
    validate_change(changeset, :website, fn :website, website ->
      uri = URI.parse(website)
      if uri.scheme, do: check_uri_scheme(uri.scheme), else: [website: "Enter a valid website"]
    end)
  end

  defp validate_website_authority(changeset) do
    validate_change(changeset, :website, fn :website, website ->
      authority = URI.parse(website).authority
      if String.match?(authority, ~r/^[a-zA-Z0-9.-]*$/) do
        []
      else
        [website: "Enter a valid website"]
      end
    end)
  end

  defp check_uri_scheme(scheme) when scheme == "http", do: []
  defp check_uri_scheme(scheme) when scheme == "https", do: []
  defp check_uri_scheme(_scheme), do: [website: "Enter a valid website"]
lib/instagram_clone/accounts.exalias InstagramClone.Accounts.Follows
  @doc """
  Creates a follow to the given followed user, and builds
  user association to be able to preload the user when associations are loaded,
  gets users to update counts, then performs 3 Repo operations,
  creates the follow, updates user followings count, and user followers count,
  we select the user in our updated followers count query, that gets returned
  """
  def create_follow(follower, followed, user) do
    follower = Ecto.build_assoc(follower, :following)
    follow = Ecto.build_assoc(followed, :followers, follower)
    update_following_count = from(u in User, where: u.id == ^user.id)
    update_followers_count = from(u in User, where: u.id == ^followed.id, select: u)

    Ecto.Multi.new()
    |> Ecto.Multi.insert(:follow, follow)
    |> Ecto.Multi.update_all(:update_following, update_following_count, inc: [following_count: 1])
    |> Ecto.Multi.update_all(:update_followers, update_followers_count, inc: [followers_count: 1])
    |> Repo.transaction()
    |> case do
      {:ok,   %{update_followers: update_followers}} ->
        {1, user} = update_followers
        hd(user)
    end
  end

  @doc """
  Deletes following association with given user,
  then performs 3 Repo operations, to delete the association,
  update followings count, update and select followers count,
  updated followers count gets returned
  """
  def unfollow(follower_id, followed_id) do
    follow = following?(follower_id, followed_id)
    update_following_count = from(u in User, where: u.id == ^follower_id)
    update_followers_count = from(u in User, where: u.id == ^followed_id, select: u)

    Ecto.Multi.new()
    |> Ecto.Multi.delete(:follow, follow)
    |> Ecto.Multi.update_all(:update_following, update_following_count, inc: [following_count: -1])
    |> Ecto.Multi.update_all(:update_followers, update_followers_count, inc: [followers_count: -1])
    |> Repo.transaction()
    |> case do
      {:ok,   %{update_followers: update_followers}} ->
        {1, user} = update_followers
        hd(user)
    end
  end

  @doc """
  Returns nil if not found
  """
  def following?(follower_id, followed_id) do
    Repo.get_by(Follows, [follower_id: follower_id, followed_id: followed_id])
  end

  @doc """
  Returns all user's followings
  """
  def list_following(user) do
    user = user |> Repo.preload(:following)
    user.following |> Repo.preload(:followed)
  end

  @doc """
  Returns all user's followers
  """
  def list_followers(user) do
    user = user |> Repo.preload(:followers)
    user.followers |> Repo.preload(:follower)
  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

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok, socket}
  end

  @impl true
  def handle_info({FollowComponent, :update_totals, updated_user}, socket) do
    {:noreply, socket |> assign(user: updated_user)}
  end

end

我们将在组件内设置关注按钮,然后向父级实时视图发送消息以更新计数。

lib/instagram_clone_web/live/user_live/profile.html.leex
<%  @current_user ->  %>
  <%= live_component @socket,
    InstagramCloneWeb.UserLive.FollowComponent,
    id:  @user.id,
    user:  @user,
    current_user:  @current_user %>

<%  true  ->  %>
  <%= link "Follow",  to: Routes.user_session_path(@socket,  :new),  class:  "user-profile-follow-btn"  %>
  
assets/css/app.scss
/* This file is for your main application css. */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "../node_modules/nprogress/nprogress.css";

@layer components {
  .user-profile-unfollow-btn {
    @apply  py-1  px-2  text-red-500  border-2  rounded  font-semibold  hover:bg-gray-50
  }

  .user-profile-follow-btn {
    @apply  py-1  px-5  border-none  shadow  rounded  text-gray-50  hover:bg-light-blue-600  bg-light-blue-500
  }
}

/* Styles for handling buttons click events */
.while-submitting  {  display: none;  }

.phx-click-loading  {
  .while-submitting  {  display: inline;  }
  .btns  {  display: none;  }
}
lib/instagram_clone_web/live/user_live/follow_component.ex
defmodule InstagramCloneWeb.UserLive.FollowComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Accounts

  @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="focus:outline-none">

      <span class="while-submitting">
        <span class="<%= @follow_btn_styles %> inline-flex items-center transition ease-in-out duration-150 cursor-not-allowed">
          <svg class="animate-spin -ml-1 mr-3 h-5 w-5 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>
          Saving
        </span>
      </span>

      <span class="<%= @follow_btn_styles %>"><%= @follow_btn_name %><span>
    </button>
    """
  end

  @impl true
  def handle_event("toggle-status", _params, socket) do
    current_user = socket.assigns.current_user
    user = socket.assigns.user

    :timer.sleep(300)
    if Accounts.following?(current_user.id, user.id) do
      unfollow(socket, current_user.id, user.id)
    else
      follow(socket, current_user, user)
    end
  end

  defp get_btn_status(socket, assigns) do
    if Accounts.following?(assigns.current_user.id, assigns.user.id) do
      get_socket_assigns(socket, assigns, "Unfollow", "user-profile-unfollow-btn")
    else
      get_socket_assigns(socket, assigns, "Follow", "user-profile-follow-btn")
    end
  end

  defp get_socket_assigns(socket, assigns, btn_name, btn_styles) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign(follow_btn_name: btn_name)
      |> assign(follow_btn_styles: btn_styles)}
  end

  defp follow(socket, current_user, user) do
    updated_user = Accounts.create_follow(current_user, user, current_user)
    # Message sent to the parent liveview to update totals
    send(self(), {__MODULE__, :update_totals, updated_user})
    {:noreply,
      socket
      |> assign(follow_btn_name: "Unfollow")
      |> assign(follow_btn_styles: "user-profile-unfollow-btn")}
  end

  defp unfollow(socket, current_user_id, user_id) do
    updated_user = Accounts.unfollow(current_user_id, user_id)
    # Message sent to the parent liveview to update totals
    send(self(), {__MODULE__, :update_totals, updated_user})
    {:noreply,
      socket
      |> assign(follow_btn_name: "Follow")
      |> assign(follow_btn_styles: "user-profile-follow-btn")}
  end

end
lib/instagram_clone_web/live/user_live/profile.html.leex
<li class="ml-11"><b><%=  @user.followers_count %></b> Followers</li>
<li class="ml-11"><b><%=  @user.following_count %></b> Following</li>

一切都应该工作正常,但是出现了一个问题,您可以在下面的 gif 图片中看到它。

instagram-phoenix-p3-1.gif

live_patch/2@user
link/2redirect/2
live_redirect/2push_redirect/2
live_patch/2push_patch/2
live_redirect/2push_redirect/2
live_redirect/2lib/instagram_clone_web/live/header_nav_component.html.leex
<%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Profile,  @current_user.username)  do  %>
handle_params()live_patch/2mount()
lib/instagram_clone_web/live/user_live/profile.exmount()
  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)

    {:ok,
      socket
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end
lib/instagram_clone_web.exlive_view()handle_params()
      @impl true
      def handle_params(_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end
lib/instagram_clone_web/live/header_nav_component.htm.leex
<li class="py-2 px-4 hover:bg-gray-50">Profile</li>
live_patch/2
lib/instagram_clone_web/live/user_live/profile.ex
defp get_action(user, current_user) do
  cond do
    current_user && current_user == user -> :edit_profile
    current_user -> :follow_component
    true -> :login_btn
  end
end
lib/instagram_clone_web/live/user_live/profile.exmy_action
  @impl true
  def mount(%{"username" => username}, session, socket) do
    socket = assign_defaults(session, socket)
    user = Accounts.profile(username)
    my_action = get_action(user, socket.assigns.current_user)

    {:ok,
      socket
      |> assign(my_action: my_action)
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end
lib/instagram_clone_web/live/user_live/profile.html.leex
<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 @my_action in [: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 @my_action in [:follow_component] do %>
            <%= live_component @socket,
                InstagramCloneWeb.UserLive.FollowComponent,
                id: @user.id,
                user: @user,
                current_user: @current_user %>
          <% end %>

          <%= if @my_action in [: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>0</b> Posts</li>
          <li class="ml-11"><b><%= @user.followers_count %></b> Followers</li>
          <li class="ml-11"><b><%= @user.following_count %></b> Following</li>
      </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">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>
handle_params()
lib/instagram_clone_web.exhandle_params()live_view()
	  # DELETE THIS FUNCTION AND MOVE IT TO: 
	  #lib/instagram_clone_web/live/user_live/settings.ex
	  #lib/instagram_clone_web/live/user_live/pass_settings.ex
	  #lib/instagram_clone_web/live/page_live.ex
      @impl true
      def handle_params(_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end
current_uri_path
lib/instagram_clone_web/router.ex
	scope "/", InstagramCloneWeb do
	  pipe_through :browser
	  
	  live "/", PageLive,  :index
	  live "/:username", UserLive.Profile, :index # THIS LINE WAS UPDATED
	end

	scope "/", InstagramCloneWeb do
	  pipe_through [:browser, :require_authenticated_user]

	  get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
	  live "/accounts/edit", UserLive.Settings
	  live "/accounts/password/change", UserLive.PassSettings
	  live "/:username/following", UserLive.Profile, :following  # THIS LINE WAS ADDED
	  live "/:username/followers", UserLive.Profile, :followers # THIS LINE WAS ADDED
	end
lib/instagram_clone_web/live/header_nav_component.html.leex
<%= live_patch to: Routes.user_profile_path(@socket, :index, @current_user.username)  do  %>
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

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

    {:ok,
      socket
      |> assign(user: user)
      |> assign(page_title: "#{user.full_name} (@#{user.username})")}
  end

  @impl true
  def handle_params(_params, uri, socket) do
    socket = socket |> assign(current_uri_path: URI.parse(uri).path)
    {: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
	live_action = get_live_action(socket.assigns.user, socket.assigns.current_user)
	
    socket |> assign(live_action: live_action)
  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 get_live_action(user, current_user) do
    cond do
      current_user && current_user == user -> :edit_profile
      current_user -> :follow_component
      true -> :login_btn
    end
  end

end
lib/instagram_clone_web/live/user_live/profile.html.leex
<%= if @live_action == :following do %>
  <%= live_modal @socket, InstagramCloneWeb.UserLive.Profile.FollowingComponent,
    id: @user.id || :following,
    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,
    id: @user.id || :followers,
    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>0</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">
    <li class="pt-4 px-1 text-sm text-gray-600 border-t-2 border-black -mt-0.5">
       POSTS
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      IGTV
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      SAVED
    </li>
    <li class="pt-4 px-1 text-sm text-gray-400">
      TAGGED
    </li>
  </ul>
</section>
lib/instagram_clone_web/live/render_helpers.ex
  import Phoenix.LiveView.Helpers

      @doc """
  Renders a component inside the `LiveviewPlaygroundWeb.ModalComponent` component.

  The rendered modal receives a `:return_to` option to properly update
  the URL when the modal is closed.
  The rendered modal also receives a `:width` option for the style width

  ## Examples

      <%= live_modal @socket, LiveviewPlaygroundWeb.PostLive.FormComponent,
        id: @post.id || :new,
        width: "w-1/2",
        post: @post,
        return_to: Routes.post_index_path(@socket, :index) %>
  """
  def live_modal(socket, component, opts) do
    path = Keyword.fetch!(opts, :return_to)
    width = Keyword.fetch!(opts,  :width)
    modal_opts = [id: :modal, return_to: path, width: width, component: component, opts: opts]
    live_component(socket, InstagramCloneWeb.ModalComponent, modal_opts)
  end
lib/instagram_clone_web/live/modal_component.ex
defmodule InstagramCloneWeb.ModalComponent do
  use InstagramCloneWeb, :live_component

  @impl true
  def render(assigns) do
    ~L"""
    <div
      class="fixed top-0 left-0 flex items-center justify-center w-full h-screen bg-black bg-opacity-40 z-50"
      phx-capture-click="close"
      phx-window-keydown="close"
      phx-key="escape"
      phx-target="<%= @myself %>"
      phx-page-loading>

      <div class="<%= @width %> h-auto bg-white rounded-xl shadow-xl">
        <%= live_patch raw("&times;"), to: @return_to, class: "float-right text-gray-500 text-4xl px-4" %>
        <%= live_component @socket, @component, @opts %>
      </div>
    </div>
    """
  end

    @impl true
  def handle_event("close", _, socket) do
    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
  end
end
lib/instagram_clone_web/live/user_live/followers_component.ex
defmodule InstagramCloneWeb.UserLive.Profile.FollowersComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
end
lib/instagram_clone_web/live/user_live/followers_component.html.leex
<header class="bg-gray-50 p-2 border-b-2 rounded-t-xl">
  <h1 class="flex justify-center text-xl font-semibold">Followers</h1>
</header>

<%= for follow <- @followers do %>
  <div class="p-4">
    <div class="flex items-center">
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, follow.follower.username) do %>
        <%= img_tag Avatar.get_thumb(follow.follower.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>
      <% end %>

      <div class="ml-3">
        <%= live_redirect  follow.follower.username,
          to: Routes.user_profile_path(@socket, :index, follow.follower.username),
          class: "font-semibold text-sm truncate text-gray-700 hover:underline" %>
        <h6 class="font-semibold text-sm truncate text-gray-400">
          <%= follow.follower.full_name %>
        </h6>
      </div>
      <%= if @current_user !== follow.follower do %>
        <span class="ml-auto">
          <%= live_component @socket,
            InstagramCloneWeb.UserLive.FollowComponent,
            id: follow.follower.id,
            user: follow.follower,
            current_user: @current_user %>
        </span>
      <% end %>
    </div>

  </div>
<% end %>
lib/instagram_clone_web/live/user_live/following_component.ex
defmodule InstagramCloneWeb.UserLive.Profile.FollowingComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
end
lib/instagram_clone_web/live/user_live/following_component.html.leex
<header class="bg-gray-50 p-2 border-b-2 rounded-t-xl">
  <h1 class="flex justify-center text-xl font-semibold">Following</h1>
</header>
<%= for follow <- @following do %>
  <div class="p-4">
    <div class="flex items-center">
      <%= live_redirect to: Routes.user_profile_path(@socket, :index, follow.followed.username) do %>
        <%= img_tag Avatar.get_thumb(follow.followed.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>
      <% end %>

      <div class="ml-3">
        <%= live_redirect  follow.followed.username,
          to: Routes.user_profile_path(@socket, :index, follow.followed.username),
          class: "font-semibold text-sm truncate text-gray-700 hover:underline" %>
        <h6 class="font-semibold text-sm truncate text-gray-400">
          <%= follow.followed.full_name %>
        </h6>
      </div>
      <%= if @current_user !== follow.followed do %>
        <span class="ml-auto">
          <%= live_component @socket,
            InstagramCloneWeb.UserLive.FollowComponent,
            id: follow.followed.id,
            user: follow.followed,
            current_user: @current_user %>
        </span>
      <% end %>
    </div>
  </div>
<% end %>
lib/instagram_clone_web/live/uploaders/avatar.ex
  # This was added to return the default image when no avatar uploaded
  def get_thumb(avatar_url) when avatar_url == "/images/default-avatar.png" do
    avatar_url
  end

  def get_thumb(avatar_url) do
    file_name = String.replace_leading(avatar_url, "/uploads/", "")
    ["/#{@upload_directory_name}", "thumb_#{file_name}"] |> Path.join()
  end

instagram-phoenix-p3-2.gif

page_live
lib/instagram_clone_web/live/page_live.ex
defmodule InstagramCloneWeb.PageLive do
  use InstagramCloneWeb, :live_view

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    {:ok, socket}
  end

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

  defp apply_action(current_user) do
    if !current_user, do: :root_path
  end
end
lib/instagram_clone_web/live/page_live_component.ex
defmodule InstagramCloneWeb.PageLiveComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User

  @impl true
  def mount(socket) do
    changeset = Accounts.change_user_registration(%User{})
    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(trigger_submit: false)}
  end

  @impl true
  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      %User{}
      |> User.registration_changeset(user_params)
      |> Map.put(:action, :validate)
    {:noreply, socket |> assign(changeset: changeset)}
  end

  def handle_event("save", _, socket) do
    {:noreply, assign(socket, trigger_submit: true)}
  end
end
lib/instagram_clone_web/live/page_live_component.html.leex
  <%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
    phx_change: "validate",
    phx_submit: "save",
    phx_target: @myself, # <-- THIS LINE WAS ADDED
    phx_trigger_action: @trigger_submit,
    class: "flex flex-col space-y-4 w-full px-6" %>
lib/instagram_clone_web/live/page_live.html.leex
<%= if @current_user do %>
  <h1>User Logged In Homepage</h1>
<% else %>
  <%= live_component @socket,
    InstagramCloneWeb.PageLiveComponent,
    id: 1 %>
<% end %>
lib/instagram_clone_web/templates/layout/live.html.leex
<%= if @current_user do %>
  <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>

<% else %>
  <%= if @live_action !== :root_path do %>
    <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
  <% end %>
<% end %>
@curent_uri_pathlib/instagram_clone_web/live/user_live/profile.exhandle_params()
  @impl true
  def handle_params(_params, uri, socket) do
    socket = socket |> assign(current_uri_path: URI.parse(uri).path) # <-- DELETE THIS LINE
    {:noreply, apply_action(socket, socket.assigns.live_action)}
  end

制作这一部分比我预期的要困难,它变得有点具有挑战性,这个应用程序并不像我们想象的那么容易做好。我的完美主义战胜了我,这就是为什么我花了更长的时间才发布它,我对一些我必须弄清楚的事情一无所知,但毫无疑问,我很享受这个过程并学到了很多东西。在下一部分中,我们将处理用户的帖子。