Wire space listener email invites and restyle Meet index pages.
Deploy Ladill Meet / deploy (push) Successful in 42s

Enforce invite-only room access, validate invite emails on create, assign
attendee role for listener invitations, and add invite UI on the room show
page. Redesign meetings, rooms, conferences, and webinars indexes to match
the Events hero, stats cards, and icon list layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-04 02:07:27 +00:00
co-authored by Cursor
parent 21092015b8
commit 46d630221a
17 changed files with 463 additions and 92 deletions
@@ -1,22 +1,17 @@
<x-app-layout title="Conferences">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-slate-900">Conferences</h1>
<p class="mt-1 text-sm text-slate-500">Multi-speaker events with hosts and presenters. Billed at GHS {{ number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2) }} per participant.</p>
</div>
<a href="{{ route('meet.conferences.create') }}" class="btn-primary">Schedule conference</a>
</div>
<div class="mt-4 space-y-2">
@forelse ($conferences as $room)
@include('meet.partials.room-card', [
'room' => $room,
'detailsRoute' => route('meet.conferences.show', $room),
])
@empty
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No conferences yet.</p>
@endforelse
</div>
@include('meet.partials.index-page', [
'badge' => 'Multi-speaker · Stage · Panels',
'title' => 'Your conferences',
'description' => 'Run multi-speaker events with hosts and presenters. Billed at GHS '.number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2).' per participant.',
'createUrl' => route('meet.conferences.create'),
'createLabel' => 'Schedule conference',
'stats' => $stats,
'listTitle' => 'Your conferences',
'emptyMessage' => 'No conferences yet. Schedule your first conference to get started.',
'items' => $items,
'icon' => 'meet-icons/panel.svg',
'iconBg' => 'bg-violet-100',
])
<div class="mt-4">{{ $conferences->links() }}</div>
</x-app-layout>
@@ -24,10 +24,10 @@
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<form method="POST" action="{{ route('meet.invitations.store', $room) }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<h2 class="font-medium text-slate-900">Invite people</h2>
<h2 class="font-medium text-slate-900">{{ $room->isSpace() ? 'Invite listeners' : 'Invite people' }}</h2>
<div>
<label class="block text-sm text-slate-700">Email addresses</label>
<textarea name="emails" rows="3" placeholder="one@example.com, two@example.com" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
<textarea name="emails" rows="3" placeholder="{{ $room->isSpace() ? 'listener@example.com, guest@example.com' : 'one@example.com, two@example.com' }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm"></textarea>
</div>
@if ($groups->isNotEmpty())
<div>
+1 -1
View File
@@ -7,7 +7,7 @@
</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 '.($room->isConference() ? 'conference' : 'meeting').'.' }}</p>
<p class="mt-3 text-sm text-slate-600">{{ $reason ?? 'You do not have access to this '.match (true) { $room->isSpace() => 'room', $room->isConference() => 'conference', $room->isWebinar() => 'webinar', default => '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>
@@ -0,0 +1,83 @@
@props([
'badge',
'title',
'description',
'createUrl',
'createLabel',
'stats' => [],
'listTitle',
'emptyMessage',
'items' => [],
'icon' => 'meet-icons/schedule.svg',
'iconBg' => 'bg-violet-100',
])
<div class="space-y-6">
@foreach (['success', 'error'] as $flash)
@if (session($flash))
<div class="rounded-lg border px-4 py-3 {{ $flash === 'success' ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-red-200 bg-red-50 text-red-700' }}">
<p class="text-sm">{{ session($flash) }}</p>
</div>
@endif
@endforeach
<div class="relative overflow-hidden rounded-2xl border border-slate-200 bg-white">
<div class="absolute inset-0 bg-gradient-to-br from-violet-50/80 via-white to-indigo-50/60"></div>
<div class="relative px-6 py-8 sm:px-10">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="max-w-xl">
<div class="inline-flex items-center gap-1.5 rounded-full bg-violet-100 px-3 py-1 text-xs font-semibold text-violet-700">
{{ $badge }}
</div>
<h1 class="mt-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">{{ $title }}</h1>
<p class="mt-2 text-sm leading-6 text-slate-600">{{ $description }}</p>
<div class="mt-6">
<a href="{{ $createUrl }}" class="btn-primary">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
{{ $createLabel }}
</a>
</div>
</div>
@if (count($stats) > 0)
<div class="scrollbar-hide flex snap-x snap-mandatory gap-3 overflow-x-auto pb-2 sm:grid sm:grid-cols-3 sm:overflow-visible sm:pb-0 lg:gap-4" style="-ms-overflow-style:none;scrollbar-width:none;">
@foreach ($stats as $stat)
<div class="mobile-stats-card shrink-0 snap-start rounded-xl border border-slate-200 bg-white/90 px-4 py-3 text-center backdrop-blur-sm">
<p class="whitespace-nowrap text-xl font-bold text-slate-900 sm:text-2xl">{{ $stat['value'] }}</p>
<p class="mt-0.5 whitespace-nowrap text-[11px] font-medium text-slate-500">{{ $stat['label'] }}</p>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<div class="border-b border-slate-100 px-6 py-4">
<h2 class="text-sm font-semibold text-slate-900">{{ $listTitle }}</h2>
</div>
@if (count($items) === 0)
<p class="px-6 py-10 text-center text-sm text-slate-500">{{ $emptyMessage }}</p>
@else
<div class="divide-y divide-slate-100">
@foreach ($items as $item)
<a href="{{ $item['url'] }}" class="flex items-center gap-4 px-6 py-4 transition hover:bg-slate-50">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg {{ ($item['active'] ?? true) ? $iconBg : 'bg-slate-100 opacity-50' }}">
<img src="{{ asset('images/'.$icon) }}?v={{ @filemtime(public_path('images/'.$icon)) ?: '1' }}"
alt="" class="h-5 w-5 object-contain" width="20" height="20">
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-slate-900">{{ $item['title'] }}</p>
<p class="truncate text-xs text-slate-500">{{ $item['subtitle'] }}</p>
</div>
<div class="text-right text-xs text-slate-500">
<p class="font-semibold text-slate-900">{{ $item['meta'] }}</p>
<p>{{ $item['status'] }}</p>
</div>
</a>
@endforeach
</div>
@endif
</div>
</div>
+13 -18
View File
@@ -1,22 +1,17 @@
<x-app-layout title="Meetings">
<div class="flex items-center justify-between gap-3">
<h1 class="text-xl font-semibold text-slate-900">All meetings</h1>
<a href="{{ route('meet.rooms.create') }}"
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-slate-300 bg-white text-slate-700 shadow-sm transition hover:bg-slate-50 lg:hidden"
title="Schedule meeting"
aria-label="Schedule meeting">
@include('meet.room.partials.meet-icon', ['icon' => 'schedule', 'inverted' => false])
</a>
<a href="{{ route('meet.rooms.create') }}" class="btn-primary hidden lg:inline-flex">Schedule meeting</a>
</div>
<div class="mt-4 space-y-2">
@forelse ($rooms as $room)
@include('meet.partials.room-card', ['room' => $room])
@empty
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No meetings yet.</p>
@endforelse
</div>
@include('meet.partials.index-page', [
'badge' => 'Video · Screen share · Chat',
'title' => 'Your meetings',
'description' => 'Schedule or start instant video meetings, share your screen, and collaborate with your team.',
'createUrl' => route('meet.rooms.create'),
'createLabel' => 'Schedule meeting',
'stats' => $stats,
'listTitle' => 'Your meetings',
'emptyMessage' => 'No meetings yet. Schedule your first meeting to get started.',
'items' => $items,
'icon' => 'meet-icons/schedule.svg',
'iconBg' => 'bg-violet-100',
])
<div class="mt-4">{{ $rooms->links() }}</div>
</x-app-layout>
+5 -1
View File
@@ -33,7 +33,11 @@
<div>
<label class="block text-sm font-medium text-slate-700">Invite listeners by email (optional)</label>
<input type="text" name="invite_emails" value="{{ old('invite_emails') }}" placeholder="guest@example.com" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<p class="mt-0.5 text-xs text-slate-500">Comma-separated addresses. Each listener receives a join link by email.</p>
<input type="text" name="invite_emails" value="{{ old('invite_emails') }}" placeholder="listener@example.com, guest@example.com" class="mt-2 w-full rounded-lg border-slate-300 text-sm">
@error('invite_emails')
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
+13 -18
View File
@@ -1,22 +1,17 @@
<x-app-layout title="Rooms">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-slate-900">Rooms</h1>
<p class="mt-1 text-sm text-slate-500">Audio-only discussions with a host, assigned speakers, and listeners.</p>
</div>
<a href="{{ route('meet.spaces.create') }}" class="btn-primary">Create room</a>
</div>
<div class="mt-4 space-y-2">
@forelse ($spaces as $room)
@include('meet.partials.room-card', [
'room' => $room,
'detailsRoute' => route('meet.spaces.show', $room),
])
@empty
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No rooms yet. Create one to host a private discussion.</p>
@endforelse
</div>
@include('meet.partials.index-page', [
'badge' => 'Audio · Speakers · Listeners',
'title' => 'Your rooms',
'description' => 'Host private audio discussions, assign speakers, invite listeners by email, and promote people to speak during the live session.',
'createUrl' => route('meet.spaces.create'),
'createLabel' => 'Create room',
'stats' => $stats,
'listTitle' => 'Your rooms',
'emptyMessage' => 'No rooms yet. Create your first room to host a private discussion.',
'items' => $items,
'icon' => 'meet-icons/speak.svg',
'iconBg' => 'bg-emerald-100',
])
<div class="mt-4">{{ $spaces->links() }}</div>
</x-app-layout>
+52 -1
View File
@@ -6,6 +6,12 @@
<p class="mt-1 text-sm text-slate-500">{{ config('meet.room_statuses')[$room->status] ?? $room->status }}</p>
</div>
@if (session('success'))
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
{{ session('success') }}
</div>
@endif
@if ($errors->any())
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ $errors->first() }}
@@ -36,7 +42,6 @@
</form>
@endif
<a href="{{ route('meet.rooms.qr', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">QR code</a>
<a href="{{ route('meet.invitations.index', $room) }}" class="btn-secondary btn-secondary-sm">Manage invitations</a>
</div>
@if ($speakers->isNotEmpty())
@@ -59,6 +64,52 @@
@endif
</div>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<div class="flex items-start justify-between gap-3">
<div>
<h2 class="text-sm font-medium text-slate-900">Listener invitations</h2>
<p class="mt-1 text-sm text-slate-500">Email join links to listeners. Invited people can enter private rooms.</p>
</div>
<a href="{{ route('meet.invitations.index', $room) }}" class="shrink-0 text-sm text-indigo-600 hover:underline">Bulk import</a>
</div>
<form method="POST" action="{{ route('meet.invitations.store', $room) }}" class="mt-4 space-y-3">
@csrf
<div>
<label class="block text-sm text-slate-700">Email addresses</label>
<textarea name="emails" rows="2" placeholder="listener@example.com, guest@example.com" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('emails') }}</textarea>
@error('emails')
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="btn-primary btn-primary-sm">Send invitations</button>
</form>
@if ($room->invitations->isNotEmpty())
<ul class="mt-5 divide-y divide-slate-100 border-t border-slate-100 pt-4 text-sm">
@foreach ($room->invitations->take(8) as $invitation)
<li class="flex items-center justify-between gap-3 py-2">
<div class="min-w-0">
<p class="truncate font-medium text-slate-900">{{ $invitation->display_name ?? $invitation->email }}</p>
<p class="truncate text-xs text-slate-500">{{ $invitation->email }} · {{ ucfirst($invitation->status) }}</p>
</div>
<form method="POST" action="{{ route('meet.invitations.resend', $invitation) }}">
@csrf
<button type="submit" class="text-xs text-indigo-600 hover:underline">Resend</button>
</form>
</li>
@endforeach
</ul>
@if ($room->invitations->count() > 8)
<a href="{{ route('meet.invitations.index', $room) }}" class="mt-2 inline-block text-sm text-indigo-600 hover:underline">
View all {{ $room->invitations->count() }} invitations
</a>
@endif
@else
<p class="mt-4 text-sm text-slate-500">No listener invitations sent yet.</p>
@endif
</div>
@if ($room->sessions->isNotEmpty())
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Past sessions</h2>
+13 -18
View File
@@ -1,22 +1,17 @@
<x-app-layout title="Webinars">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-slate-900">Webinars</h1>
<p class="mt-1 text-sm text-slate-500">Stage-mode events billed at GHS {{ number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2) }} per participant.</p>
</div>
<a href="{{ route('meet.webinars.create') }}" class="btn-primary">Schedule webinar</a>
</div>
<div class="mt-4 space-y-2">
@forelse ($webinars as $room)
@include('meet.partials.room-card', [
'room' => $room,
'detailsRoute' => route('meet.webinars.show', $room),
])
@empty
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No webinars yet.</p>
@endforelse
</div>
@include('meet.partials.index-page', [
'badge' => 'Stage mode · Q&A · Registration',
'title' => 'Your webinars',
'description' => 'Host stage-mode events for large audiences. Billed at GHS '.number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2).' per participant.',
'createUrl' => route('meet.webinars.create'),
'createLabel' => 'Schedule webinar',
'stats' => $stats,
'listTitle' => 'Your webinars',
'emptyMessage' => 'No webinars yet. Schedule your first webinar to get started.',
'items' => $items,
'icon' => 'meet-icons/webinar.svg',
'iconBg' => 'bg-indigo-100',
])
<div class="mt-4">{{ $webinars->links() }}</div>
</x-app-layout>