defmodule Rsh26poolWeb.VoteLive do
@moduledoc """
The voting page. Choices are shown in a per-viewer shuffled order and their
vote counts are hidden while voting is open. In grouping mode the participant
posts a single opinion and votes for the groups they want to join; once voting
closes they see their assigned group.
"""
use Rsh26poolWeb, :live_view
alias Rsh26pool.Voting
@impl true
def mount(%{"room_number" => number}, _session, socket) do
participant = socket.assigns.current_participant
room = number |> Voting.get_room_by_number() |> Voting.ensure_closed()
if connected?(socket), do: Voting.subscribe(room.id)
{:ok,
socket
|> assign(:room, room)
|> assign(:participant, participant)
|> assign(:page_title, "投票 - #{room.name}")
|> load_state()}
end
@impl true
def handle_event("vote", %{"id" => id}, socket) do
case Voting.vote(socket.assigns.participant, String.to_integer(id)) do
{:ok, _} ->
{:noreply, refresh_votes(socket)}
{:error, :already_voted} ->
{:noreply, refresh_votes(socket)}
{:error, :limit_reached} ->
{:noreply, put_flash(socket, :error, "投票できる数の上限に達しました。")}
{:error, :closed} ->
{:noreply, socket |> put_flash(:error, "投票は終了しました。") |> reload_room()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "投票できませんでした。")}
end
end
def handle_event("unvote", %{"id" => id}, socket) do
case Voting.unvote(socket.assigns.participant, String.to_integer(id)) do
:ok ->
{:noreply, refresh_votes(socket)}
{:error, :closed} ->
{:noreply, socket |> put_flash(:error, "投票は終了しました。") |> reload_room()}
{:error, _} ->
{:noreply, refresh_votes(socket)}
end
end
def handle_event("post_idea", %{"idea" => %{"text" => text}}, socket) do
case Voting.create_idea(socket.assigns.room, socket.assigns.participant, %{"text" => text}) do
{:ok, _idea} ->
message = if socket.assigns.room.grouping_mode, do: "意見を保存しました。", else: "アイデアを投稿しました。"
{:noreply, socket |> put_flash(:info, message) |> load_state()}
{:error, :closed} ->
{:noreply, socket |> put_flash(:error, "投票は終了しました。") |> reload_room()}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :idea_form, to_form(changeset, as: :idea))}
end
end
@impl true
def handle_info({:voting, :ideas_changed}, socket) do
{:noreply,
assign(
socket,
:ideas,
Voting.shuffled_ideas(socket.assigns.room, socket.assigns.participant.token)
)}
end
def handle_info({:voting, :room_updated}, socket) do
{:noreply, reload_room(socket)}
end
def handle_info({:voting, _event}, socket), do: {:noreply, socket}
## State helpers --------------------------------------------------------
defp load_state(socket) do
room = socket.assigns.room
participant = socket.assigns.participant
open? = Voting.room_open?(room)
socket
|> assign(:open?, open?)
|> assign(:votes_remaining, Voting.votes_remaining(room, participant))
|> assign(:voted_ids, Voting.voted_idea_ids(participant))
|> assign(:ideas, Voting.shuffled_ideas(room, participant.token))
|> assign_idea_form()
|> assign_results(open?)
end
defp refresh_votes(socket) do
participant = socket.assigns.participant
socket
|> assign(:voted_ids, Voting.voted_idea_ids(participant))
|> assign(:votes_remaining, Voting.votes_remaining(socket.assigns.room, participant))
end
defp reload_room(socket) do
room =
socket.assigns.room.room_number |> Voting.get_room_by_number() |> Voting.ensure_closed()
socket |> assign(:room, room) |> load_state()
end
defp assign_idea_form(socket) do
room = socket.assigns.room
if room.grouping_mode do
opinion = Voting.get_participant_idea(room, socket.assigns.participant)
text = if opinion, do: opinion.text, else: ""
socket
|> assign(:my_opinion, opinion)
|> assign(:idea_form, to_form(%{"text" => text}, as: :idea))
else
socket
|> assign(:my_opinion, nil)
|> assign(:idea_form, to_form(%{"text" => ""}, as: :idea))
end
end
defp assign_results(socket, true) do
socket |> assign(:results, []) |> assign(:grouping, nil) |> assign(:my_group, nil)
end
defp assign_results(socket, false) do
room = socket.assigns.room
if room.grouping_mode do
grouping = Voting.group_assignments(room)
socket
|> assign(:grouping, grouping)
|> assign(:my_group, find_my_group(grouping, socket.assigns.participant))
|> assign(:results, [])
else
socket
|> assign(:results, Voting.results(room))
|> assign(:grouping, nil)
|> assign(:my_group, nil)
end
end
defp find_my_group(%{groups: groups}, participant) do
Enum.find(groups, fn g ->
g.leader.id == participant.id or Enum.any?(g.members, &(&1.id == participant.id))
end)
end
defp find_my_group(_grouping, _participant), do: nil
## Render ---------------------------------------------------------------
@impl true
def render(assigns) do
~H"""
まだ投稿がありません。最初の一つを投稿してみましょう。
{idea.text}
提案者:{idea.participant.name}
<%= if @room.grouping_mode do %>
あなたの意見(1つだけ投稿できます)
<% else %>
新しいアイデアを投稿
<% end %>
<.form for={@idea_form} phx-submit="post_idea" id="idea-form" class="flex flex-col gap-2">
<.input
field={@idea_form[:text]}
type="text"
placeholder={if @room.grouping_mode, do: "例:みんなで公園を掃除したい", else: "例:焼肉に行きたい"}
/>
<.icon name="hero-hand-thumb-up" class="size-5 text-primary" />
{if @room.grouping_mode, do: "参加したいグループに投票", else: "アイデアに投票"}
投稿はありませんでした。
あなたのグループ
<.icon name="hero-user-group" class="size-5 text-primary" /> {@my_group.leader.name} さんのグループ
テーマ:{@my_group.opinion && @my_group.opinion.text}
リーダーになる人が決まりませんでした(十分な票を集めた意見がありません)。
テーマ:{group.opinion && group.opinion.text}