mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 20:20:41 -05:00
Merge pull request 'Add user profile data' (#37) from waffle.lord/forge:user-profile-info into profile-data
Reviewed-on: SPT/forge#37
This commit is contained in:
commit
b67dd2fd97
@ -21,6 +21,11 @@ class UserController extends Controller
|
|||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not sure if this is optimal. Some way to do $user->with(...) ???
|
||||||
|
$user = User::where('id', $user->id)
|
||||||
|
->with(['followers', 'following'])
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
return view('user.show', compact('user'));
|
return view('user.show', compact('user'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
app/Livewire/User/Profile.php
Normal file
54
app/Livewire/User/Profile.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\User;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
class Profile extends Component
|
||||||
|
{
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
public User $user;
|
||||||
|
|
||||||
|
#[Url]
|
||||||
|
public string $section = 'wall';
|
||||||
|
|
||||||
|
public $followers;
|
||||||
|
|
||||||
|
public $following;
|
||||||
|
|
||||||
|
protected $listeners = ['refreshNeeded' => 'render'];
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$this->followers = $this->user->followers;
|
||||||
|
$this->following = $this->user->following;
|
||||||
|
|
||||||
|
$mods = $this->user->mods()->withWhereHas('latestVersion')->paginate(6);
|
||||||
|
|
||||||
|
return view('livewire.user.profile', compact('mods'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSection(string $name)
|
||||||
|
{
|
||||||
|
$this->section = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
// todo: not implemented yet
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followUser(User $user)
|
||||||
|
{
|
||||||
|
auth()->user()->follow($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unfollowUser(User $user)
|
||||||
|
{
|
||||||
|
auth()->user()->unfollow($user);
|
||||||
|
}
|
||||||
|
}
|
61
app/Livewire/UserStack.php
Normal file
61
app/Livewire/UserStack.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Attributes\Reactive;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class UserStack extends Component
|
||||||
|
{
|
||||||
|
#[Reactive]
|
||||||
|
public $users;
|
||||||
|
|
||||||
|
public string $parentUserName;
|
||||||
|
|
||||||
|
public $authFollowingIds = [];
|
||||||
|
|
||||||
|
public string $label = 'Users';
|
||||||
|
|
||||||
|
public int $limit = 5;
|
||||||
|
|
||||||
|
public bool $viewAll = false;
|
||||||
|
|
||||||
|
public bool $refreshNeeded = false;
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
if (Auth::check()) {
|
||||||
|
$this->authFollowingIds = Auth::user()->following()->pluck('following_id')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('livewire.user-stack');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleViewAll()
|
||||||
|
{
|
||||||
|
$this->viewAll = ! $this->viewAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeDialog()
|
||||||
|
{
|
||||||
|
if ($this->refreshNeeded) {
|
||||||
|
$this->dispatch('refreshNeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->toggleViewAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function followUser(User $user)
|
||||||
|
{
|
||||||
|
Auth::user()->follow($user);
|
||||||
|
$this->refreshNeeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unfollowUser(User $user)
|
||||||
|
{
|
||||||
|
Auth::user()->unfollow($user);
|
||||||
|
$this->refreshNeeded = true;
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,51 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
return $this->belongsToMany(Mod::class);
|
return $this->belongsToMany(Mod::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and users they follow
|
||||||
|
*/
|
||||||
|
public function following(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'user_follows', 'follower_id', 'following_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and users that follow them
|
||||||
|
*/
|
||||||
|
public function followers(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'user_follows', 'following_id', 'follower_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFollowing(User|int $user): bool
|
||||||
|
{
|
||||||
|
$userId = $user instanceof User ? $user->id : $user;
|
||||||
|
|
||||||
|
return $this->following()->where('following_id', $userId)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function follow(User|int $user): void
|
||||||
|
{
|
||||||
|
$userId = $user instanceof User ? $user->id : $user;
|
||||||
|
|
||||||
|
if ($this->id === $userId) {
|
||||||
|
// don't allow following yourself
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->following()->syncWithoutDetaching($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unfollow(User|int $user): void
|
||||||
|
{
|
||||||
|
$userId = $user instanceof User ? $user->id : $user;
|
||||||
|
|
||||||
|
// make sure the user is being followed before trying to detach
|
||||||
|
if ($this->isFollowing($userId)) {
|
||||||
|
$this->following()->detach($userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data that is searchable by Scout.
|
* The data that is searchable by Scout.
|
||||||
*/
|
*/
|
||||||
|
@ -6,6 +6,7 @@ use App\Models\User;
|
|||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Random\RandomException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<User>
|
* @extends Factory<User>
|
||||||
@ -21,6 +22,8 @@ class UserFactory extends Factory
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the user's default state.
|
* Define the user's default state.
|
||||||
|
*
|
||||||
|
* @throws RandomException
|
||||||
*/
|
*/
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
@ -29,6 +32,7 @@ class UserFactory extends Factory
|
|||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'about' => fake()->paragraphs(random_int(1, 10), true),
|
||||||
'two_factor_secret' => null,
|
'two_factor_secret' => null,
|
||||||
'two_factor_recovery_codes' => null,
|
'two_factor_recovery_codes' => null,
|
||||||
'remember_token' => Str::random(10),
|
'remember_token' => Str::random(10),
|
||||||
|
@ -22,6 +22,7 @@ return new class extends Migration
|
|||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
|
$table->longText('about');
|
||||||
$table->foreignIdFor(UserRole::class)
|
$table->foreignIdFor(UserRole::class)
|
||||||
->nullable()
|
->nullable()
|
||||||
->default(null)
|
->default(null)
|
||||||
|
31
database/migrations/2024_08_28_141058_user_follows.php
Normal file
31
database/migrations/2024_08_28_141058_user_follows.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?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('user_follows', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('follower_id');
|
||||||
|
$table->unsignedBigInteger('following_id');
|
||||||
|
$table->foreign('follower_id')->references('id')->on('users')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->foreign('following_id')->references('id')->on('users')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_follows');
|
||||||
|
}
|
||||||
|
};
|
112
resources/views/livewire/user-stack.blade.php
Normal file
112
resources/views/livewire/user-stack.blade.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<div class="w-full text-gray-600 bg-white shadow-md dark:shadow-gray-950 drop-shadow-xl dark:text-gray-200 dark:bg-gray-900 rounded-xl p-4 m-4">
|
||||||
|
{{-- Card Header --}}
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<h2 class="text-2xl">{{$label}}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($users->count() === 0)
|
||||||
|
<div class="flex justify-center text-sm">
|
||||||
|
<span>{{__('nothing here yet')}}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex ml-6 py-2 justify-center items-center">
|
||||||
|
@foreach($users->slice(0, $limit) as $user)
|
||||||
|
{{-- User Badge --}}
|
||||||
|
<div class="relative group">
|
||||||
|
<a href="{{$user->profileUrl()}}"
|
||||||
|
class="rounded-full -ml-6 z-20 bg-[#ebf4ff] h-16 w-16 flex justify-center items-center border">
|
||||||
|
<img class="h-full w-full rounded-full" src="{{$user->profile_photo_url}}"
|
||||||
|
alt="{{$user->name[0]}}" />
|
||||||
|
</a>
|
||||||
|
{{-- tooltip --}}
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
||||||
|
{{$user->name}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@if($users->count() > $limit)
|
||||||
|
{{-- Count Badge --}}
|
||||||
|
<div class="relative group">
|
||||||
|
<span
|
||||||
|
class="rounded-full -ml-6 z-20 bg-cyan-500 dark:bg-cyan-700 h-16 w-16 flex justify-center items-center border text-white">+{{$users->count()-$limit}}</span>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full -ml-3 left-1/2 transform -translate-x-1/2 mb-2 w-max px-2 py-1 text-sm text-white bg-gray-700 rounded shadow-lg opacity-0 group-hover:opacity-100">
|
||||||
|
{{$users->count()}} total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($users->count() > $limit)
|
||||||
|
{{-- View all button --}}
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<button wire:click="toggleViewAll">view all</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- view all dialog --}}
|
||||||
|
<x-dialog-modal wire:model.live="viewAll">
|
||||||
|
<x-slot name="title">
|
||||||
|
<h2 class="text-2xl">{{$parentUserName}}'s {{$label}}</h2>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="content">
|
||||||
|
<div class="h-96 overflow-y-auto divide-y">
|
||||||
|
@foreach($users as $user)
|
||||||
|
{{-- user tile --}}
|
||||||
|
<div class="flex group/item">
|
||||||
|
<img class="h-16 w-16 m-2 rounded-full" src="{{$user->profile_photo_url}}"
|
||||||
|
alt="{{$user->name}}" />
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full justify-center">
|
||||||
|
<a class="text-2xl" href="{{$user->profileUrl()}}">{{$user->name}}</a>
|
||||||
|
<span class="">{{__("Member Since")}} {{ $user->created_at->format("M d, h:m a") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->id() != $user->id)
|
||||||
|
@if(count($authFollowingIds) !== 0 && in_array($user->id, $authFollowingIds))
|
||||||
|
{{-- following button --}}
|
||||||
|
<div class="flex invisible group-hover/item:visible justify-end items-center mr-10">
|
||||||
|
<button type="button" wire:click="unfollowUser({{$user}})"
|
||||||
|
class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-red-400 dark:text-red-600"
|
||||||
|
viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="m12.82 5.58-.82.822-.824-.824a5.375 5.375 0 1 0-7.601 7.602l7.895 7.895a.75.75 0 0 0 1.06 0l7.902-7.897a5.376 5.376 0 0 0-.001-7.599 5.38 5.38 0 0 0-7.611 0Z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{__('Following')}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- follow button --}}
|
||||||
|
<div class="flex invisible group-hover/item:visible justify-end items-center mr-10">
|
||||||
|
<button type="button" wire:click="followUser({{$user}})"
|
||||||
|
class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
|
viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="m12.82 5.58-.82.822-.824-.824a5.375 5.375 0 1 0-7.601 7.602l7.895 7.895a.75.75 0 0 0 1.06 0l7.902-7.897a5.376 5.376 0 0 0-.001-7.599 5.38 5.38 0 0 0-7.611 0Zm6.548 6.54L12 19.485 4.635 12.12a3.875 3.875 0 1 1 5.48-5.48l1.358 1.357a.75.75 0 0 0 1.073-.012L13.88 6.64a3.88 3.88 0 0 1 5.487 5.48Z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{__("Follow")}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
{{-- 'you' card for auth user in list --}}
|
||||||
|
<div class="flex invisible group-hover/item:visible justify-end items-center mr-10">
|
||||||
|
<p class="p-2 h-fit text-black bg-gray-200 dark:text-white dark:bg-gray-700 rounded-lg">{{__('You')}}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<x-slot name="footer">
|
||||||
|
<x-button wire:click="closeDialog">
|
||||||
|
{{__('Close')}}
|
||||||
|
</x-button>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
</x-dialog-modal>
|
||||||
|
</div>
|
148
resources/views/livewire/user/profile.blade.php
Normal file
148
resources/views/livewire/user/profile.blade.php
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<div class="sm:-mt-12 dark:bg-gray-800 dark:text-gray-100">
|
||||||
|
<div>
|
||||||
|
<img class="h-32 w-full object-cover lg:h-48" src="{{ $user->cover_photo_url }}"
|
||||||
|
alt="profile cover photo of {{ $user->name }}">
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
|
||||||
|
<div class="flex">
|
||||||
|
<img class="h-24 w-24 rounded-full ring-4 ring-white dark:ring-gray-800 sm:h-32 sm:w-32"
|
||||||
|
src="{{ $user->profile_photo_url }}" alt="profile photo of {{ $user->name }}" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1">
|
||||||
|
<div class="mt-6 min-w-0 flex-1 sm:hidden md:block">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
<p>{{__("Member Since")}} {{ $user->created_at->format("M d, h:m a") }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(auth()->check() && auth()->id() != $user->id)
|
||||||
|
@if(auth()->user()->isFollowing($user))
|
||||||
|
{{-- Following button --}}
|
||||||
|
<div
|
||||||
|
class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||||
|
<button type="button" wire:click="unfollowUser({{$user}})"
|
||||||
|
class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-red-400 dark:text-red-600"
|
||||||
|
viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="m12.82 5.58-.82.822-.824-.824a5.375 5.375 0 1 0-7.601 7.602l7.895 7.895a.75.75 0 0 0 1.06 0l7.902-7.897a5.376 5.376 0 0 0-.001-7.599 5.38 5.38 0 0 0-7.611 0Z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{__("Following")}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- Follow button --}}
|
||||||
|
<div
|
||||||
|
class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||||
|
<button type="button" wire:click="followUser({{$user}})"
|
||||||
|
class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
|
viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="m12.82 5.58-.82.822-.824-.824a5.375 5.375 0 1 0-7.601 7.602l7.895 7.895a.75.75 0 0 0 1.06 0l7.902-7.897a5.376 5.376 0 0 0-.001-7.599 5.38 5.38 0 0 0-7.611 0Zm6.548 6.54L12 19.485 4.635 12.12a3.875 3.875 0 1 1 5.48-5.48l1.358 1.357a.75.75 0 0 0 1.073-.012L13.88 6.64a3.88 3.88 0 0 1 5.487 5.48Z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{__("Follow")}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
{{-- Message button --}}
|
||||||
|
<div class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||||
|
<button type="button" wire:click="message"
|
||||||
|
class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
||||||
|
<path
|
||||||
|
d="M19 8.839l-7.77 3.885a2.75 2.75 0 01-2.46 0L1 8.839V14a2 2 0 002 2h14a2 2 0 002-2V8.839z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{__("Message")}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 hidden min-w-0 flex-1 sm:block md:hidden">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
<p>{{__("Member Since")}} {{ $user->created_at->format("M d, h:m a") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-4">
|
||||||
|
<div class="lg:hidden flex flex-col justify-top items-center">
|
||||||
|
<div class="flex w-full max-w-sm">
|
||||||
|
<livewire:user-stack :parentUserName="$user->name" :label="__('Followers')" :users="$followers"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full max-w-sm">
|
||||||
|
<livewire:user-stack :parentUserName="$user->name" :label="__('Following')" :users="$following"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col col-span-3">
|
||||||
|
<div>
|
||||||
|
{{-- Mobile Dropdown --}}
|
||||||
|
<div class="sm:hidden m-4">
|
||||||
|
<label for="tabs" class="sr-only">{{ __('Select a tab') }}</label>
|
||||||
|
<select wire:model.change="section" id="tabs" name="tabs" class="block w-full rounded-md dark:text-white bg-gray-100 dark:bg-gray-950 border-gray-300 dark:border-gray-700 focus:border-grey-500 dark:focus:border-grey-600 focus:ring-grey-500 dark:focus:ring-grey-600">
|
||||||
|
<option value="wall">{{ __('Wall') }}</option>
|
||||||
|
<option value="mods">{{ __('Mods') }}</option>
|
||||||
|
<option value="recentActivity">{{ __('Recent Activity') }}</option>
|
||||||
|
<option value="aboutMe">{{ __('About Me') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Desktop Tabs --}}
|
||||||
|
<div class="hidden sm:block">
|
||||||
|
<nav class="isolate m-4 flex divide-x divide-gray-200 dark:divide-gray-800 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl" aria-label="Tabs">
|
||||||
|
<button wire:click="setSection('wall')" class="tab rounded-l-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10" aria-current="page">
|
||||||
|
<span>{{ __('Wall') }}</span>
|
||||||
|
<span aria-hidden="true" class="{{$section === 'wall' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'}}"></span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="setSection('mods')" class="tab group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||||
|
<span>{{ __('Mods') }}</span>
|
||||||
|
<span aria-hidden="true" class="{{$section === 'mods' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'}}"></span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="setSection('recentActivity')" class="tab group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||||
|
<span>{{ __('Recent Activity') }}</span>
|
||||||
|
<span aria-hidden="true" class="{{$section === 'recentActivity' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'}}"></span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="setSection('aboutMe')" class="tab rounded-r-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||||
|
<span>{{ __('About Me') }}</span>
|
||||||
|
<span aria-hidden="true" class="{{$section === 'aboutMe' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'}}"></span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@switch($section)
|
||||||
|
@case('wall')
|
||||||
|
<p class="m-4">This is the wall. I don't think this can be implemented yet? requires comments or something</p>
|
||||||
|
@break
|
||||||
|
@case('mods')
|
||||||
|
<div class="m-4">
|
||||||
|
{{ $mods->links() }}
|
||||||
|
</div>
|
||||||
|
<div class="m-4 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
@foreach($mods as $mod)
|
||||||
|
<x-mod-card :mod="$mod" :version="$mod->latestVersion" />
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@break
|
||||||
|
@case('recentActivity')
|
||||||
|
<p class="m-4">This is the recent activity. Probably need to implement some kind of activity tracking for this?</p>
|
||||||
|
@break
|
||||||
|
@case('aboutMe')
|
||||||
|
<div class="text-gray-600 bg-white shadow-md dark:shadow-gray-950 drop-shadow-xl dark:text-gray-200 dark:bg-gray-900 rounded-xl p-2 m-4">
|
||||||
|
<p class="m-4">{{$user->about}}</p>
|
||||||
|
</div>
|
||||||
|
@break
|
||||||
|
@endswitch
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-lg:hidden flex flex-col justify-top items-center">
|
||||||
|
<div class="flex w-full max-w-sm">
|
||||||
|
<livewire:user-stack :parentUserName="$user->name" :label="__('Followers')" :users="$followers"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full max-w-sm">
|
||||||
|
<livewire:user-stack :parentUserName="$user->name" :label="__('Following')" :users="$following"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,35 +1,3 @@
|
|||||||
<x-app-layout>
|
<x-app-layout>
|
||||||
|
@livewire('user.profile', ['user' => $user])
|
||||||
<div class="sm:-mt-12 dark:bg-gray-800 dark:text-gray-100">
|
|
||||||
<div>
|
|
||||||
<img class="h-32 w-full object-cover lg:h-48" src="{{ $user->cover_photo_url }}" alt="{{ $user->name }}">
|
|
||||||
</div>
|
|
||||||
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
|
|
||||||
<div class="flex">
|
|
||||||
<img class="h-24 w-24 rounded-full ring-4 ring-white dark:ring-gray-800 sm:h-32 sm:w-32" src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1">
|
|
||||||
<div class="mt-6 min-w-0 flex-1 sm:hidden md:block">
|
|
||||||
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
|
||||||
</div>
|
|
||||||
{{--
|
|
||||||
<div class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
|
||||||
<button type="button" class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
|
||||||
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
||||||
<path d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
|
||||||
<path d="M19 8.839l-7.77 3.885a2.75 2.75 0 01-2.46 0L1 8.839V14a2 2 0 002 2h14a2 2 0 002-2V8.839z" />
|
|
||||||
</svg>
|
|
||||||
<span>Message</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
--}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-6 hidden min-w-0 flex-1 sm:block md:hidden">
|
|
||||||
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</x-app-layout>
|
</x-app-layout>
|
||||||
|
99
tests/Feature/User/FollowTest.php
Normal file
99
tests/Feature/User/FollowTest.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
test('confirm a user cannot follow themself', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user->follow($user);
|
||||||
|
|
||||||
|
$this->assertEmpty($user->follwers);
|
||||||
|
$this->assertEmpty($user->following);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm a user can follow and unfollow another user', function () {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
|
||||||
|
$user1->follow($user2);
|
||||||
|
|
||||||
|
$this->assertTrue($user1->isFollowing($user2));
|
||||||
|
|
||||||
|
$user1->unfollow($user2);
|
||||||
|
|
||||||
|
$this->assertFalse($user1->isFollowing($user2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm following a user cannot be done twice', function () {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
|
||||||
|
$user1->follow($user2);
|
||||||
|
$user1->follow($user2);
|
||||||
|
|
||||||
|
$this->assertCount(1, $user1->following);
|
||||||
|
$this->assertCount(1, $user2->followers);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm unfollowing a user that isnt being followed doesnt throw', function () {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
|
||||||
|
$user1->unfollow($user2);
|
||||||
|
|
||||||
|
$this->assertEmpty($user1->following);
|
||||||
|
$this->assertEmpty($user2->followers);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm unfollowing random number doesnt perform detach all', function () {
|
||||||
|
$user1 = User::factory()->create();
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
$user3 = User::factory()->create();
|
||||||
|
|
||||||
|
$user1->follow($user2);
|
||||||
|
$user1->follow($user3);
|
||||||
|
|
||||||
|
$this->assertTrue($user1->isFollowing($user2));
|
||||||
|
$this->assertTrue($user1->isFollowing($user3));
|
||||||
|
|
||||||
|
$this->assertCount(2, $user1->following);
|
||||||
|
$this->assertCount(1, $user2->followers);
|
||||||
|
$this->assertCount(1, $user3->followers);
|
||||||
|
|
||||||
|
$user1->unfollow(111112222233333);
|
||||||
|
|
||||||
|
$this->assertTrue($user1->isFollowing($user2));
|
||||||
|
$this->assertTrue($user1->isFollowing($user3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm null follow input fails', function () {
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user->follow(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm empty follow input fails', function () {
|
||||||
|
$this->expectException(ArgumentCountError::class);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user->follow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm null unfollow input fails', function () {
|
||||||
|
$this->expectException(TypeError::class);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user->unfollow(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm empty unfollow input fails', function () {
|
||||||
|
$this->expectException(ArgumentCountError::class);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user->unfollow();
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user