Add form components and migrate templates

This commit is contained in:
Alex Lion
2026-07-06 21:58:19 +02:00
parent 53b31c13e2
commit 61ec82ef43
10 changed files with 657 additions and 392 deletions

View File

@@ -113,6 +113,9 @@ defmodule ClaperWeb do
import ClaperWeb.Component.Button
import ClaperWeb.Component.Badge
import ClaperWeb.Component.Tabs
import ClaperWeb.Component.TextInput
import ClaperWeb.Component.Checkbox
import ClaperWeb.Component.Select
unquote(verified_routes())
end

View File

@@ -6,57 +6,60 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-6"
>
<div class="my-3 mb-10">
<ClaperWeb.Component.Input.text
<div class="flex flex-col gap-4">
<.text_field
form={f}
key={:title}
name={gettext("Title")}
autofocus="true"
required="true"
label={gettext("Title")}
placeholder={gettext("Enter your title")}
autocomplete="off"
required
autofocus
/>
<div class="mt-3 flex gap-x-2 items-center">
<ClaperWeb.Component.Input.select
form={f}
key={:provider}
name={gettext("Provider")}
array={@providers}
required="true"
/>
<div class="flex-1">
<p></p>
<ClaperWeb.Component.Input.text
<div class="flex gap-4 items-end">
<div class="w-44 shrink-0">
<.select_field
form={f}
key={:provider}
label={gettext("Provider")}
options={@providers}
required
/>
</div>
<div class="flex-1 min-w-0">
<.text_field
form={f}
key={:content}
name={
label={
if Ecto.Changeset.get_field(@changeset, :provider) == "custom",
do: gettext("Iframe code"),
else: gettext("Link to the content")
}
autofocus="true"
required="true"
placeholder={gettext("Add a link")}
autocomplete="off"
required
/>
</div>
</div>
<p class="text-gray-700 text-xl font-semibold mt-5">{gettext("Options")}</p>
<div class="flex gap-x-2 mb-5 mt-3">
{checkbox(f, :attendee_visibility, class: "h-4 w-5")}
{label(
f,
:attendee_visibility,
gettext("Attendees can view the web content on their device"),
class: "text-sm font-medium"
)}
</div>
</div>
<div class="flex space-x-3">
<button type="submit" phx_disable_with="Loading..." class="btn btn-primary w-full lg:w-auto">
<div class="flex flex-col gap-1">
<p class="text-sm font-bold text-base-content">{gettext("Options")}</p>
<.checkbox
form={f}
key={:attendee_visibility}
label={gettext("Attendees can view the web content on their device")}
/>
</div>
<div class="flex flex-col gap-2">
<button type="submit" phx-disable-with={gettext("Loading...")} class="btn btn-primary w-full">
{case @live_action do
:new -> gettext("Create")
:new -> gettext("Add web content")
:edit -> gettext("Save")
end}
</button>
@@ -69,7 +72,7 @@
data: [
confirm: gettext("This will delete the web content, are you sure?")
],
class: "btn btn-error w-full lg:w-auto"
class: "btn btn-outline btn-error w-full"
)}
<% end %>
</div>

View File

@@ -242,8 +242,8 @@
</ul>
<% end %>
<%= if @create=="poll" do %>
<div class="scroll-py-3 overflow-y-auto bg-gray-100 p-3">
<p class="text-xl font-bold">
<div class="scroll-py-3 overflow-y-auto p-6">
<p class="text-base font-bold text-secondary mb-5">
{case @create_action do
:new -> gettext("New poll")
:edit -> gettext("Edit poll")
@@ -262,8 +262,8 @@
</div>
<% end %>
<%= if @create=="form" do %>
<div class="scroll-py-3 overflow-y-auto bg-gray-100 p-3">
<p class="text-xl font-bold">
<div class="scroll-py-3 overflow-y-auto p-6">
<p class="text-base font-bold text-secondary mb-5">
{case @create_action do
:new -> gettext("New form")
:edit -> gettext("Edit form")
@@ -282,8 +282,8 @@
</div>
<% end %>
<%= if @create == "embed" do %>
<div class="scroll-py-3 overflow-y-auto bg-gray-100 p-3">
<p class="text-xl font-bold">
<div class="scroll-py-3 overflow-y-auto p-6">
<p class="text-base font-bold text-secondary mb-5">
{case @create_action do
:new -> gettext("New web content")
:edit -> gettext("Edit web content")
@@ -302,8 +302,8 @@
</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">
<div class="scroll-py-3 overflow-y-auto p-6">
<p class="text-base font-bold text-secondary mb-5">
{case @create_action do
:new -> gettext("New quiz")
:edit -> gettext("Edit quiz")

View File

@@ -6,87 +6,94 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-6"
>
<div class="my-3 mb-10">
<ClaperWeb.Component.Input.text
<div class="flex flex-col gap-4">
<.text_field
form={f}
key={:title}
name={gettext("Title of your form")}
autofocus="true"
required="true"
label={gettext("Form's title")}
placeholder={gettext("Enter your title")}
autocomplete="off"
required
autofocus
/>
<%= inputs_for f, :fields, fn i -> %>
<div class="flex flex-col gap-2">
<div class="flex gap-3 items-end">
<div class="flex-1 min-w-0">
<.text_field
form={i}
key={:name}
label={ngettext("Field %{count}", "Field %{count}", i.index + 1)}
autocomplete="off"
required
/>
</div>
<div class="w-40 shrink-0">
<.select_field
form={i}
key={:type}
label={gettext("Type")}
options={[{gettext("Text"), "text"}, {gettext("Email"), "email"}]}
required
/>
</div>
<%= if i.index >= 1 do %>
<button
type="button"
phx-click="remove_field"
phx-value-field={i.index}
phx-target={@myself}
aria-label={gettext("Remove field")}
class="btn btn-circle btn-error !size-12 shrink-0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-5 w-5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7h16M10 11v6M14 11v6M5 7l1 13a2 2 0 002 2h8a2 2 0 002-2l1-13M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3" />
</svg>
</button>
<% end %>
</div>
<label class="flex items-center gap-2 cursor-pointer w-fit pl-1">
{checkbox(i, :required, class: "toggle toggle-sm toggle-primary")}
<span class="text-sm font-medium text-neutral">{gettext("Required")}</span>
</label>
</div>
<% end %>
<button
type="button"
phx-click="add_field"
phx-target={@myself}
class="btn btn-sm bg-base-200 text-base-content border-none hover:bg-base-300 gap-1.5 self-start !h-9"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-4 w-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{gettext("Add")}
</button>
</div>
<%= inputs_for f, :fields, fn i -> %>
<div class="flex gap-x-3 mt-3 items-center justify-start">
<div class="flex items-center gap-x-2">
<ClaperWeb.Component.Input.text
form={i}
key={:name}
name={ngettext("Field %{count}", "Field %{count}", i.index + 1)}
autofocus="true"
required="true"
/>
<ClaperWeb.Component.Input.select
form={i}
array={[{:"#{gettext("Text")}", "text"}, {:"#{gettext("Email")}", "email"}]}
key={:type}
name={gettext("Type")}
required="true"
/>
<ClaperWeb.Component.Input.toggle form={i} key={:required} label={gettext("Required")} />
</div>
<%= if i.index >= 1 do %>
<button
type="button"
phx-click="remove_field"
phx-value-field={i.index}
phx-target={@myself}
class="rounded-md bg-red-500 hover:bg-red-600 transition block mt-6"
>
<svg
class="text-white h-8 transform rotate-45"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<% end %>
</div>
<% end %>
<button
type="button"
phx-click="add_field"
phx-target={@myself}
class="rounded-md bg-primary-500 hover:bg-primary-600 transition block mt-3 mb-8"
>
<svg
class="text-white h-10"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<div class="flex space-x-3">
<button type="submit" phx_disable_with="Loading..." class="btn btn-primary w-full lg:w-auto">
<div class="flex flex-col gap-2">
<button type="submit" phx-disable-with={gettext("Loading...")} class="btn btn-primary w-full">
{case @live_action do
:new -> gettext("Create")
:new -> gettext("Create form")
:edit -> gettext("Save")
end}
</button>
@@ -102,7 +109,7 @@
"This will delete all responses associated and the form itself, are you sure?"
)
],
class: "btn btn-error w-full lg:w-auto"
class: "btn btn-outline btn-error w-full"
)}
<% end %>
</div>

View File

@@ -6,96 +6,87 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-6"
>
<div class="my-3 mb-5">
<ClaperWeb.Component.Input.text
<div class="flex flex-col gap-4">
<.text_field
form={f}
key={:title}
name={gettext("Title of your poll")}
autofocus="true"
required="true"
label={gettext("Poll's title")}
placeholder={gettext("Enter your title")}
autocomplete="off"
required
autofocus
/>
</div>
<%= inputs_for f, :poll_opts, fn i -> %>
<div class="flex space-x-3 mt-3 items-center justify-start">
<div class="flex-1">
<ClaperWeb.Component.Input.text
form={i}
key={:content}
name={ngettext("Choice %{count}", "Choice %{count}", i.index + 1)}
autofocus="true"
required="true"
/>
</div>
<%= if i.index >= 2 do %>
<button
type="button"
phx-click="remove_opt"
phx-value-opt={i.index}
phx-target={@myself}
class="rounded-md bg-red-500 hover:bg-red-600 transition block mt-4"
>
<svg
class="text-white h-10 transform rotate-45"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
<%= inputs_for f, :poll_opts, fn i -> %>
<.text_field
form={i}
key={:content}
label={ngettext("Choice %{count}", "Choice %{count}", i.index + 1)}
autocomplete="off"
required
>
<:trailing :if={i.index >= 2}>
<button
type="button"
phx-click="remove_opt"
phx-value-opt={i.index}
phx-target={@myself}
aria-label={gettext("Remove choice")}
class="btn btn-circle btn-error !size-12 shrink-0"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<% end %>
</div>
<% end %>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-5 w-5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7h16M10 11v6M14 11v6M5 7l1 13a2 2 0 002 2h8a2 2 0 002-2l1-13M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3" />
</svg>
</button>
</:trailing>
</.text_field>
<% end %>
<button
type="button"
phx-click="add_opt"
phx-target={@myself}
class="rounded-md bg-primary-500 hover:bg-primary-600 transition block mt-3 mb-8"
>
<svg
class="text-white h-10"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
<button
type="button"
phx-click="add_opt"
phx-target={@myself}
class="btn btn-sm bg-base-200 text-base-content border-none hover:bg-base-300 gap-1.5 self-start !h-9"
>
<path
fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<p class="text-gray-700 text-xl font-semibold">{gettext("Options")}</p>
<div class="flex gap-x-2 mb-5 mt-3">
{checkbox(f, :show_results, class: "h-4 w-4")}
{label(
f,
:show_results,
gettext("Attendees can see the results on their device"),
class: "text-sm font-medium"
)}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-4 w-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{gettext("Add")}
</button>
</div>
<div class="flex gap-x-2 mb-5">
{checkbox(f, :multiple, class: "h-4 w-4")}
{label(f, :multiple, gettext("Multiple answers"), class: "text-sm font-medium")}
<div class="flex flex-col gap-1">
<p class="text-sm font-bold text-base-content">{gettext("Options")}</p>
<.checkbox
form={f}
key={:show_results}
label={gettext("Attendees can see the results on their device")}
/>
<.checkbox form={f} key={:multiple} label={gettext("Multiple answers")} />
</div>
<div class="flex space-x-3">
<button type="submit" phx_disable_with="Loading..." class="btn btn-primary w-full lg:w-auto">
<div class="flex flex-col gap-2">
<button type="submit" phx-disable-with={gettext("Loading...")} class="btn btn-primary w-full">
{case @live_action do
:new -> gettext("Create")
:new -> gettext("Create a poll")
:edit -> gettext("Save")
end}
</button>
@@ -111,7 +102,7 @@
"This will delete all responses associated and the poll itself, are you sure?"
)
],
class: "btn btn-error w-full lg:w-auto"
class: "btn btn-outline btn-error w-full"
)}
<% end %>
</div>

View File

@@ -6,211 +6,239 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-6"
>
<div class="my-3 mb-5">
<ClaperWeb.Component.Input.text
form={f}
key={:title}
name={gettext("Title")}
autofocus="true"
required="true"
/>
</div>
<.text_field
form={f}
key={:title}
label={gettext("Title")}
placeholder={gettext("Enter a title")}
autocomplete="off"
required
autofocus
/>
<div>
<div class="w-full bg-gray-300 text-gray-700 h-9 text-sm flex items-center rounded-t-md">
<%= for i <- 0..(Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length()) - 1 do %>
<% question_count = Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() %>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-1">
<%= for i <- 0..(question_count - 1) do %>
<button
type="button"
phx-click="set_current_quiz_question_index"
phx-value-index={i}
phx-target={@myself}
class={[
"px-3 py-1 h-full",
if(@current_quiz_question_index == i, do: "bg-white text-gray-800"),
if(i == 0, do: "rounded-tl-md")
"size-8 rounded-full text-sm font-bold flex items-center justify-center transition-colors",
if(@current_quiz_question_index == i,
do: "bg-primary-50 text-primary border-b-2 border-primary",
else: "bg-base-200 text-base-content hover:bg-base-300"
)
]}
>
{i + 1}
</button>
<% end %>
<%= if Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() < 10 do %>
<%= if question_count < 10 do %>
<button
type="button"
phx-click="add_quiz_question"
class="text-xs px-3"
phx-target={@myself}
class="btn btn-sm bg-base-200 text-base-content border-none hover:bg-base-300 gap-1.5 !h-8"
>
+ {gettext("Add Question")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-4 w-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{gettext("Add a question")}
</button>
<% end %>
</div>
<%= inputs_for f, :quiz_questions, fn q -> %>
<div class={[
"mb-4 p-4 border rounded-b-md",
if(@current_quiz_question_index != q.index, do: "hidden", else: "")
]}>
<div class="flex gap-x-3 mt-3 items-center justify-start">
<ClaperWeb.Component.Input.text
<div class="flex flex-col gap-4">
<%= inputs_for f, :quiz_questions, fn q -> %>
<div class={[
"flex flex-col gap-4 p-4 border border-gray-200 rounded-2xl",
if(@current_quiz_question_index != q.index, do: "hidden")
]}>
<div class="flex items-center gap-3">
<div class="flex items-center justify-center size-12 rounded-full bg-base-200 text-base-content font-bold shrink-0">
{q.index + 1}
</div>
<p class="font-bold text-base text-base-content">
{gettext("Question %{number}", number: q.index + 1)}
</p>
</div>
<.text_field
form={q}
key={:content}
name={gettext("Your question")}
autofocus="true"
required="true"
label={gettext("Your question")}
placeholder={gettext("Enter your question")}
autocomplete="off"
required
/>
</div>
<%= if Keyword.has_key?(q.errors, :quiz_question_opts) do %>
<p class="text-supporting-red-500 text-sm my-2">
{elem(Keyword.get(q.errors, :quiz_question_opts), 0)}
</p>
<% end %>
<%= if Keyword.has_key?(q.errors, :quiz_question_opts) do %>
<p class="text-supporting-red-500 text-sm">
{elem(Keyword.get(q.errors, :quiz_question_opts), 0)}
</p>
<% end %>
<div class="mt-5">
<%= inputs_for q, :quiz_question_opts, fn o -> %>
<div class="mt-2" id={"answer-#{o.index}"}>
<div class="flex items-center gap-x-2">
<div class="flex-1">
<ClaperWeb.Component.Input.text
form={o}
key={:content}
name={gettext("Answer %{index}", index: o.index + 1)}
required="true"
/>
</div>
<div>
<%= label(class: "mt-6 cursor-pointer flex items-center text-white rounded-md px-2.5 py-2.5 h-full #{if (o.source.changes[:is_correct] != nil && o.source.changes[:is_correct]) || (!Map.has_key?(o.source.changes, :is_correct) && o.source.data.is_correct), do: "bg-green-500", else: "bg-red-500"}") do %>
<% is_correct =
(o.source.changes[:is_correct] != nil && o.source.changes[:is_correct]) ||
(!Map.has_key?(o.source.changes, :is_correct) && o.source.data.is_correct) %>
<div id={"answer-#{q.index}-#{o.index}"}>
<.text_field
form={o}
key={:content}
label={gettext("Answer %{index}", index: o.index + 1)}
autocomplete="off"
required
>
<:trailing>
<label
title={gettext("Mark as correct answer")}
class={[
"btn btn-circle !size-12 shrink-0 border-none cursor-pointer",
if(is_correct,
do: "btn-success text-white",
else: "bg-base-200 hover:bg-base-300 text-base-content/40"
)
]}
>
{checkbox(o, :is_correct, class: "hidden")}
<%= if (o.source.changes[:is_correct] != nil && o.source.changes[:is_correct]) || (!Map.has_key?(o.source.changes, :is_correct) && o.source.data.is_correct) do %>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</label>
<%= if o.index > 1 do %>
<label
title={gettext("Delete answer")}
class="btn btn-circle btn-error !size-12 shrink-0 cursor-pointer"
>
<input
type="checkbox"
name={"quiz[quiz_questions][#{q.index}][quiz_question_opts_delete][]"}
value={o.index}
class="hidden"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-6 h-6"
class="h-5 w-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 12.75 6 6 9-13.5"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7h16M10 11v6M14 11v6M5 7l1 13a2 2 0 002 2h8a2 2 0 002-2l1-13M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3" />
</svg>
<% else %>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
<% end %>
</label>
<% end %>
</div>
</div>
<%= if o.index > 1 do %>
<label>
<div class="cursor-pointer mt-1 text-xs font-semibold text-red-600">
{gettext("Delete")}
</div>
<input
type="checkbox"
name={
"quiz[quiz_questions][#{q.index}][quiz_question_opts_delete][]"
}
value={o.index}
class="hidden"
/>
</label>
<% end %>
</:trailing>
</.text_field>
</div>
<% end %>
</div>
<button
type="button"
phx-click="add_quiz_question_opt"
phx-value-question_index={q.index}
phx-target={@myself}
class="mt-5 text-xs text-gray-700"
>
+ {gettext("Add answer")}
</button>
<button
type="button"
phx-click="add_quiz_question_opt"
phx-value-question_index={q.index}
phx-target={@myself}
class="btn btn-sm bg-base-200 text-base-content border-none hover:bg-base-300 gap-1.5 self-start !h-9"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-4 w-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{gettext("Add an answer")}
</button>
<%= if Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() > 1 do %>
<label phx-click="remove_quiz_question" phx-target={@myself}>
<div class="btn btn-error cursor-pointer mt-4 w-full lg:w-auto">
<%= if question_count > 1 do %>
<label
phx-click="remove_quiz_question"
phx-target={@myself}
class="btn btn-outline btn-error w-full cursor-pointer"
>
{gettext("Remove question")}
</div>
<input
type="checkbox"
name="quiz[quiz_questions_delete][]"
value={q.index}
class="hidden"
/>
</label>
<input
type="checkbox"
name="quiz[quiz_questions_delete][]"
value={q.index}
class="hidden"
/>
</label>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="flex flex-col gap-1">
<p class="text-sm font-bold text-base-content">{gettext("Options")}</p>
<.checkbox
form={f}
key={:allow_anonymous}
label={gettext("Allow anonymous submissions")}
/>
</div>
<div class="flex flex-col gap-2">
<button
type="submit"
phx-disable-with={gettext("Loading...")}
disabled={!@changeset.valid?}
class="btn btn-primary w-full disabled:opacity-50 disabled:cursor-not-allowed"
>
{case @live_action do
:new -> gettext("Create quiz")
: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: "btn btn-outline btn-error w-full"
)}
<div class="text-center">
<%= link to: ~p"/export/quizzes/#{@quiz.id}/qti",
class: "text-xs text-primary-500 font-medium inline-flex items-center gap-1",
method: :post,
target: "_blank" do %>
{gettext("Export to QTI (XML)")}
<% end %>
</div>
<% end %>
</div>
<p class="text-gray-700 text-xl font-semibold">{gettext("Options")}</p>
<div class="flex gap-x-2 mb-5 mt-3">
{checkbox(f, :allow_anonymous, class: "h-4 w-4")}
{label(
f,
:allow_anonymous,
gettext("Allow anonymous submissions"),
class: "text-sm font-medium"
)}
</div>
<div class="flex justify-between items-center">
<div class="flex space-x-3">
<button
type="submit"
phx_disable_with="Loading..."
disabled={!@changeset.valid?}
class="btn btn-primary w-full lg:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
>
{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: "btn btn-error w-full lg:w-auto"
)}
<% end %>
</div>
<%= if @live_action == :edit do %>
<%= link to: ~p"/export/quizzes/#{@quiz.id}/qti", class: "text-xs text-primary-500 font-medium flex items-center gap-1", method: :post, target: "_blank" do %>
{gettext("Export to QTI (XML)")}
<% end %>
<% end %>
</div>
</.form>
</div>

View File

@@ -6,52 +6,51 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="flex flex-col gap-6"
>
<div class="my-3 mb-10">
<ClaperWeb.Component.Input.select
<.select_field
form={f}
key={:language}
label={gettext("Language")}
options={@languages}
/>
<div class="flex flex-col gap-4">
<p class="text-sm font-bold text-base-content">{gettext("Options")}</p>
<.select_field
form={f}
key={:language}
name={gettext("Language")}
array={@languages}
key={:visibility}
label={gettext("Show subtitles on")}
options={[
{gettext("Presenter and attendee"), "both"},
{gettext("Presenter only"), "presenter"},
{gettext("Attendee only"), "attendee"}
]}
/>
<div class="mt-3">
<p class="text-gray-700 text-xl font-semibold mt-5">{gettext("Options")}</p>
<ClaperWeb.Component.Input.select
form={f}
key={:visibility}
name={gettext("Show subtitles on")}
array={[
{gettext("Presenter and attendee"), "both"},
{gettext("Presenter only"), "presenter"},
{gettext("Attendee only"), "attendee"}
]}
/>
</div>
<div class="mt-3">
<label class="block text-sm font-medium text-gray-700">{gettext("Microphone")}</label>
<div class="mt-1">
<select
id="mic-select"
phx-hook="MicSelector"
data-event-uuid={@event_uuid}
class="bg-white outline-hidden shadow-base focus:ring-primary-500 focus:border-primary-500 block w-full text-lg border-gray-300 rounded-md py-2 px-3"
>
<option>{gettext("Loading microphones...")}</option>
</select>
</div>
<p class="text-xs text-gray-500 mt-1">
<div class="flex flex-col gap-1">
<label for="mic-select" class="text-sm font-bold text-base-content">
{gettext("Microphone")}
</label>
<select
id="mic-select"
phx-hook="MicSelector"
data-event-uuid={@event_uuid}
class="select w-full h-12 bg-white"
>
<option>{gettext("Loading microphones...")}</option>
</select>
<p class="text-xs text-base-content/60">
{gettext("Microphone selection is saved locally in your browser.")}
</p>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-3">
<button type="submit" phx_disable_with="Loading..." class="btn btn-primary w-full lg:w-auto">
<div class="flex flex-col gap-2">
<button type="submit" phx-disable-with={gettext("Loading...")} class="btn btn-primary w-full">
{case @live_action do
:new -> gettext("Create")
:new -> gettext("Add transcription")
:edit -> gettext("Save")
end}
</button>
@@ -64,7 +63,7 @@
data: [
confirm: gettext("This will delete the transcription configuration, are you sure?")
],
class: "btn btn-error w-full lg:w-auto"
class: "btn btn-outline btn-error w-full"
)}
<% end %>
</div>

View File

@@ -0,0 +1,66 @@
defmodule ClaperWeb.Component.Checkbox do
@moduledoc """
DaisyUI checkbox with an inline label for Claper forms.
Wraps the daisyUI [`checkbox`](https://daisyui.com/components/checkbox/)
component and renders the hidden fallback input so an unchecked box still
submits `false` inside a `<.form>` bound to an Ecto changeset. Form-aware: pass
a `Phoenix.HTML.Form` and the field `key`.
## Colors
- `:primary` - Claper purple (default)
- `:secondary` - navy purple
- `:neutral` - grey
- `:default` - theme default
## Sizes
- `:sm`, `:md` (default), `:lg`
## Examples
<.checkbox form={f} key={:multiple} label={gettext("Multiple answers")} />
<.checkbox form={f} key={:accept} color={:secondary}>
{gettext("I accept the")} <a href="/terms" class="link">{gettext("terms")}</a>
</.checkbox>
"""
use ClaperWeb, :view_component
attr :form, Phoenix.HTML.Form, required: true
attr :key, :atom, required: true
attr :label, :string, default: nil
attr :color, :atom, default: :primary, values: [:primary, :secondary, :neutral, :default]
attr :size, :atom, default: :md, values: [:sm, :md, :lg]
attr :class, :string, default: nil
attr :rest, :global, include: ~w(phx-click phx-change disabled)
slot :inner_block
def checkbox(assigns) do
~H"""
<label class="flex items-center gap-3 cursor-pointer py-2">
<input type="hidden" name={input_name(@form, @key)} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
name={input_name(@form, @key)}
id={input_id(@form, @key)}
value="true"
checked={Phoenix.HTML.Form.normalize_value("checkbox", input_value(@form, @key))}
class={["checkbox shrink-0", color_class(@color), size_class(@size), @class]}
{@rest}
/>
<span :if={@label} class="text-base text-neutral">{@label}</span>
{render_slot(@inner_block)}
</label>
"""
end
defp color_class(:primary), do: "checkbox-primary"
defp color_class(:secondary), do: "checkbox-secondary"
defp color_class(:neutral), do: "checkbox-neutral"
defp color_class(:default), do: nil
defp size_class(:sm), do: "checkbox-sm"
defp size_class(:md), do: nil
defp size_class(:lg), do: "checkbox-lg"
end

View File

@@ -0,0 +1,75 @@
defmodule ClaperWeb.Component.Select do
@moduledoc """
DaisyUI select field for Claper forms.
Wraps the daisyUI [`select`](https://daisyui.com/components/select/) component
(pill-shaped and `!font-display` via `assets/css/app.css`) together with a bold
field label and inline validation errors. Form-aware: pass a
`Phoenix.HTML.Form`, the field `key` and the `options`.
`options` accepts anything
[`options_for_select/2`](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#options_for_select/2)
understands, e.g. `[{"Text", "text"}, {"Email", "email"}]`.
## Sizes
- `:sm` - 40px height
- `:md` - 48px height (default)
- `:lg` - 56px height
## Examples
<.select_field
form={i}
key={:type}
label={gettext("Type")}
options={[{gettext("Text"), "text"}, {gettext("Email"), "email"}]}
/>
"""
use ClaperWeb, :view_component
attr :form, Phoenix.HTML.Form, required: true
attr :key, :atom, required: true
attr :options, :list, required: true
attr :label, :string, default: nil
attr :prompt, :string, default: nil
attr :required, :boolean, default: false
attr :size, :atom, default: :md, values: [:sm, :md, :lg]
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled multiple phx-change)
def select_field(assigns) do
assigns = assign_new(assigns, :errors, fn -> error_tag(assigns.form, assigns.key) end)
~H"""
<div class="flex flex-col gap-1">
<label
:if={@label}
for={input_id(@form, @key)}
class="text-sm font-bold text-base-content"
>
{@label}
</label>
<select
name={input_name(@form, @key)}
id={input_id(@form, @key)}
required={@required}
class={[
"select w-full bg-white",
size_class(@size),
@errors != [] && "select-error",
@class
]}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, input_value(@form, @key))}
</select>
<p :if={@errors != []} class="text-supporting-red-500 text-sm">{@errors}</p>
</div>
"""
end
defp size_class(:sm), do: "h-10"
defp size_class(:md), do: "h-12"
defp size_class(:lg), do: "h-14"
end

View File

@@ -0,0 +1,93 @@
defmodule ClaperWeb.Component.TextInput do
@moduledoc """
DaisyUI text input field for Claper forms.
Wraps the daisyUI [`input`](https://daisyui.com/components/input/) component
(pill-shaped and `!font-display` via `assets/css/app.css`) together with a bold
field label and inline validation errors. The component is form-aware: pass a
`Phoenix.HTML.Form` and the field `key` and it derives the name, id, value and
errors — including for nested forms built with `inputs_for/3`.
## Sizes
- `:sm` - 40px height
- `:md` - 48px height (default)
- `:lg` - 56px height
## Examples
<.text_field form={f} key={:title} label={gettext("Poll's title")} required />
<.text_field form={f} key={:email} type="email" placeholder="you@example.com" />
<.text_field form={i} key={:content} label={gettext("Choice")} required>
<:trailing>
<.icon_button style={:ghost} shape={:circle} size={:sm}>…</.icon_button>
</:trailing>
</.text_field>
"""
use ClaperWeb, :view_component
attr :form, Phoenix.HTML.Form, required: true
attr :key, :atom, required: true
attr :type, :string,
default: "text",
values: ~w(text email url tel search password number)
attr :label, :string, default: nil
attr :placeholder, :string, default: nil
attr :required, :boolean, default: false
attr :autofocus, :boolean, default: false
attr :autocomplete, :string, default: nil
attr :size, :atom, default: :md, values: [:sm, :md, :lg]
attr :minlength, :any, default: nil
attr :maxlength, :any, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(phx-debounce phx-blur inputmode pattern step readonly disabled)
slot :trailing, doc: "content rendered to the right of the input, e.g. a remove button"
def text_field(assigns) do
assigns = assign_new(assigns, :errors, fn -> error_tag(assigns.form, assigns.key) end)
~H"""
<div class="flex flex-col gap-1">
<label
:if={@label}
for={input_id(@form, @key)}
class="text-sm font-bold text-base-content"
>
{@label}
</label>
<div class="flex items-center gap-2">
<input
type={@type}
name={input_name(@form, @key)}
id={input_id(@form, @key)}
value={input_value(@form, @key)}
placeholder={@placeholder}
required={@required}
autofocus={@autofocus}
autocomplete={@autocomplete}
minlength={@minlength}
maxlength={@maxlength}
class={[
"input w-full bg-white",
size_class(@size),
@errors != [] && "input-error",
@class
]}
{@rest}
/>
{render_slot(@trailing)}
</div>
<p :if={@errors != []} class="text-supporting-red-500 text-sm">{@errors}</p>
</div>
"""
end
defp size_class(:sm), do: "h-10"
defp size_class(:md), do: "h-12"
defp size_class(:lg), do: "h-14"
end