Wire space listener email invites and restyle Meet index pages.
Deploy Ladill Meet / deploy (push) Successful in 42s
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:
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Meet\Concerns;
|
||||
|
||||
use App\Models\Room;
|
||||
use App\Services\Billing\BillingClient;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait BuildsMeetIndexPage
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{value: string, label: string}>
|
||||
*/
|
||||
protected function meetIndexStats(Request $request, Builder $roomQuery, string $countLabel, string $secondaryLabel): array
|
||||
{
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$balance = '—';
|
||||
try {
|
||||
$minor = app(BillingClient::class)->balanceMinor($owner);
|
||||
$currency = (string) config('billing.currency', 'GHS');
|
||||
$balance = $currency.' '.number_format($minor / 100, 2);
|
||||
} catch (\Throwable) {
|
||||
// Wallet API unavailable — show placeholder.
|
||||
}
|
||||
|
||||
$total = (clone $roomQuery)->count();
|
||||
$live = (clone $roomQuery)->where('status', 'live')->count();
|
||||
|
||||
return [
|
||||
['value' => $balance, 'label' => 'Account balance'],
|
||||
['value' => (string) $total, 'label' => $countLabel],
|
||||
['value' => (string) $live, 'label' => $secondaryLabel],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{title: string, subtitle: string, meta: string, status: string, url: string, active: bool}>
|
||||
*/
|
||||
protected function meetIndexItems(iterable $rooms, callable $detailsUrl): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($rooms as $room) {
|
||||
/** @var Room $room */
|
||||
$typeLabel = match (true) {
|
||||
$room->isSpace() => 'Room',
|
||||
$room->isWebinar() => 'Webinar',
|
||||
$room->isConference() => 'Conference',
|
||||
default => ucfirst(config('meet.room_types')[$room->type] ?? $room->type),
|
||||
};
|
||||
|
||||
$schedule = $room->scheduled_at
|
||||
? $room->scheduled_at->timezone($room->timezone)->format('M j, Y g:i A')
|
||||
: ($room->isConference() ? 'Instant conference' : ($room->isSpace() ? 'Audio room' : 'Instant meeting'));
|
||||
|
||||
$statusLabel = config('meet.room_statuses')[$room->status] ?? ucfirst($room->status);
|
||||
|
||||
$items[] = [
|
||||
'title' => $room->title,
|
||||
'subtitle' => $typeLabel.' · '.$room->joinUrl(),
|
||||
'meta' => $schedule,
|
||||
'status' => $statusLabel,
|
||||
'url' => $detailsUrl($room),
|
||||
'active' => $room->status !== 'cancelled' && $room->status !== 'ended',
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Member;
|
||||
use App\Models\Room;
|
||||
@@ -19,7 +20,7 @@ use Illuminate\View\View;
|
||||
|
||||
class ConferenceController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
use BuildsMeetIndexPage, ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected RoomService $rooms,
|
||||
@@ -44,7 +45,10 @@ class ConferenceController extends Controller
|
||||
|
||||
$conferences = $query->orderByDesc('scheduled_at')->paginate(20);
|
||||
|
||||
return view('meet.conferences.index', compact('conferences', 'organization'));
|
||||
$stats = $this->meetIndexStats($request, $query, 'Conferences', 'Live now');
|
||||
$items = $this->meetIndexItems($conferences, fn (Room $room) => route('meet.conferences.show', $room));
|
||||
|
||||
return view('meet.conferences.index', compact('conferences', 'organization', 'stats', 'items'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Room;
|
||||
use App\Services\Meet\AttendanceService;
|
||||
@@ -22,7 +23,7 @@ use Illuminate\View\View;
|
||||
|
||||
class RoomController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
use BuildsMeetIndexPage, ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected RoomService $rooms,
|
||||
@@ -47,7 +48,10 @@ class RoomController extends Controller
|
||||
|
||||
$rooms = $query->orderByDesc('scheduled_at')->paginate(20);
|
||||
|
||||
return view('meet.rooms.index', compact('rooms', 'organization'));
|
||||
$stats = $this->meetIndexStats($request, $query, 'Meetings', 'Live now');
|
||||
$items = $this->meetIndexItems($rooms, fn (Room $room) => route('meet.rooms.show', $room));
|
||||
|
||||
return view('meet.rooms.index', compact('rooms', 'organization', 'stats', 'items'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Member;
|
||||
use App\Models\Room;
|
||||
@@ -17,7 +18,7 @@ use Illuminate\View\View;
|
||||
|
||||
class SpaceController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
use BuildsMeetIndexPage, ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected RoomService $rooms,
|
||||
@@ -41,7 +42,10 @@ class SpaceController extends Controller
|
||||
|
||||
$spaces = $query->orderByDesc('updated_at')->paginate(20);
|
||||
|
||||
return view('meet.spaces.index', compact('spaces', 'organization'));
|
||||
$stats = $this->meetIndexStats($request, $query, 'Rooms', 'Live now');
|
||||
$items = $this->meetIndexItems($spaces, fn (Room $room) => route('meet.spaces.show', $room));
|
||||
|
||||
return view('meet.spaces.index', compact('spaces', 'organization', 'stats', 'items'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
@@ -76,7 +80,18 @@ class SpaceController extends Controller
|
||||
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
|
||||
'timezone' => ['nullable', 'timezone'],
|
||||
'passcode' => ['nullable', 'string', 'max:20'],
|
||||
'invite_emails' => ['nullable', 'string'],
|
||||
'invite_emails' => ['nullable', 'string', 'max:2000', function (string $attribute, mixed $value, \Closure $fail): void {
|
||||
if (! is_string($value) || trim($value) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (preg_split('/[\s,;]+/', $value) as $email) {
|
||||
$email = trim($email);
|
||||
if ($email !== '' && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$fail("Invalid email address: {$email}");
|
||||
}
|
||||
}
|
||||
}],
|
||||
'speaker_refs' => ['nullable', 'array'],
|
||||
'speaker_refs.*' => ['string', 'max:64'],
|
||||
'invite_only' => ['sometimes', 'boolean'],
|
||||
@@ -115,7 +130,7 @@ class SpaceController extends Controller
|
||||
if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) {
|
||||
$invites = collect(preg_split('/[\s,;]+/', $emails))
|
||||
->filter()
|
||||
->map(fn ($email) => ['email' => $email])
|
||||
->map(fn ($email) => ['email' => $email, 'role' => 'attendee'])
|
||||
->all();
|
||||
$this->invitations->inviteToRoom($room, $invites, $request->user());
|
||||
}
|
||||
@@ -124,7 +139,18 @@ class SpaceController extends Controller
|
||||
$this->calendar->syncRoomCreated($room, $request->user());
|
||||
}
|
||||
|
||||
return redirect()->route('meet.spaces.show', $room)->with('success', 'Room created.');
|
||||
return redirect()->route('meet.spaces.show', $room)->with('success', $this->spaceCreatedMessage($room, $validated['invite_emails'] ?? ''));
|
||||
}
|
||||
|
||||
protected function spaceCreatedMessage(Room $room, string $inviteEmails): string
|
||||
{
|
||||
$count = count(array_filter(preg_split('/[\s,;]+/', trim($inviteEmails)) ?: []));
|
||||
|
||||
if ($count === 0) {
|
||||
return 'Room created.';
|
||||
}
|
||||
|
||||
return "Room created. {$count} listener invitation(s) sent.";
|
||||
}
|
||||
|
||||
public function show(Request $request, Room $room): View
|
||||
@@ -133,7 +159,7 @@ class SpaceController extends Controller
|
||||
$this->authorizeOwner($request, $room);
|
||||
abort_unless($room->isSpace(), 404);
|
||||
|
||||
$room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
|
||||
$room->load(['sessions.participants', 'invitations' => fn ($q) => $q->orderByDesc('created_at'), 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
|
||||
|
||||
$speakerRefs = (array) $room->setting('speaker_refs', []);
|
||||
$speakers = Member::owned($this->ownerRef($request))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Room;
|
||||
use App\Services\Meet\CalendarService;
|
||||
@@ -15,7 +16,7 @@ use Illuminate\View\View;
|
||||
|
||||
class WebinarController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
use BuildsMeetIndexPage, ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected RoomService $rooms,
|
||||
@@ -38,7 +39,10 @@ class WebinarController extends Controller
|
||||
|
||||
$webinars = $query->orderByDesc('scheduled_at')->paginate(20);
|
||||
|
||||
return view('meet.webinars.index', compact('webinars', 'organization'));
|
||||
$stats = $this->meetIndexStats($request, $query, 'Webinars', 'Live now');
|
||||
$items = $this->meetIndexItems($webinars, fn (Room $room) => route('meet.webinars.show', $room));
|
||||
|
||||
return view('meet.webinars.index', compact('webinars', 'organization', 'stats', 'items'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
|
||||
Reference in New Issue
Block a user