Project Scope
This application is a subset of gist functionality from the github.com website.
- allows users to create, read, update, and delete gists.
- All gists are public and can be accessed by anyone.
- Only the owner of a gist can update or delete it.
- Users can comment on gists.
- The database is a simple sqlite3 database.
- We will not be auto generating the live view files, but we will be creating the schemas, contexts, and migrations.
Preamble
Install hex
mix local.hexInstall phoenix
mix archive.install hex phx_new
mix phx.new --versionCreate a new phoenix project
mix phx.new elixir_gist --database sqlite3
cd elixir_gistCreate a new phoenix project with binary_id and without installing dependencies
mix phx.new elixir_gist --no-install --binary-id --database sqlite3
cd elixir_gistmix deps.getSetup Authentication
mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.setupStart the Phoenix server
mix phx.serveror
iex -S mix phx.serverThere are a few urls under /dev/ that you can use to test the application, when running from localhost. For example you can view a fake email inbox for the logged in user at /dev/mailbox. Check out the registration email
Create Schemas and Migration files
We will be creating the schemas using phx.gen.context this will create the schema, context, and migration files and some tests.
It will not create the views, controllers, or templates.
Create Gists
mix phx.gen.context Gists Gist gists user_id:references:users name:string description:text markup_test:textIn the migration file, change this line from
add :user_id, references(:users, on_delete: :nothing, type: :binary_id)to delete gists when a user is deleted.
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)Create Saved Gists
mix phx.gen.context Gists SavedGist saved_gists user_id:references:users gist_id:references:gistsWARNING
phoenix will complain that we are creatng SavedGists in the Gists context, and that this is not a good practice. We will ignore this warning for now.
In the migration file, change these lines from
add :user_id, references(:users, on_delete: :nothing, type: :binary_id)
add :gist_id, references(:gists, on_delete: :nothing, type: :binary_id)to
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
add :gist_id, references(:gists, on_delete: :delete_all, type: :binary_id)Create Comments
mix phx.gen.context Comments Comment comments user_id:references:users gist_id:references:gists markup_text:textIn the migration file, change these lines from
add :user_id, references(:users, on_delete: :nothing, type: :binary_id)
add :gist_id, references(:gists, on_delete: :nothing, type: :binary_id)to
add :user_id, references(:users, on_delete: :delete_all, type: :binary_id)
add :gist_id, references(:gists, on_delete: :delete_all, type: :binary_id)Some Pre Migrate User Changes
Schema Changes
Alter the user.ex and add
has_many :gists, ElixirGist.Gists.GistSome Pre Migrate Gist Changes
Schema Changes
Alter the gist.ex and change
field :user_id, :binary_idand
|> cast(attrs, [:name, :description, :markup_test])
|> validate_required([:name, :description, :markup_test])to
belongs_to :user, ElixirGist.Accounts.User
has_many :comments, ElixirGist.Comments.Commentand
|> cast(attrs, [:name, :description, :markup_test, user_id])
|> validate_required([:name, :description, :markup_test, user_id])Repo Changes
Alter the gists.ex and change
def create_gist(attrs \\ %{}) do
%Gist{}
|> Gist.changeset(attrs)
|> Repo.insert()
end
to
def create_gist(user, attrs \\ %{}) do
user
## Associates the user with the gist schema
|> Ecto.build_assoc(:gists)
|> Gist.changeset(attrs)
|> Repo.insert()
endSome Pre Migrate SavedGist Changes
Schema Changes
Alter the saved_gist.ex and change
field :user_id, :binary_id
field :gist_id, :binary_idand
|> cast(attrs, [])
|> validate_required([])to
belongs_to :user, ElixirGist.Accounts.User
belongs_to :gist, ElixirGist.Gists.Gistand
|> cast(attrs, [:user_id, :gist_id])
|> validate_required([:user_id, :gist_id])Repo Changes
Alter the saved_gists.ex and change
def create_saved_gist(attrs \\ %{}) do
%SavedGist{}
|> SavedGist.changeset(attrs)
|> Repo.insert()
endto
def create_saved_gist(user, attrs \\ %{}) do
user
## Associates the user with the saved_gist schema
|> Ecto.build_assoc(:saved_gists)
|> SavedGist.changeset(attrs)
|> Repo.insert()
endSome Pre Migrate Comment Changes
Schema Changes
Alter the comment.ex and change
field :user_id, :binary_id
field :gist_id, :binary_idand
|> cast(attrs, [:markup_text])
|> validate_required([:markup_text])to
belongs_to :user, ElixirGist.Accounts.User
belongs_to :gist, ElixirGist.Gists.Gistand
|> cast(attrs, [:user_id, :gist_id, :markup_text])
|> validate_required([:user_id, :gist_id, :markup_text])Repo Changes
Alter the comments.ex and change
def create_comment(attrs \\ %{}) do
%Comment{}
|> Comment.changeset(attrs)
|> Repo.insert()
endto
def create_comment(user, attrs \\ %{}) do
user
## Associates the user with the comment schema
|> Ecto.build_assoc(:comments)
|> Comment.changeset(attrs)
|> Repo.insert()
endMigrate the database
mix ecto.migrateSetup Colors and fonts using
IMPORTANT
- link assets
- Copy the
imageassets topriv/static/imagesfolder- copy the
fontsassets topriv/static/fonts- link to the css file
- link to taiwind file
Setup the layout
We did not include live view in the project, so we will be using the default layout. Which is a simple layout with a header, footer, and main content area.
This content lives in the lib/elixir_gist_web/controllers/page_html/home.html.eex file.
IMPORTANT
Lets delete the default html content for now, but keep the file.
In page_controller.ex change
render(conn, :home, layout: false)to
render(conn, :home)this will remove the default layout, and we will be able to see the default navigation bar.
Long Story short, in this simple app all code ill reside in the app.html.hex file, so we don’t need home.html.heex and we don’t need the nav bar in the app.html.hex file.
so
- Delete
app.html.heexcontents - Delete
home.html.heexfile
We should have a blank page now.
Build Navigation bar
in app.html.heex add the following code
<header class="flex justify-between items-center px-6 py-3 bg-emDark">
<div class="flex relative">
<a href={~p"/"}>
<img src="/images/gist-logo.svg" alt="Logo" class="h-8 w-auto" />
</a>
<a href={~p"/"} class="mr-6">
<div class="text-white font-brand font-bold text-3xl">Gist</div>
</a>
<div>
<input
type="text"
class="rounded-lg focus:outline-none focus:border-emLavender focus:ring-0 px-3 py-1 bg-emDark-dark placeholder-emDark-light text-white font-brand font-regular text-sm mr-5 border-white"
placeholder="Search..."
/>
<button class="mt-2 mr-2 text-white text-[1rem] font-brand font-bold hover:text-emDark-light">
All gists
</button>
</div>
</div>
<div class="relative">
<button class="img-down-arrow" type="button" id="user-menu-button">
<img src="/images/user-image.svg" alt="Profile Image" class="round-image-padding w-8 h-8" />
</button>
</div>
</header>and adding this css to the app.css file
.round-image-padding {
border-radius: 50%;
border: 1.5px solid #FFFFFF;
padding: 4px;
}
.img-down-arrow::after {
content: "";
position: absolute;
right: -10px;
top: 45%;
transform: translateY(-50%);
border-width: 3px 3px 0 3px;
border-color: white transparent transparent transparent;
border-style: solid;
}Drop down menu
defmodule ElixirGistWeb.Layouts.App do
alias Phoenix.LiveView.JS
def toggle_dropdown_menu do
JS.toggle(
to: "#dropdown_menu",
in:
{"transition ease-out duration-100", "transform opacity-0 translate-y-[-10%]",
"transform opacity-100 translate-y-0"},
out:
{"transition ease-in duration-75", "transform opacity-100 translate-y-0",
"transform opacity-0 translate-y-[-10%]"}
)
end
endCreat Gist Live View
In the live folder create a new file name create_gist_live.ex and add the following code.
defmodule ElixirGistWeb.CreateGistLive do
use ElixirGistWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_params(params, _uri, socket) do
{:noreply, socket}
end
def render(assigns) do
~H"""
<.flash_group flash={@flash} />
<div class="em-gradient flex items-center justify-center">
<h1 class="font-brand font-bold text-3xl text-white">
Instantly share Elixir code, notes, and snippets
</h1>
</div>
"""
end
end
Add the live view to the router
In the router.ex file add the following code
under the live_session with :require_authenticated_user
live "/create", CreateGistLiveUpdate the page controller
In the page_controller.ex file add the following code so that the controller looks liek this
defmodule ElixirGistWeb.PageController do
use ElixirGistWeb, :controller
def home(conn, _params) do
redirect(conn, to: "/create")
end
endAdd the gist form
In the create_gist_live.ex file add the following code to the render function
<.form for={@form}>
<div class="justify-center px-28 w-full space-y-4 mb-10">
<.input field={@form[:description]} placeholder="Gist description.." autocomplete="off" />
<div>
<div class="flex p-2 items-center bg-emDark rounded-t-md border">
<div class="w-[300px] mb-2">
<.input
field={@form[:name]}
placeholder="Filename including extension..."
autocomplete="off"
/>
</div>
</div>
<.input
type="textarea"
field={@form[:markup_text]}
placeholder="Insert code..."
autocomplete="off"
class="textarea w-full rounded-b-md"
/>
</div>
<div class="flex justify-end">
<.button class="create_button">Create gist</.button>
</div>
</div>
</.form>CoreComponent fix
The core component hard codes the styles for the text area input fields, so lets make some changes
- take the
classvalue from thetextarea - create an
attrvalue namedclasswhich defaults to this value. - use the
@classin place of the class string intextarea
This means that the classes will be set correctly on the component
make sure that mount sets the value to model
def mount(_params, _session, socket) do
socket =
assign(
socket,
form: to_form(Gists.change_gist(%Gist{}))
)
{:ok, socket}
end
Save the Gist
In the create_gist_live.ex file add the following code to the handle_event function
def handle_event("create", %{"gist" => gist_params}, socket) do
IO.inspect(gist_params)
case Gists.create_gist(socket.assigns.current_user, gist_params) do
{:ok, _gist} ->
changeset = Gists.change_gist(%Gist{})
{:noreply, assign(socket, form: to_form(changeset))}
# match whatever is in changest, it will have errors
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
Validate the form
remember to add
phx-submit="create"to the form tagphx-click="validate"to the form tagphx-debounce="blurto the input tags
def handle_event("validate", %{"gist" => gist_params}, socket) do
changeset =
%Gist{}
|> Gists.change_gist(gist_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
Why always me?