はじめに
この記事はElixirアドベントカレンダー2024のシリーズ2、14日目の記事です
アプリ全体の設定を作成していきます
作る機能
- UIに使うテキストの辞書データ作成
- 設定一覧画面を作成
- ユーザー設定を配下に追加
- お問い合わせを配下に追加
- プライバシーポリシーを配下に追加
- 利用規約を配下に追加
- ユーザー削除ボタンを追加
- ログアウトボタンを追加
辞書データを作成
今回使う文言を末尾に追加
priv/gettext/default.pot:L186
# Setting
msgid "General"
msgstr ""
msgid "Privacy Policy"
msgstr ""
msgid "Terms of Service"
msgstr ""
msgid "Delete Account"
msgstr ""
msgid "Sign out"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Contact"
msgstr ""
以下のコマンドを実行して他の辞書に反映します
mix gettext.merge priv/gettext
日本語テキストを追加
priv/gettext/ja/LC_MESSAGES/default.po
# Setting
msgid "General"
msgstr "ユーザー設定"
msgid "Privacy Policy"
msgstr "プライバシーポリシー"
msgid "Terms of Service"
msgstr "利用規約"
msgid "Delete Account"
msgstr "アカウント削除"
msgid "Sign out"
msgstr "ログアウト"
msgid "Setting"
msgstr "設定"
msgid "Contact"
msgstr "問い合わせ"
設定一覧画面を作成
ベースとなるページを作成
lib/trarecord_web/live/setting_live/index.ex
defmodule TrarecordWeb.SettingsLive.Index do
use TrarecordWeb, :live_view
@impl true
def render(assigns) do
~H"""
<.header_nav title={gettext("Setting")} />
<div id="settings" class="h-screen mt-20 flex flex-col gap-y-8 mx-2">
<table class="table bg-white text-base">
<tbody>
<tr>
<td>
<.link class="flex justify-between" navigate={~p"/users/settings"}>
<p><%= gettext("General") %></p>
<.icon name="hero-chevron-right-solid" />
</.link>
</td>
</tr>
</tbody>
</table>
</div>
<.bottom_tab current="Setting" />
"""
end
@impl true
def mount(_params, session, socket) do
Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
{:ok, socket}
end
end
ルートに追加
lib/trarecord_web/router.ex
scope "/", TrarecordWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
+ live "/settings", SettingsLive, :infex
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/onboarding", OnboardingLive.Index, :index
live "/folders", FolderLive.Index, :index
live "/folders/new", FolderLive.Index, :new
live "/folders/:id/edit", FolderLive.Index, :edit
live "/folders/:id/delete", FolderLive.Index, :delete
live "/folders/:id", FolderLive.Show, :show
live "/folders/:id/show/edit", FolderLive.Show, :edit
end
end
ナビゲーションリンクの差し替え
lib/trarecord_web/components/navigation.ex:L50
defp links() do
[
{"Folder", "hero-book-open-solid", "/folders"},
- {"Setting", "hero-cog-6-tooth-solid", "/users/settings"}
+ {"Setting", "hero-cog-6-tooth-solid", "/settings"}
]
end
ユーザー設定を配下に追加
導線・ページ自体は作ってあるので、戻るボタンを表示します
lib/trarecord_web/live/user_settings_live.ex
defmodule TrarecordWeb.UserSettingsLive do
use TrarecordWeb, :live_view
alias Trarecord.Accounts
def render(assigns) do
~H"""
- <.header_nav title={gettext("Setting")} />
+ <.header_nav title={gettext("Setting")}>
+ <:back>
+ <.link navigate={~p"/settings"}>
+ <%= gettext("Back") %>
+ </.link>
+ </:back>
+ </.header_nav>
...
"""
end
- def mount(%{"token" => token}, _session, socket) do
+ def mount(%{"token" => token}, session, socket) do
+ Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
socket =
case Accounts.update_user_email(socket.assigns.current_user, token) do
:ok ->
put_flash(socket, :info, gettext("Email changed successfully."))
:error ->
put_flash(socket, :error, "Email change link is invalid or it has expired.")
end
{:ok, push_navigate(socket, to: ~p"/users/settings")}
end
- def mount(_params, _session, socket) do
+ def mount(_params, session, socket) do
+ Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
user = socket.assigns.current_user
email_changeset = Accounts.change_user_email(user)
password_changeset = Accounts.change_user_password(user)
socket =
socket
|> assign(:current_password, nil)
|> assign(:email_form_current_password, nil)
|> assign(:current_email, user.email)
|> assign(:email_form, to_form(email_changeset))
|> assign(:password_form, to_form(password_changeset))
|> assign(:trigger_submit, false)
{:ok, socket}
end
...
end
- お問い合わせを配下に追加
お問い合わせはgoogle formが楽なのでそちらのURLを設置します
open_safariのnative bridgeを使用して開くのでphx-hookをセットしてクリックイベントを追加します
lib/trarecord_web/live/setting_live/index.ex
defmodule TrarecordWeb.SettingLive.Index do
use TrarecordWeb, :live_view
@contact_form [goole form url]
def render(assigns) do
~H"""
<.header_nav title={gettext("Setting")} />
- <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2">
+ <div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">
<table class="table bg-white text-base">
<tbody>
<tr>
<td>
<.link class="flex justify-between" navigate={~p"/users/settings"}>
<p><%= gettext("General") %></p>
<.icon name="hero-chevron-right-solid" />
</.link>
</td>
</tr>
</tbody>
</table>
+ <table class="table bg-white text-base">
+ <tbody>
+ <tr>
+ <td>
+ <a
+ href={@contact_form}
+ target="_blank"
+ class="flex justify-between"
+ phx-click="open_contact"
+ >
+ <p><%= gettext("Contact") %></p>
+ <.icon name="hero-chevron-right-solid" />
+ </a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
</div>
<.bottom_tab current="Setting" />
"""
end
@impl true
def mount(_params, session, socket) do
Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
- {:ok, socket}
+ {:ok, assign(socket, :contact_form, @contact_form)}
end
+ @impl true
+ def handle_event("open_contact", _params, socket) do
+ {:noreply, push_event(socket, "open_safari", %{url: @contact_form})}
+ end
end
利用規約、プライバシーポリシーを配下に追加
lib/trarecord_web/live/setting_live/index.ex:L7
def render(assigns) do
~H"""
<.header_nav title={gettext("Setting")} />
<div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">
<table class="table bg-white text-base">
...
</table>
<table class="table bg-white text-base">
...
</table>
+ <table class="table bg-white text-base">
+ <tbody>
+ <tr>
+ <td>
+ <.link class="flex justify-between" navigate={~p"/settings/gp"}>
+ <p><%= gettext("Terms of Service") %></p>
+ <.icon name="hero-chevron-right-solid" />
+ </.link>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <.link class="flex justify-between" navigate={~p"/settings/privacy"}>
+ <p><%= gettext("Privacy Policy") %></p>
+ <.icon name="hero-chevron-right-solid" />
+ </.link>
+ </td>
+ </tr>
+ </tbody>
+ </table>
</div>
<.bottom_tab current="Setting" />
"""
end
利用規約のページを作ります
lib/trarecord_web/live/setting_live/gp.ex
defmodule TrarecordWeb.SettingsLive.Gp do
use TrarecordWeb, :live_view
def render(assigns) do
~H"""
<.header_nav title={gettext("Terms of Service")}>
<:back>
<.link navigate={~p"/settings"}>
<.icon name="hero-chevron-left-solid" class="h-6 w-6" />
</.link>
</:back>
</.header_nav>
<div class="flex flex-col gap-y-2 p-4 bg-white mb-20">
<h2 class="text-xl">利用規約</h2>
<p>
この利用規約(以下,「本規約」といいます。)は,Trarecord(以下,「当社」といいます。)がこのウェブ上で提供するサービス(以下,「本サービス」といいます。)の利用条件を定めるものです。登録ユーザーの皆さま(以下,「ユーザー」といいます。)には,本規約に従って,本サービスをご利用いただきます。
</p>
ここに規約を書く
<p class="tR">以上</p>
</div>
<.bottom_tab current="Setting" />
"""
end
def mount(_params, session, socket) do
Gettext.put_locale(TrarecoWeb.Gettext, Map.get(session, "locale", "en"))
{:ok, socket}
end
end
プライバシーポリシーのページを作ります
lib/trarecord_web/live/setting_live/privacy.ex
defmodule TrarecoWeb.SettingsLive.Privacy do
use TrarecordWeb, :live_view
def render(assigns) do
~H"""
<.header_nav title={gettext("Privacy Policy")}>
<:back>
<.link navigate={~p"/settings"}>
<.icon name="hero-chevron-left-solid" class="h-6 w-6" />
</.link>
</:back>
</.header_nav>
<div class="flex flex-col gap-y-2 p-4 bg-white mb-20">
<h2 class="text-xl">プライバシーポリシー</h2>
<p>
Trareco(以下,「当社」といいます。)は,本ウェブ上で提供するサービス(以下,「本サービス」といいます。)における,ユーザーの個人情報の取扱いについて,以下のとおりプライバシーポリシー(以下,「本ポリシー」といいます。)を定めます。
</p>
ここにプライバシーポリシーを書く
<p class="tR">以上</p>
</div>
<.bottom_tab current="Setting" />
"""
end
def mount(_params, session, socket) do
Gettext.put_locale(TrarecoWeb.Gettext, Map.get(session, "locale", "en"))
{:ok, socket}
end
end
ルーターに追加して完了です
lib/trarecord_web/router.ex
scope "/", TrarecordWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
live "/settings", SettingLive.Index, :infex
+ live "/settings/gp", SettingsLive.Gp
+ live "/settings/privacy", SettingsLive.Privacy
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/onboarding", OnboardingLive.Index, :index
live "/folders", FolderLive.Index, :index
live "/folders/new", FolderLive.Index, :new
live "/folders/:id/edit", FolderLive.Index, :edit
live "/folders/:id/delete", FolderLive.Index, :delete
live "/folders/:id", FolderLive.Show, :show
live "/folders/:id/show/edit", FolderLive.Show, :edit
end
end
ユーザー削除ボタンを追加
削除関数を追加
lib/trarecord/accounts.ex:L61
def get_user!(id), do: Repo.get!(User, id)
+ def delete_user(user), do: Repo.delete(user)
削除確認モーダルと削除イベントを作ります
lib/trarecord_web/live/setting_live/index.ex
+ alias Trarecord.Accounts
@impl true
def render(assigns) do
~H"""
<.header_nav title={gettext("Setting")} />
<div id="settings" class="h-screen my-20 flex flex-col gap-y-8 mx-2" phx-hook="NativeBridge">
<table class="table bg-white text-base">
...
</table>
+ <.link patch={~p"/settings/delete"} class="btn btn-error text-white normal-case text-xl">
+ <%= gettext("Delete Account") %>
+ </.link>
</div>
<.bottom_tab current="Setting" />
+ <.modal
+ :if={@live_action in [:delete]}
+ id="user-delete-modal"
+ show
+ on_cancel={JS.navigate(~p"/settings")}
+ >
+ <p class="m-8"><%= gettext("Are you sure?") %></p>
+ <div class="flex justify-between mx-4 gap-x-4">
+ <button class="btn w-28" phx-click={JS.navigate(~p"/settings")}>
+ <%= gettext("Cancel") %>
+ </button>
+ <button
+ class="btn btn-error text-white w-28"
+ phx-click={JS.push("delete", value: %{id: @current_user.id})}
+ >
+ <%= gettext("Delete") %>
+ </button>
+ </div>
+ </.modal>
"""
end
@impl true
def mount(_params, session, socket) do
Gettext.put_locale(TrarecordWeb.Gettext, Map.get(session, "locale", "ja"))
{:ok, assign(socket, :contact_form, @contact_form)}
end
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+ defp apply_action(socket, :delete, _params) do
+ socket
+ |> assign(:page_title, "Delete Account")
+ end
+ defp apply_action(socket, _action, _params), do: socket
@impl true
def handle_event("open_contact", _params, socket) do
{:noreply, push_event(socket, "open_safari", %{url: @contact_form})}
end
+ def handle_event("delete", %{"id" => id}, socket) do
+ user = Accounts.get_user!(id)
+ case Accounts.delete_user(user) do
+ {:ok, _} ->
+ File.rm(Trareco.config_dir() <> "/token")
+ {
+ :noreply,
+ socket
+ |> put_flash(:info, "Account deleted successfully")
+ |> push_navigate(to: ~p"/")
+ }
+
+ {:error, _} ->
+ {
+ :noreply,
+ socket
+ |> put_flash(:error, "Account deleted error")
+ }
+ end
end
end
routerに削除モーダル表示用のパスを追加
lib/trarecord_web/router.ex
scope "/", TrarecordWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{TrarecordWeb.UserAuth, :ensure_authenticated}] do
live "/settings", SettingLive.Index, :infex
+ live "/settings/delete", SettingsLive.Index, :delete
live "/settings/gp", SettingsLive.Gp
live "/settings/privacy", SettingsLive.Privacy
live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
live "/onboarding", OnboardingLive.Index, :index
live "/folders", FolderLive.Index, :index
live "/folders/new", FolderLive.Index, :new
live "/folders/:id/edit", FolderLive.Index, :edit
live "/folders/:id/delete", FolderLive.Index, :delete
live "/folders/:id", FolderLive.Show, :show
live "/folders/:id/show/edit", FolderLive.Show, :edit
end
end
ログアウトボタンを追加
リンクとログアウト機能自体はあるのでリンクを設置して完了です
lib/trarecord_web/live/setting_live/index.ex:L61
<.link patch={~p"/settings/delete"} class="btn btn-error text-white normal-case text-xl">
<%= gettext("Delete Account") %>
</.link>
+ <.link
+ method="delete"
+ href="/users/log_out"
+ class="btn btn-error text-white normal-case text-xl"
+ >
+ <%= gettext("Sign out") %>
+ </.link>
最後に
全体の設定画面を作成して、問い合わせフォーム、利用規約、プライバシーポリシー等のアプリの運用上必要なものの設置
ログアウト、ユーザー削除を実装しました
次はGoogle Places APIからスポット情報を取得する方法を解説します
本記事は以上になりますありがとうございました