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:
@@ -5,7 +5,12 @@
|
|||||||
"Bash(git add *)",
|
"Bash(git add *)",
|
||||||
"Bash(git commit -m ' *)",
|
"Bash(git commit -m ' *)",
|
||||||
"Bash(git push *)",
|
"Bash(git push *)",
|
||||||
"Bash(php artisan *)"
|
"Bash(php artisan *)",
|
||||||
|
"Bash(ls resources/views/chat/)",
|
||||||
|
"Bash(ls resources/views/)",
|
||||||
|
"Bash(npm run *)",
|
||||||
|
"Bash(mv database/migrations/2026_04_28_170416_create_post_likes_table.php database/migrations/2026_04_28_170418_create_post_likes_table.php)",
|
||||||
|
"Bash(mv database/migrations/2026_04_28_170417_create_post_comments_table.php database/migrations/2026_04_28_170419_create_post_comments_table.php)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
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()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::creating(function ($user) {
|
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('posts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('body')->nullable();
|
||||||
|
$table->string('image_path')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('posts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('post_likes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('post_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->unique(['user_id', 'post_id']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('post_likes');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('post_comments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('post_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->text('body');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('post_comments');
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
-298
@@ -969,18 +969,6 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1335,18 +1323,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jiti": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/laravel-echo": {
|
"node_modules/laravel-echo": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.0.tgz",
|
||||||
@@ -1381,280 +1357,6 @@
|
|||||||
"vite": "^7.0.0"
|
"vite": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"lightningcss-android-arm64": "1.30.2",
|
|
||||||
"lightningcss-darwin-arm64": "1.30.2",
|
|
||||||
"lightningcss-darwin-x64": "1.30.2",
|
|
||||||
"lightningcss-freebsd-x64": "1.30.2",
|
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
|
||||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
|
||||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
|
||||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
|
||||||
"lightningcss-linux-x64-musl": "1.30.2",
|
|
||||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
|
||||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-android-arm64": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
|
||||||
"version": "1.30.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
|
||||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/parcel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
@endif
|
@endif
|
||||||
@if($msg->attachment)
|
@if($msg->attachment)
|
||||||
@if($msg->attachment_type === 'image')
|
@if($msg->attachment_type === 'image')
|
||||||
<img src="{{ route('files.serve', ['path' => $msg->attachment]) }}" class="img-fluid rounded mt-1">
|
<img src="{{ route('files.serve', ['path' => $msg->attachment]) }}" class="img-fluid rounded mt-1" style="max-width:240px; cursor:pointer" onclick="window.open(this.src,'_blank')">
|
||||||
@elseif($msg->attachment_type === 'audio')
|
@elseif($msg->attachment_type === 'audio')
|
||||||
<audio controls class="mt-1" style="max-width:250px">
|
<audio controls class="mt-1" style="max-width:250px">
|
||||||
<source src="{{ route('files.serve', ['path' => $msg->attachment]) }}">
|
<source src="{{ route('files.serve', ['path' => $msg->attachment]) }}">
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
@section('title', 'Solicitudes de Amistad')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container" style="max-width:680px">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white font-weight-bold">
|
||||||
|
<i class="fas fa-user-friends mr-2 text-primary"></i> Solicitudes de amistad
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@forelse($requests as $friendship)
|
||||||
|
<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="50" height="50" style="object-fit:cover">
|
||||||
|
</a>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<a href="{{ route('social.profile', $friendship->sender) }}" class="font-weight-bold text-dark">
|
||||||
|
{{ $friendship->sender->name }} {{ $friendship->sender->apellidoPaterno }}
|
||||||
|
</a>
|
||||||
|
<div class="text-muted small">{{ $friendship->created_at->diffForHumans() }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<form action="{{ route('friendships.accept', $friendship) }}" method="POST" class="mr-2">
|
||||||
|
@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 mr-1"></i> Rechazar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="fas fa-user-check fa-3x mb-3 d-block text-success"></i>
|
||||||
|
No tienes solicitudes pendientes.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -9,17 +9,17 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Topbar Search -->
|
<!-- 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">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control bg-light border-0 small" placeholder="Buscar..."
|
<input type="text" id="global-search" class="form-control bg-light border-0 small"
|
||||||
aria-label="Search" aria-describedby="basic-addon2">
|
placeholder="Buscar personas..." autocomplete="off" style="min-width:220px">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-primary" type="button">
|
<span class="btn btn-primary"><i class="fas fa-search fa-sm"></i></span>
|
||||||
<i class="fas fa-search fa-sm"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<ul class="navbar-nav ml-auto">
|
||||||
|
|
||||||
@@ -293,4 +293,84 @@ function markAllNotifsRead() {
|
|||||||
.forEach(el => el.classList.remove('font-weight-bold'));
|
.forEach(el => el.classList.remove('font-weight-bold'));
|
||||||
}).catch(() => {});
|
}).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>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,29 @@
|
|||||||
<i class="fas fa-fw fa-tachometer-alt"></i>
|
<i class="fas fa-fw fa-tachometer-alt"></i>
|
||||||
<span>Inicio</span></a>
|
<span>Inicio</span></a>
|
||||||
</li>
|
</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 -->
|
<!-- Divider -->
|
||||||
<hr class="sidebar-divider">
|
<hr class="sidebar-divider">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
@section('title', $user->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container" style="max-width:680px">
|
||||||
|
|
||||||
|
{{-- Tarjeta de perfil --}}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img src="{{ $user->profile_photo_url }}"
|
||||||
|
class="rounded-circle mr-4" width="72" height="72" style="object-fit:cover">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h5 class="mb-0 font-weight-bold">
|
||||||
|
{{ $user->name }} {{ $user->apellidoPaterno }} {{ $user->apellidoMaterno }}
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">{{ $user->email }}</small>
|
||||||
|
</div>
|
||||||
|
@if($user->id !== auth()->id())
|
||||||
|
<div class="d-flex flex-column align-items-end" style="gap:.5rem">
|
||||||
|
@php
|
||||||
|
$me = auth()->user();
|
||||||
|
$status = $friendship?->status ?? 'none';
|
||||||
|
$isSender = $friendship?->sender_id === $me->id;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
{{-- Botón de amistad --}}
|
||||||
|
@if($status === 'accepted')
|
||||||
|
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-user-check mr-1"></i> Amigos
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@elseif($status === 'pending' && $isSender)
|
||||||
|
<form action="{{ route('friendships.destroy', $friendship) }}" method="POST">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-user-clock mr-1"></i> Cancelar solicitud
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@elseif($status === 'pending' && !$isSender)
|
||||||
|
<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">Rechazar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<form action="{{ route('friendships.send', $user) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<button class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-user-plus mr-1"></i> Agregar amigo
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Botón de mensaje (solo si son amigos) --}}
|
||||||
|
@if($status === 'accepted')
|
||||||
|
<a href="{{ route('chat.show', $user) }}" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-comment mr-1"></i> Mensaje
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Publicaciones del usuario --}}
|
||||||
|
@forelse($posts as $post)
|
||||||
|
@include('feed._post', ['post' => $post])
|
||||||
|
@empty
|
||||||
|
<div class="text-center text-muted py-4">
|
||||||
|
<i class="fas fa-stream fa-2x mb-2 d-block"></i>
|
||||||
|
Este usuario no tiene publicaciones aún.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center mt-3">
|
||||||
|
{{ $posts->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
// Like toggle (mismo que feed)
|
||||||
|
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;
|
||||||
|
document.getElementById('like-count-' + postId).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.querySelector('i').classList.toggle('far', !res.data.liked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.toggle-comments').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
document.getElementById('comments-' + this.dataset.postId).classList.toggle('d-none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
+23
-1
@@ -8,6 +8,10 @@ use App\Http\Controllers\CampanController;
|
|||||||
use App\Http\Controllers\CarreraController;
|
use App\Http\Controllers\CarreraController;
|
||||||
use App\Http\Controllers\ChatController;
|
use App\Http\Controllers\ChatController;
|
||||||
use App\Http\Controllers\FileController;
|
use App\Http\Controllers\FileController;
|
||||||
|
use App\Http\Controllers\FriendshipController;
|
||||||
|
use App\Http\Controllers\PostController;
|
||||||
|
use App\Http\Controllers\SocialProfileController;
|
||||||
|
use App\Http\Controllers\UserSearchController;
|
||||||
use App\Http\Controllers\CodeController;
|
use App\Http\Controllers\CodeController;
|
||||||
use App\Http\Controllers\ConceptoController;
|
use App\Http\Controllers\ConceptoController;
|
||||||
use App\Http\Controllers\DocenteController;
|
use App\Http\Controllers\DocenteController;
|
||||||
@@ -58,7 +62,25 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified',
|
|||||||
Route::post('/chat/send', [ChatController::class, 'send'])->name('chat.send');
|
Route::post('/chat/send', [ChatController::class, 'send'])->name('chat.send');
|
||||||
Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve');
|
Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve');
|
||||||
Route::post('/notifications/{id}/read', fn($id) => auth()->user()->notifications()->findOrFail($id)->markAsRead())->name('notifications.read');
|
Route::post('/notifications/{id}/read', fn($id) => auth()->user()->notifications()->findOrFail($id)->markAsRead())->name('notifications.read');
|
||||||
Route::post('/notifications/read-all', fn() => auth()->user()->unreadNotifications->markAsRead())->name('notifications.read-all');Route::resource('/docente',DocenteController::class);
|
Route::post('/notifications/read-all', fn() => auth()->user()->unreadNotifications->markAsRead())->name('notifications.read-all');
|
||||||
|
|
||||||
|
// Social
|
||||||
|
Route::get('/feed', [PostController::class, 'index'])->name('feed');
|
||||||
|
Route::post('/feed', [PostController::class, 'store'])->name('posts.store');
|
||||||
|
Route::post('/posts/{post}/like', [PostController::class, 'like'])->name('posts.like');
|
||||||
|
Route::post('/posts/{post}/comments', [PostController::class, 'comment'])->name('posts.comment');
|
||||||
|
Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
|
||||||
|
|
||||||
|
Route::get('/friends', [FriendshipController::class, 'index'])->name('friendships.index');
|
||||||
|
Route::post('/friends/{user}/send', [FriendshipController::class, 'send'])->name('friendships.send');
|
||||||
|
Route::post('/friends/{friendship}/accept', [FriendshipController::class, 'accept'])->name('friendships.accept');
|
||||||
|
Route::delete('/friends/{friendship}', [FriendshipController::class, 'destroy'])->name('friendships.destroy');
|
||||||
|
Route::post('/friends/{user}/toggle', [FriendshipController::class, 'toggle'])->name('friendships.toggle');
|
||||||
|
|
||||||
|
Route::get('/search', UserSearchController::class)->name('users.search');
|
||||||
|
Route::get('/perfil/{user}', [SocialProfileController::class, 'show'])->name('social.profile');
|
||||||
|
|
||||||
|
Route::resource('/docente',DocenteController::class);
|
||||||
Route::resource('/documento',DocumentoController::class);
|
Route::resource('/documento',DocumentoController::class);
|
||||||
Route::get('/documento/ver/{id}', function ($id) { $doc = Documento::findOrFail($id); $alumno = auth()->user()->alumnos()->first(); if (!$alumno || $doc->alumno_id !== $alumno->id) { abort(403); } return response()->file( storage_path('app/public/'.$doc->archivo) ); });
|
Route::get('/documento/ver/{id}', function ($id) { $doc = Documento::findOrFail($id); $alumno = auth()->user()->alumnos()->first(); if (!$alumno || $doc->alumno_id !== $alumno->id) { abort(403); } return response()->file( storage_path('app/public/'.$doc->archivo) ); });
|
||||||
Route::get('/documento/ver/{id}', [DocumentoController::class, 'ver'])->name('documento.ver');
|
Route::get('/documento/ver/{id}', [DocumentoController::class, 'ver'])->name('documento.ver');
|
||||||
|
|||||||
Reference in New Issue
Block a user