Redesign recording show page with subtle buttons and status badges.
Deploy Ladill Meet / deploy (push) Successful in 49s

Matches meeting detail layout, uses btn-secondary actions, and adds confirm dialog for delete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 17:47:44 +00:00
co-authored by Cursor
parent b765b5dd97
commit 827d36ddc1
3 changed files with 194 additions and 22 deletions
@@ -39,9 +39,12 @@ class RecordingController extends Controller
{ {
$this->authorizeAbility($request, 'meetings.view'); $this->authorizeAbility($request, 'meetings.view');
$this->authorizeOwner($request, $recording); $this->authorizeOwner($request, $recording);
$recording->load(['session.room', 'session.participants']); $recording->load(['session.room', 'session.aiSummaries', 'session.participants']);
return view('meet.recordings.show', compact('recording')); $canManage = app(\App\Services\Meet\MeetPermissions::class)
->can($this->member($request), 'meetings.manage');
return view('meet.recordings.show', compact('recording', 'canManage'));
} }
public function download(Request $request, Recording $recording): StreamedResponse public function download(Request $request, Recording $recording): StreamedResponse
@@ -0,0 +1,69 @@
@props([
'name',
'title',
'message' => null,
'action',
'method' => 'POST',
'confirmLabel' => 'Confirm',
'cancelLabel' => 'Cancel',
'variant' => 'danger',
])
@php
$confirmBtnClass = $variant === 'danger'
? 'bg-red-600 hover:bg-red-700'
: 'bg-violet-600 hover:bg-violet-700';
$iconWrapClass = $variant === 'danger'
? 'bg-red-100 text-red-600'
: 'bg-violet-100 text-violet-600';
@endphp
@if(isset($trigger))
<span @click.stop="$dispatch('open-modal', {{ Js::from($name) }})" class="inline-flex">
{{ $trigger }}
</span>
@endif
<x-modal :name="$name" maxWidth="md">
<div class="px-5 pb-6 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
<div class="flex flex-col items-center text-center sm:items-start sm:text-left">
<div @class(['flex h-12 w-12 items-center justify-center rounded-full', $iconWrapClass])>
@if($variant === 'danger')
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
</svg>
@else
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"/>
</svg>
@endif
</div>
<h2 class="mt-4 text-lg font-semibold text-slate-900">{{ $title }}</h2>
@if($message)
<p class="mt-2 text-sm leading-relaxed text-slate-500">{{ $message }}</p>
@endif
@isset($details)
<div class="mt-3 w-full">{{ $details }}</div>
@endisset
</div>
<form method="post" action="{{ $action }}" class="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
@csrf
@if(strtoupper($method) !== 'POST')
@method($method)
@endif
@isset($fields)
{{ $fields }}
@endisset
<button type="button"
@click="$dispatch('close-modal', {{ Js::from($name) }})"
class="btn-secondary btn-secondary-sm">
{{ $cancelLabel }}
</button>
<button type="submit"
@class(['rounded-full px-4 py-2 text-sm font-semibold text-white transition', $confirmBtnClass])>
{{ $confirmLabel }}
</button>
</form>
</div>
</x-modal>
+120 -20
View File
@@ -1,27 +1,127 @@
@php
$room = $recording->session->room;
$statusLabel = config('meet.recording_statuses')[$recording->status] ?? ucfirst($recording->status);
$statusClass = match ($recording->status) {
'ready' => 'bg-emerald-50 text-emerald-700',
'processing' => 'bg-amber-50 text-amber-700',
'recording' => 'bg-red-50 text-red-700',
'failed' => 'bg-red-50 text-red-700',
default => 'bg-slate-100 text-slate-600',
};
$summary = $recording->session->aiSummaries->where('status', 'ready')->sortByDesc('created_at')->first();
$fileExtension = $recording->storage_path ? strtoupper(pathinfo($recording->storage_path, PATHINFO_EXTENSION)) : null;
$fileSizeLabel = null;
if ($recording->file_size) {
$bytes = (int) $recording->file_size;
if ($bytes >= 1073741824) {
$fileSizeLabel = number_format($bytes / 1073741824, 2).' GB';
} elseif ($bytes >= 1048576) {
$fileSizeLabel = number_format($bytes / 1048576, 1).' MB';
} elseif ($bytes >= 1024) {
$fileSizeLabel = number_format($bytes / 1024, 1).' KB';
} else {
$fileSizeLabel = $bytes.' B';
}
}
@endphp
<x-app-layout title="Recording"> <x-app-layout title="Recording">
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<h1 class="text-2xl font-semibold text-slate-900">{{ $recording->session->room->title }}</h1> <a href="{{ route('meet.recordings.index') }}" class="inline-flex items-center gap-1.5 text-sm text-slate-500 transition hover:text-slate-700">
<p class="mt-1 text-sm text-slate-500">{{ config('meet.recording_statuses')[$recording->status] ?? $recording->status }}</p> <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/>
</svg>
Recordings
</a>
<div class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6"> <div class="mt-4 flex items-start justify-between gap-4">
@if ($recording->duration_seconds) <div class="min-w-0">
<p class="text-sm text-slate-600">Duration: {{ gmdate('H:i:s', $recording->duration_seconds) }}</p> <h1 class="text-2xl font-semibold text-slate-900">{{ $room->title }}</h1>
@endif <p class="mt-1 text-sm text-slate-500">
@if ($recording->isReady()) {{ $recording->started_at?->timezone($room->timezone)->format('l, M j, Y · g:i A') ?? $recording->created_at->format('M j, Y g:i A') }}
<a href="{{ route('meet.recordings.download', $recording) }}" class="btn-primary inline-block">Download recording</a> </p>
@else </div>
<p class="text-sm text-amber-700">Recording is still processing. Refresh shortly.</p> <span class="shrink-0 rounded-full px-2.5 py-1 text-xs font-medium {{ $statusClass }}">{{ $statusLabel }}</span>
@endif
@php $summary = $recording->session->aiSummaries()->latest()->first(); @endphp
@if ($summary?->status === 'ready')
<a href="{{ route('meet.summaries.show', $summary) }}" class="block text-sm text-indigo-600 hover:underline">View AI summary </a>
@endif
</div> </div>
<form method="POST" action="{{ route('meet.recordings.destroy', $recording) }}" class="mt-4" onsubmit="return confirm('Delete this recording?')"> <div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
@csrf @method('DELETE') <dl class="grid gap-4 sm:grid-cols-2">
<button type="submit" class="text-sm text-red-600 hover:underline">Delete recording</button> @if ($recording->duration_seconds)
</form> <div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Duration</dt>
<dd class="mt-1 text-sm font-medium text-slate-900">{{ gmdate('H:i:s', $recording->duration_seconds) }}</dd>
</div>
@endif
@if ($fileSizeLabel)
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-500">File size</dt>
<dd class="mt-1 text-sm font-medium text-slate-900">{{ $fileSizeLabel }}@if ($fileExtension) <span class="font-normal text-slate-500">· {{ $fileExtension }}</span>@endif</dd>
</div>
@endif
@if ($recording->ended_at)
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Ended</dt>
<dd class="mt-1 text-sm font-medium text-slate-900">{{ $recording->ended_at->timezone($room->timezone)->format('M j, Y g:i A') }}</dd>
</div>
@endif
<div>
<dt class="text-xs font-medium uppercase tracking-wide text-slate-500">Layout</dt>
<dd class="mt-1 text-sm font-medium capitalize text-slate-900">{{ $recording->layout ?? 'gallery' }}</dd>
</div>
</dl>
@if ($recording->isReady())
<p class="mt-5 text-sm text-slate-600">Your recording is stored on Ladill and ready to download.</p>
@elseif ($recording->status === 'processing')
<div class="mt-5 flex items-start gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
<svg class="mt-0.5 h-5 w-5 shrink-0 animate-spin text-amber-600" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<div>
<p class="text-sm font-medium text-amber-900">Processing recording</p>
<p class="mt-0.5 text-sm text-amber-800">This usually takes a moment after the meeting ends. Refresh shortly.</p>
</div>
</div>
@elseif ($recording->status === 'failed')
<div class="mt-5 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
{{ $recording->failure_reason ?? 'This recording could not be processed.' }}
</div>
@endif
<div class="btn-group mt-6">
@if ($recording->isReady())
<a href="{{ route('meet.recordings.download', $recording) }}" class="btn-secondary btn-secondary-sm">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12M12 16.5V3"/>
</svg>
Download
</a>
@endif
<a href="{{ route('meet.rooms.show', $room) }}" class="btn-secondary btn-secondary-sm">View meeting</a>
@if ($summary)
<a href="{{ route('meet.summaries.show', $summary) }}" class="btn-secondary btn-secondary-sm">AI summary</a>
@endif
</div>
</div>
@if ($canManage)
<div class="mt-4">
<x-confirm-dialog
:name="'delete-recording-'.$recording->id"
title="Delete this recording?"
message="This permanently removes the recording file from Ladill storage. This cannot be undone."
:action="route('meet.recordings.destroy', $recording)"
method="DELETE"
confirm-label="Delete recording"
>
<x-slot:trigger>
<button type="button" class="btn-secondary btn-secondary-sm text-red-600 hover:border-red-200 hover:bg-red-50 hover:text-red-700">
Delete recording
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@endif
</div> </div>
</x-app-layout> </x-app-layout>