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:
@@ -1,47 +1,156 @@
|
||||
@extends('layouts.landing')
|
||||
@section('title', 'Solicitudes de Amistad')
|
||||
@section('title', 'Amigos')
|
||||
|
||||
@section('content')
|
||||
<div class="container" style="max-width:680px">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white font-weight-bold">
|
||||
<i class="fas fa-user-friends mr-2 text-primary"></i> Solicitudes de amistad
|
||||
<div class="container" style="max-width:760px">
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show py-2">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert"><span>×</span></button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($requests as $friendship)
|
||||
<div class="d-flex align-items-center p-3 border-bottom">
|
||||
<a href="{{ route('social.profile', $friendship->sender) }}">
|
||||
<img src="{{ $friendship->sender->profile_photo_url }}"
|
||||
class="rounded-circle mr-3" width="50" height="50" style="object-fit:cover">
|
||||
@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>
|
||||
<div class="flex-grow-1">
|
||||
<a href="{{ route('social.profile', $friendship->sender) }}" class="font-weight-bold text-dark">
|
||||
{{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }}
|
||||
</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 class="text-muted small">{{ $friendship->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<form action="{{ route('friendships.accept', $friendship) }}" method="POST" class="mr-2">
|
||||
@csrf
|
||||
<button class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-check mr-1"></i> Aceptar
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times mr-1"></i> Rechazar
|
||||
</button>
|
||||
</form>
|
||||
@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>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-user-check fa-3x mb-3 d-block text-success"></i>
|
||||
No tienes solicitudes pendientes.
|
||||
</div>
|
||||
@endforelse
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ── Tab: Solicitudes pendientes ── --}}
|
||||
<div class="tab-pane fade" id="tab-requests">
|
||||
@forelse($requests as $friendship)
|
||||
<div class="d-flex align-items-center p-3 border-bottom">
|
||||
<a href="{{ route('social.profile', $friendship->sender) }}">
|
||||
<img src="{{ $friendship->sender->profile_photo_url }}"
|
||||
class="rounded-circle mr-3" width="48" height="48" style="object-fit:cover">
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<a href="{{ route('social.profile', $friendship->sender) }}"
|
||||
class="font-weight-bold text-dark d-block">
|
||||
{{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }}
|
||||
</a>
|
||||
<small class="text-muted">{{ $friendship->created_at->diffForHumans() }}</small>
|
||||
</div>
|
||||
<div class="d-flex" style="gap:.4rem">
|
||||
<form action="{{ route('friendships.accept', $friendship) }}" method="POST">
|
||||
@csrf
|
||||
<button class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-check mr-1"></i> Aceptar
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-check-circle fa-3x mb-3 d-block text-success"></i>
|
||||
No tienes solicitudes pendientes.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-results" class="shadow bg-white rounded position-absolute w-100 d-none"
|
||||
style="top:100%; left:0; z-index:9999; max-height:320px; overflow-y:auto"></div>
|
||||
<div id="search-results" class="bg-white position-absolute d-none"
|
||||
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>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@@ -173,6 +177,13 @@
|
||||
</nav>
|
||||
</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>
|
||||
// ── Mensajes en tiempo real ──────────────────────────────
|
||||
function updateBadge(id, increment) {
|
||||
@@ -311,32 +322,48 @@ function markAllNotifsRead() {
|
||||
|
||||
function renderResults(users) {
|
||||
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');
|
||||
return;
|
||||
}
|
||||
results.innerHTML = users.map(u => {
|
||||
const btn = friendshipLabel(u.friendship);
|
||||
return `
|
||||
<div class="d-flex align-items-center px-3 py-2 border-bottom search-result-item">
|
||||
<a href="${u.profile_url}" class="d-flex align-items-center flex-grow-1 text-dark text-decoration-none">
|
||||
<img src="${u.avatar}" class="rounded-circle mr-2" width="36" height="36" style="object-fit:cover">
|
||||
<span class="font-weight-bold small">${u.name}</span>
|
||||
</a>
|
||||
<div class="d-flex align-items-center" style="gap:.4rem">
|
||||
<button class="btn btn-sm ${btn.cls} friend-toggle-btn"
|
||||
data-user-id="${u.id}"
|
||||
data-friendship-id="${u.friendship.id ?? ''}"
|
||||
data-status="${u.friendship.status}"
|
||||
data-is-receiver="${u.friendship.is_receiver}">
|
||||
<i class="fas ${btn.icon} mr-1"></i>${btn.label}
|
||||
</button>
|
||||
${u.friendship.status === 'accepted'
|
||||
? `<a href="${u.chat_url}" class="btn btn-sm btn-outline-primary"><i class="fas fa-comment"></i></a>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
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 isLast = i === users.length - 1;
|
||||
return `
|
||||
<div class="d-flex align-items-center px-3 py-2 search-result-item"
|
||||
style="${!isLast ? 'border-bottom:1px solid #f1f1f1' : ''}">
|
||||
<a href="${u.profile_url}"
|
||||
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>
|
||||
<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 rounded-pill"
|
||||
style="font-size:.72rem;padding:.2rem .65rem"
|
||||
data-user-id="${u.id}"
|
||||
data-status="${u.friendship.status}"
|
||||
data-is-receiver="${u.friendship.is_receiver}">
|
||||
<i class="fas ${btn.icon} mr-1"></i>${btn.label}
|
||||
</button>
|
||||
${u.friendship.status === 'accepted'
|
||||
? `<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>`;
|
||||
}).join('');
|
||||
results.classList.remove('d-none');
|
||||
|
||||
// Bind toggle buttons
|
||||
|
||||
Reference in New Issue
Block a user