DEV Community

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

Posted on • Edited on • Originally published at elixirprogrammer.com

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

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.

User Settings

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

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

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

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

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

...
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

...
Enter fullscreen mode Exit fullscreen mode

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

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

...
Enter fullscreen mode Exit fullscreen mode

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

 

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

 
 
 

Join The Elixir Army

Top comments (6)

Collapse
 
johankool profile image
Johan Kool

Very nice tutorials! Learning a lot! Question, shouldn't defmodule InstagramClone.Uploaders.Avatar be defmodule InstagramCloneWeb.Uploaders.Avatar?

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

Not really, you're free to do so, but that module is just a helper to handle uploads, you don't even need to use the module you can just use functions inside the LiveView.

Collapse
 
leonardb profile image
leonardb

Coming into Elixir from a pure Erlang background and I'm really enjoying following this

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

Glad you like it! Stay tuned for part 3, coming out soon!

Collapse
 
dado_borsa profile image
eBorsa

Great job, man.
Anxiously waiting for part 3

Collapse
 
elixirprogrammer profile image
Anthony Gonzalez

Thanks! Part 3 almost done, I'm stuck on a bug, but I might drop it tomorrow.