diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index b0554cc..81ecf92 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -21,6 +21,11 @@ class UserController extends Controller 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')); } } diff --git a/app/Livewire/User/Profile.php b/app/Livewire/User/Profile.php new file mode 100644 index 0000000..f6336db --- /dev/null +++ b/app/Livewire/User/Profile.php @@ -0,0 +1,54 @@ + '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); + } +} diff --git a/app/Livewire/UserStack.php b/app/Livewire/UserStack.php new file mode 100644 index 0000000..874fff1 --- /dev/null +++ b/app/Livewire/UserStack.php @@ -0,0 +1,61 @@ +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; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 74df4fd..8df99dd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -52,6 +52,51 @@ class User extends Authenticatable implements MustVerifyEmail 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. */ diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 3b311ce..f90525c 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -6,6 +6,7 @@ use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use Random\RandomException; /** * @extends Factory @@ -21,6 +22,8 @@ class UserFactory extends Factory /** * Define the user's default state. + * + * @throws RandomException */ public function definition(): array { @@ -29,6 +32,7 @@ class UserFactory extends Factory 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), + 'about' => fake()->paragraphs(random_int(1, 10), true), 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'remember_token' => Str::random(10), diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 5d26cb2..1e03166 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -22,6 +22,7 @@ return new class extends Migration $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->longText('about'); $table->foreignIdFor(UserRole::class) ->nullable() ->default(null) diff --git a/database/migrations/2024_08_28_141058_user_follows.php b/database/migrations/2024_08_28_141058_user_follows.php new file mode 100644 index 0000000..7b7fca3 --- /dev/null +++ b/database/migrations/2024_08_28_141058_user_follows.php @@ -0,0 +1,31 @@ +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'); + } +}; diff --git a/resources/views/livewire/user-stack.blade.php b/resources/views/livewire/user-stack.blade.php new file mode 100644 index 0000000..a5d3e61 --- /dev/null +++ b/resources/views/livewire/user-stack.blade.php @@ -0,0 +1,112 @@ +
+ {{-- Card Header --}} +
+

{{$label}}

+
+ + @if($users->count() === 0) +
+ {{__('nothing here yet')}} +
+ @else +
+ @foreach($users->slice(0, $limit) as $user) + {{-- User Badge --}} +
+ + {{$user->name[0]}} + + {{-- tooltip --}} +
+ {{$user->name}} +
+
+ @endforeach + @if($users->count() > $limit) + {{-- Count Badge --}} +
+ +{{$users->count()-$limit}} +
+ {{$users->count()}} total +
+
+ @endif +
+ @endif + @if($users->count() > $limit) + {{-- View all button --}} +
+ +
+ @endif + + {{-- view all dialog --}} + + +

{{$parentUserName}}'s {{$label}}

+
+ + +
+ @foreach($users as $user) + {{-- user tile --}} +
+ {{$user->name}} + +
+ {{$user->name}} + {{__("Member Since")}} {{ $user->created_at->format("M d, h:m a") }} +
+ + @if(auth()->id() != $user->id) + @if(count($authFollowingIds) !== 0 && in_array($user->id, $authFollowingIds)) + {{-- following button --}} + + @else + {{-- follow button --}} + + @endif + @else + {{-- 'you' card for auth user in list --}} + + @endif +
+ @endforeach +
+
+ + + + {{__('Close')}} + + + +
+
diff --git a/resources/views/livewire/user/profile.blade.php b/resources/views/livewire/user/profile.blade.php new file mode 100644 index 0000000..8eb010d --- /dev/null +++ b/resources/views/livewire/user/profile.blade.php @@ -0,0 +1,148 @@ +
+
+ profile cover photo of {{ $user->name }} +
+
+
+
+ profile photo of {{ $user->name }} +
+
+
+

{{ $user->name }}

+

{{__("Member Since")}} {{ $user->created_at->format("M d, h:m a") }}

+
+ + @if(auth()->check() && auth()->id() != $user->id) + @if(auth()->user()->isFollowing($user)) + {{-- Following button --}} +
+ +
+ @else + {{-- Follow button --}} +
+ +
+ @endif + {{-- Message button --}} +
+ +
+ @endif +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ {{-- Mobile Dropdown --}} +
+ + +
+ + {{-- Desktop Tabs --}} + +
+
+ @switch($section) + @case('wall') +

This is the wall. I don't think this can be implemented yet? requires comments or something

+ @break + @case('mods') +
+ {{ $mods->links() }} +
+
+ @foreach($mods as $mod) + + @endforeach +
+ @break + @case('recentActivity') +

This is the recent activity. Probably need to implement some kind of activity tracking for this?

+ @break + @case('aboutMe') +
+

{{$user->about}}

+
+ @break + @endswitch +
+
+
+
+ +
+
+ +
+
+
+
diff --git a/resources/views/user/show.blade.php b/resources/views/user/show.blade.php index 8e2874c..99917e8 100644 --- a/resources/views/user/show.blade.php +++ b/resources/views/user/show.blade.php @@ -1,35 +1,3 @@ - -
-
- {{ $user->name }} -
-
-
-
- {{ $user->name }} -
-
-
-

{{ $user->name }}

-
- {{-- -
- -
- --}} -
-
- -
-
- + @livewire('user.profile', ['user' => $user])
diff --git a/tests/Feature/User/FollowTest.php b/tests/Feature/User/FollowTest.php new file mode 100644 index 0000000..d86275c --- /dev/null +++ b/tests/Feature/User/FollowTest.php @@ -0,0 +1,99 @@ +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(); +});