mirror of
https://github.com/ClaperCo/Claper.git
synced 2025-12-16 20:07:59 +01:00
WIP
This commit is contained in:
215
lib/claper/quizzes.ex
Normal file
215
lib/claper/quizzes.ex
Normal file
@@ -0,0 +1,215 @@
|
||||
defmodule Claper.Quizzes do
|
||||
import Ecto.Query, warn: false
|
||||
alias Claper.Repo
|
||||
|
||||
alias Claper.Quizzes.Quiz
|
||||
alias Claper.Quizzes.QuizQuestion
|
||||
alias Claper.Quizzes.QuizQuestionOpt
|
||||
alias Claper.Quizzes.QuizResponse
|
||||
|
||||
@doc """
|
||||
Returns the list of quizzes for a given presentation file.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_quizzes(123)
|
||||
[%Quiz{}, ...]
|
||||
|
||||
"""
|
||||
def list_quizzes(presentation_file_id) do
|
||||
from(p in Quiz,
|
||||
where: p.presentation_file_id == ^presentation_file_id,
|
||||
order_by: [asc: p.id, asc: p.position]
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Repo.preload([:quiz_questions, quiz_questions: :quiz_question_opts])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of quizzes for a given presentation file and a given position.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_quizzes_at_position(123, 0)
|
||||
[%Quiz{}, ...]
|
||||
|
||||
"""
|
||||
def list_polls_at_position(presentation_file_id, position) do
|
||||
from(p in Quiz,
|
||||
where: p.presentation_file_id == ^presentation_file_id and p.position == ^position,
|
||||
order_by: [asc: p.id]
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Repo.preload([:quiz_questions, quiz_questions: :quiz_question_opts])
|
||||
end
|
||||
|
||||
|
||||
|
||||
@doc """
|
||||
Gets a single quiz by ID.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Quiz does not exist.
|
||||
|
||||
## Parameters
|
||||
|
||||
- id: The ID of the quiz.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_quiz!(123)
|
||||
%Quiz{}
|
||||
|
||||
iex> get_quiz!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_quiz!(id) do
|
||||
Quiz
|
||||
|> Repo.get!(id)
|
||||
|> Repo.preload([:quiz_questions, quiz_questions: :quiz_question_opts])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a quiz.
|
||||
|
||||
## Parameters
|
||||
|
||||
- attrs: A map of attributes for creating a quiz.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_quiz(%{field: value})
|
||||
{:ok, %Quiz{}}
|
||||
|
||||
iex> create_quiz(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_quiz(attrs \\ %{}) do
|
||||
%Quiz{}
|
||||
|> Quiz.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a quiz.
|
||||
|
||||
## Parameters
|
||||
|
||||
- quiz: The quiz struct to update.
|
||||
- attrs: A map of attributes to update.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_quiz(quiz, %{field: new_value})
|
||||
{:ok, %Quiz{}}
|
||||
|
||||
iex> update_quiz(quiz, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_quiz(event_uuid, %Quiz{} = quiz, attrs) do
|
||||
quiz
|
||||
|> Quiz.changeset(attrs)
|
||||
|> Repo.update()
|
||||
|> case do
|
||||
{:ok, quiz} ->
|
||||
broadcast({:ok, quiz, event_uuid}, :quiz_updated)
|
||||
|
||||
{:error, changeset} ->
|
||||
{:error, %{changeset | action: :update}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a quiz.
|
||||
|
||||
## Parameters
|
||||
|
||||
- event_uuid: The UUID of the event.
|
||||
- quiz: The quiz struct to delete.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_quiz(event_uuid, quiz)
|
||||
{:ok, %Quiz{}}
|
||||
|
||||
iex> delete_quiz(event_uuid, quiz)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_quiz(event_uuid, %Quiz{} = quiz) do
|
||||
{:ok, quiz} = Repo.delete(quiz)
|
||||
broadcast({:ok, quiz, event_uuid}, :quiz_deleted)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking quiz changes.
|
||||
|
||||
## Parameters
|
||||
|
||||
- quiz: The quiz struct to create a changeset for.
|
||||
- attrs: A map of attributes (optional).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_quiz(quiz)
|
||||
%Ecto.Changeset{data: %Quiz{}}
|
||||
|
||||
"""
|
||||
def change_quiz(%Quiz{} = quiz, attrs \\ %{}) do
|
||||
Quiz.changeset(quiz, attrs)
|
||||
end
|
||||
|
||||
def add_quiz_question(changeset) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_assoc(:quiz_questions, Ecto.Changeset.get_field(changeset, :quiz_questions) ++ [%QuizQuestion{
|
||||
quiz_question_opts: [
|
||||
%QuizQuestionOpt{},
|
||||
%QuizQuestionOpt{}
|
||||
]
|
||||
}])
|
||||
end
|
||||
|
||||
def remove_quiz_question(changeset, quiz_question) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_assoc(
|
||||
:quiz_questions,
|
||||
Ecto.Changeset.get_field(changeset, :quiz_questions) -- [quiz_question]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add an empty quiz opt to a quiz changeset.
|
||||
"""
|
||||
def add_quiz_question_opt(changeset, question_index) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_assoc(:quiz_questions, Ecto.Changeset.get_field(changeset, :quiz_questions) |> List.update_at(question_index, fn question ->
|
||||
Ecto.Changeset.put_assoc(question, :quiz_question_opts,
|
||||
(question.quiz_question_opts || []) ++ [%QuizQuestionOpt{}])
|
||||
end))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove a quiz question opt from a quiz question changeset.
|
||||
"""
|
||||
def remove_quiz_question_opt(changeset, quiz_question_opt) do
|
||||
changeset
|
||||
|> Ecto.Changeset.put_assoc(
|
||||
:quiz_question_opts,
|
||||
Ecto.Changeset.get_field(changeset, :quiz_question_opts) -- [quiz_question_opt]
|
||||
)
|
||||
end
|
||||
|
||||
defp broadcast({:error, _reason} = error, _quiz), do: error
|
||||
|
||||
defp broadcast({:ok, quiz, event_uuid}, event) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{event_uuid}",
|
||||
{event, quiz}
|
||||
)
|
||||
|
||||
{:ok, quiz}
|
||||
end
|
||||
end
|
||||
24
lib/claper/quizzes/quiz.ex
Normal file
24
lib/claper/quizzes/quiz.ex
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule Claper.Quizzes.Quiz do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "quizzes" do
|
||||
field :title, :string
|
||||
field :position, :integer, default: 0
|
||||
field :enabled, :boolean, default: false
|
||||
field :show_results, :boolean, default: false
|
||||
|
||||
belongs_to :presentation_file, Claper.Presentations.PresentationFile
|
||||
has_many :quiz_questions, Claper.Quizzes.QuizQuestion
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(quiz, attrs) do
|
||||
quiz
|
||||
|> cast(attrs, [:title, :position, :presentation_file_id, :enabled, :show_results])
|
||||
|> validate_required([:title, :position, :presentation_file_id])
|
||||
|> cast_assoc(:quiz_questions)
|
||||
end
|
||||
end
|
||||
22
lib/claper/quizzes/quiz_question.ex
Normal file
22
lib/claper/quizzes/quiz_question.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Claper.Quizzes.QuizQuestion do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "quiz_questions" do
|
||||
field :content, :string
|
||||
field :type, :string, default: "qcm"
|
||||
|
||||
belongs_to :quiz, Claper.Quizzes.Quiz
|
||||
has_many :quiz_question_opts, Claper.Quizzes.QuizQuestionOpt
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(quiz_question, attrs) do
|
||||
quiz_question
|
||||
|> cast(attrs, [:content, :type, :quiz_id])
|
||||
|> validate_required([:content, :type, :quiz_id])
|
||||
|> cast_assoc(:quiz_question_opts)
|
||||
end
|
||||
end
|
||||
21
lib/claper/quizzes/quiz_question_opt.ex
Normal file
21
lib/claper/quizzes/quiz_question_opt.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule Claper.Quizzes.QuizQuestionOpt do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "quiz_question_opts" do
|
||||
field :content, :string
|
||||
field :is_correct, :boolean, default: false
|
||||
field :response_count, :integer, default: 0
|
||||
|
||||
belongs_to :quiz_question, Claper.Quizzes.QuizQuestion
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(quiz_question_opt, attrs) do
|
||||
quiz_question_opt
|
||||
|> cast(attrs, [:content, :is_correct, :response_count, :quiz_question_id])
|
||||
|> validate_required([:content, :is_correct, :quiz_question_id])
|
||||
end
|
||||
end
|
||||
22
lib/claper/quizzes/quiz_response.ex
Normal file
22
lib/claper/quizzes/quiz_response.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Claper.Quizzes.QuizResponse do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "quiz_responses" do
|
||||
field :attendee_identifier, :string
|
||||
|
||||
belongs_to :quiz, Claper.Quizzes.Quiz
|
||||
belongs_to :quiz_question, Claper.Quizzes.QuizQuestion
|
||||
belongs_to :quiz_question_opt, Claper.Quizzes.QuizQuestionOpt
|
||||
belongs_to :user, Claper.Accounts.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(quiz_response, attrs) do
|
||||
quiz_response
|
||||
|> cast(attrs, [:attendee_identifier, :quiz_id, :quiz_question_id, :quiz_question_opt_id, :user_id])
|
||||
|> validate_required([:quiz_id, :quiz_question_id, :quiz_question_opt_id])
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,8 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
alias Claper.Polls
|
||||
alias Claper.Forms
|
||||
alias Claper.Embeds
|
||||
# Add this line
|
||||
alias Claper.Quizzes
|
||||
|
||||
@impl true
|
||||
def mount(%{"code" => code}, session, socket) do
|
||||
@@ -403,6 +405,38 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
|> interactions_at_position(socket.assigns.state.position)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("quiz-set-active", %{"id" => id}, socket) do
|
||||
with quiz <- Quizzes.get_quiz!(id), :ok <- Claper.Interactions.enable_interaction(quiz) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{socket.assigns.event.uuid}",
|
||||
{:current_interaction, quiz}
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_interaction, quiz)
|
||||
|> interactions_at_position(socket.assigns.state.position)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("quiz-set-inactive", %{"id" => id}, socket) do
|
||||
with quiz <- Quizzes.get_quiz!(id),
|
||||
{:ok, _} <- Claper.Interactions.disable_interaction(quiz) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{socket.assigns.event.uuid}",
|
||||
{:current_interaction, nil}
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_interaction, nil)
|
||||
|> interactions_at_position(socket.assigns.state.position)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"ban",
|
||||
@@ -645,6 +679,14 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete-quiz", %{"id" => id}, socket) do
|
||||
quiz = Quizzes.get_quiz!(id)
|
||||
{:ok, _} = Quizzes.delete_quiz(socket.assigns.event.uuid, quiz)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle-preview", _params, %{assigns: %{preview: preview}} = socket) do
|
||||
{:noreply, socket |> assign(:preview, !preview)}
|
||||
@@ -736,6 +778,35 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
|> assign(:embed, embed)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :add_quiz, _params) do
|
||||
socket
|
||||
|> assign(:create, "quiz")
|
||||
|> assign(:quiz, %Quizzes.Quiz{
|
||||
quiz_questions: [
|
||||
%Quizzes.QuizQuestion{
|
||||
id: 0,
|
||||
quiz_question_opts: [
|
||||
%Quizzes.QuizQuestionOpt{
|
||||
id: 0
|
||||
},
|
||||
%Quizzes.QuizQuestionOpt{
|
||||
id: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit_quiz, %{"id" => id}) do
|
||||
quiz = Quizzes.get_quiz!(id)
|
||||
|
||||
socket
|
||||
|> assign(:create, "quiz")
|
||||
|> assign(:create_action, :edit)
|
||||
|> assign(:quiz, quiz)
|
||||
end
|
||||
|
||||
defp pin(post, socket) do
|
||||
{:ok, _updated_post} = Claper.Posts.toggle_pin_post(post)
|
||||
|
||||
|
||||
@@ -234,6 +234,38 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li id="option-4" role="option" tabindex="-1">
|
||||
<a
|
||||
data-phx-link="patch"
|
||||
data-phx-link-state="push"
|
||||
href={~p"/e/#{@event.code}/manage/add/quiz"}
|
||||
class="group flex select-none rounded-xl p-3 w-full hover:bg-gray-200 cursor-pointer"
|
||||
>
|
||||
<div class="flex h-12 w-12 flex-none text-white items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-secondary-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-7 h-7"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-
|
||||
linejoin="round"
|
||||
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4 flex-auto text-left">
|
||||
<p class="font-medium text-gray-700"><%= gettext("Quiz") %></p>
|
||||
<p class="text-gray-500">
|
||||
<%= gettext("Add a quiz to test your public.") %>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<% end %>
|
||||
<%= if @create=="poll" do %>
|
||||
@@ -299,6 +331,27 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @create=="quiz" do %>
|
||||
<div class="scroll-py-3 overflow-y-auto bg-gray-100 p-3">
|
||||
<p class="text-xl font-bold">
|
||||
<%= case @create_action do
|
||||
:new -> gettext("New quiz")
|
||||
:edit -> gettext("Edit quiz")
|
||||
end %>
|
||||
</p>
|
||||
<.live_component
|
||||
module={ClaperWeb.QuizLive.QuizComponent}
|
||||
id="quiz-create"
|
||||
event_uuid={@event.uuid}
|
||||
presentation_file={@event.presentation_file}
|
||||
quiz={@quiz}
|
||||
live_action={@create_action}
|
||||
position={@state.position}
|
||||
return_to={~p"/e/#{@event.code}/manage"}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @create == "import" do %>
|
||||
<div class="scroll-py-3 overflow-y-auto bg-gray-100 p-3">
|
||||
<p class="text-xl font-bold">
|
||||
|
||||
124
lib/claper_web/live/quiz_live/quiz_component.ex
Normal file
124
lib/claper_web/live/quiz_live/quiz_component.ex
Normal file
@@ -0,0 +1,124 @@
|
||||
defmodule ClaperWeb.QuizLive.QuizComponent do
|
||||
alias Claper.Quizzes.QuizQuestionOpt
|
||||
use ClaperWeb, :live_component
|
||||
|
||||
alias Claper.Quizzes
|
||||
|
||||
@impl true
|
||||
def update(%{quiz: quiz} = assigns, socket) do
|
||||
changeset = Quizzes.change_quiz(quiz)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:dark, fn -> false end)
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
quiz = Quizzes.get_quiz!(id)
|
||||
{:ok, _} = Quizzes.delete_quiz(socket.assigns.event_uuid, quiz)
|
||||
|
||||
{:noreply, socket |> push_navigate(to: socket.assigns.return_to)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"quiz" => quiz_params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.quiz
|
||||
|> Quizzes.change_quiz(quiz_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, socket |> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"quiz" => quiz_params}, socket) do
|
||||
save_quiz(socket, socket.assigns.live_action, quiz_params)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("add_quiz_question", _params, %{assigns: %{changeset: changeset}} = socket) do
|
||||
{:noreply, assign(socket, :changeset, changeset |> Quizzes.add_quiz_question())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("add_quiz_question_opt", %{"question_index" => index}, %{assigns: %{changeset: changeset}} = socket) do
|
||||
index = String.to_integer(index)
|
||||
{:noreply, assign(socket, :changeset, changeset |> Quizzes.add_quiz_question_opt(index))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("remove_quiz_question", %{"index" => index}, socket) do
|
||||
index = String.to_integer(index)
|
||||
changeset =
|
||||
socket.assigns.changeset
|
||||
|> Ecto.Changeset.update_change(:quiz_questions, fn questions ->
|
||||
List.delete_at(questions, index)
|
||||
end)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("remove_opt",
|
||||
%{"opt" => opt} = _params,
|
||||
%{assigns: %{changeset: changeset}} = socket
|
||||
) do
|
||||
{opt, _} = Integer.parse(opt)
|
||||
|
||||
quiz_opt = Enum.at(Ecto.Changeset.get_field(changeset, :quiz_question_opts), opt)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset |> Quizzes.remove_quiz_opt(quiz_opt))}
|
||||
end
|
||||
|
||||
defp save_quiz(socket, :edit, quiz_params) do
|
||||
case Quizzes.update_quiz(
|
||||
socket.assigns.event_uuid,
|
||||
socket.assigns.quiz,
|
||||
quiz_params
|
||||
) do
|
||||
{:ok, _quiz} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_quiz(socket, :new, quiz_params) do
|
||||
case Quizzes.create_quiz(
|
||||
quiz_params
|
||||
|> Map.put("presentation_file_id", socket.assigns.presentation_file.id)
|
||||
|> Map.put("position", socket.assigns.position)
|
||||
|> Map.put("enabled", false)
|
||||
) do
|
||||
{:ok, quiz} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> maybe_change_current_quiz(quiz)
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_change_current_quiz(socket, %{enabled: true} = quiz) do
|
||||
quiz = Quizzes.get_quiz!(quiz.id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{socket.assigns.event_uuid}",
|
||||
{:current_quiz, quiz}
|
||||
)
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
defp maybe_change_current_quiz(socket, _), do: socket
|
||||
|
||||
end
|
||||
113
lib/claper_web/live/quiz_live/quiz_component.html.heex
Normal file
113
lib/claper_web/live/quiz_live/quiz_component.html.heex
Normal file
@@ -0,0 +1,113 @@
|
||||
<div>
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
id="form-quiz"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<div class="my-3 mb-10">
|
||||
<ClaperWeb.Component.Input.text
|
||||
form={f}
|
||||
key={:title}
|
||||
name={gettext("Title")}
|
||||
labelClass={if @dark, do: "text-white"}
|
||||
fieldClass={if @dark, do: "bg-gray-700 text-white"}
|
||||
autofocus="true"
|
||||
required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<%= inputs_for f, :quiz_questions, fn q -> %>
|
||||
<div class="mb-8 p-4 border rounded-md">
|
||||
<div class="flex gap-x-3 mt-3 items-center justify-start">
|
||||
<ClaperWeb.Component.Input.text
|
||||
form={q}
|
||||
labelClass={if @dark, do: "text-white"}
|
||||
fieldClass={if @dark, do: "bg-gray-700 text-white"}
|
||||
key={:content}
|
||||
name={gettext("Question")}
|
||||
autofocus="true"
|
||||
required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<%= inputs_for q, :quiz_question_opts, fn o -> %>
|
||||
<div class="ml-4 mt-2">
|
||||
<ClaperWeb.Component.Input.text
|
||||
form={o}
|
||||
labelClass={if @dark, do: "text-white"}
|
||||
fieldClass={if @dark, do: "bg-gray-700 text-white"}
|
||||
key={:content}
|
||||
name={gettext("Option %{index}", index: o.index + 1)}
|
||||
required="true"
|
||||
/>
|
||||
<%= checkbox(o, :is_correct, class: "mr-2") %>
|
||||
<%= label(o, :is_correct, gettext("Correct answer"), class: if(@dark, do: "text-white")) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
phx-click="add_quiz_question_opt"
|
||||
phx-value-question_index={q.index}
|
||||
phx-target={@myself}
|
||||
class="mt-2 px-3 py-1 text-sm rounded-md bg-secondary-500 hover:bg-secondary-600 text-white transition"
|
||||
>
|
||||
<%= gettext("Add Option") %>
|
||||
</button>
|
||||
|
||||
<%= if q.index > 0 do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="remove_quiz_question"
|
||||
phx-value-index={q.index}
|
||||
phx-target={@myself}
|
||||
class="mt-2 ml-2 px-3 py-1 text-sm rounded-md bg-red-500 hover:bg-red-600 text-white transition"
|
||||
>
|
||||
<%= gettext("Remove Question") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
phx-click="add_quiz_question"
|
||||
phx-target={@myself}
|
||||
class="mb-4 px-4 py-2 rounded-md bg-primary-500 hover:bg-primary-600 text-white transition"
|
||||
>
|
||||
<%= gettext("Add Question") %>
|
||||
</button>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
type="submit"
|
||||
phx_disable_with="Loading..."
|
||||
class="w-full lg:w-auto px-6 text-white py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline bg-gradient-to-tl from-primary-500 to-secondary-500 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
|
||||
>
|
||||
<%= case @live_action do
|
||||
:new -> gettext("Create")
|
||||
:edit -> gettext("Save")
|
||||
end %>
|
||||
</button>
|
||||
<%= if @live_action == :edit do %>
|
||||
<%= link(gettext("Delete"),
|
||||
to: "#",
|
||||
phx_click: "delete",
|
||||
phx_target: @myself,
|
||||
phx_value_id: @quiz.id,
|
||||
data: [
|
||||
confirm:
|
||||
gettext(
|
||||
"This will delete all responses associated and the quiz itself, are you sure?"
|
||||
)
|
||||
],
|
||||
class:
|
||||
"w-full lg:w-auto px-6 text-center text-white py-2 rounded-md tracking-wide font-bold focus:outline-none focus:shadow-outline bg-gradient-to-tl from-supporting-red-600 to-supporting-red-400 bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-500"
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
@@ -78,6 +78,8 @@ defmodule ClaperWeb.Router do
|
||||
live("/e/:code/manage/edit/form/:id", EventLive.Manage, :edit_form)
|
||||
live("/e/:code/manage/add/embed", EventLive.Manage, :add_embed)
|
||||
live("/e/:code/manage/edit/embed/:id", EventLive.Manage, :edit_embed)
|
||||
live("/e/:code/manage/add/quiz", EventLive.Manage, :add_quiz)
|
||||
live("/e/:code/manage/edit/quiz/:id", EventLive.Manage, :edit_quiz)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
50
priv/repo/migrations/20240928085505_create_quizzes.exs
Normal file
50
priv/repo/migrations/20240928085505_create_quizzes.exs
Normal file
@@ -0,0 +1,50 @@
|
||||
defmodule Claper.Repo.Migrations.CreateQuizzes do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:quizzes) do
|
||||
add :title, :string, size: 255
|
||||
add :position, :integer, default: 0
|
||||
add :presentation_file_id, references(:presentation_files, on_delete: :delete_all)
|
||||
add :enabled, :boolean, default: false
|
||||
add :show_results, :boolean, default: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:quiz_questions) do
|
||||
add :content, :string, size: 255
|
||||
add :type, :string, default: "qcm"
|
||||
add :quiz_id, references(:quizzes, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:quiz_question_opts) do
|
||||
add :content, :string, size: 255
|
||||
add :is_correct, :boolean, default: false
|
||||
add :response_count, :integer, default: 0
|
||||
add :quiz_question_id, references(:quiz_questions, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:quiz_responses) do
|
||||
add :attendee_identifier, :string
|
||||
add :quiz_id, references(:quizzes, on_delete: :delete_all)
|
||||
add :quiz_question_id, references(:quiz_questions, on_delete: :delete_all)
|
||||
add :quiz_question_opt_id, references(:quiz_question_opts, on_delete: :delete_all)
|
||||
add :user_id, references(:users, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:quizzes, [:presentation_file_id])
|
||||
create index(:quiz_questions, [:quiz_id])
|
||||
create index(:quiz_question_opts, [:quiz_question_id])
|
||||
create index(:quiz_responses, [:quiz_id])
|
||||
create index(:quiz_responses, [:quiz_question_id])
|
||||
create index(:quiz_responses, [:quiz_question_opt_id])
|
||||
create index(:quiz_responses, [:user_id])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user