Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 2]
Anthony Gonzalez
Posted on April 16, 2021
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
Posted on April 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.