mirror of
https://github.com/ClaperCo/Claper.git
synced 2026-07-10 04:24:14 +02:00
Add form components and migrate templates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
66
lib/claper_web/views/components/checkbox_component.ex
Normal file
66
lib/claper_web/views/components/checkbox_component.ex
Normal 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
|
||||
75
lib/claper_web/views/components/select_component.ex
Normal file
75
lib/claper_web/views/components/select_component.ex
Normal 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
|
||||
93
lib/claper_web/views/components/text_input_component.ex
Normal file
93
lib/claper_web/views/components/text_input_component.ex
Normal 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
|
||||
Reference in New Issue
Block a user