Media
Video Playlist code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/video-playlist.blade.php
@props([
'items' => [],
'title' => 'Up next',
'player' => 'default',
'activeId' => null,
'orientation' => 'vertical',
])
@php
/*
| Playlist / up-next queue for <x-ui.video-player>. Each item:
| id, title, src, poster?, duration?, uploader?, views?, active?
|
| Selecting an item dispatches `video-source` scoped by `player`.
|
| Usage:
| <x-ui.video-playlist player="watch" :active-id="$id" :items="[...]" />
*/
$normalized = collect($items)->map(function ($item, $index) use ($activeId) {
$id = (string) ($item['id'] ?? $index);
return [
'id' => $id,
'title' => $item['title'] ?? 'Untitled',
'src' => $item['src'] ?? null,
'poster' => $item['poster'] ?? null,
'duration' => $item['duration'] ?? null,
'uploader' => $item['uploader'] ?? null,
'uploaderAvatar' => $item['uploaderAvatar'] ?? ($item['uploader_avatar'] ?? null),
'views' => $item['views'] ?? null,
'uploadedAt' => $item['uploadedAt'] ?? ($item['uploaded_at'] ?? null),
'active' => array_key_exists('active', $item)
? (bool) $item['active']
: ($activeId !== null && (string) $activeId === $id),
];
})->values()->all();
@endphp
<section
x-data="{
player: @js((string) $player),
items: @js($normalized),
activeId: @js($activeId !== null ? (string) $activeId : ($normalized[0]['id'] ?? null)),
select(item) {
if (! item?.src) { return; }
this.activeId = item.id;
this.$dispatch('video-source', {
player: this.player,
src: item.src,
poster: item.poster,
title: item.title,
play: true,
});
},
}"
{{ $attributes->merge(['class' => 'flex min-h-0 flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900']) }}
aria-label="{{ $title }}"
>
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-800">
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">
<span x-text="items.length"></span> videos
</p>
</div>
</div>
<div class="max-h-[28rem] space-y-2 overflow-y-auto p-2 sm:max-h-[36rem] sm:p-3">
<template x-for="item in items" :key="item.id">
<button
type="button"
@click="select(item)"
class="group flex w-full items-stretch gap-3 rounded-xl border border-transparent p-2 text-start transition hover:border-gray-200 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:hover:border-gray-800 dark:hover:bg-gray-950/60"
:class="activeId === item.id ? 'border-brand-200 bg-brand-50/70 dark:border-brand-900 dark:bg-brand-950/40' : ''"
:aria-current="activeId === item.id ? 'true' : 'false'"
>
<div class="relative aspect-video w-28 shrink-0 overflow-hidden rounded-lg bg-gray-100 sm:w-32 dark:bg-gray-800">
<template x-if="item.poster">
<img :src="item.poster" alt="" class="h-full w-full object-cover">
</template>
<template x-if="! item.poster">
<div class="flex h-full w-full items-center justify-center text-gray-400">
<i class="ph-fill ph-play text-lg" aria-hidden="true"></i>
</div>
</template>
<span
x-show="item.duration"
x-cloak
class="absolute bottom-1 end-1 rounded bg-black/75 px-1 py-0.5 font-mono text-[10px] text-white"
x-text="item.duration"
></span>
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="line-clamp-2 text-sm font-semibold text-gray-900 dark:text-white" x-text="item.title"></p>
<p class="mt-1 truncate text-xs text-gray-500 dark:text-gray-400">
<span x-show="item.uploader" x-text="item.uploader"></span>
<span x-show="item.uploader && item.views"> · </span>
<span x-show="item.views" x-text="item.views + ' views'"></span>
</p>
<p x-show="activeId === item.id" x-cloak class="mt-1 text-[11px] font-semibold uppercase tracking-wide text-brand-600 dark:text-brand-400">Playing</p>
</div>
</button>
</template>
<div x-show="items.length === 0" x-cloak class="px-3 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
No videos in this playlist.
</div>
</div>
</section>