ElixirDesktopプロジェクトにphx.gen.auth + tailwind + DaisyUIで認証機能を実装

# はじめに
本記事は以下の記事で作成したプロジェクトに 
1 Tailwind + DaisyUIでスマホUIの作成
2 phx.gen.authで認証機能
3 phx.gen.liveでCRUD作成
をそれぞれ実装していきます

https://qiita.com/the_haigo/items/27bf18acd28971af31e9
# tailwind追加
いつもどおり本家を参考に追加していきます

https://tailwindcss.com/docs/guides/phoenix


```trarecord/mix.exs
defmodule Trarecord.MixProject do
  use Mix.Project

  defp deps do
    [
      ...
      {:tailwind, "~> 0.1", runtime: Mix.env() == :dev} # 追加
    ]
  end
end
```

```trarecord/config/config.ex
import Config

...
#　以下追加
config :tailwind,
  version: "3.1.8",
  default: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/assets/app.css
    ),
    cd: Path.expand("../assets", __DIR__)
  ]

```

```trarecord/mix.exs
defmodule Trarecord.MixProject do
  use Mix.Project

  ...
  defp aliases do
    [
      ...
      # assets.deploy差し替え
      "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"] 
    ]
  end
end
```
desktopとwebにそれぞれ開発環境時にtailwindをホットリロードできるようにします

```trarecord/config/desktop.exs
if config_env() == :dev do
  config :trarecord, TrarecordWeb.EndpointWeb,
    ...
    watchers: [
      # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
      esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
      tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} # 追加
    ]
  ...
end
```

```trarecord/config/web.exs
if config_env() == :dev do
  config :trarecord, TrarecordWeb.EndpointWeb,
    ...
    watchers: [
      # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
      esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
      tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} # 追加
    ]
  ...
end
```
最後に関連ファイルをインストールします

```
mix tailwind.install
```

# DaisyUI
こちらも公式通りにおこないます
npm関連はassets以下でおこなうので`cd assets`しておきましょう

https://daisyui.com/docs/install/

```
cd assets
npm i daisyui
cd .. 
```

```trarecord/assets/tailwind.config.js
let plugin = require('tailwindcss/plugin')

module.exports = {
  content: [
    './js/**/*.js',
    '../lib/*_web.ex',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('daisyui'), // 追加
    ...
  ]
}
```

スマホアプリビルド時に npm iで関連ライブインストールするように前記事でコメントアウトした箇所をコメントインします

```shell:android/app/run_mix
# 以下をコメントイン
if [ ! -d "assets/node_modules" ]; then
  cd assets && npm i && cd ..
fi
```

```shell:ios/run_mix
# 以下をコメントイン
if [ ! -d "assets/node_modules" ]; then
  cd assets && npm i && cd ..
fi
```

TailwindとDaisyUIのセットアップは以上になります

# 認証機能実装
次に認証機能を実装します
例のごとくコマンド一発で完了

```
mix phx.gen.auth Users User users
mix deps.get
mix ecto.migrate
```
# Bcrypt -> Pbkdf2 に変更
Android/iOSでパスワードハッシュライブラリがBcryptだとエラーになるのでライブラリをPbkdf2に変更します

https://github.com/riverrun/pbkdf2_elixir

```trarecord/mix.exs
defmodule Trarecord.MixProject do
  use Mix.Project
  ...
  defp deps do
    [
      ...
      {:pbkdf2_elixir, "~> 2.0"}
    ]
  end
  ...
end
```


```trarecord/lib/trarecord/users/user.ex
defmodule Trarecord.Users.User do
  use Ecto.Schema
  import Ecto.Changeset

  ...
  defp maybe_hash_password(changeset, opts) do
    hash_password? = Keyword.get(opts, :hash_password, true)
    password = get_change(changeset, :password)

    if hash_password? && password && changeset.valid? do
      changeset
      # If using Bcrypt, then further validate it is at most 72 bytes long
      |> validate_length(:password, max: 72, count: :bytes)
      |> put_change(:hashed_password, Pbkdf2.hash_pwd_salt(password)) # Pbkdf2に変更
      |> delete_change(:password)
    else
      changeset
    end
  end

  @doc """
  Verifies the password.

  If there is no user or the user doesn't have a password, we call
  `Pbkdf2.no_user_verify/0` to avoid timing attacks.　# Pbkdf2に変更
  """
  def valid_password?(%Trarecord.Users.User{hashed_password: hashed_password}, password)
      when is_binary(hashed_password) and byte_size(password) > 0 do
    Pbkdf2.verify_pass(password, hashed_password)　# Pbkdf2に変更
  end

  def valid_password?(_, _) do
    Pbkdf2.no_user_verify()　# Pbkdf2に変更
    false
  end

end
```

# ログイン前の画面
desktopの方にはリロードボタンがなくて面倒なので基本ブラウザで開発していきます
デベロッパーコンソール等でスマホサイズに画面をしておきましょう

```bash
MIX_TARGET=host iex -S mix phx.server
```

## トップページ 
### header箇所を削除
headerタグを削除します
ついでにbg-colorもセットします
```leex:trarecord/lib/trarecord_web/templates/layout/root.html.heex
<!DOCTYPE html>
<html lang="en">
  <head>
  ...
  </head>
 <body class="bg-base-200">
    <%= @inner_content %>
  </body>
</html>
```

### トップページの作成
daisyuiのheroのサンプルを貼り付けます

https://daisyui.com/components/hero/#hero-with-overlay-image

priv/static/images に top.pngという名前でおしゃれな写真を入れておいてください
pタグ内にそれっぽい内容に書き換えます

```leex:trarecord/lib/trarecord_web/templates/page/index.html.heex
<div class="hero min-h-screen" style={"background-image: url(#{Routes.static_path(@conn, "/images/top.png")});"}>
  <div class="hero-overlay bg-opacity-60"></div>
  <div class="hero-content text-center text-neutral-content">
    <div class="max-w-md">
      <h1 class="mb-5 text-5xl font-bold">Welcome to Trarecord</h1>
      <p class="mb-5">Trarecord is travel note. we provides pick up spot, record visited spot information and more...</p>
      <%= link "Get Started", to: Routes.user_registration_path(@conn, :new), class: "btn btn-primary" %>
    </div>
  </div>
</div>
```

![スクリーンショット 2022-09-08 21.05.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ae7393bc-585a-a3d1-242d-e544d3c3397f.png)


# 各種 phx.gen.authのページをスタイリング
基本的にはTailblockのCTAの２つ目をベースにします

https://tailblocks.cc/

https://github.com/mertJF/tailblocks/blob/master/src/blocks/cta/light/b.js

## register

```leex:trarecord/lib/trarecord_web/templates/user_registration/new.html.heex
<div class="hero min-h-screen">
  <div class="hero-content flex-col lg:flex-row-reverse">
    <div class="text-center lg:text-left">
      <h1 class="text-5xl font-bold">Register now!</h1>
      <p class="py-6">Trarecord is Travel notes.</p>
      <p class="py6">pick up want to spot. record visited spot information and more...</p>
    </div>
    <div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
      <div class="card-body">
        <.form let={f} for={@changeset} action={Routes.user_registration_path(@conn, :create)}>
          <%= if @changeset.action do %>
            <div class="alert alert-danger">
              <p>Oops, something went wrong! Please check the errors below.</p>
            </div>
          <% end %>
          <div class="form-control">
            <%= label f, :email, class: "label" %>
            <%= email_input f, :email, required: true, class: "input input-bordered" %>
            <%= error_tag f, :email %>
          </div>

          <div class="form-control">
            <%= label f, :password, class: "label" %>
            <%= password_input f, :password, required: true, class: "input input-bordered" %>
            <%= error_tag f, :password %>
          </div>

          <label class="label">
            <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
          </label>
          <label class="label">
            <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new), class: "label-text-alt link link-hover" %>
          </label>

          <div class="form-control mt-6">
            <%= submit "Register", class: "btn btn-primary" %>
          </div>
        </.form>
      </div>
    </div>
  </div>
</div>
```

![スクリーンショット 2022-09-08 21.07.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/a6eb47da-69c3-f612-7de9-375cf9f877f0.png)

## Login
```leex:trarecord/lib/trarecord_web/templates/user_session/new.html.heex
<div class="hero min-h-screen">
  <div class="hero-content flex-col lg:flex-row-reverse">
    <div class="text-center lg:text-left">
      <h1 class="text-5xl font-bold">Login now!</h1>
      <p class="py-6">Trarecord is Travel notes.</p>
      <p class="py-6">pick up want to spot. record visited spot information and more...</p>
    </div>
    <div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
      <div class="card-body">
        <.form let={f} for={@conn} action={Routes.user_session_path(@conn, :create)} as={:user}>
          <%= if @error_message do %>
            <div class="alert alert-danger">
              <p><%= @error_message %></p>
            </div>
          <% end %>
          <div class="form-control">
            <%= label f, :email, class: "label" %>
            <%= email_input f, :email, required: true, class: "input input-bordered" %>
            <%= error_tag f, :email %>
          </div>

          <div class="form-control">
            <%= label f, :password, class: "label" %>
            <%= password_input f, :password, required: true, class: "input input-bordered" %>
            <%= error_tag f, :password %>
          </div>

          <label class="label">
            <%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %> 
          </label>
          <label class="label">
            <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new), class: "label-text-alt link link-hover" %>
          </label>

          <div class="form-control mt-6">
            <%= submit "Login", class: "btn btn-primary" %>
          </div>
        </.form>
      </div>
    </div>
  </div>
</div>
```

ほぼ同じなので割愛

## forgot password?
こちらはTailBlockのContact１つめのフォーム部分のみ流用しています

https://tailblocks.cc/

https://github.com/mertJF/tailblocks/blob/master/src/blocks/contact/light/a.js

```leex:trarecord/lib/trarecord_web/templates/user_reset_password/new.html.heex
<section class="text-gray-600 body-font relative">
  <div class="container px-5 py-24 mx-auto">
    <div class="flex flex-col text-center  md:w-2/3 mx-auto mb-12">
      <h1 class="normal-case text-4xl">Trarecord</h1>
    </div>    
    <div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
      <h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Forgot your password?</h1>
    </div>
    <div class="lg:w-1/2 md:w-2/3 mx-auto">
      <.form let={f} for={:user} action={Routes.user_reset_password_path(@conn, :create)}>
        <%= label f, :email, class: "label" %>
        <%= email_input f, :email, required: true, class: "input input-bordered w-full" %>

        <div>
          <%= submit "Send instructions to reset password", class: "btn btn-primary mt-6" %>
        </div>
      </.form>

      <p class="mt-6">
        <%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %>  |
        <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
      </p>
    </div>
  </div>
</section>
```
![スクリーンショット 2022-09-08 21.08.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/0d4fc4c1-1632-d53c-78aa-f78b814f8cf9.png)


```leex:trarecord/lib/trarecord_web/templates/user_reset_password/edit.html.heex
<section class="text-gray-600 body-font relative">
  <div class="container px-5 py-24 mx-auto">
    <div class="flex flex-col text-center  md:w-2/3 mx-auto mb-12">
      <h1 class="normal-case text-4xl">Trarecord</h1>
    </div>    
    <div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
      <h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Reset password</h1>
    </div>
    <div class="lg:w-1/2 md:w-2/3 mx-auto">
      <.form let={f} for={@changeset} action={Routes.user_reset_password_path(@conn, :update, @token)}>
        <%= if @changeset.action do %>
          <div class="alert alert-danger">
            <p>Oops, something went wrong! Please check the errors below.</p>
          </div>
        <% end %>

        <%= label f, :password, "New password", class: "label" %>
        <%= password_input f, :password, required: true, class: "input input-bordered w-full" %>
        <%= error_tag f, :password %>

        <%= label f, :password_confirmation, "Confirm new password", class: "label" %>
        <%= password_input f, :password_confirmation, required: true, class: "input input-bordered w-full" %>
        <%= error_tag f, :password_confirmation %>

        <div class="mt-6">
          <%= submit "Reset password", class: "btn btn-primary" %>
        </div>
      </.form>

      <p class="mt-6">
        <%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %>  |
        <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
      </p>
    </div>
  </div>
</section>
```

ほとんど同じなので割愛

# ログイン後の画面
live_viewと通常のcontoller両方で使えるように Phoenix.Componentで共通コンポーネントを作成します
headerは左側を戻るボタン、右側をアクションを指定できるようにします

## Navigation
```trarecord/lib/trarecord_web/live/components/navigation.ex
defmodule TrarecordWeb.Components.Navigation do
  use Phoenix.Component

  def header(assigns) do
    ~H"""
    <div class="fixed z-20 navbar bg-primary text-primary-content w-full">
      <div class="navbar-start">
        <%= if @back do %>
          <%= live_redirect "Back", to: @back, class: "btn btn-ghost normal-case text-xl" %>
        <% end %>
      </div>
      <div class="navbar-center">
        <a class="btn btn-ghost normal-case text-4xl"><%= @title %></a>
      </div>
      <div class="navbar-end">
        <%= if @action do %>
          <%= @action %>
        <% end %>
      </div>
    </div>
    """
  end

  def bottom_tab(assigns) do
    ~H"""
    <div class="btm-nav">
    <button class={if @title == "Home", do: "active", else: ""}>
     <a href="/home">
       <span class="btm-nav-label">Home</span>
     </a>
    </button>
    <button class={if @title == "Items", do: "active", else: ""}>
     <a href="/home">
       <span class="btm-nav-label">Items</span>
     </a>
    </button>

    <button class={if @title == "Setting", do: "active", else: ""}>
     <a href="users/settings">
       <span class="btm-nav-label">Setting</span>
     </a>
    </button>
    </div>
    """
  end
end
```

現在はログイン後のHome, CRUD予定のItems, 3つ目にSettingになります

## Home
共通コンポーネントのヘッダーと下タブができたのでログイン後のホーム画面に追加します

```trarecord/lib/trarecord_web/live/home_live.ex
defmodule TrarecordWeb.HomeLive do
  use TrarecordWeb, :live_view
  alias TrarecordWeb.Components.Navigation

  @impl true
  def mount(_args, _session, socket) do
    socket
    |> assign(:title, "Home")
    |> then(&{:ok, &1})
  end
end
```
画面構成はDaisyUIのHeroにします

https://daisyui.com/components/hero/#centered-hero

```leex:trarecord/lib/trarecord_web/live/home_live.html.heex
<Navigation.header title={@title} back={false} action={false} />
<div class="hero min-h-screen bg-base-200">
  <div class="hero-content text-center">
    <div class="max-w-md">
      <h1 class="text-5xl font-bold">Hello there</h1>
      <p class="py-6">Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.</p>
      <button class="btn btn-primary">Get Started</button>
    </div>
  </div>
</div>
<Navigation.bottom_tab title={@title} />
```
最後にrouterに追加して完了です

```trarecord/lib/trarecord_web/router.ex
defmodule TrarecordWeb.Router do
  use TrarecordWeb, :router
  ...
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings", UserSettingsController, :update
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
    live "/home", HomeLive # 追加
  end  
  ...
end
```

## Settingページ
phx.gen.authのsettings_controllerを表示します

ベースはTailBlockのContact１つめのフォーム部分

https://tailblocks.cc/

https://github.com/mertJF/tailblocks/blob/master/src/blocks/contact/light/a.js

共通コンポーネントのaliasはcontrollerではなくviewですので注意が必要です

```trarecord/lib/trarecord_web/views/user_settings_view.ex
defmodule TrarecordWeb.UserSettingsView do
  use TrarecordWeb, :view

  alias TrarecordWeb.Components.Navigation # 追加
end
```


```leex:trarecord/lib/trarecord_web/templates/user_settings/edit.html.heex
<Navigation.header title="Setting" back={false} action={false} />
<section class="text-gray-600 body-font relative">
  <div class="container px-5 py-24 mx-auto">
    <div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
      <h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Settings</h1>
    </div>
    <div class="lg:w-1/2 md:w-2/3 mx-auto">
      <div class="mt-5">
        <h3 class="text-xl text-gray-900">Change email</h3>

        <.form let={f} for={@email_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_email">
          <%= if @email_changeset.action do %>
            <div class="alert alert-danger">
              <p>Oops, something went wrong! Please check the errors below.</p>
            </div>
          <% end %>

          <%= hidden_input f, :action, name: "action", value: "update_email" %>

          <%= label f, :email, class: "label" %>
          <%= email_input f, :email, required: true, class: "input input-bordered w-full" %>
          <%= error_tag f, :email %>

          <%= label f, :current_password, for: "current_password_for_email", class: "label" %>
          <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email", class: "input input-bordered w-full" %>
          <%= error_tag f, :current_password %>

          <div class="mt-5">
            <%= submit "Change email", class: "btn btn-block btn-primary" %>
          </div>
        </.form>
      </div>
      <div class="mt-6">
        <h3 class="text-xl text-gray-900">Change password</h3>

        <.form let={f} for={@password_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_password">
          <%= if @password_changeset.action do %>
            <div class="alert alert-danger">
              <p>Oops, something went wrong! Please check the errors below.</p>
            </div>
          <% end %>

          <%= hidden_input f, :action, name: "action", value: "update_password" %>

          <%= label f, :password, "New password", class: "label" %>
          <%= password_input f, :password, required: true, class: "input input-bordered w-full" %>
          <%= error_tag f, :password %>

          <%= label f, :password_confirmation, "Confirm new password", class: "label" %>
          <%= password_input f, :password_confirmation, required: true, class: "input input-bordered w-full" %>
          <%= error_tag f, :password_confirmation %>

          <%= label f, :current_password, for: "current_password_for_password", class: "label" %>
          <%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password", class: "input input-bordered w-full" %>
          <%= error_tag f, :current_password %>

          <div class="mt-6">
            <%= submit "Change password", class: "btn btn-block btn-primary" %>
          </div>
        </.form>
      </div>
      <div class="mt-6">
        <%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete, class: "btn btn-block btn-error" %>
      </div>

    </div>
  </div>
</section>
<Navigation.bottom_tab title="Setting" />
```

ページの最後にログアウトボタンも追加しておきます

## トップページ切り替え
次にログイン時はHome、非ログイン時にはユーザー登録ページにリダイレクトするようにします

```trarecord/lib/trarecord_web/controllers/page_controller.ex
defmodule TrarecordWeb.PageController do
  use TrarecordWeb, :controller

  alias Trarecord.Users.User

  def index(conn, _params) do
    redirect_to(conn, conn.assigns.current_user)
  end

  def redirect_to(conn, %User{}) do
    redirect(conn, to: Routes.live_path(TrarecordWeb.Endpoint, TrarecordWeb.HomeLive))
  end

  def redirect_to(conn, nil) do
    redirect(conn, to: Routes.user_registration_path(TrarecordWeb.Endpoint, :new))
  end
end
```

![300a38b588d71912c69618bc716aa907.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/5b893914-20e1-9195-64ac-d4f2c616d2d2.gif)

## Flashをtoastに変更
今のままだとヘッダーの上の段に出てしまうので、スマホでよくある右下に出るやつに変更します

fadeoutアニメーションが無いのでこちらを参考に追加します

https://zenn.dev/k_neko3/articles/1aee5b86d07f4f

```trarecord/assets/tailwind.config.js
let plugin = require('tailwindcss/plugin')

module.exports = {
  content: [...],
  theme: {
    extend: {
      // ここから
      keyframes: {
        disappear: {
          "0%": { opacity: 1 },
          "100%": { opacity: 0 },
        },
      },
      animation: {
        disappear: "disappear 3s ease 2s 1 forwards",
      },
      // ここまで追加
    },
  },
  plugins: [...]
}

```

コンポーネントはこちら

https://daisyui.com/components/toast/#toast-with-alert-inside


appは通常のcontrollerで表示するページで適応されます
下タブに重ならないようにz-40としています

```leex:trarecord/lib/trarecord_web/templates/layout/app.html.heex
<main class="container">
  <div class="toast toast-end z-40 animate-disappear">
      <div>
        <p 
          class={"alert alert-info " <> if is_nil(get_flash(@conn, :info)), do: "hidden", else: ""}
          role="alert"
        ><%= get_flash(@conn, :info) %></p>
      </div>
      <div>
        <p 
          class={"alert alert-danger " <> if is_nil(get_flash(@conn, :error)), do: "hidden", else: ""}
          role="alert"
        ><%= get_flash(@conn, :error) %></p>
      </div>
  </div>
  <%= @inner_content %>
</main>
```
liveは名前の通りLiveViewで表示するページで適応されます


```leex:trarecord/lib/trarecord_web/templates/layout/live.html.heex
<main class="container">
  <div class="toast toast-end z-40 animate-disappear">
    <div>
      <p class={"alert alert-info " <> if is_nil(live_flash(@flash, :info)), do: "hidden", else: ""}
          role="alert"
          phx-click="lv:clear-flash"
          phx-value-key="info">
        <%= live_flash(@flash, :info) %>
      </p>
    </div>
    <div>
      <p  class={"alert alert-danger " <> if is_nil(live_flash(@flash, :error)), do: "hidden", else: ""}
          role="alert"
          phx-click="lv:clear-flash"
          phx-value-key="error">
        <%= live_flash(@flash, :error) %>
      </p>
    </div>
  </div>
  <%= @inner_content %>
</main>
```

![d98eb4ddee1a569b06bc47d0dd32f520.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/ac690d76-ea96-81f5-0279-5bf05512125c.gif)

# CRUD画面の追加
最後にCRUD画面を作ります
例のごとくphx.gen.liveで
リレーションまで加えると長くなるので今回は割愛します

```
mix phx.gen.live Items Item items name:string url:string image:string category:string status:boolean
mix ecto.migrate
```

出てきたルーティングを追加します

```trarecord/lib/trarecord_web/router.ex
defmodule TrarecordWeb.Router do
  use TrarecordWeb, :router

  ...
  scope "/", TrarecordWeb do
    pipe_through [:browser, :require_authenticated_user]

    get "/users/settings", UserSettingsController, :edit
    put "/users/settings", UserSettingsController, :update
    get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
    live "/settings", SettingLive.Index, :index
    live "/home", HomeLive
    
    # 以下追加
    live "/items", ItemLive.Index, :index
    live "/items/new", ItemLive.Index, :new
    live "/items/:id/edit", ItemLive.Index, :edit

    live "/items/:id", ItemLive.Show, :show
    live "/items/:id/show/edit", ItemLive.Show, :edit
  end
  ...
end
```

## confirm modalの実装
ネイティブアプリだとalertやconfirmダイアログがでないので、モーダルで表示するようにします

```trarecord/lib/trarecord_web/live/item_live/confirm_component.ex
defmodule TrarecordWeb.ItemLive.ConfirmComponent do
  use TrarecordWeb, :live_component

  alias Trarecord.Items
  alias Phoenix.LiveView.JS

  @impl true
  def update(%{item: _item} = assigns, socket) do
    {:ok, assign(socket, assigns)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    item = Items.get_item!(id)
    {:ok, _} = Items.delete_item(item)

    socket
    |> put_flash(:info, "Item delete successfully")
    |> push_redirect(to: socket.assigns.return_to)
    |> then(&{:noreply, &1})
  end

  defp list_items do
    Items.list_items()
  end
end
```
cancelボタンは live_hlperに実装されているモーダルの閉じるボタンへのクリックイベントを発火させています

```trarecord/lib/trarecord_web/live/live_helpers.ex
<a id="close" href="#" class="phx-modal-close" phx-click={hide_modal()}>✖</a>

  defp hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(to: "#modal", transition: "fade-out")
    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
  end
```


```leex:trarecord/lib/trarecord_web/live/item_live/confirm_component.html.heex
<div class="card w-80">
  <div class="card-body items-center text-center">
    <h2 class="card-title mb-4">Are you sure?</h2>
    <div class="card-actions justify-end">
      <button class="btn btn-ghost" phx-click={JS.dispatch("click", to: "#close")}>Cancel</button>
      <button class="btn btn-error" 
              phx-click="delete" 
              phx-value-id={@item.id}
              phx-target={@myself}
      >Delete</button>
    </div>
  </div>
</div>
```

indexとshowからのルーティングを追加します

```trarecord/lib/trarecord_web/router.ex
defmodule TrarecordWeb.Router do
  use TrarecordWeb, :router

  ...
  scope "/", TrarecordWeb do
    pipe_through([:browser, :require_authenticated_user])

    get("/users/settings", UserSettingsController, :edit)
    put("/users/settings", UserSettingsController, :update)
    get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
    live("/settings", SettingLive.Index, :index)
    live("/home", HomeLive)

    live("/items", ItemLive.Index, :index)
    live("/items/new", ItemLive.Index, :new)
    live("/items/:id/edit", ItemLive.Index, :edit)
    live("/items/:id/delete", ItemLive.Index, :delete) # 追加

    live("/items/:id", ItemLive.Show, :show)
    live("/items/:id/show/edit", ItemLive.Show, :edit)
    live("/items/:id/show/delete", ItemLive.Show, :delete) # 追加
  end

```

## フォームモーダルのスタイリング
inputとlabelにクラスを追加します

```leex:trarecord/lib/trarecord_web/live/item_live/form_component.html.heex
<div>
  <h2 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900"><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="item-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= label f, :name, class: "label" %>
    <%= text_input f, :name, class: "input input-bordered w-full" %>
    <%= error_tag f, :name %>
  
    <%= label f, :url, class: "label" %>
    <%= text_input f, :url, class: "input input-bordered w-full" %>
    <%= error_tag f, :url %>
  
    <%= label f, :image, class: "label" %>
    <%= text_input f, :image, class: "input input-bordered w-full" %>
    <%= error_tag f, :image %>
  
    <%= label f, :category, class: "label" %>
    <%= text_input f, :category, class: "input input-bordered w-full" %>
    <%= error_tag f, :category %>
  
    <%= label f, "Have It", class: "label" %>
    <%= checkbox f, :status,class: "checkbox" %>
    <%= error_tag f, :status %>
  
    <div>
      <%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-primary mt-4" %>
    </div>
  </.form>
</div>

```

## モーダルのスタイリング
といっても角を丸くするだけです

```trarecord/lib/trarecord_web/live/live_helpers.ex
defmodule TrarecordWeb.LiveHelpers do
  import Phoenix.LiveView
  import Phoenix.LiveView.Helpers

  alias Phoenix.LiveView.JS
  def modal(assigns) do
    assigns = assign_new(assigns, :return_to, fn -> nil end)

    ~H"""
    <div id="modal" class="phx-modal fade-in" phx-remove={hide_modal()}>
      <div
        id="modal-content"
        class="phx-modal-content fade-in-scale rounded-lg" # rouded-lgを追加
        phx-click-away={JS.dispatch("click", to: "#close")}
        phx-window-keydown={JS.dispatch("click", to: "#close")}
        phx-key="escape"
      >
      ...
      </div>
    </div>
    """
  end
  ...
end
```
## indexのスタイリング

コンポーネントの追加とモーダルに切り出したdelete部分を改修します

```diff_elixir:trarecord/lib/trarecord_web/live/item_live/index.ex
defmodule TrarecordWeb.ItemLive.Index do
  use TrarecordWeb, :live_view

  alias Trarecord.Items
  alias Trarecord.Items.Item
  alias TrarecordWeb.Components.Navigation # 追加
  ... 

+  defp apply_action(socket, :delete, %{"id" => id}) do
+    socket
+    |> assign(:page_title, "Delete Item")
+    |> assign(:item, Items.get_item!(id))
+  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Items")
    |> assign(:item, nil)
  end

-  @impl true
-  def handle_event("delete", %{"id" => id}, socket) do
-    item = Items.get_item!(id)
-    {:ok, _} = Items.delete_item(item)
-
-    {:noreply, assign(socket, :items, list_items())}
-  end
end
```

Headerはbackは無いのでfalse, actionは新規追加モーダル表示用のリンクを追加します

デザインはECOMMERCEの１つめをベースにします

https://tailblocks.cc/

https://github.com/mertJF/tailblocks/blob/master/src/blocks/ecommerce/light/a.js

実際の表示部分はCardコンポーネントを使っています

https://daisyui.com/components/card/#card-with-badge

```leex:trarecord/lib/trarecord_web/live/item_live/index.html.heex
<Navigation.header 
  title="Items" 
  back={false} 
  action={live_patch "Add Item", to: Routes.item_index_path(@socket, :new), class: "btn btn-ghost normal-case"}
/>
<section class="text-gray-600 body-font relative">
  <div class="container px-5 py-24 mx-auto">
    <div class="lg:w-1/2 md:w-2/3 flex flex-col mb-4 mx-auto">
      <h1 class="sm:text-3xl text-2xl font-medium title-font text-gray-900">Listing Items</h1>
    </div>
    <section>
      <div class="container px-5 py-4 mx-auto">
        <div class="flex flex-wrap -m-4">
          <%= for item <- @items do %>
            <div id={"item-#{item.id}"} class="card card-compact bg-base-100 shadow-2xl lg:w-1/4 md:w-1/2 p-4 w-full mb-4">
              <figure><img src={item.image} alt="" /></figure>
              <div class="card-body">
                <h2 class="card-title">
                  <%= item.name %>
                  <%= if item.status do %>
                    <div class="badge badge-secondary">Have It</div>
                  <% else %>
                    <div class="badge badge-secondary">Wants It</div>
                  <% end %>
                  <div class="badge"><%= item.category %></div>
                </h2>
                
                <a class="link" href={item.url}><%= item.url %></a>
              </div>
              <div class="card-actions justify-end">
                <span><%= live_redirect "Show", to: Routes.item_show_path(@socket, :show, item), class: "btn normal-case" %></span>
                <span><%= live_patch "Edit", to: Routes.item_index_path(@socket, :edit, item), class: "btn normal-case" %></span>
                <!--- deleteを live_patchにしてモーダルを表示するように変更 --->
                <span><%= live_patch "Delete", to: Routes.item_index_path(@socket, :delete, item), class: "btn btn-error normal-case" %></span>
              </div>
            </div>
          <% end %>
        </div>
      </div>
    </section>

    <%= if @live_action in [:new, :edit] do %>
      <.modal return_to={Routes.item_index_path(@socket, :index)}>
        <.live_component
          module={TrarecordWeb.ItemLive.FormComponent}
          id={@item.id || :new}
          title={@page_title}
          action={@live_action}
          item={@item}
          return_to={Routes.item_index_path(@socket, :index)}
        />
      </.modal>
    <% end %>

    <!--- 以下追加 --->
    <%= if @live_action in [:delete] do %>
      <.modal return_to={Routes.item_index_path(@socket, :index)}>
        <.live_component
          module={TrarecordWeb.ItemLive.ConfirmComponent}
          id={@item.id}
          title={@page_title}
          action={@live_action}
          item={@item}
          return_to={Routes.item_index_path(@socket, :index)}
        />
      </.modal>
    <% end %>
  </div>
</section>
<Navigation.bottom_tab title="Items" />
```

## Showのスタイリング

コンポーネントの追加と deleteモーダルのpage_titleを追加します

```trarecord/lib/trarecord_web/live/item_live/show.ex
efmodule TrarecordWeb.ItemLive.Show do
  use TrarecordWeb, :live_view

  alias Trarecord.Items
  alias TrarecordWeb.Components.Navigation # 追加

  ...
  defp page_title(:show), do: "Show Item"
  defp page_title(:edit), do: "Edit Item"
  defp page_title(:delete), do: "Delete Item" # 追加
end
```

headerのbackにindex, actionにedit ページ下部に削除ボタンをつけました
削除モーダルですが
.modal の return_toはモーダルを閉じた時に戻る箇所
.live_componentのreturn_toは削除後に戻る箇所になります

閉じたらshow、削除したらindexに戻るようにします

```leex:trarecord/lib/trarecord_web/live/item_live/show.html.heex
<Navigation.header 
  title="Items" 
  back={Routes.item_index_path(@socket, :index)} 
  action={live_patch "Edit", to: Routes.item_show_path(@socket, :edit, @item), class: "btn btn-ghost normal-case text-xl"}
/>
<section class="text-gray-600 body-font relative">
  <div class="container px-5 py-24 mx-auto">
    <div class="lg:w-1/2 md:w-2/3 flex flex-col mb-4 mx-auto">
      <h1 class="sm:text-3xl text-2xl font-medium title-font text-gray-900">Show Item</h1>
    </div>

    <%= if @live_action in [:edit] do %>
      <.modal return_to={Routes.item_show_path(@socket, :show, @item)}>
        <.live_component
          module={TrarecordWeb.ItemLive.FormComponent}
          id={@item.id}
          title={@page_title}
          action={@live_action}
          item={@item}
          return_to={Routes.item_show_path(@socket, :show, @item)}
        />
      </.modal>
    <% end %>

    <%= if @live_action in [:delete] do %>
      <.modal return_to={Routes.item_show_path(@socket, :show, @item)}>
        <.live_component
          module={TrarecordWeb.ItemLive.ConfirmComponent}
          id={@item.id}
          title={@page_title}
          action={@live_action}
          item={@item}
          return_to={Routes.item_index_path(@socket, :index)}
        />
      </.modal>
    <% end %>

    <ul>

      <li>
        <strong>Name:</strong>
        <%= @item.name %>
      </li>

      <li>
        <strong>Url:</strong>
        <a href={@item.url}><%= @item.url %></a>
      </li>

      <li>
        <strong>Image:</strong>
        <img class="object-contain w-1/2" src={@item.image} />
      </li>

      <li>
        <strong>Category:</strong>
        <%= @item.category %>
      </li>

      <li>
        <strong>Have It:</strong>
        <%= @item.status %>
      </li>

    </ul>
    <span><%= live_patch "Delete", to: Routes.item_show_path(@socket, :delete, @item), class: "btn btn-block btn-error normal-case" %></span>
  </div>
</section>
<Navigation.bottom_tab title="Items" />
```
画面は以上になります

# DEMO

![7adbe0634b5992a44e33f89bc3900928.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/66dbe678-37c3-31a3-4e0d-c11178b1c048.gif)

# 最後に

スタイリングしたため膨大な記事になりましたが
グローバルナビゲーションとconfirmモーダルをつければ割と簡単にスマホアプリが作れれそうです！

今は NIFを使うライブラリが使えないですが、iOSでEvision(openCV wrapper)が動いたらしいので、
NIFを使うライブラリもそのうち使えるようなるかもしれません本時期は以上になりますありがとうございました


# コード

https://github.com/thehaigo/trarecord/tree/crud_phx_gen_auth_sample

# 参考サイト
https://qiita.com/the_haigo/items/27bf18acd28971af31e9
https://tailwindcss.com/docs/guides/phoenix
https://daisyui.com/docs/install/
https://daisyui.com/components/hero/#hero-with-overlay-image
https://daisyui.com/components/hero/#centered-hero
https://daisyui.com/components/card/#card-with-badge
https://daisyui.com/components/toast/#toast-with-alert-inside
https://github.com/riverrun/pbkdf2_elixir
https://tailblocks.cc/
https://github.com/mertJF/tailblocks/blob/master/src/blocks/cta/light/b.js
https://github.com/mertJF/tailblocks/blob/master/src/blocks/contact/light/a.js
https://github.com/mertJF/tailblocks/blob/master/src/blocks/ecommerce/light/a.js
https://zenn.dev/k_neko3/articles/1aee5b86d07f4f