Skip to main content

Overlays

Notification Center code

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

resources/views/components/ui/notification-center.blade.php

@props([
    'notifications' => [],
    'label' => 'Notifications',
    'align' => 'right',
    'emptyText' => "You're all caught up.",
    'viewAllHref' => null,
    'ghost' => false,
])

@php
    /*
    | Notification center — a bell trigger with a dropdown feed of recent
    | notifications, unread badge and a "Mark all read" action. Pass rows as:
    |   ['title' => '...', 'description' => '...', 'time' => '2m', 'icon' => 'git-branch',
    |    'unread' => true, 'href' => '/...', 'variant' => 'brand']
    |
    | Usage:
    |   <x-ui.notification-center :notifications="$rows" view-all-href="/notifications" />
    |   <x-ui.notification-center ghost />
    |
    | ghost — use a borderless icon-button trigger so the bell sits in a toolbar
    |         next to theme and account controls. An empty feed still opens the
    |         dropdown and shows emptyText.
    */
    $items = collect($notifications)->map(function (array $n, int $i): array {
        return [
            'id' => $n['id'] ?? 'n'.$i,
            'title' => $n['title'] ?? '',
            'description' => $n['description'] ?? null,
            'time' => $n['time'] ?? null,
            'icon' => $n['icon'] ?? 'bell',
            'unread' => (bool) ($n['unread'] ?? false),
            'href' => $n['href'] ?? null,
            'variant' => $n['variant'] ?? 'brand',
        ];
    })->values()->all();

    $menuAlign = $align === 'left' ? 'start-0' : 'end-0';

    $variantDot = [
        'brand' => 'bg-brand-500',
        'green' => 'bg-green-500',
        'amber' => 'bg-amber-500',
        'red' => 'bg-red-500',
        'blue' => 'bg-accent-500',
        'gray' => 'bg-gray-400',
    ];
    $variantIcon = [
        'brand' => 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/60',
        'green' => 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950/60',
        'amber' => 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60',
        'red' => 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60',
        'blue' => 'text-accent-600 dark:text-accent-400 bg-accent-50 dark:bg-accent-950/60',
        'gray' => 'text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-800',
    ];
@endphp

<div
    x-data="{
        open: false,
        items: @js($items),
        get unread() { return this.items.filter((n) => n.unread).length; },
        markAllRead() { this.items.forEach((n) => (n.unread = false)); },
    }"
    {{ $attributes->merge(['class' => 'relative inline-block']) }}
>
    <button
        type="button"
        @click="open = ! open"
        :aria-expanded="open.toString()"
        aria-haspopup="true"
        data-notification-trigger
        @class([
            'relative inline-flex cursor-pointer items-center justify-center transition focus:outline-none',
            'h-10 w-10 rounded-xl border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900 focus:ring-2 focus:ring-brand-500/30 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white' => ! $ghost,
            'h-9 w-9 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:ring-offset-gray-900' => $ghost,
        ])
        aria-label="{{ $label }}"
    >
        <x-ui.icon name="bell" weight="regular" size="lg" />
        <span
            x-show="unread > 0"
            x-cloak
            class="absolute -end-1 -top-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-[0.6875rem] font-bold text-white ring-2 ring-white dark:ring-gray-900"
            x-text="unread > 9 ? '9+' : unread"
        ></span>
    </button>

    <div
        x-show="open"
        x-cloak
        x-transition.origin.top
        @click.outside="open = false"
        @keydown.escape.window="open = false"
        class="absolute {{ $menuAlign }} z-40 mt-2 w-80 origin-top overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl shadow-gray-900/10 dark:border-gray-800 dark:bg-gray-900 sm:w-96"
        role="dialog"
        aria-label="{{ $label }}"
    >
        <div class="flex items-center justify-between gap-3 border-b border-gray-100 px-4 py-3 dark:border-gray-800">
            <p class="text-sm font-semibold text-gray-900 dark:text-white">
                {{ $label }}
                <span x-show="unread > 0" x-cloak class="ms-1 text-gray-400" x-text="'(' + unread + ')'"></span>
            </p>
            <button
                type="button"
                @click="markAllRead()"
                x-show="unread > 0"
                x-cloak
                class="text-xs font-semibold text-brand-600 transition hover:text-brand-500 dark:text-brand-400"
            >Mark all read</button>
        </div>

        <div class="max-h-96 divide-y divide-gray-100 overflow-y-auto dark:divide-gray-800">
            <template x-for="item in items" :key="item.id">
                <a
                    :href="item.href || '#'"
                    class="flex items-start gap-3 px-4 py-3 transition hover:bg-gray-50 dark:hover:bg-gray-800/60"
                    :class="item.unread ? 'bg-brand-50/40 dark:bg-brand-950/20' : ''"
                    @click="item.unread = false"
                >
                    <span
                        class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
                        :class="{
                            'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/60': item.variant === 'brand',
                            'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950/60': item.variant === 'green',
                            'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60': item.variant === 'amber',
                            'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60': item.variant === 'red',
                            'text-accent-600 dark:text-accent-400 bg-accent-50 dark:bg-accent-950/60': item.variant === 'blue',
                            'text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-800': item.variant === 'gray',
                        }"
                    >
                        <i :class="'ph-fill ph-' + item.icon + ' text-lg leading-none'" aria-hidden="true"></i>
                    </span>

                    <span class="min-w-0 flex-1">
                        <span class="flex items-center gap-2">
                            <span class="truncate text-sm font-semibold text-gray-900 dark:text-white" x-text="item.title"></span>
                            <span x-show="item.unread" x-cloak class="h-2 w-2 shrink-0 rounded-full bg-brand-500"></span>
                        </span>
                        <span x-show="item.description" x-cloak class="mt-0.5 block text-xs leading-5 text-gray-500 dark:text-gray-400" x-text="item.description"></span>
                        <span x-show="item.time" x-cloak class="mt-1 block text-[0.6875rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500" x-text="item.time"></span>
                    </span>
                </a>
            </template>

            <div x-show="items.length === 0" x-cloak data-notification-empty class="px-4 py-10 text-center">
                <x-ui.icon name="check-circle" weight="fill" size="xl" class="text-gray-300 dark:text-gray-600" />
                <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $emptyText }}</p>
            </div>
        </div>

        @if ($viewAllHref)
            <a href="{{ $viewAllHref }}" class="block border-t border-gray-100 px-4 py-3 text-center text-sm font-semibold text-brand-600 transition hover:bg-gray-50 dark:border-gray-800 dark:text-brand-400 dark:hover:bg-gray-800/60">
                View all notifications
            </a>
        @endif
    </div>
</div>