Redesign event form UI and file upload flow

This commit is contained in:
Alex Lion
2026-08-01 12:00:46 +07:00
parent c20fe53c17
commit d57e517055
16 changed files with 2250 additions and 1488 deletions

View File

@@ -1,255 +1,215 @@
<div id="wrapper" class={if assigns[:container] == :drawer, do: "flex flex-col h-full", else: ""}>
<div class={[
"py-4 flex flex-col sm:flex-row sm:items-center justify-between flex-shrink-0",
if(assigns[:container] != :drawer, do: "border-b border-gray-200")
<div id="wrapper" class={if @container == :drawer, do: "flex h-full flex-col", else: ""}>
<% upload_entry = Enum.at(@uploads.presentation_file.entries, 0) %>
<% upload_progress = if upload_entry, do: upload_entry.progress, else: 0 %>
<% can_submit = upload_progress in [0, 100] && @changeset.valid? %>
<% presentation_file = Map.get(@event, :presentation_file) %>
<% presentation_attached =
not is_nil(presentation_file) && Map.get(presentation_file, :length, 0) > 0 &&
not is_nil(Map.get(presentation_file, :id)) %>
<% leaders = Ecto.Changeset.get_field(@changeset, :leaders, []) %>
<header class={[
"flex shrink-0 items-center gap-3 border-b border-base-300 bg-base-100 px-4 py-4 sm:px-6",
@container != :drawer && "rounded-t-lg"
]}>
<div class="flex items-center gap-3 flex-1 min-w-0">
<%= if assigns[:container] == :drawer do %>
<.link patch={~p"/events"} class="p-1 rounded-full hover:bg-gray-100 transition">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</.link>
<% end %>
<h1 class="text-2xl font-medium leading-6 text-gray-900 sm:truncate">
<.link
:if={@container == :drawer}
patch={~p"/events"}
aria-label={gettext("Close event editor")}
class="btn btn-ghost btn-circle !size-10 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"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</.link>
<div class="min-w-0">
<h1 id="event-editor-title" class="truncate text-xl font-bold text-secondary">
{@page_title}
</h1>
<p class="mt-0.5 text-xs text-base-content/60">
{if @action == :edit,
do: gettext("Update the event details your attendees will see."),
else: gettext("Set up the details attendees will use to join.")}
</p>
</div>
<%= if assigns[:container] != :drawer do %>
<div class="flex mt-4 space-x-5 sm:mt-0">
<%= if (@uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 0 || @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 100) && @changeset.valid? do %>
<button
type="submit"
form="event-form"
phx_disable_with="Loading..."
class="btn btn-primary w-full lg:w-auto"
</header>
<div class={[
"flex-1 space-y-7 overflow-y-auto bg-base-100 px-4 py-6 sm:px-6",
@container != :drawer && "rounded-b-lg"
]}>
<section
class="space-y-4 border-b border-base-300 pb-7"
aria-labelledby="presentation-heading"
>
<div class="space-y-4">
<div>
<h2 id="presentation-heading" class="text-base font-bold text-secondary">
{gettext("Presentation")}
</h2>
<p class="mt-1 text-xs leading-5 text-base-content/60">
<%= if presentation_attached do %>
{gettext("Your presentation is attached and ready to use.")}
<% else %>
{gettext("Optional. Add a PDF or PowerPoint now, or upload one later.")}
<% end %>
</p>
</div>
<form
id="file-form"
phx-change="validate-file"
phx-submit="save-file"
phx-target={@myself}
class="min-w-0"
>
<div
class={[
"rounded-2xl border-2 bg-base-200/50 p-4 transition-colors",
upload_entry && upload_progress == 100 && "border-success/40",
(!upload_entry || upload_progress < 100) && "border-dashed border-base-300"
]}
phx-drop-target={@uploads.presentation_file.ref}
>
{case @action do
:edit -> gettext("Save")
:new -> gettext("Create")
end}
</button>
<% else %>
<div class="btn btn-primary btn-disabled opacity-25 cursor-default w-full lg:w-auto">
{case @action do
:edit -> gettext("Save")
:new -> gettext("Create")
end}
</div>
<% end %>
<%= if @action == :edit && !@event.expired_at do %>
{link(gettext("Delete"),
to: "#",
phx_click: "delete",
phx_value_id: @event.uuid,
data: [confirm: gettext("Are you sure?")],
class: "btn btn-error w-full lg:w-auto"
)}
<% end %>
</div>
<% end %>
</div>
<%= if upload_entry do %>
<div class="flex min-w-0 items-start gap-3">
<div class={[
"flex size-10 shrink-0 items-center justify-center rounded-full",
upload_progress == 100 && "bg-success/10 text-success",
upload_progress < 100 && "bg-primary/10 text-primary"
]}>
<%= if upload_progress == 100 do %>
<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"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m4.5 12.75 6 6 9-13.5"
/>
</svg>
<% else %>
<span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
<% end %>
</div>
<div class={if assigns[:container] == :drawer, do: "p-2 flex-1 overflow-y-auto", else: ""}>
<%= if Map.get(@event, :presentation_file) == nil || Map.get(@event.presentation_file, :length) == 0 || Map.get(@event.presentation_file, :id) == nil do %>
<div class="mb-3 mt-12">
<label class="block text-sm font-medium text-gray-700 mb-2">
{gettext("Select your presentation (optional)")}
</label>
<div class="max-w-lg flex flex-col justify-center items-center px-6 pt-5 pb-6 border-2 bg-white shadow-base border-gray-300 border-dashed rounded-md">
<%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) < 100 do %>
<div class="space-y-1 text-center" phx-drop-target={@uploads.presentation_file.ref}>
<svg
class="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="flex text-sm text-gray-600">
<label class="relative cursor-pointer bg-white rounded-md font-medium text-primary-600">
<form
id="file-form"
phx-change="validate-file"
phx-submit="save-file"
phx-target={@myself}
<div class="min-w-0 flex-1">
<p class="text-sm font-bold text-base-content">
<%= if upload_progress == 100 do %>
{gettext("New presentation ready")}
<% else %>
{gettext("Uploading... %{progress}%", progress: upload_progress)}
<% end %>
</p>
<p class="mt-0.5 truncate text-xs text-base-content/60">
{upload_entry.client_name}
</p>
<progress
:if={upload_progress < 100}
class="progress progress-primary mt-3 h-2 w-full"
max="100"
value={upload_progress}
aria-label={gettext("Presentation upload progress")}
>
<span>{gettext("Upload a file")}</span>
<.live_file_input upload={@uploads.presentation_file} class="sr-only" />
</form>
</label>
<p class="pl-1">{gettext("or drag and drop")}</p>
</div>
<p class="text-xs text-gray-500">
{gettext("PDF, PPT, PPTX up to %{size} MB", size: @max_file_size)}
</p>
<%= for entry <- @uploads.presentation_file.entries do %>
<progress id="file" max="100" value={entry.progress}>
{entry.progress}
</progress>
<%= for err <- upload_errors(@uploads.presentation_file, entry) do %>
<p class="text-red-500 text-sm px-4 py-2 border border-red-600 rounded-md my-3">
{error_to_string(err)}
{upload_progress}
</progress>
<p :if={upload_progress == 100} class="mt-2 text-xs text-base-content/60">
{gettext("Save the event to start processing this presentation.")}
</p>
<% end %>
<% end %>
</div>
<% else %>
<%= for entry <- @uploads.presentation_file.entries do %>
<div class="flex space-x-1">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-supporting-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
</div>
<button
type="button"
phx-click="remove-file"
phx-value-ref={upload_entry.ref}
phx-target={@myself}
aria-label={gettext("Remove selected presentation")}
class="btn btn-ghost btn-circle btn-sm shrink-0 text-error"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<p class="text-gray-700">{gettext("Presentation uploaded")}</p>
stroke="currentColor"
class="h-4 w-4"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-xs text-gray-400">{entry.client_name}</p>
<p>
<a
href="#"
phx-click="remove-file"
phx-value-ref={entry.ref}
phx-target={@myself}
class="text-red-500 text-sm"
>
{gettext("Remove")}
</a>
</p>
<% end %>
<% end %>
</div>
</div>
<% else %>
<div class="mt-12 mb-3">
<label class="block text-sm font-medium text-gray-700 mb-2">
{gettext("Select your presentation")}
</label>
<div class="max-w-lg flex flex-col justify-center items-center px-6 pt-5 pb-6 border-2 bg-white shadow-base border-gray-300 border-dashed rounded-md">
<%= if @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) < 100 do %>
<div class="flex space-x-1">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-supporting-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<p class="text-gray-700">{gettext("Presentation attached")}</p>
</div>
<div class="flex flex-col space-y-3 items-center">
<label class="text-primary-500 text-sm">
<form
id="file-form"
phx-change="validate-file"
phx-submit="save-file"
phx-target={@myself}
>
<span>{gettext("Change file")}</span>
<.live_file_input upload={@uploads.presentation_file} class="sr-only" />
</form>
</label>
<%= for entry <- @uploads.presentation_file.entries do %>
<progress id="file" max="100" value={entry.progress}>
{entry.progress}
</progress>
<%= for err <- upload_errors(@uploads.presentation_file, entry) do %>
<p class="text-red-500 text-sm px-4 py-2 border border-red-600 rounded-md my-3">
{error_to_string(err)}
</p>
<% end %>
<% end %>
<p class="text-supporting-red-500 text-sm italic text-center hidden">
{gettext(
"Changing your file will remove all interaction elements like polls associated."
)}
</p>
</div>
<% else %>
<div class="flex space-x-1">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-supporting-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<p class="text-gray-700">{gettext("Presentation replaced")}</p>
</div>
<%= for entry <- @uploads.presentation_file.entries do %>
<p class="text-xs text-gray-400">{entry.client_name}</p>
<div :if={entry.progress < 100}>
<progress id="file" max="100" value={entry.progress}>
{entry.progress}
</progress>
</div>
<%= for err <- upload_errors(@uploads.presentation_file, entry) do %>
<p class="text-red-500 text-sm px-4 py-2 border border-red-600 rounded-md my-3">
{error_to_string(err)}
<% else %>
<div class="flex flex-col items-center py-2 text-center">
<div class="flex size-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.8"
stroke="currentColor"
class="h-6 w-6"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5h10.5A2.25 2.25 0 0 0 19.5 17.25v-10.5A2.25 2.25 0 0 0 17.25 4.5H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5A2.25 2.25 0 0 0 6.75 19.5Z"
/>
</svg>
</div>
<p class="mt-3 text-sm font-bold text-base-content">
<%= if presentation_attached do %>
<span class="badge badge-success badge-outline mr-1">
{gettext("Attached")}
</span>
{gettext("Replace presentation")}
<% else %>
{gettext("Add a presentation")}
<% end %>
</p>
<% end %>
<p>
<a
href="#"
phx-click="remove-file"
phx-value-ref={entry.ref}
phx-target={@myself}
class="text-red-500 text-sm"
<p class="mt-1 text-xs text-base-content/60">
{gettext("Drag and drop a file here, or")}
</p>
<label
for={@uploads.presentation_file.ref}
class="btn btn-sm btn-outline btn-primary mt-3 !h-9 cursor-pointer"
>
{gettext("Remove")}
</a>
</p>
{gettext("Choose file")}
</label>
<.live_file_input upload={@uploads.presentation_file} class="sr-only" />
<p class="mt-3 text-xs text-base-content/50">
{gettext("PDF, PPT, or PPTX up to %{size} MB", size: @max_file_size)}
</p>
</div>
<% end %>
<% end %>
<%= for entry <- @uploads.presentation_file.entries do %>
<%= for err <- upload_errors(@uploads.presentation_file, entry) do %>
<p class="text-red-500 text-sm px-4 py-2 border border-red-600 rounded-md my-3">
{error_to_string(err)}
</p>
<%= if upload_entry do %>
<%= for err <- upload_errors(@uploads.presentation_file, upload_entry) do %>
<div class="alert alert-error mt-3 py-2 text-sm" role="alert">
{error_to_string(err)}
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
</form>
</div>
<% end %>
</section>
<.form
:let={f}
@@ -258,157 +218,239 @@
phx-target={@myself}
phx-change="validate"
phx-submit="save"
class="space-y-7"
>
<div class="relative">
<div class="my-3">
<ClaperWeb.Component.Input.text
<section
class={[
"space-y-4",
@action == :edit && "border-b border-base-300 pb-7"
]}
aria-labelledby="event-details-heading"
>
<div class="space-y-4">
<div>
<h2 id="event-details-heading" class="text-base font-bold text-secondary">
{gettext("Event details")}
</h2>
<p class="mt-1 text-xs leading-5 text-base-content/60">
{gettext("These details help attendees find and join your event.")}
</p>
</div>
<.text_field
form={f}
key={:name}
name={gettext("Name of your event")}
autofocus="true"
required="true"
label={gettext("Event name")}
autofocus={@action == :new}
required
minlength="5"
maxlength="50"
/>
</div>
<div class="my-3">
<ClaperWeb.Component.Input.code
form={f}
key={:code}
name={gettext("Code")}
required="true"
/>
</div>
<div class="my-3" phx-update="ignore" id="date-picker">
<ClaperWeb.Component.Input.date
form={f}
key={:started_at}
name={gettext("When your event will start?")}
required="true"
from={Date.add(Date.utc_today(), -1)}
to={Date.add(Date.utc_today(), 365)}
/>
</div>
</div>
<%= if @action == :edit do %>
<div class="mt-7 mb-3">
<span class="text-lg block font-medium text-gray-700">
{gettext("Facilitators can present and manage interactions")}
</span>
<button
type="button"
phx-click="add-leader"
phx-target={@myself}
class="rounded-md bg-primary-500 hover:bg-primary-600 transition flex items-center mt-3 md:w-max text-white py-5 px-3 text-sm max-h-0"
>
<svg
class="text-white h-6 transform"
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>
<span>{gettext("Add facilitator")}</span>
</button>
</div>
<%= inputs_for f, :leaders, fn l -> %>
<div class="flex items-center space-x-4 mt-2" id={l.data.temp_id || l.id}>
<img
class="h-10 w-10 rounded-full"
src={"https://www.gravatar.com/avatar/#{:crypto.hash(:md5, (l.data.email || "") |> String.downcase() |> String.trim() ) |> Base.encode16() |> String.downcase()}"}
alt=""
<div class="flex flex-col gap-1">
<ClaperWeb.Component.Input.code
form={f}
key={:code}
name={gettext("Join code")}
required="true"
/>
<p class="text-xs text-base-content/60">
{gettext("Attendees enter this code to join.")}
</p>
</div>
<%= if is_nil(l.data.temp_id) do %>
<div class="relative">
<ClaperWeb.Component.Input.text
form={l}
readonly={true}
placeholder={gettext("User email address")}
key={:email}
name=""
/>
<div class="flex flex-col gap-1" phx-update="ignore" id="date-picker">
<ClaperWeb.Component.Input.date
form={f}
key={:started_at}
name={gettext("Start date and time")}
required="true"
from={Date.add(Date.utc_today(), -1)}
to={Date.add(Date.utc_today(), 365)}
/>
<p class="text-xs text-base-content/60">{gettext("Shown in your local time.")}</p>
</div>
</div>
</section>
<section
:if={@action == :edit}
id="facilitators-section"
class={[
"space-y-4",
!@event.expired_at && "border-b border-base-300 pb-7"
]}
aria-labelledby="facilitators-heading"
>
<div class="space-y-4">
<div class="flex items-start justify-between gap-3">
<div>
<h2 id="facilitators-heading" class="text-base font-bold text-secondary">
{gettext("Facilitators")}
</h2>
<p class="mt-1 text-xs leading-5 text-base-content/60">
{gettext("Invite people who can present and manage audience interactions.")}
</p>
</div>
<button
type="button"
phx-click="add-leader"
phx-target={@myself}
class="btn btn-sm shrink-0 border-none bg-base-200 text-base-content hover:bg-base-300 !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"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
{gettext("Add")}
</button>
</div>
<p
:if={Enum.empty?(leaders)}
id="facilitators-empty-state"
class="text-sm text-base-content/50"
>
{gettext("No facilitators have been added yet.")}
</p>
<div class="flex flex-col gap-3">
<%= inputs_for f, :leaders, fn l -> %>
<div
class="flex min-w-0 items-end gap-2"
id={"facilitator-#{l.data.temp_id || l.id}"}
>
<div class="avatar avatar-placeholder hidden shrink-0 pb-1 sm:flex">
<div class="w-10 rounded-full bg-secondary text-secondary-content">
<span class="text-sm font-bold">
{(l.data.email || "?") |> String.first() |> String.upcase()}
</span>
</div>
</div>
<div class="min-w-0 flex-1">
<.text_field
form={l}
key={:email}
type="email"
label={gettext("Email address")}
placeholder={gettext("name@example.com")}
readonly={is_nil(l.data.temp_id)}
autocomplete="email"
/>
</div>
{hidden_input(l, :temp_id)}
{hidden_input(l, :event_id, value: @event.id)}
{hidden_input(l, :user_email, value: @current_user.email)}
<%= if is_nil(l.data.temp_id) do %>
{checkbox(l, :delete, class: "hidden")}
<button
type="button"
phx-click={
JS.set_attribute({"checked", "checked"}, to: "##{input_id(l, :delete)}")
|> JS.hide(to: "#facilitator-#{l.id}")
}
aria-label={gettext("Remove facilitator")}
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"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
<% else %>
<button
type="button"
phx-click="remove-leader"
phx-value-remove={l.data.temp_id}
phx-target={@myself}
aria-label={gettext("Remove facilitator")}
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"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
<% end %>
</div>
<label
phx-click={JS.hide(to: "##{l.data.temp_id || l.id}")}
class="cursor-pointer md:ml-3 rounded-md bg-supporting-red-500 hover:bg-supporting-red-600 transition flex items-center mt-2 md:w-max text-white py-7 px-3 text-sm max-h-0"
>
<span>{gettext("Remove")}</span>
{checkbox(l, :delete, class: "hidden")}
</label>
<% else %>
<div class="relative">
<ClaperWeb.Component.Input.text
form={l}
placeholder={gettext("User email address")}
key={:email}
name=""
/>
</div>
{hidden_input(l, :temp_id)}
{hidden_input(l, :event_id, value: @event.id)}
{hidden_input(l, :user_email, value: @current_user.email)}
<button
type="button"
phx-click="remove-leader"
phx-value-remove={l.data.temp_id}
phx-target={@myself}
class="md:ml-3 rounded-md bg-supporting-red-500 hover:bg-supporting-red-600 transition flex items-center mt-2 md:w-max text-white py-5 px-3 text-sm max-h-0"
>
<span>{gettext("Remove")}</span>
</button>
<% end %>
</div>
<% end %>
<% end %>
</.form>
</div>
<%= if assigns[:container] == :drawer do %>
<div class="pt-4 mt-auto flex-shrink-0 flex flex-col gap-3">
<%= if (@uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 0 || @uploads.presentation_file.entries |> Enum.at(0, %{}) |> Map.get(:progress, 0) == 100) && @changeset.valid? do %>
<button
type="submit"
form="event-form"
phx_disable_with="Loading..."
class="btn btn-primary w-full"
>
{case @action do
:edit -> gettext("Save")
:new -> gettext("Create")
end}
</button>
<% else %>
<div class="btn btn-primary btn-disabled opacity-25 cursor-default w-full">
{case @action do
:edit -> gettext("Save")
:new -> gettext("Create")
end}
</div>
<% end %>
<%= if @action == :edit && !@event.expired_at do %>
{link(gettext("Delete"),
</section>
</.form>
<section
:if={@action == :edit && !@event.expired_at}
id="event-danger-zone"
class="space-y-3"
aria-labelledby="danger-zone-heading"
>
<div class="space-y-3">
<div>
<h2 id="danger-zone-heading" class="text-base font-bold text-error">
{gettext("Delete event")}
</h2>
<p class="mt-1 text-xs leading-5 text-base-content/60">
{gettext("Permanently delete this event. This action cannot be undone.")}
</p>
</div>
{link(gettext("Delete event"),
to: "#",
phx_click: "delete",
phx_value_id: @event.uuid,
data: [confirm: gettext("Are you sure?")],
class: "btn btn-error w-full"
data: [confirm: gettext("Delete this event? This action cannot be undone.")],
class: "btn btn-outline btn-error w-full"
)}
<% end %>
</div>
<% end %>
</div>
</section>
</div>
<footer class={[
"shrink-0 border-t border-base-300 bg-base-100 px-4 py-4 sm:px-6",
@container != :drawer && "rounded-b-lg"
]}>
<button
type="submit"
form="event-form"
disabled={!can_submit}
phx-disable-with={
if @action == :edit, do: gettext("Saving..."), else: gettext("Creating...")
}
class="btn btn-primary w-full disabled:cursor-not-allowed disabled:opacity-40"
>
{if @action == :edit, do: gettext("Save changes"), else: gettext("Create event")}
</button>
</footer>
</div>

View File

@@ -165,7 +165,7 @@ defmodule ClaperWeb.EventLive.Index do
{:ok, socket |> assign(:event, event)}
socket
|> assign(:page_title, gettext("Edit"))
|> assign(:page_title, gettext("Edit event"))
|> assign(:event, event)
end
rescue
@@ -179,7 +179,7 @@ defmodule ClaperWeb.EventLive.Index do
code = for _ <- 1..5, into: "", do: <<Enum.random(~c"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")>>
socket
|> assign(:page_title, gettext("New presentation"))
|> assign(:page_title, gettext("Create event"))
|> assign(:event, %Event{
started_at: NaiveDateTime.utc_now(),
code: code,

View File

@@ -284,12 +284,16 @@
<!-- Sliding Drawer Panel -->
<div
id="drawer-panel"
class={"fixed top-0 right-0 h-full w-full lg:w-[480px] bg-gray-100 shadow-xl z-50 transform transition-transform duration-300 ease-in-out overflow-hidden #{if @live_action in [:new, :edit], do: "translate-x-0", else: "translate-x-full"}"}
role={if @live_action in [:new, :edit], do: "dialog"}
aria-modal={if @live_action in [:new, :edit], do: "true"}
aria-labelledby={if @live_action in [:new, :edit], do: "event-editor-title"}
aria-hidden={@live_action not in [:new, :edit]}
class={"fixed top-0 right-0 h-full w-full sm:w-[520px] bg-base-100 shadow-xl z-50 transform transition-transform duration-300 ease-in-out overflow-hidden #{if @live_action in [:new, :edit], do: "translate-x-0", else: "translate-x-full"}"}
phx-window-keydown={if @live_action in [:new, :edit], do: JS.patch(~p"/events")}
phx-key="Escape"
>
<%= if @live_action in [:new, :edit] do %>
<div class="p-6 flex flex-col h-full">
<div class="flex flex-col h-full">
<.live_component
module={ClaperWeb.EventLive.EventFormComponent}
id="event-create"

View File

@@ -19,21 +19,23 @@
{hidden_input(f, :action, name: "action", value: "update_profile")}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<ClaperWeb.Component.Input.text
<.text_field
form={f}
key={:first_name}
name={gettext("First name")}
required="true"
label={gettext("First name")}
required
maxlength="160"
autofocus="true"
autofocus
autocomplete="given-name"
/>
<ClaperWeb.Component.Input.text
<.text_field
form={f}
key={:last_name}
name={gettext("Last name")}
required="true"
label={gettext("Last name")}
required
maxlength="160"
autocomplete="family-name"
/>
</div>
@@ -59,11 +61,13 @@
<.form :let={f} for={@email_changeset} phx-submit="save" id="update_email" class="mt-5">
{hidden_input(f, :action, name: "action", value: "update_email")}
<ClaperWeb.Component.Input.email
<.text_field
form={f}
key={:email}
name={gettext("Email")}
required="true"
type="email"
label={gettext("Email")}
required
autocomplete="email"
/>
{submit(gettext("Save"),
@@ -95,17 +99,21 @@
{hidden_input(f, :action, name: "action", value: "update_password")}
<div class="md:flex md:items-end gap-x-2">
<ClaperWeb.Component.Input.password
<.text_field
form={f}
key={:current_password}
name={gettext("Current password")}
required="true"
type="password"
label={gettext("Current password")}
required
autocomplete="current-password"
/>
<ClaperWeb.Component.Input.password
<.text_field
form={f}
key={:password}
name={gettext("New password")}
required="true"
type="password"
label={gettext("New password")}
required
autocomplete="new-password"
/>
</div>
@@ -138,18 +146,22 @@
{hidden_input(f, :action, name: "action", value: "set_password")}
<div class="md:flex md:items-end gap-x-2">
<ClaperWeb.Component.Input.password
<.text_field
form={f}
key={:password}
name={gettext("New password")}
required="true"
type="password"
label={gettext("New password")}
required
autocomplete="new-password"
/>
<ClaperWeb.Component.Input.password
<.text_field
form={f}
key={:password_confirmation}
name={gettext("Confirm password")}
required="true"
type="password"
label={gettext("Confirm password")}
required
autocomplete="new-password"
/>
</div>

View File

@@ -225,13 +225,27 @@ defmodule ClaperWeb.Component.Input do
~H"""
<div class="relative">
{label(@form, @key, @name, class: "block text-sm font-medium #{@labelClass}")}
{label(@form, @key, @name, class: "block text-sm font-bold text-base-content")}
<div class="mt-1 relative">
<img
class="icon absolute transition-all top-2.5 left-2 duration-100 h-6"
src="/images/icons/hashtag.svg"
alt="code"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="pointer-events-none absolute left-4 top-3 z-10 h-6 w-6 text-gray-500"
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 9l14 0" />
<path d="M5 15l14 0" />
<path d="M11 4l-4 16" />
<path d="M17 4l-4 16" />
</svg>
{text_input(@form, @key,
required: @required,
readonly: @readonly,
@@ -241,7 +255,7 @@ defmodule ClaperWeb.Component.Input do
minlength: 5,
maxlength: 10,
class:
"#{@fieldClass} read-only:opacity-50 outline-hidden shadow-base focus:ring-primary-500 focus:border-primary-500 block w-full text-lg border-gray-300 rounded-md py-2 pr-3 pl-9 uppercase"
"input h-12 w-full bg-white pl-12 pr-4 uppercase read-only:opacity-50 #{if Keyword.has_key?(@form.errors, @key), do: "input-error", else: ""}"
)}
</div>
<%= if Keyword.has_key?(@form.errors, @key) do %>
@@ -264,7 +278,7 @@ defmodule ClaperWeb.Component.Input do
~H"""
<div>
<div class="relative" id="date" phx-hook="Pickr">
{label(@form, @key, @name, class: "block text-sm font-medium #{@labelClass}")}
{label(@form, @key, @name, class: "block text-sm font-bold text-base-content")}
<div class="mt-1 relative">
{hidden_input(@form, @key)}
{text_input(@form, :local_date,
@@ -272,7 +286,7 @@ defmodule ClaperWeb.Component.Input do
placeholder: @placeholder,
autocomplete: false,
class:
"#{@fieldClass} 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 read-only:opacity-50"
"input h-12 w-full bg-white px-4 read-only:opacity-50 #{if Keyword.has_key?(@form.errors, @key), do: "input-error", else: ""}"
)}
</div>