はじめに
ひとりLiveView Advent Calendar の8日目の記事です
この記事はElixir Conf US 2021の発表したシステムの構築と関連技術の解説を目的とした記事です
今回はBulmaを使用してデザインを整えていきます
- Bulmaの読み込み
- Layout
- TopPage
- phx.gen.auth
- phx.gen.live
Bulmaの読み込み
トレンドからは少しずれていますがBulmaでスタイリングしていきます
npmでもいいですが、本番環境ではないのでcdnで十分でしょう
phoenix.cssは競合するので使用しないようにします
- @import "./phoenix.css";
+ @import "https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css";
layoutファイルのスタイリング
phoenix.cssとmiligramを消したので現在こんな感じです
プロジェクト全体のレイアウトはroot.html.heexです
bulmaでheader部分をスタイリングしていきます
phoenixロゴではなくてアプリ名に変えます
<!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のリンクもリストからボタングループに変更します
<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で余白をもたせて全体に広げています
<main role="main" class="container is-fluid">
...
</main>
Top Page
トップページを変更しましょうhero unitでアプリ名とサブタイトルをつけます
<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>
phx.gen.auhまわり
こちらもデザインが崩れているのでいい感じにしていきます
以下のようにスタイリングすると画面の半分の幅で中央寄せにしてくれます
column is-centerered is-mobile
column is-half
フォームは基本的にfiledで囲って、label,inputのクラスを付与します
submitにbuttonクラスを付与します
<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>
<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>
<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>
<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>
最後にログイン後に/mapsに飛ぶように指定します
defmodule LiveLoggerWeb.UserAuth do
...
defp signed_in_path(_conn), do: "/maps"
end
phx.gen.live まわり
phx.gen.live周りはグリッドシステムは使用せずに基本的にcontentクラスをつけるくらいです
<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>
formはfield,label,input,buttonを付与します
<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>
<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>
最後に
だいたいいい感じになりました
実際に使うのはlayoutとshow.html.heexくらいなので、ほかは飛ばしても構いません
次はPhoenixから少し離れてGPS LoggerのクライアントをExpoでつくります
コード