Files
mesreleves-php/resources/views/components/user-picker.blade.php
T
yann64 f530f55577 Mode sombre, option désactivation mises à jour, user-picker avec recherche
- Dark mode complet : darkMode:'class' Tailwind, sélecteur clair/sombre/auto
  dans la navigation (mémorisé dans localStorage, sans flash au chargement) ;
  53 vues et 8 composants Breeze mis à jour avec classes dark:
- Composant user-picker : fenêtre modale avec recherche temps réel (nom/email)
  remplace les <select> d'ajout de membres dans sections et sources
- Paramètres : option "Désactiver la vérification automatique des mises à jour"
  (case à cochage auto-soumise, route POST parametres/updates)
- Panneau "Paramètres généraux" remonté en tête de la page de paramètres
- README recentré sur l'installation manuelle hébergement PHP+MySQL
- VERSION 1.0.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:46:22 +02:00

160 lines
7.7 KiB
PHP

{{--
Sélecteur d'utilisateur avec recherche (fenêtre modale).
Props :
name — nom du champ hidden (défaut : user_id)
users — collection/array d'objets avec {id, name, email}
placeholder texte affiché avant sélection
required ajoute l'attribut required sur le champ hidden (pour validation formulaire)
--}}
@props([
'name' => 'user_id',
'users' => [],
'placeholder' => 'Sélectionner un utilisateur…',
'required' => false,
])
@php
$usersJson = collect($users)->map(fn ($u) => [
'id' => $u->id,
'name' => $u->name,
'email' => $u->email,
])->values()->toJson();
@endphp
<div x-data="{
open: false,
search: '',
selectedId: null,
selectedLabel: '',
users: {{ $usersJson }},
get filtered() {
if (! this.search.trim()) return this.users;
const q = this.search.toLowerCase();
return this.users.filter(u =>
u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
);
},
select(u) {
this.selectedId = u.id;
this.selectedLabel = u.name + ' (' + u.email + ')';
this.open = false;
this.search = '';
},
clear() {
this.selectedId = null;
this.selectedLabel = '';
}
}" @keydown.escape.window="open = false">
<input type="hidden" name="{{ $name }}" :value="selectedId" {{ $required ? 'required' : '' }}>
{{-- Bouton déclencheur --}}
<button type="button" @click="open = true"
class="w-full flex items-center justify-between gap-2 px-3 py-2
bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600
rounded-md shadow-sm text-sm text-left
hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800">
<span :class="selectedId ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500'"
x-text="selectedLabel || '{{ $placeholder }}'"></span>
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
{{-- Fenêtre modale --}}
<div x-show="open" x-cloak
class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
{{-- Fond semi-transparent --}}
<div class="absolute inset-0 bg-gray-900/50 dark:bg-gray-950/70" @click="open = false"></div>
{{-- Panneau --}}
<div class="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-xl dark:shadow-gray-900/50 overflow-hidden"
@click.stop
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0">
{{-- Champ de recherche --}}
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" x-model="search" x-ref="searchInput"
x-init="$watch('open', v => v && $nextTick(() => $refs.searchInput && $refs.searchInput.focus()))"
placeholder="Rechercher par nom ou email…"
class="flex-1 bg-transparent border-none outline-none p-0 text-sm
text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:ring-0">
<button type="button" @click="open = false"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Liste des utilisateurs --}}
<div class="overflow-y-auto" style="max-height: 18rem;">
<template x-if="filtered.length === 0">
<p class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">
Aucun résultat pour « <span x-text="search"></span> »
</p>
</template>
<template x-for="u in filtered" :key="u.id">
<button type="button" @click="select(u)"
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors
hover:bg-gray-50 dark:hover:bg-gray-700"
:class="selectedId === u.id
? 'bg-indigo-50 dark:bg-indigo-900/30'
: ''">
{{-- Avatar initiale --}}
<div class="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/50
flex items-center justify-center shrink-0">
<span class="text-xs font-semibold text-indigo-700 dark:text-indigo-300"
x-text="u.name.charAt(0).toUpperCase()"></span>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"
x-text="u.name"></p>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"
x-text="u.email"></p>
</div>
<template x-if="selectedId === u.id">
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400 shrink-0"
fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"/>
</svg>
</template>
</button>
</template>
</div>
{{-- Pied : compteur --}}
<div class="px-4 py-2 border-t border-gray-100 dark:border-gray-700
text-xs text-gray-400 dark:text-gray-500 flex items-center justify-between">
<span>
<span x-text="filtered.length"></span> /
<span x-text="users.length"></span> utilisateur<span x-show="users.length !== 1">s</span>
</span>
<button x-show="selectedId" type="button" @click="clear()"
class="text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors">
Effacer la sélection
</button>
</div>
</div>
</div>
</div>