feat: funcionalidad de red social (feed, amigos, búsqueda)
- Migraciones: posts, post_likes, post_comments - Modelos Post, PostLike, PostComment con relaciones - User: métodos friendshipWith, isFriendWith, friendIds, posts - PostController: feed, crear publicación con imagen, like toggle, comentarios, eliminar - FriendshipController: listar solicitudes, enviar, aceptar, rechazar, toggle AJAX - SocialProfileController: perfil público con publicaciones y estado de amistad - UserSearchController: búsqueda AJAX por nombre con estado de amistad - FriendRequestNotification: notificación al recibir solicitud - Vistas: feed/index, feed/_post, friendships/index, social/profile - Header: barra de búsqueda con autocomplete AJAX (agregar amigo / mensaje) - Menú lateral: Comunidad, Amigos, Mensajes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,17 +9,17 @@
|
||||
</button>
|
||||
|
||||
<!-- Topbar Search -->
|
||||
<form class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
|
||||
<div class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search position-relative">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-light border-0 small" placeholder="Buscar..."
|
||||
aria-label="Search" aria-describedby="basic-addon2">
|
||||
<input type="text" id="global-search" class="form-control bg-light border-0 small"
|
||||
placeholder="Buscar personas..." autocomplete="off" style="min-width:220px">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-search fa-sm"></i>
|
||||
</button>
|
||||
<span class="btn btn-primary"><i class="fas fa-search fa-sm"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
|
||||
@@ -293,4 +293,84 @@ function markAllNotifsRead() {
|
||||
.forEach(el => el.classList.remove('font-weight-bold'));
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// ── Búsqueda de usuarios ─────────────────────────────────
|
||||
(function () {
|
||||
const input = document.getElementById('global-search');
|
||||
const results = document.getElementById('search-results');
|
||||
if (!input) return;
|
||||
|
||||
let timer;
|
||||
|
||||
function friendshipLabel(f) {
|
||||
if (f.status === 'accepted') return { label: 'Amigos', cls: 'btn-outline-secondary', icon: 'fa-user-check' };
|
||||
if (f.status === 'pending' && !f.is_receiver) return { label: 'Solicitud enviada', cls: 'btn-outline-secondary', icon: 'fa-user-clock' };
|
||||
if (f.status === 'pending' && f.is_receiver) return { label: 'Aceptar', cls: 'btn-success', icon: 'fa-user-plus' };
|
||||
return { label: 'Agregar', cls: 'btn-primary', icon: 'fa-user-plus' };
|
||||
}
|
||||
|
||||
function renderResults(users) {
|
||||
if (!users.length) {
|
||||
results.innerHTML = '<div class="p-3 text-muted small text-center">Sin resultados</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.classList.remove('d-none');
|
||||
|
||||
// Bind toggle buttons
|
||||
results.querySelectorAll('.friend-toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
const userId = this.dataset.userId;
|
||||
axios.post(`/friends/${userId}/toggle`)
|
||||
.then(res => {
|
||||
// Re-search to refresh states
|
||||
doSearch(input.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doSearch(q) {
|
||||
if (q.length < 2) { results.classList.add('d-none'); return; }
|
||||
axios.get('/search', { params: { q } }).then(res => renderResults(res.data));
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => doSearch(this.value.trim()), 300);
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!input.contains(e.target) && !results.contains(e.target)) {
|
||||
results.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('focus', function () {
|
||||
if (this.value.trim().length >= 2) results.classList.remove('d-none');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -16,6 +16,29 @@
|
||||
<i class="fas fa-fw fa-tachometer-alt"></i>
|
||||
<span>Inicio</span></a>
|
||||
</li>
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider">
|
||||
|
||||
<!-- Social -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('feed') }}">
|
||||
<i class="fas fa-fw fa-stream"></i>
|
||||
<span>Comunidad</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('friendships.index') }}">
|
||||
<i class="fas fa-fw fa-user-friends"></i>
|
||||
<span>Amigos</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('chat.index') }}">
|
||||
<i class="fas fa-fw fa-comment-dots"></i>
|
||||
<span>Mensajes</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider">
|
||||
<!-- Heading -->
|
||||
|
||||
Reference in New Issue
Block a user