Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack
Tags: elixir, webdev, programming, tutorial
Published: 2021-04-14 01:48:47

The better way to learn is by getting our hands dirty and building things, let's build a simplified version of the Instagram web application with the awesome PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) stack and deep dive into the dark world of functional programming and hottest kid on the block the Phoenix framework with LiveView.

I don't consider myself a teacher and no expert at anything, I'm just a regular dude just like you. Anybody can follow along even though you might get intimidated by the whole stack, it's kind of a new technology and not very popular, and not a lot of resources and materials out there. If you are an experienced developer you will have no problem, it doesn't mean that if you're a beginner you cannot follow along, I will do my best to make it beginner friendly but I will not go over every basic of the stack or web development so you've been warned.

Elixir's one of the best languages that I ever have the pleasure of learning, experimenting with, and I want to share my passion with the world, I want others to feel what I feel for the language.

**Disclaimer:** Elixir, Functional Programming, Phoenix Framework, might sound, look difficult and complicated but it is not at all, is easier than anything else out there, it might not be for everyone because we all do not think alike but for those who think like me will feel as I feel trying it. TailwindCSS might be opinionated, look not worth trying it, I know it because that's how I felt too, but just try it, the more you use it the more sense it will make and the more you will love it, it makes CSS uncomplicated, makes you not give up on front end development, CSS will still be painful, as developers, we don't have the patience that takes to get the UI right but it's a breath of fresh air.

We will not finish the whole project on this article, it will be a series of articles so this will be part 1. I will assume that you have your own development environment with Elixir installed, my development environment is on Windows 10 with WSL. We will try to be as detail as possible but keeping it simple, it is just for learning purposes only so it will not be an exact copy and it will not have every feature, we will get as close as possible to the real thing, also we will not focus on making the site responsive, we'll just make it work for large screens.

Let's start by going to the terminal and creating a fresh Phoenix app with LiveView.

`$ mix phx.new instagram_clone --live`

Once all dependencies are installed and fetched.

`$ cd instagram_clone && mix ecto.create`

I created a GitHub repo that you can visit here [Instagram Clone GitHub Repo](https://github.com/elixirprogrammer/InstagramClonePETAL) feel free to use the code as you wish, contributions are welcome.

Let's run the server to make sure that everything's working.

`$ iex -S mix phx.server`

If no errors you should have the default Phoenix framework homepage when you go to http://localhost:4000/

I use Visual Studio Code so I will open the project folder with the following command.

`$ code .`

Now let's add our mix dependencies in our mix.exs file.

```elixir
# mix.exs file

  defp deps do
    [
      {:phoenix, "~> 1.5.6"},
      {:phoenix_ecto, "~> 4.1"},
      {:ecto_sql, "~> 3.4"},
      {:postgrex, ">= 0.0.0"},
      {:floki, ">= 0.27.0", only: :test},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:phoenix_live_view, "~> 0.15.4", override: true},
      {:timex, "~> 3.6"},
      {:faker, "~> 0.16.0"}
    ]
  end
```

We updated :phoenix_live_view to 15.4 version and added timex to handle times and faker for when we want test data.

## Set Up TailwindCSS And AlpineJS 

Make sure to have the latest node and npm versions.

`$ cd assets`

`$ npm i tailwindcss postcss autoprefixer postcss-loader@4.2 --save-dev`

Next let's configure Webpack, PostCSS, and TailwindCSS.

```javascript
// /assets/webpack.config.js

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
  'sass-loader',
  'postcss-loader', // Add this
],
```

Add `/assets/postcss.config.js` file with the following:

```javascript
// /assets/postcss.config.js

module.exports = {
    plugins: {
        "postcss-import": {},
        tailwindcss: {},
        autoprefixer: {}
    }
}
```

Create a TailwindCSS configuration file.

`$ npx tailwindcss init`

Add the following configuration to that file:

```javascript
const colors = require('tailwindcss/colors')

module.exports = {
  purge: {
    enabled: process.env.NODE_ENV === "production",
    content: [
      "../lib/**/*.eex",
      "../lib/**/*.leex",
      "../lib/**/*_view.ex"
    ],
    options: {
      whitelist: [/phx/, /nprogress/]
    }
  },
  theme: {
    extend: {
      colors: {
        'light-blue': colors.lightBlue,
        cyan: colors.cyan,
      },
    },
  },
  variants: {
    extend: {
      borderWidth: ['hover'],
    }
  },
  plugins: [require('@tailwindcss/forms')],
}
```

We configure which files to purge, added a custom color, and custom forms plugin. So let's add custom forms to our npm dependencies now.  

`$ npm i @tailwindcss/forms --save-dev`

For custom components conflicts let's add postcss-import plugin.

`$ npm i postcss-import --save-dev`

Go to `/assets/css/app.scss` and add the following at the top of the file:

```scss
/* This file is for your main application css. */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
```

Delete `/assets/css/phoenix.css` we won't need it.

Let's get out of our assets folder `$ cd ..` and run our server `$ iex -S mix phx.server`

Test it out by going to `/lib/instagram_clone_web/live/page_live.html.leex` delete everything and add the following:

```html
<h1 class="text-red-500 text-5xl font-bold text-center">Instagram Clone</h1>
```

We should have a big red headline on our homepage.

Go to `/lib/instagram_clone_web/live/page_live.ex` delete everything because we won't need any of that in our homepage, and add the following:

```elixir
defmodule InstagramCloneWeb.PageLive do
  use InstagramCloneWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end
end
```

Go to `/lib/instagram_clone_web/templates/layout/root.html.leex` delete the default phoenix header, you should have the following on that file:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <%= csrf_meta_tag() %>
    <%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: " · Phoenix Framework" %>
    <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
    <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </head>
  <body>
    <!-- Remove Everything Above Here -->
    <%= @inner_content %>
  </body>
</html>
```

Now let's customize our main container with tailwind, go to `/lib/instagram_clone_web/templates/layout/live.html.leex` and add the following class to the main tag:

```html
<main role="main" class="container mx-auto max-w-full md:w-11/12 2xl:w-6/12 pt-24"> <!-- This the class that we added -->
  <p class="alert alert-info" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="info"><%= live_flash(@flash, :info) %></p>

  <p class="alert alert-danger" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="error"><%= live_flash(@flash, :error) %></p>

  <%= @inner_content %>
</main>
```

&nbsp;
### Add AlpineJS

With TailwindCSS ready to go let's add AlpineJS. Let's get into our `$ cd assets` folder again and run the following:

`$ npm i alpinejs@2.8.2 `

Open the app.js file located `/assets/js/app.js` and add the following so that we don't have any conflict with LiveView's own DOM patching:

```javascript
import Alpine from "alpinejs"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  dom: {
    onBeforeElUpdated(from, to) {
      if (from.__x) { Alpine.clone(from.__x, to) }
    }
  }
})
```

Let's get out of our assets folder `$ cd ..` and run our server `$ iex -S mix phx.server`

Test it out buy going to `/lib/instagram_clone_web/live/page_live.html.leex` and adding the following to our top of our file:

```html
<div x-data="{ open: false }">
    <button @click="open = true">Open Dropdown</button>

    <ul
        x-show="open"
        @click.away="open = false"
    >
        Dropdown Body
    </ul>
</div>
```

We should have a clickable dropdown if we go to our homepage like the example below:

![AlpineJS Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4w2rzfnpz3ms1qov0aco.gif)

&nbsp;
## Phx.Gen.Auth

With that set up out of the way the real fun begins. Let's add user authentication with phx.gen.auth package.

Let's add the package to our `mix.exs` file.

```elixir
  defp deps do
    [
      {:phoenix, "~> 1.5.6"},
      {:phoenix_ecto, "~> 4.1"},
      {:ecto_sql, "~> 3.4"},
      {:postgrex, ">= 0.0.0"},
      {:floki, ">= 0.27.0", only: :test},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:phoenix_live_view, "~> 0.15.4", override: true},
      {:timex, "~> 3.6"},
      {:faker, "~> 0.16.0"},
      {:phx_gen_auth, "~> 0.7", only: [:dev], runtime: false}
    ]
  end
```

Install and compile the dependencies

`$ mix do deps.get, deps.compile`

Install the authentication system with the following command:

`$ mix phx.gen.auth Accounts User users`

After all the files were generated run the following command:

`$ mix deps.get && mix ecto.migrate`

Now we need to add some fields to our users table by running the following command:

`$ mix ecto.gen.migration add_to_users_table`

Then open the file that was generated `priv/repo/migrations/20210409223611_add_to_users_table.exs` and add the following:

```elixir
defmodule InstagramClone.Repo.Migrations.AddToUsersTable do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :username, :string
      add :full_name, :string
      add :avatar_url, :string
      add :bio, :string
      add :website, :string
    end
  end
end
```

Then `$ mix ecto.migrate`

Next open `lib/instagram_clone/accounts/user.ex` and add the following to your users schema:

```elixir
field :username, :string
field :full_name, :string
field :avatar_url, :string, default: "/images/default-avatar.png"
field :bio, :string
field :website, :string
```

&nbsp;
![default-avatar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ogf5i7oq4wd4muyncv9u.png)
&nbsp;

Download the default avatar image above and rename it to `default-avatar.png` and add that image to `priv/static/images`

Now we need to add validations for our new users schema, so open `lib/instagram_clone/accounts/user.ex` again and change the `registration_changeset` to the following:

```elixir
  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)
    |> validate_length(:full_name, min: 4, max: 30)
    |> validate_email()
    |> validate_password(opts)
  end
```

Also, we need to change our `validate_password` function for when we update the user's account, we won't need to validate or hash the password so change it to the following:

```elixir
  defp validate_password(changeset, opts) do
    register_user? = Keyword.get(opts, :register_user, true)
    if register_user? do
      changeset
      |> validate_required([:password])
      |> validate_length(:password, min: 6, max: 80)
      # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
      # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
      # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
      |> maybe_hash_password(opts)
    else
      changeset
    end
  end
```

When updating the user's account we will send a `register_user: false` option to the changeset. Also, the minimum password length was changed to 6 for development purposes only it should be changed in production.

Let's run our server `$ iex -S mix phx.server` and open `/lib/instagram_clone_web/live/page_live.html.leex` to work on our homepage styles to add the registration form.

Before we do that we have to delete the authentication links auto generated by phx.gen.auth so go to `/lib/instagram_clone_web/templates/layout/root.html.leex` and delete the `<%= render "_user_menu.html", assigns %>` from the top of the body.

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <%= csrf_meta_tag() %>
    <%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: " · Phoenix Framework" %>
    <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
    <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </head>
  <body>
    <%= render "_user_menu.html", assigns %><!-- REMOVE IT -->
    <%= @inner_content %>
  </body>
</html>
```

And lastly, delete `/lib/instagram_clone_web/templates/layout/_user_menu.html.eex` partial file, we won't need it.

Okay now back to `/lib/instagram_clone_web/live/page_live.html.leex` add the following:

```html
<section class="w-1/2 border-2 shadow-lg flex flex-col place-items-center mx-auto p-6">
    <h1 class="text-4xl font-bold italic text-gray-600">InstagramClone</h1>
    <p class="text-gray-400 font-semibold text-lg my-6">Sign up to see photos and videos from your friends.</p>
</section>
```

We need to add a form so go to `/lib/instagram_clone_web/live/page_live.ex` change the mount function to the following:

```elixir
  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User

  @impl true
  def mount(_params, _session, socket) do
    changeset = Accounts.change_user_registration(%User{})
    {:ok,
      socket
      |> assign(changeset: changeset)}
  end
```

Let's add our form and new styles by editing `/lib/instagram_clone_web/live/page_live.html.leex` to:

```html
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
  <h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
  <p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>

  <%= f = form_for @changeset, "#",
    phx_change: "validate",
    phx_submit: "save",
    phx_trigger_action: @trigger_submit,
    class: "flex flex-col space-y-4 w-full px-6" %>

    <div class="flex flex-col">
      <%= label f, :email, class: "text-gray-400" %>
      <%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :email, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :full_name, class: "text-gray-400" %>
      <%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :username, class: "text-gray-400" %>
      <%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :username, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :password, class: "text-gray-400" %>
      <%= password_input f, :password, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :password, class: "text-red-700 text-sm" %>
    </div>

    <div>
      <%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
    </div>

  </form>

  <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>

<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
  <p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
```

We need to tweak a little bit our `error_tag()` helper functions before we go further, so we can add classes to it, open `lib/instagram_clone_web/views/error_helpers.ex` file and change the function to the following:

```elixir
  def error_tag(form, field, class \\ [class: "invalid-feedback"]) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, translate_error(error),
        class: Keyword.get(class, :class),
        phx_feedback_for: input_id(form, field)
      )
    end)
  end
```


![InstagramCloneHomepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iu4hwmlpqnhegvsl4b8x.jpg)
 

That's going to serve us as our base landing and sign-up page. We need to add our `validation()` and `save()` functions, and assign `trigger_submit` to the socket in the mount function to be able to trigger the form over HTTP to send the form to the register controller directly on our `/lib/instagram_clone_web/live/page_live.ex` liveview module so we can register our user, so let's do that next.

```elixir
  @impl true
  def mount(_params, _session, socket) do
    changeset = Accounts.change_user_registration(%User{})
    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(trigger_submit: false)}
  end

  @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)
    {:noreply, socket |> assign(changeset: changeset)}
  end

  def handle_event("save", _, socket) do
    {:noreply, assign(socket, trigger_submit: true)}
  end
```

To handle and display errors we have to edit our register user page, because they are regular Phoenix views handled by controllers, so it should look just like our liveview. Before we do that we have to add a class to the main tag in our container for regular views, open `lib/instagram_clone_web/templates/layout/app.html.eex`:

```html
<main role="main" class="container mx-auto max-w-full md:w-11/12 2xl:w-6/12 pt-24">
  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
  <%= @inner_content %>
</main>
```

Then open `lib/instagram_clone_web/templates/user_registration/new.html.eex` and edit the file with the following:

```html
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
  <h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
  <p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>

  <%= form_for @changeset, Routes.user_registration_path(@conn, :create), [class: "flex flex-col space-y-4 w-full px-6"], fn f -> %>
    <%= if @changeset.action do %>
      <div class="alert alert-danger">
        <p>Oops, something went wrong! Please check the errors below.</p>
      </div>
    <% end %>

    <div class="flex flex-col">
      <%= label f, :email, class: "text-gray-400" %>
      <%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :email, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :full_name, class: "text-gray-400" %>
      <%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :username, class: "text-gray-400" %>
      <%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :username, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :password, class: "text-gray-400" %>
      <%= password_input f, :password, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :password, class: "text-red-700 text-sm" %>
    </div>

    <div>
      <%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
    </div>
  <% end %>

  <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>

<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
  <p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
```

Let's style our login page and login. Open `lib/instagram_clone_web/templates/user_session/new.html.eex` and add the following:

```html
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
  <h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>

  <%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user, class: "flex flex-col space-y-4 w-full px-6"], fn f -> %>
    <%= if @error_message do %>
      <div class="alert alert-danger">
        <p><%= @error_message %></p>
      </div>
    <% end %>

    <div class="flex flex-col">
      <%= label f, :email, class: "text-gray-400" %>
      <%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :email, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :password, class: "text-gray-400" %>
      <%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :password, class: "text-red-700 text-sm" %>
    </div>

    <div>
      <%= submit "Log In", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
    </div>
  <% end %>

  <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold"><%= link "Forgot password?", to: Routes.user_reset_password_path(@conn, :new) %></p>
</section>

<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
  <p class="text-lg text-gray-600">Don't have an account? <%= link "Sign up", to: Routes.user_registration_path(@conn, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
```

It should look like the image below.


![InstagramCloneLoginPage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/guwmlz32emevliuu27f6.jpg)
 

Things are getting really interesting and exciting, but we don't have a way to get the currently logged in user in our liveviews, we have to get it manually on each liveview mount, we can do it manually but we are lazy so we are going to add a helper function that we can call and have access to the current user.

Add a new file to `lib/instagram_clone_web/live` folder named `lib/instagram_clone_web/live/live_helpers.ex` and add the following:

```elixir
defmodule InstagramCloneWeb.LiveHelpers do
  import Phoenix.LiveView
  alias InstagramClone.Accounts
  alias InstagramClone.Accounts.User
  alias InstagramCloneWeb.UserAuth

  def assign_defaults(session, socket) do
    if connected?(socket), do: InstagramCloneWeb.Endpoint.subscribe(UserAuth.pubsub_topic())

    socket =
      assign_new(socket, :current_user, fn ->
        find_current_user(session)
      end)
    socket
  end

  defp find_current_user(session) do
    with user_token when not is_nil(user_token) <- session["user_token"],
         %User{} = user <- Accounts.get_user_by_session_token(user_token),
         do: user
  end
end
```

It's simple we are finding the current user with the session token and assign it back to socket, we are also subscribing to a pubsub topic to logged out all live current session when login out with sockets, let's create that function next, open `lib/instagram_clone_web/controllers/user_auth.ex` and add the following:

```elixir
  # Added to the top of our file
  @pubsub_topic "user_updates"

  def pubsub_topic, do: @pubsub_topic
  # We changed a line on this function
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    Accounts.log_out_user(user_token) #Line changed

    if live_socket_id = get_session(conn, :live_socket_id) do
      InstagramCloneWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
    end

    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: "/")
  end
``` 

Now let's add the `log_out_user()` function to our `Accounts` context, open `lib/instagram_clone/accounts.ex` add:

```elixir
...

alias InstagramCloneWeb.UserAuth

...

  def log_out_user(token) do
    user = get_user_by_session_token(token)
    # Delete all user tokens
    Repo.delete_all(UserToken.user_and_contexts_query(user, :all))

    # Broadcast to all liveviews to immediately disconnect the user
    InstagramCloneWeb.Endpoint.broadcast_from(
      self(),
      UserAuth.pubsub_topic(),
      "logout_user",
      %{
        user: user
      }
    )
  end

...
```

Now we have to make our helper function available in our liveviews, open `lib/instagram_clone_web.ex` and add the following:

```elixir
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {InstagramCloneWeb.LayoutView, "live.html"}

      unquote(view_helpers())
      # Added Start   
      import InstagramCloneWeb.LiveHelpers

      alias InstagramClone.Accounts.User
      @impl true
      def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do
        with %User{id: ^id} <- socket.assigns.current_user do
          {:noreply,
            socket
            |> redirect(to: "/")
            |> put_flash(:info, "Logged out successfully.")}
        else
          _any -> {:noreply, socket}
        end
      end
      # Added END 
    end
  end
```

We also added the `handle_info()` function to automatically react to the logout message in all our liveviews.

Open `/lib/instagram_clone_web/live/page_live.ex` and changed the mount function to the following:

```elixir
  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)
    changeset = Accounts.change_user_registration(%User{})
    {:ok,
      socket
      |> assign(changeset: changeset)
      |> assign(trigger_submit: false)}
  end
```

Then open `/lib/instagram_clone_web/live/page_live.html.leex` and changed the file to the following:

```html
<%= if @current_user do %>
  <%= link "Log Out", to: Routes.user_session_path(@socket, :delete), method: :delete %>
  <h1>User Logged In Homepage</h1>
<% else %>
  <section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
    <h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
    <p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>

    <%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
      phx_change: "validate",
      phx_submit: "save",
      phx_trigger_action: @trigger_submit,
      class: "flex flex-col space-y-4 w-full px-6" %>

      <div class="flex flex-col">
        <%= label f, :email, class: "text-gray-400" %>
        <%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
        <%= error_tag f, :email, class: "text-red-700 text-sm" %>
      </div>

      <div class="flex flex-col">
        <%= label f, :full_name, class: "text-gray-400" %>
        <%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
        <%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
      </div>

      <div class="flex flex-col">
        <%= label f, :username, class: "text-gray-400" %>
        <%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
        <%= error_tag f, :username, class: "text-red-700 text-sm" %>
      </div>

      <div class="flex flex-col">
        <%= label f, :password, class: "text-gray-400" %>
        <%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
        <%= error_tag f, :password, class: "text-red-700 text-sm" %>
      </div>

      <div>
        <%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
      </div>

    </form>

    <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
  </section>

  <section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
    <p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
  </section>
<% end %>
```

At this point, you should reload your server and test your homepage by login in and login out. Everything should work fine but let's componentized our homepage, under `lib/instagram_clone_web/live` let's create 2 files, `lib/instagram_clone_web/live/page_live_component.ex` and `lib/instagram_clone_web/live/page_live_component.html.leex`

```elixir
#lib/instagram_clone_web/live/page_live_component.ex

defmodule InstagramCloneWeb.PageLiveComponent do
  use InstagramCloneWeb, :live_component
end
```

Take the form from `lib/instagram_clone_web/live/page_live.html.leex` and let's add it to `lib/instagram_clone_web/live/page_live_component.html.leex`:

```html
<section class="w-1/2 border-2 shadow flex flex-col place-items-center mx-auto p-6">
  <h1 class="text-4xl font-bold italic text-gray-700">InstagramClone</h1>
  <p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">Sign up to see photos and videos from your friends.</p>

  <%= f = form_for @changeset, Routes.user_registration_path(@socket, :create),
    phx_change: "validate",
    phx_submit: "save",
    phx_trigger_action: @trigger_submit,
    class: "flex flex-col space-y-4 w-full px-6" %>

    <div class="flex flex-col">
      <%= label f, :email, class: "text-gray-400" %>
      <%= email_input f, :email, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :email, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :full_name, class: "text-gray-400" %>
      <%= text_input f, :full_name, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :full_name, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :username, class: "text-gray-400" %>
      <%= text_input f, :username, class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :username, class: "text-red-700 text-sm" %>
    </div>

    <div class="flex flex-col">
      <%= label f, :password, class: "text-gray-400" %>
      <%= password_input f, :password, value: input_value(f, :password), class: "rounded border-gray-300 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
      <%= error_tag f, :password, class: "text-red-700 text-sm" %>
    </div>

    <div>
      <%= submit "Sign up", phx_disable_with: "Saving...", class: "block w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %>
    </div>

  </form>

  <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">By signing up, you agree to our Terms , Data Policy and Cookies Policy .</p>
</section>

<section class="w-1/2 border-2 shadow flex justify-center mx-auto p-6 mt-6">
  <p class="text-lg text-gray-600">Have an account? <%= link "Log in", to: Routes.user_session_path(@socket, :new), class: "text-light-blue-500 font-semibold" %></p>
</section>
```

Now your `lib/instagram_clone_web/live/page_live.html.leex` should look like the following example:

```html
<%= if @current_user do %>
  <h1>User Logged In Homepage</h1>
<% else %>
  <%= live_component @socket, InstagramCloneWeb.PageLiveComponent, changeset: @changeset, trigger_submit: @trigger_submit %>
<% end %>
```

That helps us to clean up our code and for later on when we start working on the homepage for logged-in users.

Lastly let's add a header navigation menu component. First we need to know the current URL path, we are going to do that for all liveviews with a macro, open `lib/instagram_clone_web.ex` add the following function to `live_view()`:

```elixir
      @impl true
      def handle_params(_unsigned_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end
```

That will give us access to the current URL path on all liveviews with the `current_uri_path` assigned to the socket. So in our `lib/instagram_clone_web.ex` file the new updated `live_view()` should look like the following:

```elixir
...
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {InstagramCloneWeb.LayoutView, "live.html"}

      unquote(view_helpers())
      import InstagramCloneWeb.LiveHelpers

      alias InstagramClone.Accounts.User
      @impl true
      def handle_info(%{event: "logout_user", payload: %{user: %User{id: id}}}, socket) do
        with %User{id: ^id} <- socket.assigns.current_user do
          {:noreply,
            socket
            |> redirect(to: "/")
            |> put_flash(:info, "Logged out successfully.")}
        else
          _any -> {:noreply, socket}
        end
      end

      @impl true
      def handle_params(_unsigned_params, uri, socket) do
        {:noreply,
          socket
          |> assign(current_uri_path: URI.parse(uri).path)}
      end
    end
  end
...
``` 

Now under `lib/instagram_clone_web/live` add 2 files, `header_nav_component.ex` and `header_nav_component.html.leex`. To `lib/instagram_clone_web/live/header_nav_component.ex` add the following:

```elixir
defmodule InstagramCloneWeb.HeaderNavComponent do
  use InstagramCloneWeb, :live_component
end
``` 

Add the following to `lib/instagram_clone_web/live/header_nav_component.html.leex`:

```html
<div class="h-14 border-b-2 flex fixed w-full bg-white z-50">
  <header class="flex items-center container mx-auto max-w-full md:w-11/12 2xl:w-6/12">
    <%= live_patch to: Routes.page_path(@socket, :index) do %>
      <h1 class="text-2xl font-bold italic">#InstagramClone</h1>
    <% end %>
    <div class="w-2/5 flex justify-end"><input type="search" placeholder="Search" class="h-7 bg-gray-50 shadow-sm border-gray-300 focus:ring-gray-300 focus:ring-opacity-50 focus:border-gray-400  px-0.5 rounded-sm"></div>
    <nav class="w-3/5 relative">
      <ul x-data="{open: false}" class="flex justify-end">
        <%= if @current_user do %>
          <li class="w-7 h-7 text-gray-600">
            <%= live_patch to: Routes.page_path(@socket, :index) do %>
              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
              </svg>
            <% end %>
          </li>
          <li class="w-7 h-7 ml-6 text-gray-600">
            <%= live_patch to: "" do %>
              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
            <% end %>
          </li>
          <li class="w-7 h-7 ml-6 text-gray-600">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
            </svg>
          </li>
          <li class="w-7 h-7 ml-6 text-gray-600">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
            </svg>
          </li>
          <li class="w-7 h-7 ml-6 text-gray-600">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
            </svg>
          </li>
          <li
            @click="open = true"
            class="w-7 h-7 ml-6 shadow-md rounded-full overflow-hidden cursor-pointer"
          >
            <%= img_tag @current_user.avatar_url,
            class: "w-full h-full object-cover object-center" %>
          </li>
          <ul class="absolute top-14 w-56 bg-white shadow-md text-sm -right-8"
              x-show="open"
              @click.away="open = false"
              x-transition:enter="transition ease-out duration-200"
              x-transition:enter-start="opacity-0 transform scale-90"
              x-transition:enter-end="opacity-100 transform scale-100"
              x-transition:leave="transition ease-in duration-200"
              x-transition:leave-start="opacity-100 transform scale-100"
              x-transition:leave-end="opacity-0 transform scale-90"
            >
              <%= live_patch to: "" do %>
                <li class="py-2 px-4 hover:bg-gray-50">Profile</li>
              <% end %>
              <li class="py-2 px-4 hover:bg-gray-50">Saved</li>
              <%= live_patch to: "" do %>
                <li class="py-2 px-4 hover:bg-gray-50">Settings</li>
              <% end %>
              <li class="border-t-2 py-2 px-4 hover:bg-gray-50"><%= link "Log Out", to: Routes.user_session_path(@socket, :delete), method: :delete %></li>
            </ul>
        <% else %>
          <li>
            <%= link "Log In", to: Routes.user_session_path(@socket, :new), class: "md:w-24 py-1 px-3 border-none shadow rounded text-gray-50 hover:bg-blue-600 bg-blue-500 font-semibold" %>
          </li>
          <li>
            <%= link "Sign Up", to: Routes.user_registration_path(@socket, :new), class: "md:w-24 py-1 px-3 border-none text-blue-500 hover:text-blue-600 font-semibold" %>
          </li>
        <% end %>
      </ul>
    </nav>
  </header>
</div>
```

Now to display that component open `lib/instagram_clone_web/templates/layout/live.html.leex` and add to the top of the file:

```html
<%= if @current_user do %>
  <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
<% else %>
  <%= if @current_uri_path !== "/" do %>
    <%= live_component @socket, InstagramCloneWeb.HeaderNavComponent, current_user: @current_user %>
  <% end %>
<% end %>
```

That will check if a user is logged in to display the header nav, if not and not the homepage, like the profile or post page that we will create later on, it will display it also otherwise if homepage and not logged in it will not get displayed.

That's enough for part 1, we still have a long way to go and a lot of really exciting and fun things to do, I'm building it as I write this series so if there are any mistakes or errors will get fixed as we go, let me know what you think in the comments down below and you can contribute to the repo [Instagram Clone GitHub Repo](https://github.com/elixirprogrammer/InstagramClonePETAL), I really appreciate your time, thank you so much for reading.


&nbsp;
&nbsp;
&nbsp;

[![Join The Elixir Army](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cv2q172ztuekdtw82vo9.png)](https://elixirprogrammer.com)