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:
2026-04-28 17:09:04 -06:00
parent 0f7de9b4d8
commit 97c04fbfb4
22 changed files with 1005 additions and 308 deletions
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\PostComment;
use App\Models\PostLike;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
public function index()
{
$me = auth()->user();
$friendIds = $me->friendIds();
$ids = array_merge($friendIds, [$me->id]);
$posts = Post::whereIn('user_id', $ids)
->with(['author', 'likes', 'comments.author'])
->latest()
->paginate(15);
return view('feed.index', compact('posts'));
}
public function store(Request $request)
{
$request->validate([
'body' => 'nullable|string|max:2000',
'image' => 'nullable|image|max:5120',
]);
if (!$request->filled('body') && !$request->hasFile('image')) {
return back()->with('error', 'Escribe algo o adjunta una imagen.');
}
$imagePath = null;
if ($request->hasFile('image')) {
$imagePath = $request->file('image')->store('posts', 'public');
}
Post::create([
'user_id' => auth()->id(),
'body' => $request->body,
'image_path' => $imagePath,
]);
return back()->with('success', 'Publicación creada.');
}
public function like(Post $post)
{
$userId = auth()->id();
$liked = PostLike::where('user_id', $userId)->where('post_id', $post->id)->first();
if ($liked) {
$liked->delete();
$liked = false;
} else {
PostLike::create(['user_id' => $userId, 'post_id' => $post->id]);
$liked = true;
}
return response()->json([
'liked' => $liked,
'count' => $post->likes()->count(),
]);
}
public function comment(Request $request, Post $post)
{
$request->validate(['body' => 'required|string|max:500']);
$comment = PostComment::create([
'user_id' => auth()->id(),
'post_id' => $post->id,
'body' => $request->body,
]);
$comment->load('author');
return response()->json([
'id' => $comment->id,
'body' => $comment->body,
'author' => $comment->author->name,
'avatar' => $comment->author->profile_photo_url,
'created_at' => $comment->created_at->diffForHumans(),
]);
}
public function destroy(Post $post)
{
abort_unless($post->user_id === auth()->id(), 403);
if ($post->image_path) {
Storage::disk('public')->delete($post->image_path);
}
$post->delete();
return back()->with('success', 'Publicación eliminada.');
}
}