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""" <.header> {@room.name} <:subtitle> ルーム番号 {@room.room_number} グルーピングモード <:actions> <.icon name="hero-hand-raised" class="size-4" /> 投票受付中 <.icon name="hero-lock-closed" class="size-4" /> 終了 <%= if @open? do %>
<.icon name="hero-ticket" class="size-5 text-primary" /> 残りの投票数:{@votes_remaining} / {@room.votes_per_user}

<%= 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: "アイデアに投票"}

まだ投稿がありません。最初の一つを投稿してみましょう。

<% else %>
<.icon name="hero-lock-closed" class="size-5" /> この投票は終了しました。
<%= if @room.grouping_mode do %> <.grouping_results grouping={@grouping} my_group={@my_group} participant={@participant} /> <% else %> <.normal_results results={@results} /> <% end %> <% end %>
""" end ## Result components ---------------------------------------------------- attr :results, :list, required: true defp normal_results(assigns) do ~H"""

<.icon name="hero-trophy" class="size-5 text-warning" /> 結果

投稿はありませんでした。

  1. {index + 1} {result.idea.text}
    <.icon name="hero-hand-thumb-up" class="size-4" /> {result.count}
""" end attr :grouping, :map, required: true attr :my_group, :map, default: nil attr :participant, :map, required: true defp grouping_results(assigns) do ~H"""

あなたのグループ

<.icon name="hero-user-group" class="size-5 text-primary" /> {@my_group.leader.name} さんのグループ

テーマ:{@my_group.opinion && @my_group.opinion.text}

リーダーになる人が決まりませんでした(十分な票を集めた意見がありません)。

<.icon name="hero-user-group" class="size-5 text-primary" /> {group.leader.name} グループ

テーマ:{group.opinion && group.opinion.text}

  • <.icon name="hero-trophy" class="size-4 text-warning" /> {group.leader.name}(リーダー)
  • {member.name}
""" end end