Add kiosk join/leave pages, waiting room, and meeting feedback.
Deploy Ladill Meet / deploy (push) Successful in 47s
Deploy Ladill Meet / deploy (push) Successful in 47s
Redesign public join flows on the Frontdesk kiosk template, enforce host admission when waiting room is enabled, and collect star ratings after leave. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
Alpine.data('waitingRoom', () => ({
|
||||
message: 'Waiting for the host to let you in…',
|
||||
denied: false,
|
||||
pollTimer: null,
|
||||
|
||||
init() {
|
||||
this.pollTimer = setInterval(() => this.poll(), 2500);
|
||||
this.poll();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
}
|
||||
},
|
||||
|
||||
async poll() {
|
||||
const url = this.$el.dataset.statusUrl;
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.denied) {
|
||||
this.denied = true;
|
||||
this.message = 'The host declined your request to join.';
|
||||
clearInterval(this.pollTimer);
|
||||
}
|
||||
} catch {
|
||||
// ignore transient errors
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data('leaveFeedback', () => ({
|
||||
rating: 0,
|
||||
audioRating: 0,
|
||||
videoRating: 0,
|
||||
hoverRating: 0,
|
||||
hoverAudio: 0,
|
||||
hoverVideo: 0,
|
||||
comment: '',
|
||||
|
||||
setRating(value) {
|
||||
this.rating = value;
|
||||
},
|
||||
|
||||
setAudioRating(value) {
|
||||
this.audioRating = value;
|
||||
},
|
||||
|
||||
setVideoRating(value) {
|
||||
this.videoRating = value;
|
||||
},
|
||||
|
||||
canSubmit() {
|
||||
return this.rating > 0;
|
||||
},
|
||||
}));
|
||||
|
||||
window.Alpine = Alpine;
|
||||
Alpine.start();
|
||||
@@ -85,6 +85,7 @@ function meetRoom() {
|
||||
whiteboardUrl: el.dataset.whiteboardUrl,
|
||||
whiteboardSaveUrl: el.dataset.whiteboardSaveUrl,
|
||||
csrf: el.dataset.csrf,
|
||||
sessionUuid: el.dataset.sessionUuid,
|
||||
};
|
||||
|
||||
this.joinUrl = el.dataset.joinUrl || '';
|
||||
@@ -334,7 +335,7 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
hostParticipant() {
|
||||
const hosts = this.sessionParticipants.filter((p) => p.role === 'host' || p.role === 'co_host');
|
||||
const hosts = this.inCallParticipants().filter((p) => p.role === 'host' || p.role === 'co_host');
|
||||
if (hosts.length) {
|
||||
return hosts[0];
|
||||
}
|
||||
@@ -352,13 +353,21 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
participantCount() {
|
||||
return Math.max(this.sessionParticipants.length, this.participants.length);
|
||||
return this.inCallParticipants().length;
|
||||
},
|
||||
|
||||
inCallParticipants() {
|
||||
return this.sessionParticipants.filter((p) => p.status === 'joined');
|
||||
},
|
||||
|
||||
waitingParticipants() {
|
||||
return this.sessionParticipants.filter((p) => p.status === 'waiting');
|
||||
},
|
||||
|
||||
sortedParticipants() {
|
||||
const order = { host: 0, co_host: 1, panelist: 2, participant: 3, guest: 4, attendee: 5 };
|
||||
|
||||
return [...this.sessionParticipants].sort((a, b) => {
|
||||
return [...this.inCallParticipants()].sort((a, b) => {
|
||||
const diff = (order[a.role] ?? 9) - (order[b.role] ?? 9);
|
||||
|
||||
return diff !== 0 ? diff : a.display_name.localeCompare(b.display_name);
|
||||
@@ -378,6 +387,26 @@ function meetRoom() {
|
||||
return labels[role] || 'Participant';
|
||||
},
|
||||
|
||||
async admitParticipant(uuid) {
|
||||
if (!this.isHost || !this.config.sessionUuid) return;
|
||||
|
||||
await fetch(`/room/${this.config.sessionUuid}/participants/${uuid}/admit`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
});
|
||||
await this.poll();
|
||||
},
|
||||
|
||||
async denyParticipant(uuid) {
|
||||
if (!this.isHost || !this.config.sessionUuid) return;
|
||||
|
||||
await fetch(`/room/${this.config.sessionUuid}/participants/${uuid}/deny`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
});
|
||||
await this.poll();
|
||||
},
|
||||
|
||||
getOrCreateParticipantTile(participant) {
|
||||
const grid = document.getElementById('video-grid');
|
||||
if (!grid) return null;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
@props([
|
||||
'title' => 'Ladill Meet',
|
||||
'organization' => null,
|
||||
'eyebrow' => null,
|
||||
])
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ $title }}</title>
|
||||
@include('partials.favicon')
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css', 'resources/js/meet-public.js'])
|
||||
@stack('head')
|
||||
</head>
|
||||
<body {{ $attributes->merge(['class' => 'min-h-screen bg-slate-100 font-sans text-slate-900 antialiased']) }}>
|
||||
<div class="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-200/40 blur-3xl"></div>
|
||||
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-200/40 blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div class="fixed inset-x-0 top-0 z-10 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
|
||||
|
||||
@if ($organization)
|
||||
@include('meet.partials.kiosk-brand', ['organization' => $organization])
|
||||
@else
|
||||
<div class="fixed left-0 top-0 z-20 p-6">
|
||||
<img src="{{ asset(\App\Support\OrganizationBranding::DEFAULT_LOGO) }}?v={{ @filemtime(public_path(\App\Support\OrganizationBranding::DEFAULT_LOGO)) ?: '1' }}"
|
||||
alt="Ladill Meet"
|
||||
class="h-8 w-auto max-w-[220px] object-contain object-left">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col pt-20 px-6 pb-12">
|
||||
@if ($eyebrow)
|
||||
<p class="text-center text-sm font-semibold uppercase tracking-wider text-indigo-600">{{ $eyebrow }}</p>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Meeting cancelled · Ladill Meet</title>
|
||||
@include('partials.favicon')
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-100 p-4 font-sans">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Meeting cancelled</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">{{ $room->title }} is no longer available.</p>
|
||||
<x-meet-kiosk-layout :title="'Meeting cancelled'" :organization="$organization ?? null" eyebrow="Ladill Meet">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<h1 class="text-2xl font-bold text-slate-900">{{ $room->title }}</h1>
|
||||
<p class="mt-3 text-sm text-slate-600">This meeting has been cancelled by the host.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</x-meet-kiosk-layout>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Access denied · Ladill Meet</title>
|
||||
@include('partials.favicon')
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-100 p-4 font-sans">
|
||||
<div class="max-w-md rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Unable to join</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">{{ $reason ?? 'You do not have access to this meeting.' }}</p>
|
||||
<a href="{{ route('sso.connect', ['redirect' => route('meet.join', $room), 'interactive' => 1]) }}" class="btn-primary mt-6 inline-block">Sign in</a>
|
||||
<x-meet-kiosk-layout :title="'Access denied · '.$room->title" :organization="$organization" eyebrow="Ladill Meet">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-amber-50 text-amber-600">
|
||||
<svg class="h-7 w-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="mt-6 text-2xl font-bold text-slate-900">Unable to join</h1>
|
||||
<p class="mt-3 text-sm text-slate-600">{{ $reason ?? 'You do not have access to this meeting.' }}</p>
|
||||
<a href="{{ route('sso.connect', ['redirect' => route('meet.join', $room), 'interactive' => 1]) }}" class="btn-primary btn-primary-lg mt-8 inline-flex w-full items-center justify-center py-3.5 font-semibold">
|
||||
Sign in with Ladill
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</x-meet-kiosk-layout>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Join · {{ $room->title }} · Ladill Meet</title>
|
||||
@include('partials.favicon')
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-900 p-4 font-sans text-white">
|
||||
<div class="w-full max-w-md rounded-2xl bg-slate-800 p-8 shadow-xl">
|
||||
<h1 class="text-xl font-semibold">{{ $room->title }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-400">Enter the meeting passcode to continue.</p>
|
||||
<x-meet-kiosk-layout :title="'Passcode · '.$room->title" :organization="$organization" eyebrow="Secure meeting">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center">
|
||||
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<h1 class="text-2xl font-bold text-slate-900">{{ $room->title }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-500">This meeting is protected. Enter the passcode to continue.</p>
|
||||
|
||||
<form method="POST" action="{{ route('meet.join.passcode', $room) }}" class="mt-6 space-y-4">
|
||||
@csrf
|
||||
<input type="password" name="passcode" required autofocus placeholder="Passcode"
|
||||
class="w-full rounded-lg border-slate-600 bg-slate-700 px-4 py-3 text-white placeholder:text-slate-400">
|
||||
@error('passcode')
|
||||
<p class="text-sm text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
<button type="submit" class="btn-primary w-full">Continue</button>
|
||||
</form>
|
||||
@if ($errors->any())
|
||||
<div class="mt-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('meet.join.passcode', $room) }}" class="mt-6 space-y-4">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Passcode</label>
|
||||
<input type="password" name="passcode" required autofocus
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm ring-1 ring-slate-200 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-primary-lg w-full py-3.5 font-semibold">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</x-meet-kiosk-layout>
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Join · {{ $room->title }} · Ladill Meet</title>
|
||||
@include('partials.favicon')
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-900 p-4 font-sans text-white">
|
||||
<div class="w-full max-w-md rounded-2xl bg-slate-800 p-8 shadow-xl">
|
||||
<h1 class="text-xl font-semibold">{{ $room->title }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-400">Ready to join?</p>
|
||||
<x-meet-kiosk-layout :title="'Join · '.$room->title" :organization="$organization" eyebrow="Ladill Meet">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-lg">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">{{ $room->title }}</h1>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mt-4 rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
||||
{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($room->scheduled_at)
|
||||
<p class="mt-3 text-base text-slate-500">
|
||||
{{ $room->scheduled_at->timezone($room->timezone)->format('l, j F Y · g:i A T') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('meet.join.enter', $room) }}" class="mt-6 space-y-4">
|
||||
@csrf
|
||||
@guest
|
||||
<div>
|
||||
<label class="block text-sm text-slate-300">Your name</label>
|
||||
<input type="text" name="display_name" required placeholder="Display name"
|
||||
class="mt-1 w-full rounded-lg border-slate-600 bg-slate-700 px-4 py-3 text-white placeholder:text-slate-400">
|
||||
@if ($room->host_user_ref && ($host = \App\Models\User::query()->where('public_id', $room->host_user_ref)->first()))
|
||||
<p class="mt-2 text-sm text-slate-500">Hosted by <span class="font-medium text-slate-700">{{ $host->name }}</span></p>
|
||||
@endif
|
||||
|
||||
@if ($room->description)
|
||||
<p class="mt-4 text-sm leading-relaxed text-slate-600">{{ $room->description }}</p>
|
||||
@endif
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mt-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-left text-sm text-red-700">
|
||||
{{ $errors->first() }}
|
||||
</div>
|
||||
@if ($isWebinar ?? false)
|
||||
<div>
|
||||
<label class="block text-sm text-slate-300">Email</label>
|
||||
<input type="email" name="email" required placeholder="you@example.com"
|
||||
class="mt-1 w-full rounded-lg border-slate-600 bg-slate-700 px-4 py-3 text-white placeholder:text-slate-400">
|
||||
</div>
|
||||
@endif
|
||||
@endguest
|
||||
<button type="submit" class="btn-primary w-full">Join meeting</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@auth
|
||||
<p class="mt-4 text-center text-xs text-slate-500">Joining as {{ auth()->user()->name }}</p>
|
||||
@endauth
|
||||
<form method="POST" action="{{ route('meet.join.enter', $room) }}" class="mt-10 text-left">
|
||||
@csrf
|
||||
|
||||
@guest
|
||||
<label class="block text-sm font-medium text-slate-700">Your name</label>
|
||||
<input type="text" name="display_name" required placeholder="How should we show your name?"
|
||||
value="{{ old('display_name') }}"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
|
||||
@if ($isWebinar ?? false)
|
||||
<label class="mt-4 block text-sm font-medium text-slate-700">Email</label>
|
||||
<input type="email" name="email" required placeholder="you@example.com"
|
||||
value="{{ old('email') }}"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
@endif
|
||||
@endguest
|
||||
|
||||
@auth
|
||||
<div class="rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600 shadow-sm">
|
||||
Joining as <span class="font-semibold text-slate-900">{{ auth()->user()->name }}</span>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
@if ($room->setting('waiting_room', true) && ! ($user && $user->ownerRef() === $room->host_user_ref))
|
||||
<p class="mt-4 text-xs text-slate-500">The host will admit you when the meeting is ready.</p>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="btn-primary btn-primary-lg mt-8 w-full py-4 text-base font-semibold">
|
||||
Join meeting
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@guest
|
||||
<p class="mt-6 text-center text-sm text-slate-500">
|
||||
Have a Ladill account?
|
||||
<a href="{{ route('sso.connect', ['redirect' => route('meet.join', $room), 'interactive' => 1]) }}" class="font-medium text-indigo-600 hover:text-indigo-700">Sign in</a>
|
||||
</p>
|
||||
@endguest
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</x-meet-kiosk-layout>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<x-meet-kiosk-layout :title="'Waiting · '.$room->title" :organization="$organization" eyebrow="Waiting room">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center"
|
||||
x-data="waitingRoom"
|
||||
x-init="init()"
|
||||
data-status-url="{{ route('meet.join.waiting.status', $room) }}">
|
||||
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-indigo-50 text-indigo-600" x-show="!denied">
|
||||
<svg class="h-8 w-8 animate-pulse" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-50 text-red-600" x-show="denied" x-cloak>
|
||||
<svg class="h-8 w-8" 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>
|
||||
</div>
|
||||
|
||||
<h1 class="mt-6 text-2xl font-bold text-slate-900">{{ $room->title }}</h1>
|
||||
<p class="mt-2 text-sm text-slate-500">Hi {{ $participant->display_name }}, please wait.</p>
|
||||
<p class="mt-4 text-base text-slate-700" x-text="message"></p>
|
||||
|
||||
<p class="mt-6 text-xs text-slate-400" x-show="!denied">You will join automatically once the host admits you.</p>
|
||||
|
||||
<a href="{{ route('meet.join', $room) }}" class="mt-8 inline-block text-sm font-medium text-indigo-600 hover:text-indigo-700" x-show="denied" x-cloak>
|
||||
Back to join page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-meet-kiosk-layout>
|
||||
@@ -0,0 +1,91 @@
|
||||
<x-meet-kiosk-layout :title="'You left · '.$room->title" :organization="$organization" eyebrow="Meeting ended">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center">
|
||||
<form method="POST" action="{{ route('meet.left.feedback', $session) }}"
|
||||
x-data="leaveFeedback"
|
||||
class="w-full max-w-lg rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
@csrf
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-slate-900">You left the meeting</h1>
|
||||
<p class="mt-2 text-sm text-slate-500">{{ $room->title }}</p>
|
||||
@if (! empty($feedback['display_name']))
|
||||
<p class="mt-1 text-sm text-slate-600">Thanks, {{ $feedback['display_name'] }}.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<p class="text-sm font-medium text-slate-700">How was the meeting overall?</p>
|
||||
<div class="mt-3 flex justify-center gap-2">
|
||||
@foreach (range(1, 5) as $star)
|
||||
<button type="button"
|
||||
@click="setRating({{ $star }})"
|
||||
@mouseenter="hoverRating = {{ $star }}"
|
||||
@mouseleave="hoverRating = 0"
|
||||
class="rounded-lg p-1 transition-transform hover:scale-110"
|
||||
aria-label="Rate {{ $star }} stars">
|
||||
<svg class="h-9 w-9"
|
||||
:class="(hoverRating || rating) >= {{ $star }} ? 'text-amber-400' : 'text-slate-300'"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
<input type="hidden" name="rating" :value="rating" required>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-700">Audio quality</p>
|
||||
<div class="mt-2 flex gap-1">
|
||||
@foreach (range(1, 5) as $star)
|
||||
<button type="button" @click="setAudioRating({{ $star }})"
|
||||
class="rounded p-0.5">
|
||||
<svg class="h-6 w-6"
|
||||
:class="(hoverAudio || audioRating) >= {{ $star }} ? 'text-amber-400' : 'text-slate-300'"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
<input type="hidden" name="audio_rating" :value="audioRating || ''">
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-700">Video quality</p>
|
||||
<div class="mt-2 flex gap-1">
|
||||
@foreach (range(1, 5) as $star)
|
||||
<button type="button" @click="setVideoRating({{ $star }})"
|
||||
class="rounded p-0.5">
|
||||
<svg class="h-6 w-6"
|
||||
:class="(hoverVideo || videoRating) >= {{ $star }} ? 'text-amber-400' : 'text-slate-300'"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
<input type="hidden" name="video_rating" :value="videoRating || ''">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<label class="block text-sm font-medium text-slate-700">Anything else we should know? <span class="font-normal text-slate-400">(optional)</span></label>
|
||||
<textarea name="comment" rows="3" x-model="comment" placeholder="Share feedback for the host or Ladill Meet…"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<button type="submit"
|
||||
class="btn-primary btn-primary-lg flex-1 py-3.5 font-semibold disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!canSubmit()">
|
||||
Submit feedback
|
||||
</button>
|
||||
<a href="{{ route('meet.left.thanks', $session) }}"
|
||||
class="inline-flex flex-1 items-center justify-center rounded-2xl border border-slate-200 bg-white px-4 py-3.5 text-sm font-medium text-slate-600 hover:bg-slate-50">
|
||||
Skip
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-meet-kiosk-layout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<x-meet-kiosk-layout :title="'Thank you'" :organization="$organization" eyebrow="Ladill Meet">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-emerald-50 text-emerald-600">
|
||||
<svg class="h-7 w-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="mt-6 text-2xl font-bold text-slate-900">
|
||||
@if (session('rated'))
|
||||
Thanks for your feedback
|
||||
@else
|
||||
You have left the meeting
|
||||
@endif
|
||||
</h1>
|
||||
<p class="mt-3 text-sm text-slate-600">{{ $room->title }}</p>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-3">
|
||||
@auth
|
||||
<a href="{{ route('meet.dashboard') }}" class="btn-primary btn-primary-lg py-3.5 font-semibold">Back to Meet</a>
|
||||
@else
|
||||
<a href="{{ route('meet.join', $room) }}" class="btn-primary btn-primary-lg py-3.5 font-semibold">Rejoin meeting</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-meet-kiosk-layout>
|
||||
@@ -0,0 +1,8 @@
|
||||
@php
|
||||
use App\Support\OrganizationBranding;
|
||||
@endphp
|
||||
<div class="fixed left-0 top-0 z-20 p-6">
|
||||
<img src="{{ OrganizationBranding::logoUrl($organization) }}"
|
||||
alt="{{ OrganizationBranding::logoAlt($organization) }}"
|
||||
class="h-8 w-auto max-w-[220px] object-contain object-left">
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
use App\Models\User;
|
||||
use App\Services\Meet\ParticipantPresenter;
|
||||
|
||||
$joined = $session->participants->where('status', 'joined');
|
||||
$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))
|
||||
@@ -61,6 +61,7 @@
|
||||
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>
|
||||
@@ -384,7 +385,28 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto rounded-2xl bg-slate-900/90 p-2">
|
||||
<template x-for="person in sortedParticipants()" :key="person.uuid">
|
||||
<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</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 inCallParticipants()" :key="person.uuid">
|
||||
<div class="flex items-center gap-3 rounded-xl px-2 py-2.5">
|
||||
<span class="relative flex h-10 w-10 shrink-0">
|
||||
<img x-show="person.avatar_url"
|
||||
@@ -413,7 +435,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="sessionParticipants.length === 0" class="px-2 py-4 text-center text-xs text-slate-500">No participants yet.</p>
|
||||
<p x-show="inCallParticipants().length === 0 && waitingParticipants().length === 0" class="px-2 py-4 text-center text-xs text-slate-500">No participants yet.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,13 @@
|
||||
|
||||
<fieldset class="space-y-2">
|
||||
<legend class="text-sm font-medium text-slate-700">Options</legend>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="waiting_room" value="1" @checked(old('waiting_room', $defaultSettings['waiting_room'] ?? true)) class="rounded border-slate-300"> Waiting room</label>
|
||||
<label class="flex items-start gap-2 text-sm">
|
||||
<input type="checkbox" name="waiting_room" value="1" @checked(old('waiting_room', $defaultSettings['waiting_room'] ?? true)) class="mt-0.5 rounded border-slate-300">
|
||||
<span>
|
||||
<span class="font-medium text-slate-800">Waiting room</span>
|
||||
<span class="mt-0.5 block text-xs text-slate-500">Guests wait until the host admits them. Uncheck to let guests join directly.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="join_before_host" value="1" @checked(old('join_before_host')) class="rounded border-slate-300"> Allow join before host</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="mute_on_join" value="1" @checked(old('mute_on_join', $defaultSettings['mute_on_join'] ?? true)) class="rounded border-slate-300"> Mute participants on join</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="auto_record" value="1" @checked(old('auto_record')) class="rounded border-slate-300"> Auto-record</label>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<input type="number" name="duration_minutes" value="60" min="15" max="480" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="is_default" value="1" class="rounded border-slate-300"> Default template</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="waiting_room" value="1" checked class="rounded border-slate-300"> Waiting room</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="waiting_room" value="1" checked class="rounded border-slate-300"> Waiting room (host admits guests)</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="auto_record" value="1" class="rounded border-slate-300"> Auto-record</label>
|
||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="live_captions" value="1" class="rounded border-slate-300"> Live captions</label>
|
||||
<button type="submit" class="btn-primary w-full">Save template</button>
|
||||
|
||||
Reference in New Issue
Block a user