feat: gestión de amigos y mejora visual del dropdown de búsqueda

- FriendshipController: carga también amigos aceptados con su friendship
- friendships/index: tabs con 'Mis amigos' (tabla con Mensaje/Quitar) y 'Solicitudes'
- Notificaciones de solicitud abren directamente la pestaña de solicitudes (#solicitudes)
- Dropdown de búsqueda: bordes redondeados, sombra suave, margen superior, hover en items, etiqueta de sección y separadores finos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 17:20:01 -06:00
parent 0273fbfddd
commit d7ae38ce27
3 changed files with 212 additions and 65 deletions
+16 -5
View File
@@ -11,14 +11,25 @@ class FriendshipController extends Controller
{ {
public function index() public function index()
{ {
$requests = auth()->user() $me = auth()->user();
->receivedFriendRequests()
$requests = $me->receivedFriendRequests()
->where('status', 'pending') ->where('status', 'pending')
->with('sender') ->with('sender')
->latest() ->latest()
->get(); ->get();
return view('friendships.index', compact('requests')); // Amigos aceptados (en ambas direcciones)
$friendIds = $me->friendIds();
$friends = User::whereIn('id', $friendIds)
->orderBy('name')
->get()
->map(function (User $user) use ($me) {
$user->friendship = $me->friendshipWith($user);
return $user;
});
return view('friendships.index', compact('requests', 'friends'));
} }
public function send(User $user) public function send(User $user)
@@ -44,7 +55,7 @@ class FriendshipController extends Controller
title: $me->name . ' te envió una solicitud de amistad', title: $me->name . ' te envió una solicitud de amistad',
icon: 'fa-user-plus', icon: 'fa-user-plus',
color: 'primary', color: 'primary',
url: route('friendships.index'), url: route('friendships.index') . '#solicitudes',
); );
return back()->with('success', 'Solicitud de amistad enviada.'); return back()->with('success', 'Solicitud de amistad enviada.');
@@ -108,7 +119,7 @@ class FriendshipController extends Controller
title: $me->name . ' te envió una solicitud de amistad', title: $me->name . ' te envió una solicitud de amistad',
icon: 'fa-user-plus', icon: 'fa-user-plus',
color: 'primary', color: 'primary',
url: route('friendships.index'), url: route('friendships.index') . '#solicitudes',
); );
return response()->json(['status' => 'pending']); return response()->json(['status' => 'pending']);
+122 -13
View File
@@ -1,27 +1,125 @@
@extends('layouts.landing') @extends('layouts.landing')
@section('title', 'Solicitudes de Amistad') @section('title', 'Amigos')
@section('content') @section('content')
<div class="container" style="max-width:680px"> <div class="container" style="max-width:760px">
<div class="card shadow-sm">
<div class="card-header bg-white font-weight-bold"> @if(session('success'))
<i class="fas fa-user-friends mr-2 text-primary"></i> Solicitudes de amistad <div class="alert alert-success alert-dismissible fade show py-2">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>
</div> </div>
<div class="card-body p-0"> @endif
<div class="card shadow-sm">
{{-- Tabs --}}
<div class="card-header bg-white p-0">
<ul class="nav nav-tabs border-0" id="friendTabs">
<li class="nav-item">
<a class="nav-link active font-weight-bold px-4 py-3" data-toggle="tab" href="#tab-friends">
<i class="fas fa-user-check mr-1 text-success"></i>
Mis amigos
<span class="badge badge-secondary ml-1">{{ $friends->count() }}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link font-weight-bold px-4 py-3" data-toggle="tab" href="#tab-requests">
<i class="fas fa-user-plus mr-1 text-primary"></i>
Solicitudes
@if($requests->count())
<span class="badge badge-danger ml-1">{{ $requests->count() }}</span>
@endif
</a>
</li>
</ul>
</div>
<div class="tab-content">
{{-- ── Tab: Mis amigos ── --}}
<div class="tab-pane fade show active" id="tab-friends">
@if($friends->isEmpty())
<div class="text-center text-muted py-5">
<i class="fas fa-user-friends fa-3x mb-3 d-block"></i>
<p class="mb-2">Aún no tienes amigos agregados.</p>
<a href="{{ route('feed') }}" class="btn btn-primary btn-sm">
<i class="fas fa-search mr-1"></i> Buscar personas
</a>
</div>
@else
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="thead-light">
<tr>
<th style="width:48px"></th>
<th>Nombre</th>
<th class="d-none d-md-table-cell text-muted small">Amigos desde</th>
<th class="text-right">Acciones</th>
</tr>
</thead>
<tbody>
@foreach($friends as $friend)
<tr>
<td class="py-2">
<a href="{{ route('social.profile', $friend) }}">
<img src="{{ $friend->profile_photo_url }}"
class="rounded-circle"
width="40" height="40" style="object-fit:cover">
</a>
</td>
<td class="py-2">
<a href="{{ route('social.profile', $friend) }}"
class="font-weight-bold text-dark">
{{ $friend->name }} {{ $friend->apellidoPaterno }}
</a>
<div class="text-muted small">{{ $friend->email }}</div>
</td>
<td class="py-2 d-none d-md-table-cell text-muted small">
{{ $friend->friendship->updated_at->format('d M Y') }}
</td>
<td class="py-2 text-right">
<a href="{{ route('chat.show', $friend) }}"
class="btn btn-sm btn-outline-primary mr-1"
title="Enviar mensaje">
<i class="fas fa-comment"></i>
<span class="d-none d-md-inline ml-1">Mensaje</span>
</a>
<form action="{{ route('friendships.destroy', $friend->friendship) }}"
method="POST" class="d-inline"
onsubmit="return confirm('¿Quitar a {{ addslashes($friend->name) }} de tus amigos?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger" title="Quitar amigo">
<i class="fas fa-user-times"></i>
<span class="d-none d-md-inline ml-1">Quitar</span>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
{{-- ── Tab: Solicitudes pendientes ── --}}
<div class="tab-pane fade" id="tab-requests">
@forelse($requests as $friendship) @forelse($requests as $friendship)
<div class="d-flex align-items-center p-3 border-bottom"> <div class="d-flex align-items-center p-3 border-bottom">
<a href="{{ route('social.profile', $friendship->sender) }}"> <a href="{{ route('social.profile', $friendship->sender) }}">
<img src="{{ $friendship->sender->profile_photo_url }}" <img src="{{ $friendship->sender->profile_photo_url }}"
class="rounded-circle mr-3" width="50" height="50" style="object-fit:cover"> class="rounded-circle mr-3" width="48" height="48" style="object-fit:cover">
</a> </a>
<div class="flex-grow-1"> <div class="flex-grow-1">
<a href="{{ route('social.profile', $friendship->sender) }}" class="font-weight-bold text-dark"> <a href="{{ route('social.profile', $friendship->sender) }}"
class="font-weight-bold text-dark d-block">
{{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }} {{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }}
</a> </a>
<div class="text-muted small">{{ $friendship->created_at->diffForHumans() }}</div> <small class="text-muted">{{ $friendship->created_at->diffForHumans() }}</small>
</div> </div>
<div class="d-flex"> <div class="d-flex" style="gap:.4rem">
<form action="{{ route('friendships.accept', $friendship) }}" method="POST" class="mr-2"> <form action="{{ route('friendships.accept', $friendship) }}" method="POST">
@csrf @csrf
<button class="btn btn-primary btn-sm"> <button class="btn btn-primary btn-sm">
<i class="fas fa-check mr-1"></i> Aceptar <i class="fas fa-check mr-1"></i> Aceptar
@@ -30,18 +128,29 @@
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST"> <form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
@csrf @method('DELETE') @csrf @method('DELETE')
<button class="btn btn-outline-secondary btn-sm"> <button class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times mr-1"></i> Rechazar <i class="fas fa-times"></i>
</button> </button>
</form> </form>
</div> </div>
</div> </div>
@empty @empty
<div class="text-center text-muted py-5"> <div class="text-center text-muted py-5">
<i class="fas fa-user-check fa-3x mb-3 d-block text-success"></i> <i class="fas fa-check-circle fa-3x mb-3 d-block text-success"></i>
No tienes solicitudes pendientes. No tienes solicitudes pendientes.
</div> </div>
@endforelse @endforelse
</div> </div>
</div>
</div> </div>
</div> </div>
@endsection @endsection
@section('scripts')
<script>
// Abrir tab de solicitudes si viene de notificación
if (window.location.hash === '#solicitudes') {
document.querySelector('[href="#tab-requests"]')?.click();
}
</script>
@endsection
@@ -17,8 +17,12 @@
<span class="btn btn-primary"><i class="fas fa-search fa-sm"></i></span> <span class="btn btn-primary"><i class="fas fa-search fa-sm"></i></span>
</div> </div>
</div> </div>
<div id="search-results" class="shadow bg-white rounded position-absolute w-100 d-none" <div id="search-results" class="bg-white position-absolute d-none"
style="top:100%; left:0; z-index:9999; max-height:320px; overflow-y:auto"></div> style="top:calc(100% + 6px); left:0; right:0; z-index:9999;
max-height:340px; overflow-y:auto;
border:1px solid rgba(0,0,0,.1); border-radius:8px;
box-shadow:0 8px 24px rgba(0,0,0,.12);
min-width:300px"></div>
</div> </div>
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
@@ -173,6 +177,13 @@
</nav> </nav>
</header> </header>
<style>
.search-result-item { transition: background .15s; }
.search-result-item:hover { background: #f8f9fc; }
#search-results > div:first-child { border-radius: 8px 8px 0 0; }
#search-results > div:last-child { border-radius: 0 0 8px 8px; }
</style>
<script> <script>
// ── Mensajes en tiempo real ────────────────────────────── // ── Mensajes en tiempo real ──────────────────────────────
function updateBadge(id, increment) { function updateBadge(id, increment) {
@@ -311,28 +322,44 @@ function markAllNotifsRead() {
function renderResults(users) { function renderResults(users) {
if (!users.length) { if (!users.length) {
results.innerHTML = '<div class="p-3 text-muted small text-center">Sin resultados</div>'; results.innerHTML = `
<div class="px-4 py-3 text-center">
<i class="fas fa-search text-muted mb-2 d-block" style="font-size:1.4rem"></i>
<span class="text-muted small">Sin resultados</span>
</div>`;
results.classList.remove('d-none'); results.classList.remove('d-none');
return; return;
} }
results.innerHTML = users.map(u => { results.innerHTML =
`<div class="px-3 pt-2 pb-1" style="font-size:.7rem;font-weight:600;letter-spacing:.05em;color:#aaa;text-transform:uppercase">Personas</div>` +
users.map((u, i) => {
const btn = friendshipLabel(u.friendship); const btn = friendshipLabel(u.friendship);
const isLast = i === users.length - 1;
return ` return `
<div class="d-flex align-items-center px-3 py-2 border-bottom search-result-item"> <div class="d-flex align-items-center px-3 py-2 search-result-item"
<a href="${u.profile_url}" class="d-flex align-items-center flex-grow-1 text-dark text-decoration-none"> style="${!isLast ? 'border-bottom:1px solid #f1f1f1' : ''}">
<img src="${u.avatar}" class="rounded-circle mr-2" width="36" height="36" style="object-fit:cover"> <a href="${u.profile_url}"
<span class="font-weight-bold small">${u.name}</span> class="d-flex align-items-center flex-grow-1 text-dark text-decoration-none"
style="min-width:0">
<img src="${u.avatar}" class="rounded-circle flex-shrink-0 mr-3"
width="38" height="38" style="object-fit:cover">
<span class="font-weight-bold small text-truncate">${u.name}</span>
</a> </a>
<div class="d-flex align-items-center" style="gap:.4rem"> <div class="d-flex align-items-center flex-shrink-0 ml-2" style="gap:.35rem">
<button class="btn btn-sm ${btn.cls} friend-toggle-btn" <button class="btn btn-sm ${btn.cls} friend-toggle-btn rounded-pill"
style="font-size:.72rem;padding:.2rem .65rem"
data-user-id="${u.id}" data-user-id="${u.id}"
data-friendship-id="${u.friendship.id ?? ''}"
data-status="${u.friendship.status}" data-status="${u.friendship.status}"
data-is-receiver="${u.friendship.is_receiver}"> data-is-receiver="${u.friendship.is_receiver}">
<i class="fas ${btn.icon} mr-1"></i>${btn.label} <i class="fas ${btn.icon} mr-1"></i>${btn.label}
</button> </button>
${u.friendship.status === 'accepted' ${u.friendship.status === 'accepted'
? `<a href="${u.chat_url}" class="btn btn-sm btn-outline-primary"><i class="fas fa-comment"></i></a>` ? `<a href="${u.chat_url}"
class="btn btn-sm btn-outline-primary rounded-pill"
style="font-size:.72rem;padding:.2rem .55rem"
title="Enviar mensaje">
<i class="fas fa-comment"></i>
</a>`
: ''} : ''}
</div> </div>
</div>`; </div>`;