Skip to main content

General

Email Viewer code

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

resources/views/components/ui/email-viewer.blade.php

@props([
    'subject' => null,
    'from' => null,
    'fromEmail' => null,
    'to' => [],
    'cc' => [],
    'date' => null,
    'labels' => [],
    'labelColors' => [],
    'attachments' => [],
    'backHref' => null,
    'backLabel' => 'Back',
    'emptyTitle' => 'Select a message',
    'emptyDescription' => 'Choose an email from the list to read it here.',
])

@php
    /*
    | Email reading pane for mailbox layouts. Shows headers, coloured labels, a
    | message body slot, attachment chips and a default reply toolbar.
    |
    | from/to/cc accept strings or ['name' => '...', 'email' => '...'] arrays.
    | labels accept strings or ['name' => '...', 'color' => '...'] arrays.
    | labelColors: map of label name to variant (gray, brand, green, red, amber, blue).
    | attachments: [['name' => '...', 'size' => '...', 'type' => 'pdf'], ...]
    |
    | Usage:
    |   <x-ui.email-viewer
    |       subject="Q3 launch timeline"
    |       :from="['name' => 'Ada Lovelace', 'email' => 'ada@example.com']"
    |       :to="[['name' => 'You', 'email' => 'you@example.com']]"
    |       date="Today, 10:42 AM"
    |       :labels="['Work', 'Launch']"
    |       :label-colors="['Work' => 'blue', 'Launch' => 'amber']"
    |       :attachments="[['name' => 'timeline.pdf', 'size' => '420 KB', 'type' => 'pdf']]"
    |   >
    |       <p>Email body copy…</p>
    |   </x-ui.email-viewer>
    */
    $labelVariants = ['gray', 'brand', 'green', 'red', 'amber', 'blue'];

    $resolvedLabels = collect($labels)
        ->map(function (mixed $label) use ($labelColors, $labelVariants): array {
            if (is_array($label)) {
                $name = (string) ($label['name'] ?? $label['label'] ?? '');
                $color = (string) ($label['color'] ?? $labelColors[$name] ?? 'gray');
            } else {
                $name = (string) $label;
                $color = (string) ($labelColors[$name] ?? 'gray');
            }

            if (! in_array($color, $labelVariants, true)) {
                $color = 'gray';
            }

            return ['name' => $name, 'color' => $color];
        })
        ->filter(fn (array $label): bool => $label['name'] !== '')
        ->values()
        ->all();
    $formatParticipant = function (mixed $participant): array {
        if (is_string($participant)) {
            return ['name' => $participant, 'email' => null];
        }

        if (! is_array($participant)) {
            return ['name' => 'Unknown', 'email' => null];
        }

        return [
            'name' => $participant['name'] ?? $participant['email'] ?? 'Unknown',
            'email' => $participant['email'] ?? null,
        ];
    };

    $fromParticipant = $from !== null ? $formatParticipant($from) : null;
    $toParticipants = collect($to)->map($formatParticipant)->values()->all();
    $ccParticipants = collect($cc)->map($formatParticipant)->values()->all();

    $formatAddressList = function (array $participants): string {
        return collect($participants)
            ->map(function (array $participant): string {
                if ($participant['email']) {
                    return $participant['name'] !== $participant['email']
                        ? "{$participant['name']} <{$participant['email']}>"
                        : $participant['email'];
                }

                return $participant['name'];
            })
            ->implode(', ');
    };

    $attachmentIcons = [
        'pdf' => 'file-pdf',
        'image' => 'image',
        'spreadsheet' => 'file-xls',
        'archive' => 'file-zip',
        'document' => 'file-doc',
        'code' => 'file-code',
        'text' => 'file-text',
    ];

    $hasMessage = filled($subject) || $fromParticipant !== null || ! $slot->isEmpty();
@endphp

<aside {{ $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']) }}>
    @if (! $hasMessage)
        <div class="flex h-full min-h-48 flex-1 flex-col items-center justify-center px-6 py-12 text-center">
            <span class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500">
                <x-ui.icon name="envelope-open" weight="duotone" size="2xl" />
            </span>
            <p class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">{{ $emptyTitle }}</p>
            <p class="mt-1 max-w-sm text-sm text-gray-500 dark:text-gray-400">{{ $emptyDescription }}</p>
        </div>
    @else
        <div class="border-b border-gray-200 px-4 py-4 dark:border-gray-800 sm:px-5">
            <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
                <div class="flex min-w-0 items-start gap-3">
                    @if ($backHref)
                        <a
                            href="{{ $backHref }}"
                            class="mt-0.5 inline-flex shrink-0 items-center justify-center rounded-lg p-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
                            aria-label="{{ $backLabel }}"
                        >
                            <x-ui.icon name="arrow-left" weight="bold" size="lg" />
                        </a>
                    @endif

                    <div class="min-w-0 flex-1">
                        <h2 class="text-base font-semibold text-gray-900 sm:text-lg dark:text-white">{{ $subject ?? 'No subject' }}</h2>

                        @if ($resolvedLabels !== [])
                            <div class="mt-2 flex flex-wrap gap-1.5">
                                @foreach ($resolvedLabels as $label)
                                    <x-ui.badge :variant="$label['color']" size="sm" dot>{{ $label['name'] }}</x-ui.badge>
                                @endforeach
                            </div>
                        @endif
                    </div>
                </div>

                @if ($date)
                    <p class="shrink-0 text-xs text-gray-400 sm:text-right dark:text-gray-500">{{ $date }}</p>
                @endif
            </div>
        </div>

        <div class="space-y-3 border-b border-gray-200 px-4 py-4 text-sm dark:border-gray-800 sm:px-5">
            @if ($fromParticipant)
                <div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
                    <span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">From</span>
                    <div class="min-w-0">
                        <p class="font-medium text-gray-900 dark:text-white">{{ $fromParticipant['name'] }}</p>
                        @if ($fromParticipant['email'])
                            <p class="break-all text-gray-500 dark:text-gray-400">{{ $fromParticipant['email'] }}</p>
                        @endif
                    </div>
                </div>
            @endif

            @if ($toParticipants !== [])
                <div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
                    <span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">To</span>
                    <p class="min-w-0 break-words text-gray-700 dark:text-gray-300">{{ $formatAddressList($toParticipants) }}</p>
                </div>
            @endif

            @if ($ccParticipants !== [])
                <div class="flex flex-col gap-1 sm:flex-row sm:gap-3">
                    <span class="w-10 shrink-0 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Cc</span>
                    <p class="min-w-0 break-words text-gray-700 dark:text-gray-300">{{ $formatAddressList($ccParticipants) }}</p>
                </div>
            @endif
        </div>

        <div class="flex items-center gap-2 overflow-x-auto border-b border-gray-200 px-4 py-3 dark:border-gray-800 sm:flex-wrap sm:px-5">
            @isset($actions)
                {{ $actions }}
            @else
                <x-ui.icon-button icon="arrow-bend-up-left" label="Reply" size="sm" class="shrink-0 sm:hidden" />
                <x-ui.button type="button" size="sm" icon="arrow-bend-up-left" class="hidden shrink-0 sm:inline-flex">Reply</x-ui.button>
                <x-ui.icon-button icon="arrow-bend-double-up-left" label="Reply all" size="sm" variant="outline" class="shrink-0 sm:hidden" />
                <x-ui.button type="button" variant="outline" size="sm" icon="arrow-bend-double-up-left" class="hidden shrink-0 sm:inline-flex">Reply all</x-ui.button>
                <x-ui.icon-button icon="arrow-bend-up-right" label="Forward" size="sm" variant="outline" class="shrink-0 sm:hidden" />
                <x-ui.button type="button" variant="outline" size="sm" icon="arrow-bend-up-right" class="hidden shrink-0 sm:inline-flex">Forward</x-ui.button>
                <x-ui.dropdown align="right" width="48">
                    <x-slot:trigger>
                        <x-ui.icon-button icon="dots-three" label="More actions" size="sm" variant="outline" class="shrink-0" />
                    </x-slot:trigger>
                    <x-ui.dropdown.item icon="archive">Archive</x-ui.dropdown.item>
                    <x-ui.dropdown.item icon="tag">Add label</x-ui.dropdown.item>
                    <x-ui.dropdown.item icon="trash" variant="danger">Delete</x-ui.dropdown.item>
                </x-ui.dropdown>
            @endisset
        </div>

        <div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-5 sm:px-5">
            <div class="space-y-4 text-sm leading-7 text-gray-700 dark:text-gray-300">
                {{ $slot }}
            </div>

            @if ($attachments !== [])
                <div class="mt-6 border-t border-gray-200 pt-5 dark:border-gray-800">
                    <p class="mb-3 text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
                        {{ count($attachments) }} attachment{{ count($attachments) === 1 ? '' : 's' }}
                    </p>

                    <div class="grid gap-2 sm:grid-cols-2">
                        @foreach ($attachments as $attachment)
                            @php
                                $type = $attachment['type'] ?? 'file';
                                $icon = $attachmentIcons[$type] ?? 'file';
                            @endphp
                            <a
                                href="{{ $attachment['href'] ?? '#' }}"
                                class="flex items-center gap-3 rounded-xl border border-gray-200 bg-gray-50 px-3 py-3 transition hover:border-gray-300 hover:bg-white dark:border-gray-800 dark:bg-gray-950/40 dark:hover:border-gray-700 dark:hover:bg-gray-900"
                            >
                                <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white text-gray-500 ring-1 ring-gray-200 dark:bg-gray-900 dark:text-gray-300 dark:ring-gray-700">
                                    <x-ui.icon :name="$icon" weight="fill" size="lg" />
                                </span>
                                <span class="min-w-0">
                                    <span class="block truncate text-sm font-medium text-gray-900 dark:text-white">{{ $attachment['name'] ?? 'Attachment' }}</span>
                                    @if (! empty($attachment['size']))
                                        <span class="block text-xs text-gray-500 dark:text-gray-400">{{ $attachment['size'] }}</span>
                                    @endif
                                </span>
                            </a>
                        @endforeach
                    </div>
                </div>
            @endif
        </div>

        @isset($footer)
            <div class="border-t border-gray-200 px-5 py-4 dark:border-gray-800">
                {{ $footer }}
            </div>
        @endisset
    @endif
</aside>