diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index 78dc382..1df4fd2 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -25,7 +25,7 @@ public function index(Request $request) 'views', 'created_at', ]) - ->with('user:id,name,username') + ->with(['user:id,name,username', 'user.roles:id,name']) ->withCount(['reactions', 'comments']) ->where('is_published', true); @@ -56,13 +56,13 @@ public function show(Blog $blog) { abort_unless($blog->is_published, 404); - $blog->load('user:id,name,username,image_path'); + $blog->load(['user:id,name,username,image_path', 'user.roles:id,name']); $blog->increment('views'); $reactionsCount = $blog->reactions()->count(); $reactors = $blog->reactions() - ->with('user:id,name,username,image_path,institution') + ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) ->latest('id') ->limit(50) ->get() @@ -71,7 +71,7 @@ public function show(Blog $blog) ->values(); $comments = $blog->comments() - ->with('user:id,name,username,image_path,institution') + ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) ->latest() ->get(); @@ -131,7 +131,7 @@ public function storeComment(Request $request, Blog $blog) 'content' => trim($validated['content']), ]); - $comment->load('user:id,name,username,image_path,institution'); + $comment->load(['user:id,name,username,image_path,institution', 'user.roles:id,name']); $blog->loadMissing('user:id,name,email,receive_emails'); if ($blog->user && $blog->user_id !== $userId && $blog->user->email && $blog->user->receive_emails !== false) { diff --git a/app/Http/Controllers/ResourceController.php b/app/Http/Controllers/ResourceController.php index 91c1f5c..656bc35 100644 --- a/app/Http/Controllers/ResourceController.php +++ b/app/Http/Controllers/ResourceController.php @@ -38,7 +38,7 @@ public function show($id) $completionsCount = ResourceCompletion::where('resource_id', $id)->count(); $completers = ResourceCompletion::where('resource_id', $id) - ->with('user:id,name,username,image_path,institution') + ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) ->latest() ->take(10) ->get() diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 5f6968f..987ca59 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -94,6 +94,29 @@ public function show(string $username) ]) ->filter(fn ($item) => $item['title'] !== null); + // Suggested / Discover community members: 2 contributors + 2 general users + $contributorUsers = User::where('id', '!=', $user->id) + ->whereNotNull('username') + ->whereHas('roles') + ->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about']) + ->with('roles:id,name') + ->inRandomOrder() + ->take(2) + ->get(); + + $excludedIds = $contributorUsers->pluck('id')->push($user->id)->all(); + $remainingNeeded = 4 - $contributorUsers->count(); + + $randomUsers = User::whereNotIn('id', $excludedIds) + ->whereNotNull('username') + ->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about']) + ->with('roles:id,name') + ->inRandomOrder() + ->take($remainingNeeded) + ->get(); + + $suggestedUsers = $contributorUsers->concat($randomUsers)->shuffle()->values(); + return Inertia::render('User/Show', [ 'profileUser' => [ 'id' => $user->id, @@ -125,6 +148,7 @@ public function show(string $username) 'reactions' => $recentReactions->values(), 'comments' => $recentComments->values(), ], + 'suggestedUsers' => $suggestedUsers, ]); } } diff --git a/app/Models/User.php b/app/Models/User.php index 3417c47..1f02793 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -51,6 +51,7 @@ class User extends Authenticatable 'instagram', 'github', ]; + protected static function booted(): void { static::created(function (User $user) { diff --git a/resources/js/components/BlogCard.vue b/resources/js/components/BlogCard.vue index 50e58c0..3fa5bc7 100644 --- a/resources/js/components/BlogCard.vue +++ b/resources/js/components/BlogCard.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/pages/Blog/Show.vue b/resources/js/pages/Blog/Show.vue index 589bf68..bb5077d 100644 --- a/resources/js/pages/Blog/Show.vue +++ b/resources/js/pages/Blog/Show.vue @@ -14,8 +14,10 @@ import { LogIn, X, Loader2, + BadgeCheck, } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; +import UserListItem from '@/components/UserListItem.vue'; const props = defineProps({ blog: { @@ -275,9 +277,16 @@ const goBack = () => { ? `/u/${blog.user.username}` : '#' " - class="font-medium text-indigo-600 transition-colors hover:text-indigo-800 hover:underline dark:text-indigo-400 dark:hover:text-indigo-300" + class="inline-flex items-center gap-1 font-medium text-indigo-600 transition-colors hover:text-indigo-800 hover:underline dark:text-indigo-400 dark:hover:text-indigo-300" > - {{ blog.user?.name }} + {{ blog.user?.name }} + @@ -622,16 +631,28 @@ const goBack = () => {
-
+
- {{ comment.user?.name || 'Anonymous' }} + {{ + comment.user?.name || 'Anonymous' + }} + { ? `/u/${blog.user.username}` : '#' " - class="mt-1 block text-lg font-semibold text-slate-900 underline transition dark:text-gray-100" + class="mt-1 inline-flex items-center gap-1.5 text-lg font-semibold text-slate-900 underline transition dark:text-gray-100" > - {{ blog.user?.name }} + {{ blog.user?.name }} + { No reactions yet.
- -
- - - {{ reactor.name?.charAt(0) || 'U' }} - -
-
-

- {{ reactor.name }} -

-

- {{ reactor.institution }} -

-
- + :user="reactor" + theme="rose" + />
{ No completions recorded yet.
- -
- - - {{ completer.name?.charAt(0) || 'U' }} - -
-
-

- {{ completer.name }} -

-

- {{ completer.institution }} -

-
- + :user="completer" + theme="emerald" + />
+ + + + + + + + @@ -760,6 +818,94 @@ const totalActivitiesCount = computed(
+ + +
+
+
+ + Discover People +
+ + Meet team → + +
+ +
+ +
+
+ +
+ {{ person.name.charAt(0).toUpperCase() }} +
+
+
+
+

+ {{ person.name }} +

+ + + +
+

+ {{ person.title || person.institution }} +

+

+ @{{ person.username }} +

+
+
+ + + +
+
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 9b9ab66..c20e3b0 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -28,6 +28,24 @@ $ogImage = data_get($blog, 'featured_image') ?: url('/feature.png'); $ogType = 'article'; $ogUrl = data_get($blog, 'slug') ? url('/blogs/' . data_get($blog, 'slug')) : url()->current(); + } elseif ($pageComponent === 'User/Show' && !empty($props['profileUser'])) { + $profileUser = $props['profileUser']; + $userName = data_get($profileUser, 'name'); + $userHandle = data_get($profileUser, 'username'); + $userTitle = data_get($profileUser, 'title'); + $userInstitution = data_get($profileUser, 'institution'); + $userAbout = data_get($profileUser, 'about'); + + $metaTitle = "{$userName} (@{$userHandle}) - " . config('app.name', 'HSCStack'); + $ogTitle = "{$userName} (@{$userHandle})"; + + $descParts = array_filter([$userTitle, $userInstitution, $userAbout]); + $metaDescription = !empty($descParts) + ? implode(' · ', array_slice($descParts, 0, 2)) + : "View {$userName}'s profile, completed study topics, and contributions on HSCStack."; + $ogImage = data_get($profileUser, 'image_url') ?: url('/feature.png'); + $ogType = 'profile'; + $ogUrl = $userHandle ? url('/u/' . $userHandle) : url()->current(); } elseif ($pageComponent === 'Node' && (!empty($props['breadcrumb']) || !empty($props['subject']))) { $crumbs = $props['breadcrumb'] ?? []; $lastCrumb = is_array($crumbs) && count($crumbs) ? end($crumbs) : null; diff --git a/tests/Feature/UserProfileTest.php b/tests/Feature/UserProfileTest.php index b2e5c91..478fd23 100644 --- a/tests/Feature/UserProfileTest.php +++ b/tests/Feature/UserProfileTest.php @@ -18,11 +18,15 @@ $response = $this->get('/u/tajim_pro'); $response->assertOk(); + $response->assertSee('', false); + $response->assertSee('', false); + $response->assertSee('', false); $response->assertInertia(fn ($page) => $page ->component('User/Show') ->where('profileUser.username', 'tajim_pro') ->where('profileUser.name', 'Tajim Ahmed') ->where('profileUser.institution', 'Notre Dame College') + ->has('suggestedUsers') ); }); @@ -95,3 +99,26 @@ ->has('recentActivities.completions', 1) ); }); + +test('public profile includes both contributors and random users in suggestions', function () { + $role = Role::firstOrCreate(['name' => 'editor']); + + $mainUser = User::factory()->create(['username' => 'current_user']); + + $contributor1 = User::factory()->create(['username' => 'contrib_1']); + $contributor1->assignRole($role); + + $contributor2 = User::factory()->create(['username' => 'contrib_2']); + $contributor2->assignRole($role); + + $student1 = User::factory()->create(['username' => 'student_1']); + $student2 = User::factory()->create(['username' => 'student_2']); + + $response = $this->get('/u/current_user'); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('User/Show') + ->has('suggestedUsers', 4) + ); +});