Redesign recording show page with subtle buttons and status badges.
Deploy Ladill Meet / deploy (push) Successful in 49s
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:
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user