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
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
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>
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
Top comments (6)
Very nice tutorials! Learning a lot! Question, shouldn't
defmodule InstagramClone.Uploaders.Avatar
bedefmodule InstagramCloneWeb.Uploaders.Avatar
?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.
Coming into Elixir from a pure Erlang background and I'm really enjoying following this
Glad you like it! Stay tuned for part 3, coming out soon!
Great job, man.
Anxiously waiting for part 3
Thanks! Part 3 almost done, I'm stuck on a bug, but I might drop it tomorrow.