CluPilotCloud/resources/views/livewire/admin/mail-templates.blade.php

162 lines
10 KiB
PHP

{{--
Thirteen answers on one screen.
The first version was one column: name, subject, two clamped lines of body
and two full-width buttons per entry. Thirteen of those is four screens of
scrolling to find the one you want, and the body preview was the least
useful part of it the whole text is one click away in the modal anyway.
So: two columns of one-line entries, actions as icons that do not compete
with the name, the create form folded away behind a button, and the
placeholder list as a compact grid rather than a tall sidebar that pushed the
entries into half the window.
--}}
<div class="mx-auto max-w-[1120px] space-y-5" x-data="{ creating: false }">
<div class="flex flex-wrap items-start gap-4 animate-rise">
<div class="min-w-0">
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('templates.title') }}</h1>
<p class="mt-1 max-w-[75ch] text-sm text-muted">{{ __('templates.subtitle') }}</p>
</div>
<x-ui.button variant="primary" class="ml-auto" x-on:click="creating = !creating">
<x-ui.icon name="plus" class="size-4" />{{ __('templates.new') }}
</x-ui.button>
</div>
{{-- Folded away by default. A form that is always open is a screen of form
between the operator and the thing they came for and creating a
template is the rarer of the two things done here. It stays a form that
IS the page (R20 exempts exactly that); only its visibility changed. --}}
<form wire:submit="create" x-show="creating" x-cloak x-transition
class="space-y-4 rounded-lg border border-accent-border bg-surface p-6 shadow-xs">
<div class="grid gap-4 sm:grid-cols-2">
<x-ui.input name="name" wire:model="name" :label="__('templates.name')" :placeholder="__('templates.name_hint')" />
<x-ui.input name="subject" wire:model="subject" :label="__('templates.subject')" :placeholder="__('templates.subject_hint')" />
</div>
<div>
<label for="body" class="block text-sm font-medium text-body">{{ __('templates.body') }}</label>
<textarea id="body" rows="8" wire:model="body"
class="mt-1.5 block w-full rounded border border-line bg-surface px-3.5 py-2.5 text-sm text-ink placeholder:text-faint"
placeholder="{{ __('templates.body_hint') }}"></textarea>
@error('body')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
</div>
<div class="flex flex-wrap gap-2">
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="create">
{{ __('templates.create') }}
</x-ui.button>
<x-ui.button variant="secondary" x-on:click="creating = false">{{ __('templates.cancel') }}</x-ui.button>
</div>
</form>
{{-- The placeholders, as a grid across the page rather than a column beside
it. Rendered from the renderer's own list, so a token added there
appears here without being written down twice. --}}
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<h2 class="font-semibold text-ink">{{ __('templates.placeholders') }}</h2>
<p class="text-xs text-muted">{{ __('templates.placeholders_note') }}</p>
</div>
<dl class="mt-4 grid gap-x-6 gap-y-3 sm:grid-cols-2 xl:grid-cols-4">
@foreach ($placeholders as $token => $explanation)
{{-- Assembled in PHP, not inline: Blade's echo tag ends at the
first }} it finds, so a literal one inside the expression
cuts the tag in half and compiles to nonsense. --}}
@php $literal = '{{'.$token.'}}'; @endphp
<div class="min-w-0">
<dt class="select-all font-mono text-xs font-semibold text-accent-text">{{ $literal }}</dt>
<dd class="mt-0.5 text-xs leading-snug text-muted">{{ __($explanation) }}</dd>
</div>
@endforeach
</dl>
<p class="mt-4 border-t border-line pt-3 text-xs leading-relaxed text-muted">{{ __('templates.unknown_note') }}</p>
</div>
{{-- The entries. Two columns, one line each: what an operator scans for is
the name, and the subject is the only other thing that tells two similar
ones apart. The text itself is in the modal. --}}
@if ($templates->isEmpty())
<div class="rounded-lg border border-line bg-surface p-10 text-center shadow-xs animate-rise">
<p class="text-sm text-muted">{{ __('templates.empty') }}</p>
</div>
@else
<div class="grid gap-3 lg:grid-cols-2 animate-rise [animation-delay:120ms]">
@foreach ($templates as $template)
{{-- `opening` is local to the row: fetching the modal is a round
trip, and a click with nothing to show for it reads as a
click that missed. It clears when the modal is up (the
package announces that) and on a timer as well, so a request
that never answers cannot leave the row spinning for ever. --}}
<div wire:key="tpl-{{ $template->uuid }}"
x-data="{ opening: false }"
x-on:modal-opened.window="opening = false"
x-on:modalclosed.window="opening = false"
wire:loading.class="opacity-60"
wire:target="move('{{ $template->uuid }}', -1), move('{{ $template->uuid }}', 1), toggleActive('{{ $template->uuid }}')"
@class([
'group flex items-center gap-3 rounded-lg border bg-surface px-4 py-3 shadow-xs transition-colors',
'border-line hover:border-accent-border' => $template->active,
// A retired one stays visible and stays quiet.
'border-line bg-surface-2' => ! $template->active,
])>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-baseline gap-x-2">
<span @class([
'truncate text-sm font-medium',
'text-ink' => $template->active,
'text-muted line-through' => ! $template->active,
])>{{ $template->name }}</span>
@if (! $template->active)
<span class="text-[10px] font-semibold uppercase tracking-wider text-warning">{{ __('templates.retired') }}</span>
@endif
</div>
<p class="truncate text-xs text-muted">{{ $template->subject }}</p>
</div>
{{-- Every button reports its own click: pressed state on
the way down, disabled while the request is out. The
global bar at the top of the window says that SOMETHING
is happening; these say which thing. --}}
<div class="flex shrink-0 items-center gap-0.5">
<button type="button" wire:click="move('{{ $template->uuid }}', -1)"
wire:loading.attr="disabled" wire:target="move('{{ $template->uuid }}', -1)"
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-40"
aria-label="{{ __('templates.up') }}" title="{{ __('templates.up') }}">
<x-ui.icon name="chevron-down" class="size-4 rotate-180" />
</button>
<button type="button" wire:click="move('{{ $template->uuid }}', 1)"
wire:loading.attr="disabled" wire:target="move('{{ $template->uuid }}', 1)"
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-40"
aria-label="{{ __('templates.down') }}" title="{{ __('templates.down') }}">
<x-ui.icon name="chevron-down" class="size-4" />
</button>
<button type="button" wire:click="toggleActive('{{ $template->uuid }}')"
wire:loading.attr="disabled" wire:target="toggleActive('{{ $template->uuid }}')"
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-warning active:bg-warning-bg disabled:opacity-40"
aria-label="{{ $template->active ? __('templates.retire') : __('templates.restore') }}"
title="{{ $template->active ? __('templates.retire') : __('templates.restore') }}">
<x-ui.icon :name="$template->active ? 'x' : 'refresh'" class="size-4" />
</button>
{{-- R20: editing opens a modal. A five-line textarea in a
list row is the height jump the rule exists to stop.
The spinner is this button's own: opening the modal
is a Livewire request made by the MODAL component,
so wire:loading on this page never sees it. --}}
<button type="button"
x-bind:disabled="opening"
x-on:click="opening = true; setTimeout(() => opening = false, 6000); $dispatch('openModal', { component: 'admin.edit-mail-template', arguments: { uuid: '{{ $template->uuid }}' } })"
class="rounded p-1.5 text-muted transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-60"
aria-label="{{ __('templates.edit') }}" title="{{ __('templates.edit') }}">
<x-ui.icon name="pencil" class="size-4" x-show="!opening" />
<x-ui.icon name="refresh" class="size-4 animate-spin text-accent-text" x-show="opening" x-cloak />
</button>
</div>
</div>
@endforeach
</div>
@endif
</div>