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:
@@ -0,0 +1,86 @@
|
||||
<div class="card shadow-sm mb-3" id="post-{{ $post->id }}">
|
||||
<div class="card-body pb-2">
|
||||
|
||||
{{-- Cabecera --}}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<a href="{{ route('social.profile', $post->author) }}">
|
||||
<img src="{{ $post->author->profile_photo_url }}"
|
||||
class="rounded-circle mr-2" width="40" height="40" style="object-fit:cover">
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<a href="{{ route('social.profile', $post->author) }}" class="font-weight-bold text-dark d-block">
|
||||
{{ $post->author->name }} {{ $post->author->apellidoPaterno }}
|
||||
</a>
|
||||
<small class="text-muted">{{ $post->created_at->diffForHumans() }}</small>
|
||||
</div>
|
||||
@if($post->user_id === auth()->id())
|
||||
<form action="{{ route('posts.destroy', $post) }}" method="POST"
|
||||
onsubmit="return confirm('¿Eliminar publicación?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm text-muted" title="Eliminar">
|
||||
<i class="fas fa-trash-alt fa-sm"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Contenido --}}
|
||||
@if($post->body)
|
||||
<p class="mb-2">{{ $post->body }}</p>
|
||||
@endif
|
||||
|
||||
@if($post->image_path)
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::url($post->image_path) }}"
|
||||
class="img-fluid rounded mb-2" style="max-height:500px; width:100%; object-fit:cover; cursor:pointer"
|
||||
onclick="window.open(this.src,'_blank')">
|
||||
@endif
|
||||
|
||||
{{-- Contadores --}}
|
||||
<div class="d-flex align-items-center text-muted small border-top pt-2">
|
||||
<span class="mr-3">
|
||||
<i class="fas fa-heart text-danger mr-1"></i>
|
||||
<span id="like-count-{{ $post->id }}">{{ $post->likes->count() }}</span> Me gusta
|
||||
</span>
|
||||
<span>
|
||||
<i class="fas fa-comment mr-1"></i>
|
||||
{{ $post->comments->count() }} comentarios
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Botones de acción --}}
|
||||
<div class="card-footer bg-white py-1 d-flex border-top-0">
|
||||
@php $liked = $post->isLikedBy(auth()->user()); @endphp
|
||||
<button class="btn btn-sm flex-fill like-btn {{ $liked ? 'text-danger' : 'text-muted' }}"
|
||||
data-post-id="{{ $post->id }}">
|
||||
<i class="{{ $liked ? 'fas' : 'far' }} fa-heart mr-1"></i>
|
||||
Me gusta (<span class="like-count">{{ $post->likes->count() }}</span>)
|
||||
</button>
|
||||
<button class="btn btn-sm flex-fill text-muted toggle-comments" data-post-id="{{ $post->id }}">
|
||||
<i class="far fa-comment mr-1"></i> Comentar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Sección de comentarios --}}
|
||||
<div id="comments-{{ $post->id }}" class="d-none px-3 pb-3">
|
||||
<div id="comment-list-{{ $post->id }}" class="mb-2">
|
||||
@foreach($post->comments as $comment)
|
||||
<div class="d-flex align-items-start mb-2">
|
||||
<img src="{{ $comment->author->profile_photo_url }}"
|
||||
class="rounded-circle mr-2" width="28" height="28" style="object-fit:cover">
|
||||
<div class="bg-light rounded px-2 py-1 flex-grow-1">
|
||||
<strong class="small">{{ $comment->author->name }}</strong>
|
||||
<div class="small">{{ $comment->body }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<form class="comment-form d-flex" data-post-id="{{ $post->id }}">
|
||||
@csrf
|
||||
<img src="{{ auth()->user()->profile_photo_url }}"
|
||||
class="rounded-circle mr-2" width="28" height="28" style="object-fit:cover">
|
||||
<input type="text" name="body" class="form-control form-control-sm rounded-pill"
|
||||
placeholder="Escribe un comentario...">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,104 @@
|
||||
@extends('layouts.landing')
|
||||
@section('title', 'Feed')
|
||||
|
||||
@section('content')
|
||||
<div class="container" style="max-width:680px">
|
||||
|
||||
{{-- Crear publicación --}}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="d-flex align-items-start">
|
||||
<img src="{{ auth()->user()->profile_photo_url }}"
|
||||
class="rounded-circle mr-3" width="42" height="42" style="object-fit:cover">
|
||||
<div class="flex-grow-1">
|
||||
<textarea name="body" class="form-control border-0 bg-light rounded-pill px-3 py-2"
|
||||
rows="2" placeholder="¿Qué estás pensando, {{ auth()->user()->name }}?"
|
||||
style="resize:none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mt-2 pt-2 border-top">
|
||||
<label class="btn btn-light btn-sm mb-0">
|
||||
<i class="fas fa-image text-success mr-1"></i> Foto
|
||||
<input type="file" name="image" accept="image/*" hidden id="post-image-input">
|
||||
</label>
|
||||
<span id="post-image-name" class="text-muted small flex-grow-1 ml-2"></span>
|
||||
<button type="submit" class="btn btn-primary btn-sm rounded-pill px-4">Publicar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Feed --}}
|
||||
@forelse($posts as $post)
|
||||
@include('feed._post', ['post' => $post])
|
||||
@empty
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-users fa-3x mb-3 d-block"></i>
|
||||
<p>Aún no hay publicaciones. Agrega amigos para ver su contenido.</p>
|
||||
<a href="{{ route('users.search') }}" class="btn btn-primary">Buscar personas</a>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.getElementById('post-image-input')?.addEventListener('change', function () {
|
||||
document.getElementById('post-image-name').textContent = this.files[0]?.name ?? '';
|
||||
});
|
||||
|
||||
// Like toggle
|
||||
document.querySelectorAll('.like-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const postId = this.dataset.postId;
|
||||
axios.post(`/posts/${postId}/like`).then(res => {
|
||||
this.querySelector('.like-count').textContent = res.data.count;
|
||||
this.classList.toggle('text-danger', res.data.liked);
|
||||
this.classList.toggle('text-muted', !res.data.liked);
|
||||
this.querySelector('i').classList.toggle('fas', res.data.liked);
|
||||
this.classList.toggle('far', !res.data.liked);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Comentarios: toggle visibilidad
|
||||
document.querySelectorAll('.toggle-comments').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const box = document.getElementById('comments-' + this.dataset.postId);
|
||||
box.classList.toggle('d-none');
|
||||
});
|
||||
});
|
||||
|
||||
// Enviar comentario
|
||||
document.querySelectorAll('.comment-form').forEach(form => {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const postId = this.dataset.postId;
|
||||
const input = this.querySelector('input[name="body"]');
|
||||
if (!input.value.trim()) return;
|
||||
|
||||
axios.post(`/posts/${postId}/comments`, { body: input.value })
|
||||
.then(res => {
|
||||
const list = document.getElementById('comment-list-' + postId);
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex align-items-start mb-2';
|
||||
item.innerHTML = `
|
||||
<img src="${res.data.avatar}" class="rounded-circle mr-2" width="28" height="28" style="object-fit:cover">
|
||||
<div class="bg-light rounded px-2 py-1 flex-grow-1">
|
||||
<strong class="small">${res.data.author}</strong>
|
||||
<div class="small">${res.data.body}</div>
|
||||
</div>`;
|
||||
list.appendChild(item);
|
||||
input.value = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user