Skip to main content

Data display

Team Members code

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

resources/views/components/ui/team-members.blade.php

@props([
    'title' => 'Team members',
    'description' => null,
    'members' => [],
    'invites' => [],
    'inviteLabel' => 'Invite member',
    'inviteHref' => null,
    'roleVariants' => [],
])

@php
    /*
    | Team / members management panel. Lists active members with an avatar, name,
    | email, role badge and a per-row actions menu, plus an optional "Pending
    | invites" group with resend / revoke actions. Use the `actions` slot for a
    | custom header action or set `inviteHref` for the default invite button.
    |
    | Each member: [
    |   'name' => ?, 'email' => ?, 'avatar' => ?(src), 'role' => ?, 'status' => ?,
    |   'you' => false, 'href' => ?(manage link),
    | ]
    | Each invite: ['email' => ?, 'role' => ?, 'sentAt' => ?]
    |
    | Usage:
    |   <x-ui.team-members invite-href="/team/invite" :members="[
    |       ['name' => 'Ada Lovelace', 'email' => 'ada@example.com', 'role' => 'Owner', 'you' => true],
    |       ['name' => 'Grace Hopper', 'email' => 'grace@example.com', 'role' => 'Admin'],
    |   ]" :invites="[['email' => 'alan@example.com', 'role' => 'Member', 'sentAt' => '2 days ago']]" />
    */
    $defaultRoleVariants = [
        'owner' => 'brand',
        'admin' => 'blue',
        'member' => 'gray',
        'billing' => 'amber',
        'viewer' => 'gray',
        'guest' => 'gray',
    ];
    $roleVariantMap = array_merge($defaultRoleVariants, array_change_key_case($roleVariants, CASE_LOWER));

    $roleVariant = fn (?string $role): string => $role
        ? ($roleVariantMap[mb_strtolower($role)] ?? 'gray')
        : 'gray';

    $normalizedMembers = collect($members)->map(fn (array $member): array => [
        'name' => $member['name'] ?? 'Unknown',
        'email' => $member['email'] ?? null,
        'avatar' => $member['avatar'] ?? $member['src'] ?? null,
        'role' => $member['role'] ?? null,
        'status' => $member['status'] ?? null,
        'you' => (bool) ($member['you'] ?? false),
        'href' => $member['href'] ?? null,
    ])->values();

    $normalizedInvites = collect($invites)->map(fn (array $invite): array => [
        'email' => $invite['email'] ?? '',
        'role' => $invite['role'] ?? null,
        'sentAt' => $invite['sentAt'] ?? $invite['sent_at'] ?? null,
    ])->values();
@endphp

<div {{ $attributes->merge(['class' => 'overflow-hidden rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900']) }}>
    {{-- Header --}}
    <div class="flex flex-col gap-4 border-b border-gray-200 p-5 dark:border-gray-800 sm:flex-row sm:items-center sm:justify-between">
        <div class="min-w-0">
            <div class="flex items-center gap-2">
                <h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
                <x-ui.badge variant="gray" size="sm">{{ $normalizedMembers->count() }}</x-ui.badge>
            </div>
            @if ($description)
                <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">{{ $description }}</p>
            @endif
        </div>
        <div class="shrink-0">
            @isset($actions)
                {{ $actions }}
            @elseif ($inviteHref)
                <x-ui.button :href="$inviteHref" size="sm" icon="user-plus">{{ $inviteLabel }}</x-ui.button>
            @endif
        </div>
    </div>

    {{-- Members --}}
    <ul class="divide-y divide-gray-100 dark:divide-gray-800">
        @forelse ($normalizedMembers as $member)
            <li class="flex items-center gap-3 p-4 transition hover:bg-gray-50/70 dark:hover:bg-gray-800/40 sm:px-5">
                <x-ui.avatar :name="$member['name']" :src="$member['avatar']" size="base" />

                <div class="min-w-0 flex-1">
                    <div class="flex flex-wrap items-center gap-1.5">
                        <p class="truncate text-sm font-medium text-gray-900 dark:text-white">{{ $member['name'] }}</p>
                        @if ($member['you'])
                            <span class="rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-gray-500 dark:bg-gray-800 dark:text-gray-400">You</span>
                        @endif
                    </div>
                    @if ($member['email'])
                        <p class="truncate text-sm text-gray-500 dark:text-gray-400">{{ $member['email'] }}</p>
                    @endif
                </div>

                @if ($member['role'])
                    <x-ui.badge :variant="$roleVariant($member['role'])" size="sm" class="hidden shrink-0 sm:inline-flex">{{ $member['role'] }}</x-ui.badge>
                @endif

                <div class="shrink-0">
                    <x-ui.dropdown align="right" width="48">
                        <x-slot:trigger>
                            <x-ui.icon-button icon="dots-three-vertical" size="sm" label="Manage {{ $member['name'] }}" />
                        </x-slot:trigger>
                        @if ($member['href'])
                            <x-ui.dropdown.item :href="$member['href']" icon="user">View profile</x-ui.dropdown.item>
                        @endif
                        <x-ui.dropdown.item icon="shield-check">Change role</x-ui.dropdown.item>
                        @unless ($member['you'])
                            <x-ui.dropdown.item icon="sign-out" variant="danger">Remove</x-ui.dropdown.item>
                        @endunless
                    </x-ui.dropdown>
                </div>
            </li>
        @empty
            <li class="px-6 py-12 text-center text-sm text-gray-400 dark:text-gray-500">No members yet.</li>
        @endforelse
    </ul>

    {{-- Pending invites --}}
    @if ($normalizedInvites->isNotEmpty())
        <div class="border-t border-gray-200 dark:border-gray-800">
            <p class="bg-gray-50/60 px-5 py-2.5 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:bg-gray-900/40 dark:text-gray-500">
                Pending invites
            </p>
            <ul class="divide-y divide-gray-100 dark:divide-gray-800">
                @foreach ($normalizedInvites as $invite)
                    <li class="flex items-center gap-3 p-4 sm:px-5">
                        <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-dashed border-gray-300 text-gray-400 dark:border-gray-700 dark:text-gray-500">
                            <x-ui.icon name="envelope-simple" size="base" />
                        </span>

                        <div class="min-w-0 flex-1">
                            <p class="truncate text-sm font-medium text-gray-700 dark:text-gray-200">{{ $invite['email'] }}</p>
                            <p class="text-xs text-gray-400 dark:text-gray-500">
                                @if ($invite['role']){{ $invite['role'] }}@endif
                                @if ($invite['role'] && $invite['sentAt']) · @endif
                                @if ($invite['sentAt'])Invited {{ $invite['sentAt'] }}@endif
                            </p>
                        </div>

                        <x-ui.badge variant="amber" size="sm" icon="clock" class="hidden shrink-0 sm:inline-flex">Pending</x-ui.badge>

                        <div class="flex shrink-0 items-center gap-1">
                            <x-ui.button variant="ghost" size="sm" icon="paper-plane-tilt">Resend</x-ui.button>
                            <x-ui.icon-button icon="x" size="sm" variant="ghost" label="Revoke invite for {{ $invite['email'] }}" />
                        </div>
                    </li>
                @endforeach
            </ul>
        </div>
    @endif
</div>