mirror of
https://github.com/ClaperCo/Claper.git
synced 2026-08-29 10:09:08 +02:00
Redesign event form UI and file upload flow
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user