Files
ladill-meet/resources/views/meet/room/show.blade.php
T
isaaccladandCursor 712dc487c6
Deploy Ladill Meet / deploy (push) Successful in 34s
Fix green room Go live and Panels hidden for conference hosts.
Stop registerPresenter from demoting hosts to panelists, recognize room owners as managers, and show stage controls in the green room banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 22:54:15 +00:00

666 lines
50 KiB
PHP

<!DOCTYPE html>
<html lang="en" class="h-full meet-room-page">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $room->title }} · Ladill Meet</title>
@include('partials.favicon')
@vite(['resources/css/app.css', 'resources/js/meet-room.js'])
</head>
<body class="h-full overflow-hidden bg-slate-950 font-sans text-white" x-data="meetRoom()" x-init="init()">
@php
use App\Models\User;
use App\Services\Meet\ParticipantPresenter;
$joined = $session->participants->whereIn('status', ['joined', 'waiting']);
$participantAvatars = ParticipantPresenter::avatarMap($joined, $room->host_user_ref);
$joinedParticipants = $joined
->map(fn ($p) => ParticipantPresenter::toArray($p, $participantAvatars, $room->host_user_ref))
->values();
$roomHostUser = User::query()->where('public_id', $room->host_user_ref)->first();
$roomHost = [
'user_ref' => $room->host_user_ref,
'display_name' => $roomHostUser?->name ?? 'Host',
'avatar_url' => $roomHostUser?->avatarUrl(),
];
$isConferenceSession = $isConference ?? false;
$sessionNoun = $isConferenceSession ? 'conference' : 'meeting';
$sessionNounTitle = $isConferenceSession ? 'Conference' : 'Meeting';
@endphp
<div id="meet-config"
data-token="{{ $mediaToken }}"
data-url="{{ $mediaUrl }}"
data-configured="{{ $mediaConfigured ? '1' : '0' }}"
data-participant="{{ $participant->uuid }}"
data-display-name="{{ $participant->display_name }}"
data-is-host="{{ ($canManageConference ?? $participant->isHost()) ? '1' : '0' }}"
data-poll-url="{{ route('meet.room.poll', $session) }}"
data-ended-url="{{ route('meet.ended', $session) }}"
data-chat-url="{{ route('meet.room.chat', $session) }}"
data-reaction-url="{{ route('meet.room.reaction', $session) }}"
data-raise-hand-url="{{ route('meet.room.raise-hand', $session) }}"
data-leave-url="{{ route('meet.room.leave', $session) }}"
data-end-url="{{ route('meet.room.end', $session) }}"
data-recording-start-url="{{ route('meet.room.recording.start', $session) }}"
data-recording-stop-url="{{ route('meet.room.recording.stop', $session) }}"
data-lock-url="{{ route('meet.room.lock', $session) }}"
data-unlock-url="{{ route('meet.room.unlock', $session) }}"
data-caption-url="{{ route('meet.room.caption', $session) }}"
data-live-captions="{{ $room->setting('live_captions') ? '1' : '0' }}"
data-mute-on-join="{{ $room->setting('mute_on_join', true) ? '1' : '0' }}"
data-video-off-on-join="{{ ($isAudioOnly ?? false) || $room->setting('video_off_on_join', false) ? '1' : '0' }}"
data-audio-only="{{ ($isAudioOnly ?? false) ? '1' : '0' }}"
data-is-space="{{ ($isSpace ?? false) ? '1' : '0' }}"
data-watermark="{{ $watermark ? '1' : '0' }}"
data-locked="{{ $session->is_locked ? '1' : '0' }}"
data-recording-active="{{ $activeRecording ? '1' : '0' }}"
data-is-webinar="{{ ($isWebinar ?? false) ? '1' : '0' }}"
data-is-conference="{{ ($isConference ?? false) ? '1' : '0' }}"
data-is-green-room="{{ ($isGreenRoom ?? false) ? '1' : '0' }}"
data-session-mode="{{ $session->session_mode }}"
data-go-live-url="{{ ($isConference ?? false) && ($isGreenRoom ?? false) && ($canManageConference ?? $participant->isHost()) ? route('meet.room.townhall.live', $session) : '' }}"
data-add-presenter-url="{{ ($isConference ?? false) && ($canManageConference ?? $participant->isHost()) ? route('meet.room.townhall.presenters', $session) : '' }}"
data-stage-update-url="{{ ($usesStageLayout ?? false) && ($canManageConference ?? $participant->isHost()) ? route('meet.room.stage.update', $session) : '' }}"
data-uses-stage-layout="{{ ($usesStageLayout ?? false) ? '1' : '0' }}"
data-panel-discussions="{{ ($stageConfig['panel_discussions'] ?? false) ? '1' : '0' }}"
data-stage-layout="{{ $stageConfig['stage_layout'] ?? 'plenary' }}"
data-panels='@json($stageConfig['panels'] ?? [])'
data-active-panel-id="{{ $stageConfig['active_panel_id'] ?? '' }}"
data-spotlight-speaker-ref="{{ $stageConfig['spotlight_speaker_ref'] ?? '' }}"
data-presenter-refs='@json($presenterRefs ?? [])'
data-can-publish="{{ ($canPublish ?? true) ? '1' : '0' }}"
data-qa-url="{{ route('meet.room.qa.submit', $session) }}"
data-polls-create-url="{{ route('meet.room.polls.create', $session) }}"
data-breakouts-create-url="{{ route('meet.room.breakouts.create', $session) }}"
data-breakouts-close-url="{{ route('meet.room.breakouts.close', $session) }}"
data-breakouts-broadcast-url="{{ route('meet.room.breakouts.broadcast', $session) }}"
data-files-upload-url="{{ route('meet.room.files.upload', $session) }}"
data-whiteboard-url="{{ route('meet.room.whiteboard.show', $session) }}"
data-whiteboard-save-url="{{ route('meet.room.whiteboard.save', $session) }}"
data-join-url="{{ $room->joinUrl() }}"
data-room-title="{{ $room->title }}"
data-passcode="{{ $room->passcode ?? '' }}"
data-csrf="{{ csrf_token() }}"
data-session-uuid="{{ $session->uuid }}"
data-initial-participants='@json($joinedParticipants)'
data-room-host='@json($roomHost)'
hidden></div>
@if (! $mediaConfigured)
<div class="absolute inset-x-0 top-0 z-50 bg-amber-500 px-4 py-2 text-center text-sm font-medium text-amber-950">
LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET in .env for video.
</div>
@endif
<div class="flex h-full min-w-0">
<div class="relative flex min-h-0 min-w-0 flex-1 flex-col">
<header class="flex shrink-0 items-center justify-between bg-slate-950 px-4 py-3">
<div class="min-w-0">
<h1 class="truncate text-sm font-semibold">{{ $room->title }}</h1>
@if ($isAudioOnly ?? false)
<p class="text-[10px] font-medium uppercase tracking-wide text-emerald-400/90">Audio room</p>
@elseif ($isConference ?? false)
<p class="text-[10px] font-medium uppercase tracking-wide"
:class="isGreenRoom ? 'text-violet-400/90' : 'text-sky-400/90'"
x-text="isGreenRoom ? 'Green room' : 'Conference live'"></p>
@endif
<p class="text-xs text-slate-400" x-text="connectionStatus"></p>
<p class="min-h-4 truncate text-xs font-medium leading-4"
:class="activeSpeakerCount() > 0 ? 'text-emerald-400' : 'text-slate-600'"
:title="activeSpeakersMessage()"
x-text="activeSpeakersMessage() || '...'"></p>
</div>
<div class="flex shrink-0 items-center gap-2 text-xs text-slate-400">
<button @click.stop="openShare()" data-share-trigger type="button"
class="inline-flex items-center gap-1.5 rounded-lg bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-800"
title="Share {{ $sessionNoun }} link">
@include('meet.room.partials.meet-icon', ['icon' => 'share', 'class' => 'h-4 w-4'])
Share
</button>
<span x-show="isRecording" @unless($activeRecording) style="display: none" @endunless class="rounded bg-red-600 px-2 py-0.5 font-medium text-white">REC</span>
<span x-show="isLocked" @unless($session->is_locked) style="display: none" @endunless class="rounded bg-amber-600 px-2 py-0.5 font-medium text-white">Locked</span>
<button @click="toggleParticipants()" type="button"
class="relative inline-flex items-center gap-2 rounded-lg bg-slate-900 px-2 py-1 text-xs font-medium text-slate-200 transition-colors hover:bg-slate-800"
:class="isHost && waitingCount > 0 ? 'ring-2 ring-amber-400/60' : ''"
:title="usesStageLayout ? 'View speakers' : 'View participants'">
<span x-show="isHost && waitingCount > 0"
x-text="waitingCount > 9 ? '9+' : waitingCount"
class="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] font-bold text-amber-950"></span>
<span x-show="hostParticipant()" class="relative flex h-7 w-7 shrink-0">
<img x-show="hostParticipant()?.avatar_url"
:src="hostParticipant()?.avatar_url"
alt=""
class="h-7 w-7 rounded-full object-cover">
<span x-show="hostParticipant() && !hostParticipant()?.avatar_url"
class="flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold text-white"
:style="avatarStyle(hostParticipant()?.display_name)"
x-text="participantInitials(hostParticipant()?.display_name || '')"></span>
</span>
<span x-text="usesStageLayout ? speakerCount() + (speakerCount() === 1 ? ' speaker' : ' speakers') : participantCount() + ' in call'"></span>
</button>
</div>
</header>
<div x-show="isConference && isGreenRoom" x-cloak
class="flex shrink-0 flex-col gap-2 border-b border-violet-500/30 bg-violet-500/15 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<div class="min-w-0 text-sm text-violet-100">
<p class="font-medium">Green room</p>
<p class="truncate text-xs text-violet-200/90">Only hosts and speakers are visible. Attendees join after you go live.</p>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<button x-show="isHost && usesStageLayout" @click="panelSetupOpen = true" type="button"
class="shrink-0 rounded-lg border border-violet-400/40 bg-violet-500/20 px-3 py-1.5 text-xs font-semibold text-violet-100 transition-colors hover:bg-violet-500/30">
@include('meet.room.partials.meet-icon', ['icon' => 'panel', 'class' => 'mr-1 inline h-3.5 w-3.5'])
Panels
</button>
<button x-show="isHost" @click="goLive()" type="button" :disabled="goingLive"
class="shrink-0 rounded-lg bg-violet-500 px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-violet-400 disabled:opacity-60">
<span x-text="goingLive ? 'Starting…' : 'Go live'">Go live</span>
</button>
</div>
</div>
<div x-show="isHost && usesStageLayout && panelDiscussions && !isGreenRoom" x-cloak
class="flex shrink-0 items-center justify-end gap-2 border-b border-violet-500/20 bg-violet-500/10 px-4 py-2">
<button @click="panelSetupOpen = true" type="button"
class="inline-flex items-center gap-1.5 rounded-lg border border-violet-400/40 bg-violet-500/20 px-3 py-1.5 text-xs font-semibold text-violet-100 hover:bg-violet-500/30">
@include('meet.room.partials.meet-icon', ['icon' => 'panel', 'class' => 'h-3.5 w-3.5'])
<span x-text="stageLayout === 'panel' ? 'Panel layout' : 'Plenary layout'"></span>
</button>
</div>
<div x-show="isHost && waitingCount > 0" x-cloak
class="flex shrink-0 items-center justify-between gap-3 border-b border-amber-500/30 bg-amber-500/15 px-4 py-2.5">
<div class="flex min-w-0 items-center gap-2 text-sm text-amber-100">
<span class="flex h-2 w-2 shrink-0 animate-pulse rounded-full bg-amber-400"></span>
<span class="truncate"
x-text="waitingCount === 1 ? '1 person is waiting to be admitted' : waitingCount + ' people are waiting to be admitted'"></span>
</div>
<button @click="participantsOpen = true" type="button"
class="shrink-0 rounded-lg bg-amber-500 px-3 py-1.5 text-xs font-semibold text-amber-950 transition-colors hover:bg-amber-400">
Review
</button>
</div>
<div x-show="audioPlaybackBlocked || needsMediaUnlock" x-cloak
class="flex shrink-0 items-center justify-between gap-3 border-b border-indigo-500/30 bg-indigo-500/15 px-4 py-2.5">
<p class="text-sm text-indigo-100"
x-text="needsMediaUnlock ? 'Microphone blocked — click to allow speaking and hearing others.' : 'Audio playback blocked — click to hear other participants.'"></p>
<button @click="unlockAllAudio()" type="button"
class="shrink-0 rounded-lg bg-indigo-500 px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-indigo-400">
Enable audio
</button>
</div>
<div x-show="shareOpen" x-cloak
@click.outside="if (! $event.target.closest('[data-share-trigger]')) shareOpen = false"
class="absolute right-4 top-14 z-50 w-80 rounded-xl bg-slate-900 p-4 shadow-2xl">
<div class="flex items-start justify-between gap-2">
<div>
<p class="text-sm font-medium text-white">Invite others</p>
<p class="mt-0.5 text-xs text-slate-400">Share this link so people can join the {{ $sessionNoun }}.</p>
</div>
<button @click="shareOpen = false" type="button" class="rounded p-1 text-slate-400 hover:bg-slate-800 hover:text-white" title="Close">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="mt-3 flex gap-2">
<input type="text" readonly :value="joinUrl"
class="min-w-0 flex-1 rounded-lg border-0 bg-slate-950 px-3 py-2 text-xs text-slate-200 ring-1 ring-slate-700/60">
<button @click="copyJoinLink()" type="button"
class="shrink-0 rounded-lg bg-indigo-600 px-3 py-2 text-xs font-medium text-white hover:bg-indigo-500"
x-text="shareCopied ? 'Copied' : 'Copy'"></button>
</div>
<p x-show="passcode" class="mt-3 text-xs text-slate-400">
Passcode: <span class="font-mono font-medium text-slate-200" x-text="passcode"></span>
</p>
</div>
<main class="relative flex min-h-0 min-w-0 flex-1 flex-col p-4 pb-2">
<div id="meet-stage" class="flex h-full min-h-0 flex-1 flex-col gap-3" :class="usesStageLayout ? 'meet-stage meet-stage--' + stageLayout : ''">
<div id="meet-stage-spotlight" class="meet-stage-spotlight relative min-h-0 flex-1 overflow-hidden rounded-xl bg-slate-900"></div>
<div id="video-grid" class="grid min-h-0 gap-2 sm:grid-cols-2 lg:grid-cols-3" :class="audioOnly ? 'meet-audio-grid' : (usesStageLayout ? 'meet-stage-panel-grid flex-1' : 'h-full')"></div>
<div id="meet-audience-strip" class="meet-audience-strip shrink-0"></div>
</div>
<div id="screen-share" class="absolute inset-4 hidden rounded-xl bg-black"></div>
<div x-show="watermark" class="pointer-events-none absolute right-6 top-6 rounded bg-black/40 px-3 py-1 text-xs text-white/80" x-text="displayName"></div>
<div x-show="liveCaptions && captionText" class="pointer-events-none absolute inset-x-0 bottom-20 flex justify-center px-4">
<p class="max-w-2xl rounded-lg bg-black/70 px-4 py-2 text-center text-sm text-white" x-text="captionText"></p>
</div>
<div class="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center gap-2">
<template x-for="reaction in floatingReactions" :key="reaction.id">
<span class="flex h-10 w-10 animate-bounce items-center justify-center rounded-full bg-slate-800/90">
<img x-show="reaction.kind === 'thumbs'" src="{{ asset('images/meet-icons/applause.svg') }}?v={{ @filemtime(public_path('images/meet-icons/applause.svg')) ?: '1' }}" alt="" class="meet-toolbar-icon h-5 w-5">
<img x-show="reaction.kind === 'heart'" src="{{ asset('images/meet-icons/react.svg') }}?v={{ @filemtime(public_path('images/meet-icons/react.svg')) ?: '1' }}" alt="" class="meet-toolbar-icon h-5 w-5">
</span>
</template>
</div>
</main>
<footer class="relative shrink-0 bg-slate-950 py-4 sm:px-4">
<div class="meet-toolbar mx-auto max-w-5xl">
<div class="meet-toolbar__track sm:px-0">
<div class="meet-toolbar__primary sm:contents">
<button @click="toggleMute()" type="button"
class="rounded-full p-3 transition-colors"
:class="isMuted ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
:title="isMuted ? 'Unmute' : 'Mute'">
<svg x-show="!isMuted" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 0 1 6 0v8.25a3 3 0 0 1-3 3Z"/></svg>
<svg x-show="isMuted" x-cloak class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 0 1 6 0v8.25a3 3 0 0 1-3 3Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/></svg>
</button>
<button @click="toggleVideo()" type="button"
x-show="!audioOnly"
class="rounded-full p-3 transition-colors"
:class="isVideoOff ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
:title="isVideoOff ? 'Turn camera on' : 'Turn camera off'">
<svg x-show="!isVideoOff" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"/></svg>
<svg x-show="isVideoOff" x-cloak class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/></svg>
</button>
<button @click="toggleScreenShare()" type="button"
x-show="!audioOnly"
class="rounded-full p-3 transition-colors"
:class="isScreenSharing ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
title="Share screen">
@include('meet.room.partials.meet-icon', ['icon' => 'screen-share'])
</button>
<button @click="toggleFeatures()" type="button"
class="rounded-full p-3 transition-colors sm:hidden"
:class="featuresOpen ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
title="More options">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
</button>
</div>
<button @click="raiseHand()" type="button" class="hidden rounded-full bg-slate-700 p-3 hover:bg-slate-600 sm:inline-flex" title="Raise hand">
@include('meet.room.partials.meet-icon', ['icon' => 'raise-hand'])
</button>
<button @click="sendReaction('thumbs')" type="button" class="hidden rounded-full bg-slate-700 p-3 hover:bg-slate-600 sm:inline-flex" title="Applause">
@include('meet.room.partials.meet-icon', ['icon' => 'applause'])
</button>
<button @click="sendReaction('heart')" type="button" class="hidden rounded-full bg-slate-700 p-3 hover:bg-slate-600 sm:inline-flex" title="React">
@include('meet.room.partials.meet-icon', ['icon' => 'react'])
</button>
<button @click="toggleChat()" type="button"
class="hidden rounded-full p-3 transition-colors sm:inline-flex"
:class="chatOpen ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
title="Chat">
@include('meet.room.partials.meet-icon', ['icon' => 'chat'])
</button>
<input type="file" id="meet-file-input" class="hidden" @change="uploadFile">
<button @click="toggleFeatures()" type="button"
class="hidden items-center gap-1.5 rounded-full px-3.5 py-3 text-xs font-medium transition-colors sm:inline-flex"
:class="featuresOpen ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
title="More options">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
More
</button>
<div class="meet-toolbar__divider sm:hidden" aria-hidden="true"></div>
<div class="meet-toolbar__actions sm:contents">
<form method="POST" action="{{ route('meet.room.leave', $session) }}" class="inline">
@csrf
<button type="submit"
class="rounded-full bg-red-600 p-3 font-medium hover:bg-red-500 sm:px-5 sm:py-3 sm:text-sm"
title="Leave {{ $sessionNoun }}"
aria-label="Leave {{ $sessionNoun }}">
<span class="sm:hidden">
@include('meet.room.partials.meet-icon', ['icon' => 'leave'])
</span>
<span class="hidden sm:inline">Leave</span>
</button>
</form>
@if ($participant->isHost())
<button @click="openEndMeetingConfirm()" type="button"
class="rounded-full bg-red-950 p-3 text-red-400 hover:bg-red-900 sm:px-4 sm:py-3 sm:text-sm"
title="End {{ $sessionNoun }} for everyone"
aria-label="End {{ $sessionNoun }} for everyone">
<span class="sm:hidden">
@include('meet.room.partials.meet-icon', ['icon' => 'end'])
</span>
<span class="hidden sm:inline">End</span>
</button>
<form id="meet-end-form" method="POST" action="{{ route('meet.room.end', $session) }}" class="hidden">
@csrf
</form>
@endif
</div>
</div>
</div>
</footer>
<div x-show="breakoutModalOpen" x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
@keydown.escape.window="breakoutModalOpen = false">
<div class="w-full max-w-sm rounded-2xl bg-slate-900 p-5 shadow-2xl" @click.outside="breakoutModalOpen = false">
<h3 class="text-base font-semibold text-white">Start breakout rooms</h3>
<p class="mt-1 text-sm text-slate-400">How many breakout rooms should be created?</p>
<form @submit.prevent="submitBreakouts()" class="mt-4 space-y-4">
<div>
<label for="breakout-count" class="sr-only">Number of rooms</label>
<input id="breakout-count" type="number" min="1" max="20" x-model="breakoutCount"
class="w-full rounded-xl border-0 bg-slate-950 px-3 py-2.5 text-sm text-white ring-1 ring-slate-700/60 focus:ring-indigo-500">
</div>
<div class="flex justify-end gap-2">
<button type="button" @click="breakoutModalOpen = false"
class="rounded-xl px-4 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800">Cancel</button>
<button type="submit"
class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500">Create rooms</button>
</div>
</form>
</div>
</div>
<div x-show="endMeetingConfirmOpen" x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
@keydown.escape.window="endMeetingConfirmOpen = false">
<div class="w-full max-w-sm rounded-2xl bg-slate-900 p-5 shadow-2xl" @click.outside="endMeetingConfirmOpen = false">
<h3 class="text-base font-semibold text-white">End {{ $sessionNoun }} for everyone?</h3>
<p class="mt-2 text-sm text-slate-400">All participants will be disconnected and the session will close.</p>
<div class="mt-5 flex justify-end gap-2">
<button type="button" @click="endMeetingConfirmOpen = false"
class="rounded-xl px-4 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800">Cancel</button>
<button type="button" @click="submitEndMeeting()"
class="rounded-xl bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500">End {{ $sessionNoun }}</button>
</div>
</div>
</div>
</div>
<x-meet.room.panel show="chatOpen" title="{{ $sessionNounTitle }} chat">
<x-slot:subtitle>
<p>Messages are visible to everyone in the call</p>
</x-slot:subtitle>
<div class="flex min-h-0 flex-1 flex-col">
<div class="flex-1 space-y-3 overflow-y-auto" id="chat-messages">
@foreach ($messages as $message)
@php $isOwn = $message->sender_name === $participant->display_name; @endphp
<div class="meet-chat-bubble {{ $isOwn ? 'meet-chat-bubble--own' : '' }}">
<div class="rounded-2xl px-3 py-2 {{ $isOwn ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-100' }}">
@unless ($isOwn)
<p class="mb-0.5 text-xs font-medium text-sky-300">{{ $message->sender_name }}</p>
@endunless
<p class="text-sm leading-relaxed">{{ $message->body }}</p>
</div>
<p class="mt-1 text-[10px] text-slate-500 {{ $isOwn ? 'text-right' : '' }}">{{ $message->created_at->format('g:i A') }}</p>
</div>
@endforeach
</div>
<form @submit.prevent="sendChat" class="mt-3 shrink-0">
<div class="flex items-end gap-2">
<input type="text" x-model="chatInput" placeholder="Write a message…"
class="flex-1 rounded-xl border-0 bg-slate-800 px-3 py-2.5 text-sm text-white placeholder:text-slate-500 focus:ring-2 focus:ring-indigo-500">
<button type="submit"
class="rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-500 disabled:opacity-50"
:disabled="!chatInput.trim()">Send</button>
</div>
</form>
</div>
</x-meet.room.panel>
<x-meet.room.panel show="participantsOpen" :title="null">
<x-slot:subtitle>
<template x-if="usesStageLayout">
<div>
<p x-text="speakerCount() + (speakerCount() === 1 ? ' speaker' : ' speakers') + ' on stage'"></p>
<p x-show="activeSpeakerCount() >= 1" x-cloak class="text-emerald-400" x-text="activeSpeakersMessage()"></p>
</div>
</template>
<template x-if="!usesStageLayout">
<div>
<p x-text="participantCount() + (participantCount() === 1 ? ' person' : ' people') + ' in this call'"></p>
<p x-show="activeSpeakerCount() >= 2" x-cloak class="text-emerald-400" x-text="activeSpeakersMessage()"></p>
</div>
</template>
</x-slot:subtitle>
<x-slot:header>
<h2 class="text-base font-semibold text-white" x-text="usesStageLayout ? 'Speakers' : 'Participants'"></h2>
</x-slot:header>
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
<template x-if="isHost && waitingParticipants().length">
<div class="mb-3 border-b border-slate-800 pb-3">
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-amber-300">
Waiting to join
<span class="ml-1 rounded-full bg-amber-500/20 px-1.5 py-0.5 text-[10px] text-amber-200"
x-text="waitingParticipants().length"></span>
</p>
<template x-for="person in waitingParticipants()" :key="'wait-'+person.uuid">
<div class="flex items-center gap-3 rounded-xl px-2 py-2.5">
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20 text-sm font-semibold text-amber-200"
x-text="participantInitials(person.display_name)"></span>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-medium text-white" x-text="person.display_name"></span>
<span class="block text-xs text-amber-200/80">Waiting for admission</span>
</span>
<span class="flex shrink-0 gap-1">
<button type="button" @click="admitParticipant(person.uuid)"
class="rounded-lg bg-emerald-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-emerald-500">Admit</button>
<button type="button" @click="denyParticipant(person.uuid)"
class="rounded-lg bg-slate-700 px-2.5 py-1 text-xs font-medium text-slate-200 hover:bg-slate-600">Deny</button>
</span>
</div>
</template>
</div>
</template>
<template x-for="person in (usesStageLayout ? speakerParticipantsSorted() : inCallParticipantsSorted())" :key="person.uuid">
<div class="flex items-center gap-3 rounded-xl px-2 py-2.5"
:class="isSpeaking(person.uuid) ? 'bg-emerald-500/10 ring-1 ring-emerald-500/40' : ''">
<span class="relative flex h-10 w-10 shrink-0">
<img x-show="person.avatar_url"
:src="person.avatar_url"
alt=""
class="h-10 w-10 rounded-full object-cover"
:class="isSpeaking(person.uuid) ? 'ring-2 ring-emerald-400 ring-offset-2 ring-offset-slate-900' : ''">
<span x-show="!person.avatar_url"
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-semibold text-white"
:class="isSpeaking(person.uuid) ? 'ring-2 ring-emerald-400 ring-offset-2 ring-offset-slate-900' : ''"
:style="avatarStyle(person.display_name)"
x-text="participantInitials(person.display_name)"></span>
</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-medium text-white" x-text="person.display_name"></span>
<span x-show="isSpeaking(person.uuid)" class="block text-xs font-medium text-emerald-400">Speaking</span>
<span x-show="!isSpeaking(person.uuid)" class="block text-xs text-slate-400" x-text="roleLabel(person.role)"></span>
</span>
<span class="flex shrink-0 items-center gap-1">
<button x-show="usesStageLayout && isHost && person.user_ref && spotlightSpeakerRef !== person.user_ref"
type="button" @click="setSpotlight(person.user_ref)"
class="rounded-lg bg-indigo-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-indigo-500">
Spotlight
</button>
<button x-show="isConference && isHost && isGreenRoom && person.user_ref && !['host', 'co_host', 'panelist'].includes(person.role)"
type="button" @click="makeSpeaker(person.user_ref)"
class="rounded-lg bg-violet-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-violet-500">
Make speaker
</button>
</span>
<span class="flex shrink-0 items-center gap-1 text-slate-400">
<span x-show="isSpeaking(person.uuid)" class="meet-speaking-badge rounded-full bg-emerald-500/20 p-1 text-emerald-300" title="Speaking">
<svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect class="meet-speaking-bar meet-speaking-bar--1" x="1" y="5" width="2.5" height="6" rx="1"/>
<rect class="meet-speaking-bar meet-speaking-bar--2" x="6.75" y="3" width="2.5" height="10" rx="1"/>
<rect class="meet-speaking-bar meet-speaking-bar--3" x="12.5" y="6" width="2.5" height="4" rx="1"/>
</svg>
</span>
<span x-show="person.hand_raised" class="rounded-full bg-amber-500/20 p-1 text-amber-300" title="Hand raised">
@include('meet.room.partials.meet-icon', ['icon' => 'raise-hand', 'class' => 'h-3.5 w-3.5'])
</span>
<svg x-show="person.is_muted" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" title="Muted">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 0 1 6 0v8.25a3 3 0 0 1-3 3Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/>
</svg>
<svg x-show="person.is_video_off" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" title="Camera off">
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/>
</svg>
</span>
</div>
</template>
<p x-show="(usesStageLayout ? speakerParticipantsSorted().length : inCallParticipants().length) === 0 && waitingParticipants().length === 0" class="px-2 py-4 text-center text-xs text-slate-500" x-text="usesStageLayout ? 'No speakers yet.' : 'No participants yet.'"></p>
</div>
</x-meet.room.panel>
<div x-show="panelSetupOpen" x-cloak
class="fixed inset-0 z-50 flex items-end justify-center bg-black/60 p-4 backdrop-blur-sm sm:items-center"
@keydown.escape.window="panelSetupOpen = false">
<div class="w-full max-w-lg rounded-2xl bg-slate-900 p-5 shadow-2xl" @click.outside="panelSetupOpen = false">
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="text-base font-semibold text-white">Stage & panels</h3>
<p class="mt-1 text-sm text-slate-400">Switch between plenary and panel layouts. Assign speakers to each panel before going live.</p>
</div>
<button @click="panelSetupOpen = false" type="button" class="rounded-lg p-1 text-slate-400 hover:bg-slate-800 hover:text-white">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="mt-5 flex gap-2">
<button type="button" @click="stageLayout = 'plenary'; applyStageLayout()"
class="flex-1 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors"
:class="stageLayout === 'plenary' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300 hover:bg-slate-700'">
Plenary
</button>
<button type="button" @click="panelDiscussions = true; stageLayout = 'panel'; applyStageLayout()"
class="flex-1 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors"
:class="stageLayout === 'panel' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300 hover:bg-slate-700'">
Panel discussion
</button>
</div>
<div x-show="stageLayout === 'panel'" x-cloak class="mt-5 space-y-3">
<template x-for="(panel, index) in panels" :key="panel.id">
<div class="rounded-xl border border-slate-700/60 bg-slate-950/60 p-3">
<div class="flex items-center gap-2">
<input type="text" x-model="panel.name" placeholder="Panel name"
class="min-w-0 flex-1 rounded-lg border-0 bg-slate-900 px-3 py-2 text-sm text-white ring-1 ring-slate-700/60">
<button type="button" @click="setActivePanel(panel.id)"
class="rounded-lg px-2.5 py-1.5 text-xs font-medium"
:class="activePanelId === panel.id ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'">
<span x-text="activePanelId === panel.id ? 'Active' : 'Show'"></span>
</button>
<button type="button" @click="removePanel(index)" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-800 hover:text-red-300">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<template x-for="speaker in speakerParticipantsSorted()" :key="panel.id + '-' + speaker.uuid">
<label class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs"
:class="panel.speaker_refs.includes(speaker.user_ref) ? 'bg-violet-600/30 text-violet-100 ring-1 ring-violet-500/50' : 'bg-slate-800 text-slate-300'">
<input type="checkbox" class="sr-only"
:checked="panel.speaker_refs.includes(speaker.user_ref)"
@change="togglePanelSpeaker(panel, speaker.user_ref)">
<span x-text="speaker.display_name"></span>
</label>
</template>
</div>
</div>
</template>
<button type="button" @click="addPanel()"
class="w-full rounded-xl border border-dashed border-slate-600 px-3 py-2.5 text-sm font-medium text-slate-300 hover:border-violet-500/50 hover:text-violet-200">
+ Add panel
</button>
</div>
<div class="mt-6 flex justify-end gap-2">
<button type="button" @click="panelSetupOpen = false"
class="rounded-xl px-4 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800">Cancel</button>
<button type="button" @click="saveStageLayout()" :disabled="savingStageLayout"
class="rounded-xl bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-500 disabled:opacity-60">
<span x-text="savingStageLayout ? 'Saving…' : 'Save layout'"></span>
</button>
</div>
</div>
</div>
<x-meet.room.panel show="featuresOpen" title="More options">
<x-slot:subtitle>
<p>{{ $sessionNounTitle }} tools and extras</p>
</x-slot:subtitle>
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
<div class="space-y-1">
<div class="space-y-1 sm:hidden">
<x-meet.room.menu-item icon="raise-hand" title="Raise hand" subtitle="Let the host know you have a question"
@click="raiseHand(); featuresOpen = false" />
<x-meet.room.menu-item icon="applause" title="Applause" subtitle="Send a quick reaction to everyone"
@click="sendReaction('thumbs'); featuresOpen = false" />
<x-meet.room.menu-item icon="react" title="React" subtitle="Send a heart to the room"
@click="sendReaction('heart'); featuresOpen = false" />
<x-meet.room.menu-item icon="chat" title="Chat" subtitle="Open the {{ $sessionNoun }} chat"
@click="toggleChat(); featuresOpen = false" />
<div class="my-1 border-t border-slate-800"></div>
</div>
<button @click="openUploadPicker()" type="button"
class="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm text-slate-200 transition-colors hover:bg-slate-800">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-800/80">
@include('meet.room.partials.meet-icon', ['icon' => 'upload-file', 'class' => 'h-5 w-5'])
</span>
<span class="min-w-0">
<span class="block font-medium">Share files</span>
<span class="block text-xs text-slate-400">Upload a file for everyone in the call</span>
</span>
</button>
@if ($participant->isHost())
<button @click="toggleRecordingFromMenu()" type="button"
class="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm text-slate-200 transition-colors hover:bg-slate-800">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-red-500/15">
<span class="h-3 w-3 rounded-full" :class="isRecording ? 'bg-red-500 animate-pulse' : 'bg-red-400'"></span>
</span>
<span class="min-w-0">
<span class="block font-medium" x-text="isRecording ? 'Stop recording' : 'Start recording'"></span>
<span class="block text-xs text-slate-400">Save this {{ $sessionNoun }} to the cloud</span>
</span>
</button>
<button @click="toggleLockFromMenu()" type="button"
class="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm text-slate-200 transition-colors hover:bg-slate-800">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-800/80">
@include('meet.room.partials.meet-icon', ['icon' => 'lock', 'class' => 'h-5 w-5'])
</span>
<span class="min-w-0">
<span class="block font-medium" x-text="isLocked ? 'Unlock ' + sessionNoun() : 'Lock ' + sessionNoun()"></span>
<span class="block text-xs text-slate-400">Control who can join</span>
</span>
</button>
<x-meet.room.menu-item icon="start-breakout" title="Start breakouts" subtitle="Split participants into smaller rooms"
@click="createBreakouts()" />
<x-meet.room.menu-item icon="close-breakout" title="Close breakouts" subtitle="Bring everyone back to the main room"
@click="closeBreakouts()" />
@endif
</div>
@if ($isWebinar ?? false)
<div class="mt-4 border-t border-slate-800 pt-4">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Q&amp;A</p>
<ul class="mt-2 max-h-36 space-y-2 overflow-y-auto">
<template x-for="item in qaItems" :key="item.uuid">
<li class="rounded-lg bg-slate-800/80 px-3 py-2 text-sm">
<span class="font-medium text-sky-300" x-text="item.asker_name"></span>
<span class="text-slate-300" x-text="': ' + item.question"></span>
</li>
</template>
<li x-show="qaItems.length === 0" class="text-xs text-slate-500">No questions yet.</li>
</ul>
<form @submit.prevent="submitQuestion" class="mt-3 flex gap-2">
<input type="text" x-model="qaInput" placeholder="Ask a question…"
class="flex-1 rounded-lg border-0 bg-slate-950 px-3 py-2 text-sm text-white placeholder:text-slate-500 ring-1 ring-slate-700/60">
<button type="submit" class="rounded-lg bg-indigo-600 px-3 py-2 text-xs font-medium hover:bg-indigo-500">Send</button>
</form>
</div>
@endif
<p x-show="breakoutBroadcast" class="mt-3 rounded-xl bg-amber-950/40 px-3 py-3 text-sm text-amber-100" x-text="breakoutBroadcast"></p>
</div>
</x-meet.room.panel>
</div>
</body>
</html>