Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
resources/views/admin/invitations/create.blade.php
Executable file
49
resources/views/admin/invitations/create.blade.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<x-layouts.admin :title="__('admin.create_invitation')">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ __('admin.new_invitation') }}</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6 max-w-lg">
|
||||
<form method="POST" action="{{ route('admin.invitations.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.email_optional') }}</label>
|
||||
<input type="email" name="email" id="email" value="{{ old('email') }}"
|
||||
placeholder="{{ __('admin.email_optional_hint') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('email') border-red-500 @enderror">
|
||||
@error('email')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="expires_in_days" class="block text-sm font-medium text-gray-700 mb-1">{{ __('admin.valid_for_days') }} *</label>
|
||||
<input type="number" name="expires_in_days" id="expires_in_days" value="{{ old('expires_in_days', 7) }}" min="1" max="90" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md @error('expires_in_days') border-red-500 @enderror">
|
||||
@error('expires_in_days')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('admin.assign_players') }}</label>
|
||||
<p class="text-xs text-gray-500 mb-2">{{ __('admin.player_assignment_hint') }}</p>
|
||||
<div class="border border-gray-300 rounded-md max-h-60 overflow-y-auto p-2 space-y-1">
|
||||
@forelse ($players as $player)
|
||||
<label class="flex items-center px-2 py-1 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input type="checkbox" name="player_ids[]" value="{{ $player->id }}"
|
||||
{{ in_array($player->id, old('player_ids', [])) ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 mr-2">
|
||||
<span class="text-sm">{{ $player->full_name }}</span>
|
||||
<span class="text-xs text-gray-400 ml-auto">{{ $player->team->name }}</span>
|
||||
</label>
|
||||
@empty
|
||||
<p class="text-sm text-gray-400 px-2 py-1">{{ __('admin.no_active_players') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
@error('player_ids')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
@error('player_ids.*')<p class="mt-1 text-sm text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">{{ __('admin.create_invitation') }}</button>
|
||||
<a href="{{ route('admin.invitations.index') }}" class="text-sm text-gray-600 hover:underline">{{ __('ui.cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-layouts.admin>
|
||||
113
resources/views/admin/invitations/index.blade.php
Executable file
113
resources/views/admin/invitations/index.blade.php
Executable file
@@ -0,0 +1,113 @@
|
||||
<x-layouts.admin :title="__('admin.invitations_title')">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">{{ __('admin.invitations_title') }}</h1>
|
||||
<a href="{{ route('admin.invitations.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm font-medium">
|
||||
{{ __('admin.new_invitation') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('admin.invite_link') }}</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('ui.email') }}</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('admin.nav_players') }}</th>
|
||||
<th class="text-center px-4 py-3 font-medium text-gray-700">{{ __('admin.status') }}</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">{{ __('admin.created_label') }}</th>
|
||||
<th class="text-right px-4 py-3 font-medium text-gray-700">{{ __('admin.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@forelse ($invitations as $invitation)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3" x-data="{ copied: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="text-xs bg-gray-100 px-2 py-1 rounded truncate max-w-[200px]" x-ref="link">{{ route('register', $invitation->token) }}</code>
|
||||
<button
|
||||
type="button"
|
||||
@click="copyToClipboard('{{ route('register', $invitation->token) }}', () => { copied = true; setTimeout(() => copied = false, 2000) })"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-medium transition-colors"
|
||||
:class="copied ? 'bg-green-50 border-green-300 text-green-700' : 'bg-white border-gray-300 text-blue-600 hover:bg-blue-50 hover:border-blue-400'"
|
||||
>
|
||||
<svg x-show="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
<svg x-show="copied" x-cloak class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span x-text="copied ? '{{ __('admin.copied') }}' : '{{ __('admin.copy_link') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600">
|
||||
{{ $invitation->email ?? '–' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-600">
|
||||
@foreach ($invitation->players as $player)
|
||||
{{ $player->full_name }} ({{ $player->team->name }})@if (!$loop->last), @endif
|
||||
@endforeach
|
||||
@if ($invitation->players->isEmpty())
|
||||
<span class="text-gray-400">{{ __('admin.no_assignment') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if ($invitation->isAccepted())
|
||||
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">{{ __('admin.used') }}</span>
|
||||
@elseif ($invitation->isValid())
|
||||
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">{{ __('admin.pending') }}</span>
|
||||
@else
|
||||
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">{{ __('admin.expired') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">
|
||||
{{ $invitation->created_at->translatedFormat(__('ui.date_format_short')) }}<br>
|
||||
<span class="text-gray-400">{{ __('admin.created_by') }} {{ $invitation->creator->name }}</span><br>
|
||||
<span class="text-gray-400">{{ __('admin.valid_until') }} {{ $invitation->expires_at->translatedFormat(__('ui.date_format_date')) }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
@if (!$invitation->isAccepted())
|
||||
<form method="POST" action="{{ route('admin.invitations.destroy', $invitation) }}" class="inline" onsubmit="return confirm(@js(__('admin.confirm_delete_invitation')))">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800 text-xs">{{ __('ui.delete') }}</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500">{{ __('admin.no_invitations_yet') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $invitations->links() }}</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function copyToClipboard(text, onSuccess) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(onSuccess).catch(function () {
|
||||
fallbackCopy(text, onSuccess);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text, onSuccess);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text, onSuccess) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
console.error('Copy failed', e);
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-layouts.admin>
|
||||
Reference in New Issue
Block a user