From 61ec82ef431608a13bd0f49c667583cfed3ec8ec Mon Sep 17 00:00:00 2001 From: Alex Lion Date: Mon, 6 Jul 2026 21:58:19 +0200 Subject: [PATCH] Add form components and migrate templates --- lib/claper_web.ex | 3 + .../live/embed_live/form_component.html.heex | 73 ++-- .../live/event_live/manage.html.heex | 16 +- .../live/form_live/form_component.html.heex | 157 ++++---- .../live/poll_live/form_component.html.heex | 147 ++++---- .../live/quiz_live/quiz_component.html.heex | 346 ++++++++++-------- .../form_component.html.heex | 73 ++-- .../views/components/checkbox_component.ex | 66 ++++ .../views/components/select_component.ex | 75 ++++ .../views/components/text_input_component.ex | 93 +++++ 10 files changed, 657 insertions(+), 392 deletions(-) create mode 100644 lib/claper_web/views/components/checkbox_component.ex create mode 100644 lib/claper_web/views/components/select_component.ex create mode 100644 lib/claper_web/views/components/text_input_component.ex diff --git a/lib/claper_web.ex b/lib/claper_web.ex index 2c7c6a9..4aa62f0 100644 --- a/lib/claper_web.ex +++ b/lib/claper_web.ex @@ -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 diff --git a/lib/claper_web/live/embed_live/form_component.html.heex b/lib/claper_web/live/embed_live/form_component.html.heex index 14d67a7..9f275e4 100644 --- a/lib/claper_web/live/embed_live/form_component.html.heex +++ b/lib/claper_web/live/embed_live/form_component.html.heex @@ -6,57 +6,60 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + class="flex flex-col gap-6" > -
- + <.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 /> -
- -
-

- +
+ <.select_field + form={f} + key={:provider} + label={gettext("Provider")} + options={@providers} + required + /> +
+
+ <.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 />
- -

{gettext("Options")}

- -
- {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" - )} -
-
- @@ -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 %>
diff --git a/lib/claper_web/live/event_live/manage.html.heex b/lib/claper_web/live/event_live/manage.html.heex index 03bc40d..d065197 100644 --- a/lib/claper_web/live/event_live/manage.html.heex +++ b/lib/claper_web/live/event_live/manage.html.heex @@ -242,8 +242,8 @@ <% end %> <%= if @create=="poll" do %> -
-

+

+

{case @create_action do :new -> gettext("New poll") :edit -> gettext("Edit poll") @@ -262,8 +262,8 @@

<% end %> <%= if @create=="form" do %> -
-

+

+

{case @create_action do :new -> gettext("New form") :edit -> gettext("Edit form") @@ -282,8 +282,8 @@

<% end %> <%= if @create == "embed" do %> -
-

+

+

{case @create_action do :new -> gettext("New web content") :edit -> gettext("Edit web content") @@ -302,8 +302,8 @@

<% end %> <%= if @create=="quiz" do %> -
-

+

+

{case @create_action do :new -> gettext("New quiz") :edit -> gettext("Edit quiz") diff --git a/lib/claper_web/live/form_live/form_component.html.heex b/lib/claper_web/live/form_live/form_component.html.heex index 322587e..1d25b26 100644 --- a/lib/claper_web/live/form_live/form_component.html.heex +++ b/lib/claper_web/live/form_live/form_component.html.heex @@ -6,87 +6,94 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + class="flex flex-col gap-6" > -

- + <.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 -> %> +
+
+
+ <.text_field + form={i} + key={:name} + label={ngettext("Field %{count}", "Field %{count}", i.index + 1)} + autocomplete="off" + required + /> +
+
+ <.select_field + form={i} + key={:type} + label={gettext("Type")} + options={[{gettext("Text"), "text"}, {gettext("Email"), "email"}]} + required + /> +
+ <%= if i.index >= 1 do %> + + <% end %> +
+ + +
+ <% end %> + +
- <%= inputs_for f, :fields, fn i -> %> -
-
- - - -
- <%= if i.index >= 1 do %> - - <% end %> -
- <% end %> - - - -
- @@ -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 %>
diff --git a/lib/claper_web/live/poll_live/form_component.html.heex b/lib/claper_web/live/poll_live/form_component.html.heex index 1e17f25..4c62591 100644 --- a/lib/claper_web/live/poll_live/form_component.html.heex +++ b/lib/claper_web/live/poll_live/form_component.html.heex @@ -6,96 +6,87 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + class="flex flex-col gap-6" > -
- + <.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 /> -
- <%= inputs_for f, :poll_opts, fn i -> %> -
-
- -
- <%= if i.index >= 2 do %> - - <% end %> -
- <% end %> + + + + + + + <% end %> - - -

{gettext("Options")}

- -
- {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" - )} + + + + {gettext("Add")} +
-
- {checkbox(f, :multiple, class: "h-4 w-4")} - {label(f, :multiple, gettext("Multiple answers"), class: "text-sm font-medium")} +
+

{gettext("Options")}

+ + <.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")} />
-
- @@ -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 %>
diff --git a/lib/claper_web/live/quiz_live/quiz_component.html.heex b/lib/claper_web/live/quiz_live/quiz_component.html.heex index d9ac63c..7dd8190 100644 --- a/lib/claper_web/live/quiz_live/quiz_component.html.heex +++ b/lib/claper_web/live/quiz_live/quiz_component.html.heex @@ -6,211 +6,239 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + class="flex flex-col gap-6" > -
- -
+ <.text_field + form={f} + key={:title} + label={gettext("Title")} + placeholder={gettext("Enter a title")} + autocomplete="off" + required + autofocus + /> -
-
- <%= for i <- 0..(Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length()) - 1 do %> + <% question_count = Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() %> +
+
+ <%= for i <- 0..(question_count - 1) do %> <% end %> - <%= if Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() < 10 do %> + <%= if question_count < 10 do %> <% end %>
- <%= inputs_for f, :quiz_questions, fn q -> %> -
-
- + <%= inputs_for f, :quiz_questions, fn q -> %> +
+
+
+ {q.index + 1} +
+

+ {gettext("Question %{number}", number: q.index + 1)} +

+
+ + <.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 /> -
- <%= if Keyword.has_key?(q.errors, :quiz_question_opts) do %> -

- {elem(Keyword.get(q.errors, :quiz_question_opts), 0)} -

- <% end %> + <%= if Keyword.has_key?(q.errors, :quiz_question_opts) do %> +

+ {elem(Keyword.get(q.errors, :quiz_question_opts), 0)} +

+ <% end %> -
<%= inputs_for q, :quiz_question_opts, fn o -> %> -
-
-
- -
-
- <%= 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) %> +
+ <.text_field + form={o} + key={:content} + label={gettext("Answer %{index}", index: o.index + 1)} + autocomplete="off" + required + > + <:trailing> + + <%= if o.index > 1 do %> + <% end %> -
-
- - <%= if o.index > 1 do %> - - <% end %> + +
<% end %> -
- + - <%= if Ecto.Changeset.get_field(@changeset, :quiz_questions) |> length() > 1 do %> - + + + <% end %> +
+ <% end %> +
+
+ +
+

{gettext("Options")}

+ + <.checkbox + form={f} + key={:allow_anonymous} + label={gettext("Allow anonymous submissions")} + /> +
+ +
+ + + <%= 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" + )} +
+ <%= 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 %>
<% end %>
- -

{gettext("Options")}

- -
- {checkbox(f, :allow_anonymous, class: "h-4 w-4")} - {label( - f, - :allow_anonymous, - gettext("Allow anonymous submissions"), - class: "text-sm font-medium" - )} -
- -
-
- - - <%= 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 %> -
- - <%= 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 %> -
diff --git a/lib/claper_web/live/transcription_live/form_component.html.heex b/lib/claper_web/live/transcription_live/form_component.html.heex index 5a36e66..050bef0 100644 --- a/lib/claper_web/live/transcription_live/form_component.html.heex +++ b/lib/claper_web/live/transcription_live/form_component.html.heex @@ -6,52 +6,51 @@ phx-target={@myself} phx-change="validate" phx-submit="save" + class="flex flex-col gap-6" > -
- + +
+

{gettext("Options")}

+ + <.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"} + ]} /> -
-

{gettext("Options")}

- - -
- -
- -
- -
-

+

+ + +

{gettext("Microphone selection is saved locally in your browser.")}

-
- @@ -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 %>
diff --git a/lib/claper_web/views/components/checkbox_component.ex b/lib/claper_web/views/components/checkbox_component.ex new file mode 100644 index 0000000..61b0d72 --- /dev/null +++ b/lib/claper_web/views/components/checkbox_component.ex @@ -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")} {gettext("terms")} + + """ + 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""" + + """ + 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 diff --git a/lib/claper_web/views/components/select_component.ex b/lib/claper_web/views/components/select_component.ex new file mode 100644 index 0000000..5b7593d --- /dev/null +++ b/lib/claper_web/views/components/select_component.ex @@ -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""" +
+ + +

{@errors}

+
+ """ + end + + defp size_class(:sm), do: "h-10" + defp size_class(:md), do: "h-12" + defp size_class(:lg), do: "h-14" +end diff --git a/lib/claper_web/views/components/text_input_component.ex b/lib/claper_web/views/components/text_input_component.ex new file mode 100644 index 0000000..4654519 --- /dev/null +++ b/lib/claper_web/views/components/text_input_component.ex @@ -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}>… + + + """ + 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""" +
+ +
+ + {render_slot(@trailing)} +
+

{@errors}

+
+ """ + end + + defp size_class(:sm), do: "h-10" + defp size_class(:md), do: "h-12" + defp size_class(:lg), do: "h-14" +end