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,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Friendship;
|
||||
use App\Models\User;
|
||||
use App\Notifications\FriendRequestNotification;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FriendshipController extends Controller
|
||||
{
|
||||
// Solicitudes pendientes recibidas
|
||||
public function index()
|
||||
{
|
||||
$requests = auth()->user()
|
||||
->receivedFriendRequests()
|
||||
->where('status', 'pending')
|
||||
->with('sender')
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return view('friendships.index', compact('requests'));
|
||||
}
|
||||
|
||||
// Enviar solicitud
|
||||
public function send(User $user)
|
||||
{
|
||||
$me = auth()->user();
|
||||
|
||||
if ($me->id === $user->id) {
|
||||
return back()->with('error', 'No puedes enviarte una solicitud a ti mismo.');
|
||||
}
|
||||
|
||||
$existing = $me->friendshipWith($user);
|
||||
if ($existing) {
|
||||
return back()->with('error', 'Ya existe una relación con este usuario.');
|
||||
}
|
||||
|
||||
$friendship = Friendship::create([
|
||||
'sender_id' => $me->id,
|
||||
'receiver_id' => $user->id,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$user->notify(new FriendRequestNotification($me));
|
||||
|
||||
return back()->with('success', 'Solicitud de amistad enviada.');
|
||||
}
|
||||
|
||||
// Aceptar solicitud
|
||||
public function accept(Friendship $friendship)
|
||||
{
|
||||
abort_unless($friendship->receiver_id === auth()->id(), 403);
|
||||
|
||||
$friendship->update(['status' => 'accepted']);
|
||||
|
||||
return back()->with('success', 'Solicitud aceptada.');
|
||||
}
|
||||
|
||||
// Rechazar o cancelar solicitud
|
||||
public function destroy(Friendship $friendship)
|
||||
{
|
||||
$me = auth()->id();
|
||||
abort_unless(
|
||||
$friendship->sender_id === $me || $friendship->receiver_id === $me,
|
||||
403
|
||||
);
|
||||
|
||||
$friendship->delete();
|
||||
|
||||
return back()->with('success', 'Solicitud eliminada.');
|
||||
}
|
||||
|
||||
// AJAX: enviar/cancelar solicitud desde búsqueda o perfil
|
||||
public function toggle(User $user)
|
||||
{
|
||||
$me = auth()->user();
|
||||
|
||||
if ($me->id === $user->id) {
|
||||
return response()->json(['error' => 'No permitido'], 422);
|
||||
}
|
||||
|
||||
$existing = $me->friendshipWith($user);
|
||||
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
return response()->json(['status' => 'none']);
|
||||
}
|
||||
|
||||
Friendship::create([
|
||||
'sender_id' => $me->id,
|
||||
'receiver_id' => $user->id,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$user->notify(new FriendRequestNotification($me));
|
||||
|
||||
return response()->json(['status' => 'pending']);
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class SocialProfileController extends Controller
|
||||
{
|
||||
public function show(User $user)
|
||||
{
|
||||
$me = auth()->user();
|
||||
$friendship = $me->friendshipWith($user);
|
||||
$posts = $user->posts()
|
||||
->with(['author', 'likes', 'comments.author'])
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('social.profile', compact('user', 'friendship', 'posts'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserSearchController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$q = trim($request->get('q', ''));
|
||||
$me = auth()->user();
|
||||
|
||||
if (strlen($q) < 2) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$users = User::where('id', '!=', $me->id)
|
||||
->where(function ($query) use ($q) {
|
||||
$query->where('name', 'like', "%{$q}%")
|
||||
->orWhere('apellidoPaterno', 'like', "%{$q}%")
|
||||
->orWhere('apellidoMaterno', 'like', "%{$q}%");
|
||||
})
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
return response()->json(
|
||||
$users->map(function (User $user) use ($me) {
|
||||
$friendship = $me->friendshipWith($user);
|
||||
$status = $friendship?->status ?? 'none';
|
||||
$isReceiver = $friendship?->receiver_id === $me->id;
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name . ' ' . $user->apellidoPaterno,
|
||||
'avatar' => $user->profile_photo_url,
|
||||
'profile_url' => route('social.profile', $user),
|
||||
'chat_url' => route('chat.show', $user),
|
||||
'friendship' => [
|
||||
'status' => $status,
|
||||
'id' => $friendship?->id,
|
||||
'is_receiver' => $isReceiver,
|
||||
],
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'body', 'image_path'];
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function likes(): HasMany
|
||||
{
|
||||
return $this->hasMany(PostLike::class);
|
||||
}
|
||||
|
||||
public function comments(): HasMany
|
||||
{
|
||||
return $this->hasMany(PostComment::class)->with('author')->orderBy('created_at');
|
||||
}
|
||||
|
||||
public function isLikedBy(User $user): bool
|
||||
{
|
||||
return $this->likes()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PostComment extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'post_id', 'body'];
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PostLike extends Model
|
||||
{
|
||||
protected $fillable = ['user_id', 'post_id'];
|
||||
}
|
||||
@@ -131,6 +131,45 @@ class User extends Authenticatable
|
||||
return $this->belongsToMany(Alumno::class);
|
||||
}
|
||||
|
||||
public function posts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
|
||||
public function sentFriendRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(Friendship::class, 'sender_id');
|
||||
}
|
||||
|
||||
public function receivedFriendRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(Friendship::class, 'receiver_id');
|
||||
}
|
||||
|
||||
public function friendshipWith(User $user): ?Friendship
|
||||
{
|
||||
return Friendship::where(function ($q) use ($user) {
|
||||
$q->where('sender_id', $this->id)->where('receiver_id', $user->id);
|
||||
})
|
||||
->orWhere(function ($q) use ($user) {
|
||||
$q->where('sender_id', $user->id)->where('receiver_id', $this->id);
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
public function isFriendWith(User $user): bool
|
||||
{
|
||||
$friendship = $this->friendshipWith($user);
|
||||
return $friendship?->status === 'accepted';
|
||||
}
|
||||
|
||||
public function friendIds(): array
|
||||
{
|
||||
$sent = Friendship::where('sender_id', $this->id)->where('status', 'accepted')->pluck('receiver_id');
|
||||
$received = Friendship::where('receiver_id', $this->id)->where('status', 'accepted')->pluck('sender_id');
|
||||
return $sent->merge($received)->unique()->values()->all();
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($user) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class FriendRequestNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(public User $sender) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->sender->name . ' te envió una solicitud de amistad',
|
||||
'body' => null,
|
||||
'url' => route('friendships.index'),
|
||||
'icon' => 'fa-user-plus',
|
||||
'color' => 'primary',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user