Files
Claper/lib/claper_web/live/event_live/show.html.heex
2026-08-05 09:05:24 +07:00

625 lines
24 KiB
Plaintext

<%= if @started || @is_leader do %>
<% focus_content? =
not is_nil(@current_slide_url) ||
match?(%Claper.Polls.Poll{}, @current_interaction) ||
match?(%Claper.Forms.Form{}, @current_interaction) ||
match?(%Claper.Quizzes.Quiz{}, @current_interaction) ||
match?(%Claper.Embeds.Embed{attendee_visibility: true}, @current_interaction) %>
<% interaction_mode? =
match?(%Claper.Polls.Poll{}, @current_interaction) ||
match?(%Claper.Forms.Form{}, @current_interaction) ||
match?(%Claper.Quizzes.Quiz{}, @current_interaction) ||
match?(%Claper.Embeds.Embed{attendee_visibility: true}, @current_interaction) %>
<% captions_visible? =
@transcription_config && @transcription_config.enabled &&
@transcription_config.visibility in ["both", "attendee"] %>
<div class="h-[100dvh] overflow-hidden bg-black lg:bg-primary-700">
<main
id="attendee-room"
class={[
"relative mx-auto grid h-[100dvh] w-full max-w-lg overflow-hidden bg-black font-display shadow-2xl",
focus_content? && captions_visible? &&
"grid-rows-[auto_auto_auto_minmax(0,1fr)]",
focus_content? != captions_visible? && "grid-rows-[auto_auto_minmax(0,1fr)]",
!focus_content? && !captions_visible? && "grid-rows-[auto_minmax(0,1fr)]"
]}
>
<div
id="side-menu-shadow"
phx-click={toggle_side_menu()}
class="fixed inset-0 z-50 hidden bg-black/70"
>
</div>
<aside
id="side-menu"
class="fixed inset-y-0 left-0 z-[60] hidden w-64 flex-col rounded-r-2xl bg-white px-4 text-gray-900 shadow-2xl animate__faster"
>
<img src="/images/logo-large-black.svg" class="my-3 h-16" alt="Claper" />
<span class="text-xl font-bold">{@event.name}</span>
<a
class="mt-5 flex items-center rounded-lg bg-gray-200 px-3 py-2"
href={~p"/?disconnected_from=#{@event.uuid}"}
>
<img src="/images/icons/exit-outline.svg" class="mr-3 h-5" />
<span>{gettext("Leave")}</span>
</a>
</aside>
<div
id="connection-status"
class="pointer-events-none absolute inset-x-4 top-16 z-50 hidden items-center justify-center rounded-full bg-supporting-yellow-100 px-4 py-2 text-sm font-semibold text-supporting-yellow-900 shadow-lg"
>
{gettext("Reconnecting...")}
</div>
<div
id="identity-menu"
phx-click-away={JS.hide(to: "#identity-menu")}
class="absolute bottom-16 left-2 z-40 hidden w-64 rounded-2xl border border-white/10 bg-gray-900 p-2 text-white shadow-2xl"
>
<p class="px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-wide text-gray-400">
{gettext("Post as")}
</p>
<button
:if={@state.anonymous_chat_enabled}
id="setAnonymous"
type="button"
phx-click={JS.push("set-nickname") |> JS.hide(to: "#identity-menu")}
phx-value-nickname=""
phx-hook="EmptyNickname"
data-storage-key={"nickname:#{@event.uuid}"}
class="flex min-h-11 w-full items-center gap-3 rounded-xl px-3 py-2 text-left hover:bg-white/10"
>
<span class="grid h-8 w-8 place-items-center rounded-full bg-gray-700">?</span>
<span class="font-semibold">{gettext("Anonymous")}</span>
</button>
<button
:if={!@state.anonymous_chat_enabled}
type="button"
disabled
class="flex min-h-11 w-full cursor-not-allowed items-center gap-3 rounded-xl px-3 py-2 text-left opacity-40"
>
<span class="grid h-8 w-8 place-items-center rounded-full bg-gray-700">?</span>
<span class="font-semibold">{gettext("Anonymous")} ({gettext("disabled")})</span>
</button>
<button
id="nicknamepicker"
type="button"
data-prompt={gettext("Enter your name")}
data-invalid={gettext("Nickname must be between 2 and 20 characters")}
data-close={JS.hide(to: "#identity-menu")}
data-storage-key={"nickname:#{@event.uuid}"}
phx-hook="NicknamePicker"
class="flex min-h-11 w-full items-center gap-3 rounded-xl px-3 py-2 text-left hover:bg-white/10"
>
<span class="grid h-8 w-8 place-items-center rounded-full bg-primary-500">
{if @nickname in [nil, ""], do: "A", else: String.first(@nickname)}
</span>
<span class="min-w-0">
<span class="block text-xs text-gray-400">{gettext("Nickname")}</span>
<span class="block truncate font-semibold">
{if @nickname in [nil, ""], do: gettext("Set your nickname"), else: @nickname}
</span>
</span>
</button>
</div>
<header
id="room-topbar"
class="relative z-30 bg-black px-4 pb-3 pt-[max(0.75rem,env(safe-area-inset-top))]"
>
<div id="banner" class="hidden w-full pb-2 text-center" phx-hook="EmbeddedBanner">
<a href="https://claper.co" target="_blank" class="text-xs text-gray-400">
{gettext("Create your next presentation with")} <span class="underline">Claper</span>
</a>
</div>
<div class="flex items-center justify-between gap-3">
<button
type="button"
phx-click={toggle_side_menu()}
class="inline-flex min-h-11 items-center gap-2 rounded-full bg-primary-500 px-4 text-sm font-bold uppercase text-white shadow-lg shadow-primary-900/30"
>
<img src="/images/icons/menu-outline.svg" class="h-5 w-5" />
<span>#{@event.code}</span>
</button>
<div class="ml-auto inline-flex min-h-11 items-center gap-2 text-sm font-semibold text-white">
<img src="/images/icons/online-users.svg" class="h-5 w-5" />
<span id="counter" phx-update="ignore" phx-hook="UpdateAttendees">
{@attendees_nb}
</span>
</div>
</div>
</header>
<section
:if={focus_content?}
id="focus-slot"
phx-hook="AttendeeFocus"
data-focus-key={focus_key(@current_interaction, @state.position)}
data-collapse-key={"focus-slot:#{@event.uuid}"}
data-interaction-mode={to_string(interaction_mode?)}
class={[
"relative z-10 min-h-0 overflow-hidden border-y border-white/10 bg-gray-950 transition-[height] duration-300",
interaction_mode? && "h-auto max-h-[40dvh]",
!interaction_mode? && "h-[40dvh] min-h-40 max-h-[26rem]"
]}
>
<div
id="new-interaction-badge"
class="pointer-events-none absolute left-1/2 top-3 z-30 hidden -translate-x-1/2 rounded-full bg-primary-500 px-3 py-1 text-xs font-bold text-white shadow-lg"
>
{gettext("New interaction")}
</div>
<%= case @current_interaction do %>
<% %Claper.Polls.Poll{} -> %>
<div class="max-h-[40dvh] overflow-y-auto overscroll-contain p-3">
<.live_component
module={ClaperWeb.EventLive.PollComponent}
id={"#{@current_interaction.id}-poll"}
poll={@current_interaction}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
selected_poll_opt={@selected_poll_opt}
current_poll_vote={@current_poll_vote}
show_results={@current_interaction.show_results}
focus_mode
/>
</div>
<% %Claper.Forms.Form{} -> %>
<div class="max-h-[40dvh] overflow-y-auto overscroll-contain p-3">
<.live_component
module={ClaperWeb.EventLive.FormComponent}
id={"#{@current_interaction.id}-form"}
form={@current_interaction}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
current_form_submit={@current_form_submit}
focus_mode
/>
</div>
<% %Claper.Quizzes.Quiz{} -> %>
<div class="max-h-[40dvh] overflow-y-auto overscroll-contain p-3">
<.live_component
module={ClaperWeb.EventLive.QuizComponent}
id={"#{@current_interaction.id}-quiz"}
quiz={@current_interaction}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
selected_quiz_question_opts={@selected_quiz_question_opts}
current_quiz_question_idx={@current_quiz_question_idx}
current_quiz_responses={@current_quiz_responses}
quiz_score={@quiz_score}
focus_mode
/>
</div>
<% %Claper.Embeds.Embed{attendee_visibility: true} -> %>
<div class="max-h-[40dvh] overflow-y-auto overscroll-contain p-3">
<.live_component
id={"#{@current_interaction.id}-embed"}
module={ClaperWeb.EventLive.EmbedComponent}
embed={@current_interaction}
focus_mode
/>
</div>
<% _ -> %>
<div id="focus-media" class="relative h-full w-full bg-black">
<img
src={@current_slide_url}
alt={gettext("Current presentation slide")}
class="h-full w-full object-contain"
/>
</div>
<% end %>
<button
:if={focus_content? && !interaction_mode?}
type="button"
data-focus-fullscreen
aria-label={gettext("Open fullscreen")}
class="absolute right-3 top-3 z-20 grid h-11 w-11 place-items-center rounded-full bg-black/65 text-white backdrop-blur"
>
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 3H3v5M16 3h5v5M8 21H3v-5m13 5h5v-5"
/>
</svg>
</button>
<button
:if={focus_content? && !interaction_mode?}
type="button"
data-focus-collapse
aria-label={gettext("Hide presentation")}
class="absolute right-16 top-3 z-20 grid h-11 w-11 place-items-center rounded-full bg-black/65 text-white backdrop-blur"
>
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m6 15 6-6 6 6" />
</svg>
</button>
<div
:if={focus_content? && !interaction_mode?}
data-focus-collapsed-bar
class="hidden h-full items-center px-4"
>
<button
type="button"
data-focus-show
class="flex min-h-11 w-full items-center justify-between text-sm font-semibold text-white"
>
<span>{gettext("Show presentation")}</span>
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" />
</svg>
</button>
</div>
</section>
<section
:if={captions_visible?}
id="caption-panel"
phx-hook="AttendeeCaptions"
data-collapse-key={"caption-panel:#{@event.uuid}"}
class="relative z-10 min-h-12 max-h-32 overflow-hidden border-b border-white/10 bg-gray-950 transition-[height] duration-300"
>
<div
data-caption-content
class="flex min-h-12 max-h-32 items-start gap-3 overflow-y-auto px-4 py-3 pr-16"
>
<span class="rounded bg-white/10 px-2 py-1 text-xs font-black text-white">CC</span>
<p
data-caption-text
class={[
"py-0.5 text-sm font-medium leading-relaxed text-white",
@transcription_text == "" && "animate-pulse italic opacity-50"
]}
>
{if @transcription_text == "",
do: gettext("Waiting for captions"),
else: @transcription_text}
</p>
</div>
<button
type="button"
data-caption-collapse
aria-label={gettext("Hide captions")}
class="absolute right-3 top-1/2 grid h-10 w-10 -translate-y-1/2 place-items-center rounded-full bg-black/65 text-white backdrop-blur"
>
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m6 15 6-6 6 6" />
</svg>
</button>
<div data-caption-collapsed-bar class="hidden h-full items-center px-4">
<button
type="button"
data-caption-show
class="flex min-h-11 w-full items-center justify-between text-sm font-semibold text-white"
>
<span>{gettext("Show captions")}</span>
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" />
</svg>
</button>
</div>
</section>
<section id="chat-panel" class="relative min-h-0 overflow-visible bg-black">
<div
id="chat-feed"
class={[
"h-full space-y-3 overflow-y-auto overscroll-contain px-4 pt-4",
@state.message_reaction_enabled && "pb-40",
!@state.message_reaction_enabled && "pb-24"
]}
data-room-reactions={to_string(@state.message_reaction_enabled)}
phx-update="stream"
phx-hook="RoomFeed"
data-chip="#new-messages-chip"
data-posts-nb={@post_count}
>
<.live_component
:for={{id, post} <- @streams.posts}
module={ClaperWeb.EventLive.PostComponent}
id={id}
post={post}
leaders={@leaders}
is_leader={@is_leader}
current_user={@current_user}
attendee_identifier={@attendee_identifier}
event={@event}
reaction_enabled={@state.message_reaction_enabled}
liked_posts={@like_posts}
loved_posts={@love_posts}
loled_posts={@lol_posts}
/>
</div>
<div
:if={@post_count == 0 && @state.chat_enabled}
class="pointer-events-none absolute inset-0 flex items-center justify-center px-8 text-center text-sm text-gray-500"
>
{gettext("Be the first to ask a question or share a thought.")}
</div>
<button
id="new-messages-chip"
type="button"
class="absolute bottom-20 left-1/2 z-20 hidden -translate-x-1/2 rounded-full bg-primary-500 px-4 py-2 text-xs font-bold text-white shadow-xl"
>
<span data-unread-count></span> {gettext("new messages")}&nbsp;↓
</button>
<div
id="reacts"
phx-hook="GlobalReacts"
data-class-name="h-12 w-12"
class="pointer-events-none absolute inset-0 z-10 overflow-hidden"
phx-update="ignore"
>
</div>
<div
:if={@state.message_reaction_enabled}
id="room-reaction-fab"
phx-hook="RoomReactionFab"
class="absolute bottom-20 left-4 z-20 flex w-14 flex-col items-center gap-2"
>
<div
data-reaction-picker
role="menu"
class="hidden flex-col-reverse gap-2 rounded-full bg-gray-900/95 p-2 shadow-2xl"
>
<button
type="button"
data-reaction="heart"
role="menuitem"
aria-label={gettext("Heart")}
class="grid h-11 w-11 place-items-center rounded-full hover:bg-white/10"
>
<img
src="/images/icons/heart.svg"
draggable="false"
class="pointer-events-none h-6 w-6 select-none"
/>
</button>
<button
type="button"
data-reaction="clap"
role="menuitem"
aria-label={gettext("Clap")}
class="grid h-11 w-11 place-items-center rounded-full hover:bg-white/10"
>
<img
src="/images/icons/clap.svg"
draggable="false"
class="pointer-events-none h-6 w-6 select-none"
/>
</button>
<button
type="button"
data-reaction="hundred"
role="menuitem"
aria-label={gettext("Hundred")}
class="grid h-11 w-11 place-items-center rounded-full hover:bg-white/10"
>
<img
src="/images/icons/hundred.svg"
draggable="false"
class="pointer-events-none h-6 w-6 select-none"
/>
</button>
<button
type="button"
data-reaction="raisehand"
role="menuitem"
aria-label={gettext("Raise hand")}
class="grid h-11 w-11 place-items-center rounded-full hover:bg-white/10"
>
<img
src="/images/icons/raisehand.svg"
draggable="false"
class="pointer-events-none h-6 w-6 select-none"
/>
</button>
</div>
<button
type="button"
data-reaction-trigger
aria-label={gettext("Send a live reaction")}
aria-haspopup="menu"
aria-expanded="false"
class="grid h-14 w-14 place-items-center rounded-full border-4 border-gray-900 bg-supporting-yellow-100 shadow-xl"
>
<img
data-reaction-icon
src="/images/icons/heart.svg"
draggable="false"
class="pointer-events-none h-7 w-7 select-none"
/>
</button>
</div>
</section>
<footer
id="room-composer"
class="absolute inset-x-0 bottom-0 z-30 bg-transparent px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2"
>
<%= if @state.chat_enabled do %>
<.form
:let={f}
for={@post_changeset}
id="post-form"
class="attendee-composer flex items-center gap-1.5 rounded-2xl border border-white/20 px-1.5 py-1 shadow-2xl"
phx-hook="PostForm"
data-nickname={@nickname}
data-storage-key={"post-draft:#{@event.uuid}"}
phx-submit="save"
>
<button
id="composer-identity-button"
type="button"
aria-label={gettext("Choose identity")}
phx-click={JS.toggle(to: "#identity-menu")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/60 text-xs font-bold text-white"
>
{if @nickname in [nil, ""], do: "A", else: String.first(@nickname)}
</button>
{textarea(f, :body,
id: "postFormTA",
disabled: !@state.anonymous_chat_enabled && @nickname in [nil, ""],
rows: 1,
maxlength: 255,
class:
"min-h-9 max-h-16 min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2 text-base leading-5 text-white outline-hidden placeholder:text-white/70 focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
placeholder:
if(!@state.anonymous_chat_enabled && @nickname in [nil, ""],
do: gettext("Set your nickname to participate"),
else: gettext("Ask, comment...")
)
)}
<button
type="submit"
id="submitBtn"
disabled
aria-label={gettext("Send message")}
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-black/20 text-white opacity-50 transition disabled:cursor-not-allowed"
>
<img src="/images/icons/send.svg" class="h-5 w-5" />
</button>
</.form>
<% else %>
<div class="attendee-composer flex min-h-10 items-center rounded-2xl border border-white/20 px-3 text-sm text-white/60 shadow-2xl">
{gettext("Messages deactivated")}
</div>
<% end %>
</footer>
</main>
</div>
<% else %>
<div class="ticket-stage">
<div class="ticket" id="waiting-ticket" phx-hook="TicketTilt">
<div class="texture"></div>
<div class="glare"></div>
<div class="ticket-body">
<span class="eyebrow">
<span class="dot"></span>{gettext("Live interactive event")}
</span>
<h1 class="event-title">{@event.name}</h1>
<p
class="event-date"
x-data={"{date: moment.utc('#{@event.started_at}').local().format('LLLL')}"}
x-text="date.charAt(0).toUpperCase() + date.slice(1)"
>
</p>
<div class="cd-row">
<div class="cd-cell">
<div class="cd-num">{if @remaining_days < 10, do: "0"}{@remaining_days}</div>
<div class="cd-label">{gettext("days")}</div>
</div>
<div class="cd-sep">:</div>
<div class="cd-cell">
<div class="cd-num">{if @remaining_hours < 10, do: "0"}{@remaining_hours}</div>
<div class="cd-label">{gettext("hours")}</div>
</div>
<div class="cd-sep">:</div>
<div class="cd-cell">
<div class="cd-num">{if @remaining_minutes < 10, do: "0"}{@remaining_minutes}</div>
<div class="cd-label">{gettext("minutes")}</div>
</div>
<div class="cd-sep">:</div>
<div class="cd-cell">
<div class="cd-num">{if @remaining_seconds < 10, do: "0"}{@remaining_seconds}</div>
<div class="cd-label">{gettext("seconds")}</div>
</div>
</div>
<div class="ticket-meta">
<div class="meta-item">
<span class="meta-k">{gettext("Admit")}</span>
<span class="meta-v">{gettext("Unlimited")}</span>
</div>
<div class="meta-item">
<span class="meta-k">{gettext("Session")}</span>
<span class="meta-v">{@event.code}</span>
</div>
<div class="meta-item">
<span class="meta-k">{gettext("No.")}</span>
<span class="meta-v">CLP-{Calendar.strftime(@event.started_at, "%m%d-%y")}</span>
</div>
</div>
</div>
<div class="perf">
<div class="notch-bottom"></div>
</div>
<div class="ticket-stub">
<div class="stub-head">{gettext("Scan to interact in real-time")}</div>
<div class="qr-frame">
<div
phx-hook="QRCode"
data-code={@event.code}
data-size="188"
id="qr"
phx-update="ignore"
>
</div>
</div>
<div class="code-label">{gettext("Or use the code:")}</div>
<div class="code-chip">{@event.code}</div>
</div>
</div>
<a href="https://claper.co" target="_blank" class="powered-by">
<span>{gettext("Powered by")}</span>
<img src="/images/logo-large.svg" alt="Claper" />
</a>
</div>
<% end %>