We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 89
LiveView 04 - Chapter 2 Your Turn
Published on: 2025-12-02
Tags:
elixir, Blog, Side Project, LiveView, Ecto, Html/CSS, Phoenix
Your Turn
Let's try to add in these features to the site:
Add in real email service to the site, (SendGrid or Mailgun)
Okay so for this one Ill go over the way to use SendGrid, for this you will need to have a SendGrid account and the API key. Once you have that you will need to head to config/dev.exs and config/runtime.exs there you will add theses lines
config :pento, Pento.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")
This will set up the config for the dev env and the runtime env so that it can use the SendGrip API. Once that is set you can then set the API key with an env variable
export SENDGRID_API_KEY="your_api_key_here"
And then test to be sure it is in the system with
echo $SENDGRID_API_KEY
Now this all assumes that you have emails that you can send with the company have verified the email domain and that you have set the correct info in the user_notifier like I will show below
# Delivers the email using the application mailer.
defp deliver(recipient, subject, body) do
email =
new()
|> to(recipient)
|> from({"Pento", "contact@example.com"})
|> subject(subject)
|> text_body(body)
with {:ok, _metadata} <- Mailer.deliver(email) do
IO.puts("Email sent to #{recipient} with subject #{subject}")
{:ok, email}
end
end
Replace the contact@example.com with the domain that you have set for yourself. With that all set you shouldn't need to do anything else.
Add in a new migration field to add a username, do you replace the email as the username? Can you still use the magic_link?
For this I will need to do a few things. In this case I will need to add in a new ecto migration with the following command:
mix phx.gen.migration add_username_to_users
This will create a new file that will be responsible for adding a new column to the users table. head to priv/repo/migrations/***_add_username_to_users
defmodule Pento.Repo.Migrations.AddUsernameToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add(:username, :string, unique: true)
end
create(unique_index(:users, [:username]))
end
end
This is an alter table change you want to use this as it will be taking a table that already exists and changing it. We can see that we are adding in a username column, we are making sure that the database is requiring that the username is unique. With this done we can migrate the database and add in the new column.
mix ecto.migrate
Okay that is now out of the way we now need to head to the schema for that table and add in some functionality for the new column. lib/pento/accounts/user.ex
schema "users" do
field :username, :string
# ...
end
Now let's deal with the changeset for the username we will follow the same process for the email and set a changeset for the username and then validate for the same process.
@doc """
A user changeset for changing the username.
It requires the username to change otherwise an error is added.
"""
def username_changeset(user, attrs) do
user
|> cast(attrs, [:username])
|> validate_required([:username])
|> validate_length(:username, min: 3, max: 50)
|> unique_constraint(:username)
end
def validate_username_changed(changeset) do
if get_field(changeset, :username) && get_change(changeset, :username) == nil do
add_error(changeset, :username, "did not change")
else
changeset
end
end
We also need to affect the Accounts file as well as that will be what we use to update the users table. lib/pento/accounts.ex
@doc """
Returns an `%Ecto.Changeset{}` for changing the username.
## Examples
iex> change_username(user)
%Ecto.Changeset{data: %User{}}
"""
def change_username(user, attrs \\ %{}) do
User.username_changeset(user, attrs)
end
@doc """
Updates the username of the user.
## Examples
iex> update_username(user, %{username: ...})
{:ok, %User{}}
iex> update_username(user, %{username: "ab"})
{:error, %Ecto.Changeset{}}
"""
def update_username(user, attrs) do
user
|> User.username_changeset(attrs)
|> User.validate_username_changed()
|> Repo.update()
end
This function builds and validates the username changeset during form updates. The update_username/2 function is responsible for actually writing the new value to the database using Repo.update/1. The LiveView settings UI uses change_username/2 for validation during typing and update_username/2 when the user submits the form.
Okay that is now out of the way we need to be sure that we are using this whenever we are running the settings changes. Let's head to the settings file to add in the new event handlers and add in the rest of the markdown. lib/pento_web/live/user_live/settings.ex
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="text-center">
<.header>
Account Settings
<:subtitle>Manage your account email address and password settings</:subtitle>
</.header>
</div>
...
<div class="divider" />
<.form for={@username_form} id="username_form" phx-submit="update_username" phx-change="validate_username">
<.input
field={@username_form[:username]}
type="text"
label="Username"
autocomplete="username"
required
/>
<.button variant="primary" phx-disable-with="Changing...">Change Username</.button>
</.form>
</Layouts.app>
"""
end
def handle_event("validate_username", params, socket) do
%{"user" => user_params} = params
username_form =
socket.assigns.current_scope.user
|> Accounts.change_username(user_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, username_form: username_form)}
end
def handle_event("update_username", %{"user" => attrs}, socket) do
user = socket.assigns.current_scope.user
true = Accounts.sudo_mode?(user)
case Accounts.update_username(user, attrs) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "Username updated")
|> assign(username_form: to_form(Accounts.change_username(user)))}
{:error, changeset} ->
{:noreply, assign(socket, username_form: to_form(changeset, action: :insert))}
end
end
Make a user auto redirect to the /guess route
This is pretty simple for the scope of the project you need to head to the lib/pento_web/user_auth.ex and then head to the log_in_user function. Once the session is created you will have the redirect function. This is there to take a user from the login page to a page there where once at or a page of your choosing.
@doc """
Logs the user in.
Redirects to the session's `:user_return_to` path
or falls back to the `signed_in_path/1`.
"""
def log_in_user(conn, user, params \\ %{}) do
user_return_to = get_session(conn, :user_return_to)
conn
|> create_or_extend_session(user, params)
|> redirect(to: user_return_to || ~p"/guess")
end
As you can see there is a normal path of singed_in_path(conn)
Add in more information into the scopes and think about displaying it on the page or the default layout.
For this at the moment Ill just talk about what we could put into the scope. As of right now we only have the user and all its information within the scope. We just recently added in the username to the user database so I'll just be sure to add in the user name into the display at the top. Let's head to the pento/lib/pento_web/components/layouts/root.html.heex
<body>
<ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_scope do %>
<li>
# This line here
{@current_scope.user.email}(<%= @current_scope.user.username || "no username" %>)
</li>
<li>
<.link href={~p"/users/settings"}>Settings</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
</li>
<% else %>
<li>
<.link href={~p"/users/register"}>Register</.link>
</li>
<li>
<.link href={~p"/users/log-in"}>Log in</.link>
</li>
<% end %>
</ul>
{@inner_content}
</body>
Now going to the scopes page let's talk about what we can add to the scope. So for this simple example I want to add in a flag for whether or not a username has been set.
defmodule Pento.Accounts.Scope do
@moduledoc """
alias Pento.Accounts.User
defstruct user: nil,
user_name_set?: false
@doc """
Creates a scope for the given user.
Returns nil if no user is given.
"""
def for_user(%User{} = user) do
%__MODULE__{
user: user,
user_name_set?: user.username != nil && user.username != ""
}
end
def for_user(nil), do: nil
end
We now can have a check for whether or not they have a username and be sure to have them set it if we want. There is also a setting that we could have for whether or not they have confirmed their email and so much more.