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:
@@ -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']);
|
||||||
|
|||||||
@@ -1,47 +1,156 @@
|
|||||||
@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>×</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
@endif
|
||||||
@forelse($requests as $friendship)
|
|
||||||
<div class="d-flex align-items-center p-3 border-bottom">
|
<div class="card shadow-sm">
|
||||||
<a href="{{ route('social.profile', $friendship->sender) }}">
|
|
||||||
<img src="{{ $friendship->sender->profile_photo_url }}"
|
{{-- Tabs --}}
|
||||||
class="rounded-circle mr-3" width="50" height="50" style="object-fit:cover">
|
<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>
|
</a>
|
||||||
<div class="flex-grow-1">
|
</li>
|
||||||
<a href="{{ route('social.profile', $friendship->sender) }}" class="font-weight-bold text-dark">
|
<li class="nav-item">
|
||||||
{{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }}
|
<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>
|
</a>
|
||||||
<div class="text-muted small">{{ $friendship->created_at->diffForHumans() }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
@else
|
||||||
<form action="{{ route('friendships.accept', $friendship) }}" method="POST" class="mr-2">
|
<div class="table-responsive">
|
||||||
@csrf
|
<table class="table table-hover align-middle mb-0">
|
||||||
<button class="btn btn-primary btn-sm">
|
<thead class="thead-light">
|
||||||
<i class="fas fa-check mr-1"></i> Aceptar
|
<tr>
|
||||||
</button>
|
<th style="width:48px"></th>
|
||||||
</form>
|
<th>Nombre</th>
|
||||||
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
|
<th class="d-none d-md-table-cell text-muted small">Amigos desde</th>
|
||||||
@csrf @method('DELETE')
|
<th class="text-right">Acciones</th>
|
||||||
<button class="btn btn-outline-secondary btn-sm">
|
</tr>
|
||||||
<i class="fas fa-times mr-1"></i> Rechazar
|
</thead>
|
||||||
</button>
|
<tbody>
|
||||||
</form>
|
@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>
|
||||||
</div>
|
@endif
|
||||||
@empty
|
</div>
|
||||||
<div class="text-center text-muted py-5">
|
|
||||||
<i class="fas fa-user-check fa-3x mb-3 d-block text-success"></i>
|
{{-- ── Tab: Solicitudes pendientes ── --}}
|
||||||
No tienes solicitudes pendientes.
|
<div class="tab-pane fade" id="tab-requests">
|
||||||
</div>
|
@forelse($requests as $friendship)
|
||||||
@endforelse
|
<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>
|
</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,32 +322,48 @@ 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 =
|
||||||
const btn = friendshipLabel(u.friendship);
|
`<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>` +
|
||||||
return `
|
users.map((u, i) => {
|
||||||
<div class="d-flex align-items-center px-3 py-2 border-bottom search-result-item">
|
const btn = friendshipLabel(u.friendship);
|
||||||
<a href="${u.profile_url}" class="d-flex align-items-center flex-grow-1 text-dark text-decoration-none">
|
const isLast = i === users.length - 1;
|
||||||
<img src="${u.avatar}" class="rounded-circle mr-2" width="36" height="36" style="object-fit:cover">
|
return `
|
||||||
<span class="font-weight-bold small">${u.name}</span>
|
<div class="d-flex align-items-center px-3 py-2 search-result-item"
|
||||||
</a>
|
style="${!isLast ? 'border-bottom:1px solid #f1f1f1' : ''}">
|
||||||
<div class="d-flex align-items-center" style="gap:.4rem">
|
<a href="${u.profile_url}"
|
||||||
<button class="btn btn-sm ${btn.cls} friend-toggle-btn"
|
class="d-flex align-items-center flex-grow-1 text-dark text-decoration-none"
|
||||||
data-user-id="${u.id}"
|
style="min-width:0">
|
||||||
data-friendship-id="${u.friendship.id ?? ''}"
|
<img src="${u.avatar}" class="rounded-circle flex-shrink-0 mr-3"
|
||||||
data-status="${u.friendship.status}"
|
width="38" height="38" style="object-fit:cover">
|
||||||
data-is-receiver="${u.friendship.is_receiver}">
|
<span class="font-weight-bold small text-truncate">${u.name}</span>
|
||||||
<i class="fas ${btn.icon} mr-1"></i>${btn.label}
|
</a>
|
||||||
</button>
|
<div class="d-flex align-items-center flex-shrink-0 ml-2" style="gap:.35rem">
|
||||||
${u.friendship.status === 'accepted'
|
<button class="btn btn-sm ${btn.cls} friend-toggle-btn rounded-pill"
|
||||||
? `<a href="${u.chat_url}" class="btn btn-sm btn-outline-primary"><i class="fas fa-comment"></i></a>`
|
style="font-size:.72rem;padding:.2rem .65rem"
|
||||||
: ''}
|
data-user-id="${u.id}"
|
||||||
</div>
|
data-status="${u.friendship.status}"
|
||||||
</div>`;
|
data-is-receiver="${u.friendship.is_receiver}">
|
||||||
}).join('');
|
<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');
|
results.classList.remove('d-none');
|
||||||
|
|
||||||
// Bind toggle buttons
|
// Bind toggle buttons
|
||||||
|
|||||||
Reference in New Issue
Block a user