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

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

User Settings

Subscribe to my newsletter and never miss my upcoming articles

In part 1 we got everything set up and with our base layout ready to go let's start working on user settings. You can catch up with the Instagram Clone GitHub Repo.

Let's start by creating our routes, open lib/instagram_clone_web/router.ex and let's add the followings 2 routes under :require_authenticated_user scope:

  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
  end

Then we need to create those liveview files, create a folder named user_live under lib/instagram_clone_web/live inside that folder add the following 4 files:

lib/instagram_clone_web/live/user_live/settings.ex lib/instagram_clone_web/live/user_live/settings.html.leex lib/instagram_clone_web/live/user_live/pass_settings.ex lib/instagram_clone_web/live/user_live/pass_settings.html.leex

In our navigation header, we need to link to that new route, open lib/instagram_clone_web/live/header_nav_component.html.leex on line 60 add the following to the Settings live_patch to:

<%= live_patch to:  Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings)  do  %>
  <li  class="py-2 px-4 hover:bg-gray-50">Settings</li>
<%  end  %>

Now when we visit that link we should have an error because the files are empty so open lib/instagram_clone_web/live/user_live/settings.ex and add the following:

defmodule  InstagramCloneWeb.UserLive.Settings  do

  use InstagramCloneWeb,  :live_view

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

    {:ok, socket}
  end

end

Now we should have a blank page just with the top nav bar, so let's go to work.

We are going to need the Accounts and User contexts, we will alias them and assign the changeset, our file should look like the following:

defmodule  InstagramCloneWeb.UserLive.Settings  do

  use InstagramCloneWeb,  :live_view

  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User

  @impl true
  def  mount(_params, session, socket) do
    socket =  assign_defaults(session, socket)
    changeset = Accounts.change_user(socket.assigns.current_user)

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

end

We need to add the change_user() function to our Accounts context, open lib/instagram_clone/accounts.ex and below change_user_registration() function add the following:

...

def  change_user(user, attrs \\  %{}) do
  User.registration_changeset(user, attrs,  register_user:  false)
end

...

Open lib/instagram_clone_web/live/user_live/settings.html.leex and let's add our form to our template:

<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}">

  <div class="w-full py-8">
    <!-- Profile Photo -->
    <div class="flex items-center">
      <div class="w-1/3">
        <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>
      </div>
      <div class="w-full pl-8">
        <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1>
      </div>
    </div>
    <!-- END PROFILE PHOTO -->

    <%= f = form_for @changeset, "#",
      phx_change: "validate",
      phx_submit: "save",
      class: "space-y-8 md:space-y-10" %>

      <div class="flex items-center">
        <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :full_name, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :username, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %>
          <%= error_tag f, :username, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :website, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :website, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :bio, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %>
          <%= error_tag f, :bio, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :email, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :email, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <label class="block w-1/3 font-semibold text-right"></label>
        <div class="w-full pl-8 pr-20">
          <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
        </div>
      </div>
    </form>
  </div>
</section>

We added the base layout of our form and the username heading is updated when you type in the username input with AlpineJs. Now we need to add the validate() and save() functions to our lib/instagram_clone_web/live/user_live/settings.ex liveview document, but let's first assign our :page_title to our mount function:

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    changeset = Accounts.change_user(socket.assigns.current_user)

    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(page_title: "Edit Profile")} #This was added
  end

Then open lib/instagram_clone_web/templates/layout/root.html.leex and update the page title suffix:

<%= live_title_tag assigns[:page_title]  ||  "InstagramClone",  suffix:  " · InstagramClone"  %>

Now let's add the functions to handle the form to our lib/instagram_clone_web/live/user_live/settings.ex:

  @impl true
  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      socket.assigns.current_user
      |> Accounts.change_user(user_params)
      |> Map.put(:action, :validate)

    {:noreply, socket |> assign(changeset: changeset)}
  end

  @impl true
  def handle_event("save", %{"user" => user_params}, socket) do
    case Accounts.update_user(socket.assigns.current_user, user_params) do
      {:ok, _user} ->
        {:noreply,
          socket
          |> put_flash(:info, "User updated successfully")
          |> push_redirect(to: Routes.live_path(socket, InstagramWeb.UserLive.Settings))}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :changeset, changeset)}
    end
  end

Now we need to add our update_user() function to our Accounts context:

...

  def update_user(user, attrs) do
    user
    |> User.registration_changeset(attrs, register_user: false)
    |> Repo.update()
  end

...

Our unique constraint for username is not working because we didn't add a unique index to our migration so let's do that now. In our terminal let's generate a migration $ mix ecto.gen.migration add_users_unique_username_index, then open the migration that was generated priv/repo/migrations/20210414220125_add_users_unique_username_index.exs and add the following:

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

  def change do
    create unique_index(:users, [:username])
  end
end

Then get back to our terminal and run the migration $ mix ecto.migrate

Now let's update our registration changeset in lib/instagram_clone/accounts/user.ex with unsafe_validate_unique(:username, InstagramClone.Repo):

...

  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) # --> This was added
    |> validate_length(:full_name, min: 4, max: 30)
    |> validate_email()
    |> validate_password(opts)
 end

...

Also while testing I realize that I made a mistake trying to delay the live validation with :timer.sleep(9000) in our lib/instagram_clone_web/live/page_live.ex so let's just remove that line from our validate() function because it creates conflicts with the form:

  @impl true
  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      %User{}
      |> User.registration_changeset(user_params)
      |> Map.put(:action, :validate)
    #:timer.sleep(9000) <-- REMOVE THIS LINE
    {:noreply, socket |> assign(changeset: changeset)}
  end

 

Alt Text  

With that done we should be able to edit the profile with no problem, so now let's work on the avatar file upload.

Avatar Uploads

Open lib/instagram_clone_web/live/user_live/settings.ex and let's allow uploads in our liveview, the new updated file should look like the following:

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

  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User
  #Files extensions accepted to be uploaded
  @extension_whitelist ~w(.jpg .jpeg .png)

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    changeset = Accounts.change_user(socket.assigns.current_user)

    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(page_title: "Edit Profile")
      |> allow_upload(:avatar_url,
      accept: @extension_whitelist,
      max_file_size: 9_000_000,
      progress: &handle_progress/3,#Function that will handle automatic uploads
      auto_upload: true)}
  end

  @impl true
  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      socket.assigns.current_user
      |> Accounts.change_user(user_params)
      |> Map.put(:action, :validate)

    {:noreply, socket |> assign(changeset: changeset)}
  end
  # Updates the socket when the upload form changes, triguers handle_progress()
  def handle_event("upload_avatar", _params, socket) do
    {:noreply, socket}
  end

  @impl true
  def handle_event("save", %{"user" => user_params}, socket) do
    case Accounts.update_user(socket.assigns.current_user, user_params) do
      {:ok, _user} ->
        {:noreply,
          socket
          |> put_flash(:info, "User updated successfully")
          |> push_redirect(to: Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings))}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :changeset, changeset)}
    end
  end
  # This will handle the upload
  defp handle_progress(:avatar_url, entry, socket) do

  end
end

Open lib/instagram_clone_web/live/user_live/settings.html.leex and let's add our upload form below our username heading, with the @uploads that is being assigned to our socket:

<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}">

  <div class="w-full py-8">
    <%= for {_ref, err} <- @uploads.avatar_url.errors do %>
      <p class="text-red-500 w-full text-center">
        <%= Phoenix.Naming.humanize(err) %>
      </p>
    <% end %>
    <!-- Profile Photo -->
    <div class="flex items-center">
      <div class="w-1/3">
        <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>
      </div>
      <div class="w-full pl-8">
        <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1>

        <!-- THIS WAS ADDED -->
        <div  class="relative">
          <%= form_for @changeset,  "#",
            phx_change:  "upload_avatar"  %>
            <%= live_file_input @uploads.avatar_url,  class:  "cursor-pointer relative block opacity-0 z-40 -left-24"  %>
            <div  class="text-center absolute top-0 left-0 m-auto">
              <span  class="font-semibold text-sm text-light-blue-500">
                Change Profile Photo
              </span>
            </div>
          </form>
        </div>
        <!-- THIS WAS ADDED END -->

      </div>
    </div>
    <!-- END PROFILE PHOTO -->

    <%= f = form_for @changeset, "#",
      phx_change: "validate",
      phx_submit: "save",
      class: "space-y-8 md:space-y-10" %>

      <div class="flex items-center">
        <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :full_name, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :username, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %>
          <%= error_tag f, :username, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :website, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :website, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :bio, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %>
          <%= error_tag f, :bio, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :email, class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %>
          <%= error_tag f, :email, class:  "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <label class="block w-1/3 font-semibold text-right"></label>
        <div class="w-full pl-8 pr-20">
          <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
        </div>
      </div>
    </form>
  </div>
</section>

Now let's create a module that would help us handle avatar uploads. Under lib/instagram_clone_web/live add a folder named uploaders, and inside that folder add a file named avatar.ex . We're going to be resizing the avatars so let's add the Mogrify dependency to handle it, make sure to have ImageMagick installed, open mix.exs and add to our project dependencies {:mogrify, "~> 0.8.0"}, then in our terminal $ mix deps.get && mix deps.compile.

Now open lib/instagram_clone_web/live/uploaders/avatar.ex and add the following:

defmodule InstagramClone.Uploaders.Avatar do
  alias InstagramCloneWeb.Router.Helpers, as: Routes

  # We are going to upload locally so this would be the name of the folder
  @upload_directory_name "uploads"
  @upload_directory_path "priv/static/uploads"

  # Returns the extensions associated with a given MIME type.
  defp ext(entry) do
    [ext | _] = MIME.extensions(entry.client_type)
    ext
  end

  # Returns the url path
  def get_avatar_url(socket, entry) do
    Routes.static_path(socket, "/#{@upload_directory_name}/#{entry.uuid}.#{ext(entry)}")
  end

  def update(socket, old_url, entry) do
    # Creates the upload directry path if not exists 
    if !File.exists?(@upload_directory_path), do: File.mkdir!(@upload_directory_path)

    # Consumes an individual uploaded entry
    Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{} = meta ->
      # Destination paths for avatar thumbs
      dest = Path.join(@upload_directory_path, "#{entry.uuid}.#{ext(entry)}")
      dest_thumb = Path.join(@upload_directory_path, "thumb_#{entry.uuid}.#{ext(entry)}")

      # meta.path is the temporary file path
      mogrify_thumbnail(meta.path, dest, 300)
      mogrify_thumbnail(meta.path, dest_thumb, 150)

      # Removes Old Urls Paths
      rm_file(old_url)
      old_url |> get_thumb() |> rm_file()
    end)

    :ok
  end

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

  def rm_file(old_avatar_url) do
    url = String.replace_leading(old_avatar_url, "/uploads/", "")
    path = [@upload_directory_path, url] |> Path.join()

    if File.exists?(path),  do: File.rm!(path)
  end

  # Resize the file with a given path, destination, and size
  defp mogrify_thumbnail(src_path, dst_path, size) do
    try do
      Mogrify.open(src_path)
      |> Mogrify.resize_to_limit("#{size}x#{size}")
      |> Mogrify.save(path: dst_path)
    rescue
      File.Error -> {:error, :invalid_src_path}
      error -> {:error, error}
    else
      _image -> {:ok, dst_path}
    end
  end

end

Open lib/instagram_clone_web/live/user_live/settings.ex alias your newly created Avatar module at the top of our file alias InstagramClone.Uploaders.Avatar and update our handle_progress() function with the following:

  defp handle_progress(:avatar_url, entry, socket) do
    # If file is already uploaded to tmp folder
    if entry.done? do
      avatar_url = Avatar.get_avatar_url(socket, entry)
      user_params = %{"avatar_url" => avatar_url}
      case Accounts.update_user(socket.assigns.current_user, user_params) do
        {:ok, _user} ->
          Avatar.update(socket, socket.assigns.current_user.avatar_url, entry)
          @doc """
            We have to update the current user and assign it back to the socket 
            to get the header nav thumbnail automatically updated
          """
          current_user = Accounts.get_user!(socket.assigns.current_user.id)
          {:noreply,
            socket
            |> put_flash(:info, "Avatar updated successfully")
            |> assign(current_user: current_user)}
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, :changeset, changeset)}
      end
    else
      {:noreply, socket}
    end
  end

Lastly, we need to serve the files from our uploads directory that's going to be created, open lib/instagram_clone_web/endpoint.ex and update line 27 in our Static Plug:

only:  ~w(css fonts images js favicon.ico robots.txt uploads)

Now everything should work just fine, but we are uploading a thumbnail so let's use it in our templates, open lib/instagram_clone_web/live/header_nav_component.html.leex and update line 43 to use our thumbnail URL instead:

<%= img_tag InstagramClone.Uploaders.Avatar.get_thumb(@current_user.avatar_url), class:  "w-full h-full object-cover object-center"  %>

Also open lib/instagram_clone_web/live/user_live/settings.html.leex and update line 7 to use our thumbnail also instead:

<%= img_tag Avatar.get_thumb(@current_user.avatar_url),  class:  "ml-auto w-10 h-10 rounded-full object-cover object-center"  %>

Password Change Settings

Now the only thing left is changing the password, we are going to need a side navbar for that, so let's create a component to handle it because it will get share with the password change LiveView. Under lib/instagram_clone_web/live/user_live add the following 2 files:

lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex

In lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex add the following:

defmodule  InstagramCloneWeb.UserLive.SettingsSidebarComponent  do
  use InstagramCloneWeb,  :live_component
end

To lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex add the following:

<div class="w-1/4 border-r-2">
  <ul>
    <%= live_patch content_tag(:li, "Edit Profile", class: "p-4 #{selected_link?(@current_uri_path,  @settings_path)}"),  to:  @settings_path %>
    <%= live_patch content_tag(:li, "Change Password", class: "p-4 #{selected_link?(@current_uri_path,  @pass_settings_path)}"),  to:  @pass_settings_path %>
  </ul>
</div>

Create a file named render_helpers.ex under lib/instagram_clone_web/live. Open lib/instagram_clone_web/live/render_helpers.ex and the following::

defmodule  InstagramCloneWeb.RenderHelpers  do

  def  selected_link?(current_uri, menu_link) when current_uri == menu_link do
    "border-l-2 border-black -ml-0.5 text-gray-900 font-semibold"
  end



  def  selected_link?(_current_uri,  _menu_link) do
    "hover:border-l-2 -ml-0.5 hover:border-gray-300 hover:bg-gray-50"
  end

end

Those functions will help us to get the right styles for our links in our side navbar. Now we need to make those functions available in our templates, open lib/instagram_clone_web.ex and update the view helpers function to the following:

  defp view_helpers do
    quote do
      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      # Import LiveView helpers (live_render, live_component, live_patch, etc)
      import Phoenix.LiveView.Helpers

      # Import basic rendering functionality (render, render_layout, etc)
      import Phoenix.View

      import InstagramCloneWeb.ErrorHelpers
      import InstagramCloneWeb.Gettext
      import InstagramCloneWeb.RenderHelpers # <-- THIS LINE WAS ADDED
      alias InstagramCloneWeb.Router.Helpers, as: Routes
    end
  end

Let's assign our paths to the socket, open lib/instagram_clone_web/live/user_live/settings.ex and in our mount add the following:

  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    changeset = Accounts.change_user(socket.assigns.current_user)
    # THIS WAS ADDED
    settings_path = Routes.live_path(socket, __MODULE__)
    pass_settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.PassSettings)


    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(page_title: "Edit Profile")
      |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)# <-- THIS WAS ADDED
      |> allow_upload(:avatar_url,
      accept: @extension_whitelist,
      max_file_size: 9_000_000,
      progress: &handle_progress/3,
      auto_upload: true)}
  end

Open lib/instagram_clone_web/live/user_live/settings.html.leex inside the section tag at the top just below the start of the tag, let's insert our component:

<%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,  
  settings_path:  @settings_path,  
  pass_settings_path:  @pass_settings_path,  
  current_uri_path:  @current_uri_path %>

Open lib/instagram_clone_web/live/user_live/pass_settings.ex add the following:

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

  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings)
    pass_settings_path = Routes.live_path(socket, __MODULE__)

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

Then open lib/instagram_clone_web/live/user_live/pass_settings.html.leex add the following:

<section class="border-2 flex">
  <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,
    settings_path: @settings_path,
    pass_settings_path: @pass_settings_path,
    current_uri_path: @current_uri_path %>
</section>

 

Alt Text

 

Let's add the form to lib/instagram_clone_web/live/user_live/pass_settings.html.leex :

<section class="border-2 flex">
  <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent,
    settings_path: @settings_path,
    pass_settings_path: @pass_settings_path,
    current_uri_path: @current_uri_path %>

  <div class="w-full py-5">
    <!-- Profile Photo -->
    <div class="flex items-center">
      <div class="w-1/3">
        <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>
      </div>
      <div class="w-full pl-8">
        <h1 class="font-semibold text-xl truncate text-gray-600"><%= @current_user.username %></h1>
      </div>
    </div>
    <!-- End Profile Photo -->
    <%= f = form_for @changeset, "#",
      phx_submit: "save",
      class: "space-y-5 md:space-y-8"  %>

      <div class="md:flex items-center">
        <%= label f, :old_password, "Old Password", class: "w-1/3 text-right font-semibold", for: "current_password_for_password" %>
        <div class="w-full pl-8 pr-20">
          <%= password_input f, :current_password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>
          <%= error_tag f, :current_password, class: "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <%= label f, :password, "New Password", class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= password_input f, :password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>
          <%= error_tag f, :password, class: "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="md:flex items-center">
        <%= label f, :password_confirmation, "Confirm New Password", class: "w-1/3 text-right font-semibold" %>
        <div class="w-full pl-8 pr-20">
          <%= password_input f, :password_confirmation, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %>
          <%= error_tag f, :password_confirmation, class: "text-red-700 text-sm block" %>
        </div>
      </div>

      <div class="flex items-center">
        <label class="w-1/3"></label>
        <div class="w-full pl-8 pr-20">
          <%= submit "Change Password", phx_disable_with: "Saving...", class: "py-1 px-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
        </div>
      </div>

      <div class="flex items-center">
        <label class="w-1/3"></label>
        <div class="w-full pl-8 pr-20 text-right">
          <%= link "Forgot Password?", to: Routes.user_reset_password_path(@socket, :new), class: "font-semibold text-xs hover:text-light-blue-600 text-light-blue-500 cursor-pointer hover:underline" %>
        </div>
      </div>
    </form>
  </div>
</section>

Finally update lib/instagram_clone_web/live/user_live/pass_settings.ex to the following:

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

  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User
  alias InstagramClone.Uploaders.Avatar

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings)
    pass_settings_path = Routes.live_path(socket, __MODULE__)
    user = socket.assigns.current_user

    {:ok,
      socket
      |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)
      |> assign(:page_title, "Change Password")
      |> assign(changeset: Accounts.change_user_password(user))}
  end

  @impl true
  def handle_event("save", %{"user" => params}, socket) do
    %{"current_password" => password} = params
    case Accounts.update_user_password(socket.assigns.current_user, password, params) do
      {:ok, _user} ->
        {:noreply,
          socket
          |> put_flash(:info, "Password updated successfully.")
          |> push_redirect(to: socket.assigns.pass_settings_path)}

      {:error, changeset} ->
        {:noreply, assign(socket, :changeset, changeset)}
    end
  end
end

Go to lib/instagram_clone/accounts.ex on line 208, and update update_user_password() to the following:

def  update_user_password(user, password, attrs) do
  user
  |> User.password_changeset(attrs)
  |> User.validate_current_password(password)
  |> Repo.update()
end

 

This is delightful, I'm really enjoying it. In the next part let's work on user's profiles. Let me know what you think in the comments down below, I really appreciate your time, thank you so much for reading.

 

CHECK OUT THE INSTAGRAM CLONE GITHUB REPO

 
Share this