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>