共计 22452 个字符,预计需要花费 57 分钟才能阅读完成。
应用 PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的 Instagram Web 应用程序
<!–more–>
在第 1 局部中,咱们已实现所有设置并筹备好根本布局,让咱们开始解决用户设置。您能够赶上 Instagram 克隆 GitHub Repo。
让咱们首先创立路由,关上 lib/instagram_clone_web/router.ex
并在范畴下增加以下 2 条路由:require_authenticated_user
:
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 |
而后咱们须要创立这些 liveview 文件,在该文件夹内创立一个名为user_live
under 的文件夹lib/instagram_clone_web/live
,增加以下 4 个文件:
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
在咱们的导航题目中,咱们须要链接到该新路线,lib/instagram_clone_web/live/header_nav_component.html.leex 在第 60 行关上,将以下内容增加到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 %> |
当初,当咱们拜访该链接时,咱们应该会呈现谬误,因为文件是空的,因而关上 lib/instagram_clone_web/live/user_live/settings.ex
并增加以下内容:
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 |
当初咱们应该有一个空白页面,只有顶部导航栏,所以让咱们开始工作吧。
咱们将须要 Accounts
和User
上下文,咱们将为它们增加别名并调配变更集,咱们的文件应如下所示:
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 |
咱们须要将 change_user()
函数增加到 Accounts
上下文中,关上 lib/instagram_clone/accounts.ex
并在 change_user_registration()
函数上面增加以下内容:
... | |
def change_user(user, attrs \\ %{}) do | |
User.registration_changeset(user, attrs, register_user: false) | |
end | |
... |
关上 lib/instagram_clone_web/live/user_live/settings.html.leex
并将表单增加到模板中:
<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> |
咱们增加了表单的根本布局,当您应用 AlpineJs 输出用户名时,用户名题目会更新。当初咱们须要将 validate()
和save()
函数增加到咱们的 lib/instagram_clone_web/live/user_live/settings.ex
实时查看文档中,但咱们首先将咱们调配 :page_title
给咱们的挂载函数:
@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 |
而后关上 lib/instagram_clone_web/templates/layout/root.html.leex
并更新页面题目后缀:
<%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: "· InstagramClone" %>
当初让咱们将解决表单的函数增加到咱们的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 |
当初咱们须要将 update_user()
函数增加到 Accounts
上下文中:
... | |
def update_user(user, attrs) do | |
user | |
|> User.registration_changeset(attrs, register_user: false) | |
|> Repo.update() | |
end | |
... |
咱们对用户名的惟一束缚不起作用,因为咱们没有在迁徙中增加惟一索引,所以咱们当初就这样做。在咱们的终端中,让咱们生成一个迁徙 $ mix ecto.gen.migration add_users_unique_username_index
,而后关上生成的迁徙priv/repo/migrations/20210414220125_add_users_unique_username_index.exs
并增加以下内容:
defmodule InstagramClone.Repo.Migrations.AddUsersUniqueUsernameIndex do | |
use Ecto.Migration | |
def change do | |
create unique_index(:users, [:username]) | |
end | |
end |
而后返回咱们的终端并运行迁徙$ mix ecto.migrate
当初让咱们更新咱们的注册变更 lib/instagram_clone/accounts/user.ex
集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 | |
... |
:timer.sleep(9000)
另外,在测试时,我意识到我在尝试提早实时验证时犯了一个谬误,lib/instagram_clone_web/live/page_live.ex
所以让咱们从 validate()
函数中删除该行,因为它会与表单产生抵触:
@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 |
实现后,咱们应该可能毫无问题地编辑配置文件,所以当初让咱们开始上传头像文件。
头像上传
关上 lib/instagram_clone_web/live/user_live/settings.ex
并容许在实时视图中上传,新的更新文件应如下所示:
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 |
关上 lib/instagram_clone_web/live/user_live/settings.html.leex
并在用户名题目下方增加上传表单,并将该表单 @uploads
调配给咱们的套接字:
<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> |
当初让咱们创立一个模块来帮忙咱们解决头像上传。在上面 lib/instagram_clone_web/live
增加一个名为 的文件夹 uploaders
,并在该文件夹内增加一个名为 的文件avatar.ex
。咱们将调整头像的大小,因而让咱们增加 Mogrify 依赖项来解决它,确保装置了 ImageMagick,关上mix.exs
并增加到咱们的我的项目依赖项{:mogrify, "~> 0.8.0"}
,而后在咱们的终端中$ mix deps.get && mix deps.compile
。
当初关上 lib/instagram_clone_web/live/uploaders/avatar.ex
并增加以下内容:
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 |
在文件顶部关上 lib/instagram_clone_web/live/user_live/settings.ex
新创建的模块的别名,并应用以下内容更新咱们的函数:Avataralias InstagramClone.Uploaders.Avatarhandle_progress()
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 |
最初,咱们须要提供上传目录中将要创立的文件,关上 lib/instagram_clone_web/endpoint.ex
并更新动态插件中的第 27 行:
only: ~w(css fonts images js favicon.ico robots.txt uploads)
当初所有都应该工作得很好,但咱们正在上传缩略图,所以让咱们在模板中应用它,关上 lib/instagram_clone_web/live/header_nav_component.html.leex
并更新第 43 行以应用咱们的缩略图 URL:
<%= img_tag InstagramClone.Uploaders.Avatar.get_thumb(@current_user.avatar_url), class: "w-full h-full object-cover object-center" %>
同时关上 lib/instagram_clone_web/live/user_live/settings.html.leex
并更新第 7 行以应用咱们的缩略图:
<%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>
明码更改设置
当初剩下的惟一一件事就是更改明码,咱们将须要一个侧面导航栏,因而让咱们创立一个组件来解决它,因为它将与明码更改 LiveView 共享。在上面 lib/instagram_clone_web/live/user_live
增加以下 2 个文件:
lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex
lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex
增加 lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex
以下内容:
defmodule InstagramCloneWeb.UserLive.SettingsSidebarComponent do | |
use InstagramCloneWeb, :live_component | |
end |
增加 lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex
以下内容:
<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> |
render_helpers.ex
在 下创立一个名为 lib/instagram_clone_web/live
. 关上lib/instagram_clone_web/live/render_helpers.ex
并执行以下操作::
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 |
这些性能将帮忙咱们为侧面导航栏中的链接获取正确的款式。当初咱们须要在模板中提供这些函数,关上 lib/instagram_clone_web.ex
视图助手函数并将其更新为以下内容:
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 |
让咱们将门路调配给套接字,关上 lib/instagram_clone_web/live/user_live/settings.ex
并在装置中增加以下内容:
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 |
lib/instagram_clone_web/live/user_live/settings.html.leex
在标签结尾下方顶部的局部标签内关上,让咱们插入咱们的组件:
<%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent, | |
settings_path: @settings_path, | |
pass_settings_path: @pass_settings_path, | |
current_uri_path: @current_uri_path %> |
关上 lib/instagram_clone_web/live/user_live/pass_settings.ex
增加以下内容:
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 |
而后关上 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 %> | |
</section> |
让咱们将表单增加到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> |
最初更新 lib/instagram_clone_web/live/user_live/pass_settings.ex
如下:
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 |
转到 lib/instagram_clone/accounts.ex
第 208 行,并更新 update_user_password()
为以下内容:
def update_user_password(user, password, attrs) do | |
user | |
|> User.password_changeset(attrs) | |
|> User.validate_current_password(password) | |
|> Repo.update() | |
end |
在下一部分中,咱们将解决用户的个人资料。
转自:Elixirprogrammer