Phoenixで作るGPS Logging System 8 Bulmaでスタイリング

# はじめに
[ひとりLiveView Advent Calendar](https://qiita.com/advent-calendar/2021/liveview_alone) の8日目の記事です

この記事は[Elixir Conf US 2021](https://www.elixirconf.com/)の[発表](https://www.youtube.com/watch?v=vHNqZmLLTj4)したシステムの構築と関連技術の解説を目的とした記事です

今回はBulmaを使用してデザインを整えていきます

- Bulmaの読み込み
- Layout
- TopPage
- phx.gen.auth
- phx.gen.live

# Bulmaの読み込み
https://bulma.io/documentation/

トレンドからは少しずれていますがBulmaでスタイリングしていきます
npmでもいいですが、本番環境ではないのでcdnで十分でしょう
phoenix.cssは競合するので使用しないようにします

```diff:assets/css/app.css
- @import "./phoenix.css";
+ @import "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css";
```

# layoutファイルのスタイリング
phoenix.cssとmiligramを消したので現在こんな感じです
![スクリーンショット 2021-12-17 20.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/c16f5c31-567a-159f-7a52-a2f6ac4aeb3b.png)
プロジェクト全体のレイアウトはroot.html.heexです
bulmaでheader部分をスタイリングしていきます
phoenixロゴではなくてアプリ名に変えます

```leex:lib/live_logger_web/templates/layout/root.html.heex
<!DOCTYPE html>
<html lang="en">
  <head>
    ...
  </head>
  <body>
    <header class="container is-fluid mb-5 pl-0 pr-0">
      <section class="hero is-link is-small">
        <nav role="navigation" class="navbar hero-body" aria-label="main navigation">
          <div class="navbar-brand">
            <h1 class="navbar-item title">
              <a href="/">LiveLogger</a>
            </h1>
          </div>
          <div class="navbar-end">
            <%= render "_user_menu.html", assigns%>
          </div>
        </nav>
      </section>
    </header>
    <%= @inner_content %>
  </body>
</html>
```
headerのリンクもリストからボタングループに変更します

```leex:lib/live_logger_web/templates/layout/_user_menu.html.heex
<div class="navbar-item">
  <div class="buttons">
    <%= if @current_user do %>
      <%= link "Maps", to: Routes.map_index_path(@conn, :index), class: "button"%>
      <%= link "Settings", to: Routes.user_settings_path(@conn, :edit), class: "button" %>
      <%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete, class: "button" %>
    <% else %>
      <%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "button" %>
      <%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "button" %>
    <% end %>
  </div>
</div>
```
LiveView時の追加のレイアウトです、containerとis-fluidで余白をもたせて全体に広げています

```leex:lib/live_logger_web/templates/layout/live.html.heex
<main role="main" class="container is-fluid">
...
</main>
```

![スクリーンショット 2021-12-17 20.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/15f34268-c7e4-1833-603e-fb127fe836f7.png)

# Top Page
トップページを変更しましょうhero unitでアプリ名とサブタイトルをつけます

```leex:lib/live_logger_web/templates/page/index.html.heex
<section class="hero">
  <div class="hero-body">
    <p class="title">LiveLogger</p>
    <p class="subtitle">Real-Time Update Multi-Client GPS Logging Viewer</p>
  </div>
</section>
```

![スクリーンショット 2021-12-17 22.28.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/12562bb7-bc2a-9261-36ff-a20d30a623ee.png)


# phx.gen.auhまわり
こちらもデザインが崩れているのでいい感じにしていきます

![スクリーンショット 2021-12-17 20.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/cedae9f3-fc7e-a87c-754d-acec6648cf57.png)
以下のようにスタイリングすると画面の半分の幅で中央寄せにしてくれます
`column is-centerered is-mobile`
`column is-half`

フォームは基本的にfiledで囲って、label,inputのクラスを付与します
submitにbuttonクラスを付与します

```leex:lib/live_logger_web/templates/user_registration/new.html.heex
<div class="columns is-centered is-mobile">
  <div class="column is-half content">
    <h1>Register</h1>
    <div class="box">
      <.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="field">
        <%= label f, :email, class: "label" %>
        <%= email_input f, :email, required: true, class: "input" %>
        <%= error_tag f, :email %>
        </div>

        <div class="field">
          <%= label f, :password, class: "label" %>
          <%= password_input f, :password, required: true, class: "input" %>
          <%= error_tag f, :password %>
        </div>
        <div class="control">
          <%= submit "Register", class: "button is-primary" %>
        </div>
      </.form>

      <p class="mt-5">
        <%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
        <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
      </p>
    </div>
  </div>
</div>
```
![スクリーンショット 2021-12-17 20.58.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/707199ee-0a23-7e22-6f1b-3879fe51f09c.png)
だいたいさっきと同じです

```leex:lib/live_logger_web/templates/user_session/new.html.heex
<div class="columns is-centered is-mobile">
  <div class="column is-half content">
    <h1>Log in</h1>
    <div class="box">
      <.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="field">
          <%= label f, :email, class: "label" %>
          <%= email_input f, :email, required: true, class: "input" %>
          <%= error_tag f, :email %>
        </div>

        <div class="field">
          <%= label f, :password, class: "label" %>
          <%= password_input f, :password, required: true, class: "input" %>
          <%= error_tag f, :password %>
        </div>

        <div class="field">
          <%= checkbox f, :remember_me %>
          <%= label f, :remember_me, "Keep me logged in for 60 days", class: "checkbox" %>
          
        </div>

        <div class="control">
          <%= submit "Log in", class: "button is-primary" %>
        </div>
      </.form>

      <p class="mt-5">
        <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
        <%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
      </p>
    </div>
  </div>
</div>
```
![スクリーンショット 2021-12-17 20.58.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/9e140ec3-01fb-1cf7-4aa3-da39bb48d8e1.png)


```leex:lib/live_logger_web/templates/user_reset_password/new.html.heex
<div class="columns is-centered is-mobile">
  <div class="column is-half content">
    <h1>Forgot your password?</h1>
    <div class="box">
      <.form let={f} for={:user} action={Routes.user_reset_password_path(@conn, :create)}>
        <div class="field">
        <%= label f, :email, class: "label" %>
        <%= email_input f, :email, required: true, class: "input" %>
        </div>
        <div class="field">
          <%= submit "Send instructions to reset password", class: "button is-primary" %>
        </div>
      </.form>

      <p class="mt-5">
        <%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
        <%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
      </p>
    </div>
  </div>
</div>
```
![スクリーンショット 2021-12-17 20.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/fd12d933-4888-cc5e-8ee2-93f335cf19d5.png)

```leex:lib/live_logger_web/templates/user_settings/edit.html.heex
<div class="columns is-centered is-mobile">
  <div class="column is-half content">
    <h1>Settings</h1>

    <h3>Generate Passcode</h3>
    <%= link "Generate Passcode", 
              to: Routes.user_settings_path(@conn, :generate_passcode), 
              method: :post, 
              class: "button is-info" 
    %>

    <%= if @conn.assigns.current_user.passcode do %>
      <p><%= "passcode:" <> @conn.assigns.current_user.passcode %></p>
    <% end %>

    <h3>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" %>
      <%= 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" %>
      <%= error_tag f, :current_password %>

      <div class="mt-5">
        <%= submit "Change email", class: "button is-info" %>
      </div>
    </.form>

    <h3>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" %>
      <%= error_tag f, :password %>

      <%= label f, :password_confirmation, "Confirm new password", class: "label" %>
      <%= password_input f, :password_confirmation, required: true, class: "input" %>
      <%= 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" %>
      <%= error_tag f, :current_password %>

      <div class="mt-5">
        <%= submit "Change password", class: "button is-info" %>
      </div>
    </.form>
  </div>
</div>
```

![スクリーンショット 2021-12-17 22.57.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/eca472e3-2dce-d827-5d3f-ef86e3ea17f5.png)

最後にログイン後に/mapsに飛ぶように指定します

```lib/live_logger_web/controllers/user_auth.ex
defmodule LiveLoggerWeb.UserAuth do
  ...
  defp signed_in_path(_conn), do: "/maps"
end
```

# phx.gen.live まわり
phx.gen.live周りはグリッドシステムは使用せずに基本的にcontentクラスをつけるくらいです

```leex:lib/live_logger_web/live/map_live/index.html.heex
<div class="content">
  <h1>Listing Maps</h1>

  <%= if @live_action in [:new, :edit] do %>
    <%= live_modal LiveLoggerWeb.MapLive.FormComponent,
      id: @map.id || :new,
      title: @page_title,
      action: @live_action,
      map: @map,
      return_to: Routes.map_index_path(@socket, :index) %>
  <% end %>

  <table class="table">
    <thead>
      <tr>
        <th>User</th>
        <th>Name</th>
        <th>Description</th>

        <th></th>
      </tr>
    </thead>
    <tbody id="maps">
      <%= for map <- @maps do %>
        <tr id={"map-#{map.id}"}>
          <td><%= map.user.email %></td>
          <td><%= map.name %></td>
          <td><%= map.description %></td>
          <td>
            <span><%= live_redirect "Show", to: Routes.map_show_path(@socket, :show, map) %></span>
            <span><%= live_patch "Edit", to: Routes.map_index_path(@socket, :edit, map) %></span>
            <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: map.id, data: [confirm: "Are you sure?"] %></span>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>

  <span><%= live_patch "New Map", to: Routes.map_index_path(@socket, :new) %></span>
</div>
```

![スクリーンショット 2021-12-17 22.59.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2b34c463-eacd-c023-19ed-975d07cd803c.png)
formはfield,label,input,buttonを付与します

```leex:lib/live_logger_web/live/map_live/form_component.html.heex
<div class="content">
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="map-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <div class="field">
      <%= label f, :name, class: "label" %>
      <%= text_input f, :name, class: "input" %>
      <%= error_tag f, :name %>
    </div>

    <div class="field">
      <%= label f, :description, class: "label" %>
      <%= text_input f, :description, class: "input" %>
      <%= error_tag f, :description %>
    </div>  

    <%= hidden_input f, :user_id %>

    <div class="field">
      <%= submit "Save", phx_disable_with: "Saving...", class: "button is-info" %>
    </div>
  </.form>
</div>
```

![スクリーンショット 2021-12-17 22.59.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/d8cf7ada-a691-9f2f-4c36-eb83905e40ff.png)
Map詳細画面はサイドバーとメイン画面に分けます

```leex:lib/live_logger_web/live/map_live/show.html.heex
<div class="columns">
  <div class="column is-2 content">
    <h1>Show Map</h1>
    <span><%= live_patch "Edit", to: Routes.map_show_path(@socket, :edit, @map), class: "button is-info" %></span>
    <span><%= live_redirect "Back", to: Routes.map_index_path(@socket, :index), class: "button" %></span>
    <ul>
      <li>
        <strong>Name:</strong>
        <%= @map.name %>
      </li>
      <li>
        <strong>Description:</strong>
        <%= @map.description %>
      </li>
    </ul>    
  </div>
  <div class="column is-10">
    <%= if @live_action in [:edit] do %>
      <%= live_modal LiveLoggerWeb.MapLive.FormComponent,
        id: @map.id,
        title: @page_title,
        action: @live_action,
        map: @map,
        return_to: Routes.map_show_path(@socket, :show, @map) %>
    <% end %>

    <div id="googleMap" phx-update="ignore" phx-hook="Map">
      <div id="map"></div>
    </div>
  </div>
</div>
```

![スクリーンショット 2021-12-17 23.07.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/5bae9a08-e7d3-b14e-8395-f39e4842b6cc.png)

# 最後に
だいたいいい感じになりました
実際に使うのはlayoutとshow.html.heexくらいなので、ほかは飛ばしても構いません

次はPhoenixから少し離れてGPS LoggerのクライアントをExpoでつくります

# コード
https://github.com/thehaigo/live_logger/tree/day8