Skip to main content

Media

Audio Playlist code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/audio-playlist.blade.php

@props([
    'items' => [],
    'title' => 'Up next',
    'player' => 'default',
    'activeId' => null,
])

@php
    /*
    | Audio playlist / queue for <x-ui.audio-player> and <x-ui.audio-wave-card>.
    | Selecting a track dispatches `audio-source` scoped by `player`.
    |
    | Usage:
    |   <x-ui.audio-playlist player="radio" active-id="ep-12" :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,
            'artwork' => $item['artwork'] ?? ($item['poster'] ?? null),
            'duration' => $item['duration'] ?? null,
            'artist' => $item['artist'] ?? ($item['uploader'] ?? 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('audio-source', {
                player: this.player,
                src: item.src,
                artwork: item.artwork,
                title: item.title,
                artist: item.artist,
                play: true,
            });
        },
    }"
    {{ $attributes->merge(['class' => 'flex min-h-0 min-w-0 max-w-full 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-3 py-3 sm:px-4 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> tracks
            </p>
        </div>
    </div>

    <div class="max-h-[24rem] space-y-1.5 overflow-y-auto p-2 sm:max-h-[32rem] sm:space-y-2 sm:p-3">
        <template x-for="item in items" :key="item.id">
            <button
                type="button"
                @click="select(item)"
                class="group flex w-full min-w-0 items-center 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 h-12 w-12 shrink-0 overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800 sm:h-14 sm:w-14">
                    <template x-if="item.artwork">
                        <img :src="item.artwork" alt="" class="h-full w-full object-cover">
                    </template>
                    <template x-if="! item.artwork">
                        <div class="flex h-full w-full items-center justify-center text-gray-400 dark:text-gray-500">
                            <x-ui.icon name="music-notes" weight="fill" size="lg" />
                        </div>
                    </template>
                </div>

                <div class="min-w-0 flex-1">
                    <p class="truncate text-sm font-semibold text-gray-900 dark:text-white" x-text="item.title"></p>
                    <p class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
                        <span x-show="item.artist" x-text="item.artist"></span>
                        <span x-show="item.artist && item.duration"> · </span>
                        <span x-show="item.duration" class="font-mono tabular-nums" x-text="item.duration"></span>
                    </p>
                </div>

                <span
                    x-show="activeId === item.id"
                    class="inline-flex shrink-0 items-center gap-1 rounded-md bg-brand-600 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white"
                >
                    <x-ui.icon name="play" weight="fill" size="xs" />
                    Now
                </span>
            </button>
        </template>

        <template x-if="items.length === 0">
            <p class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">No tracks in this queue yet.</p>
        </template>
    </div>
</section>