From 3d834fb2ecf092dbaef9b9a36bb06987ae044b04 Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Fri, 11 Sep 2026 11:24:21 +0100 Subject: [PATCH] Fix community Leading Teachers country filter and avatar placeholders. Filter approved teachers by the selected country so Lithuania (and others) are not buried in the full EU list, and fall back to a local default image when S3 avatars are missing or inaccessible. Co-authored-by: Cursor --- app/Http/Controllers/CommunityController.php | 7 ++- app/User.php | 28 ++++++++++ resources/views/community.blade.php | 51 +++++++++-------- .../CommunityAmbassadorFilteringTest.php | 55 +++++++++++++++++-- tests/Unit/UserCommunityAvatarUrlTest.php | 46 ++++++++++++++++ 5 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 tests/Unit/UserCommunityAvatarUrlTest.php diff --git a/app/Http/Controllers/CommunityController.php b/app/Http/Controllers/CommunityController.php index 0da7f9d95..70a4c3f61 100644 --- a/app/Http/Controllers/CommunityController.php +++ b/app/Http/Controllers/CommunityController.php @@ -24,7 +24,11 @@ public function index(UserFilters $filters) ->where('avatar_path', '<>', 'images/default-avatar.png') ->paginate(10); - $teachers = User::role('leading teacher')->where('approved', 1)->with('city')->get(); + $teachers = User::role('leading teacher') + ->where('approved', 1) + ->filter($filters) + ->with(['city', 'expertises']) + ->get(); $countries = Country::withCoordinators(); @@ -33,6 +37,7 @@ public function index(UserFilters $filters) 'countries' => $countries, 'teachers' => $teachers, 'country_iso' => request()->get('country_iso'), + 'default_avatar' => asset('images/default.png'), ]); } } diff --git a/app/User.php b/app/User.php index de4d0ad4d..8292554a4 100644 --- a/app/User.php +++ b/app/User.php @@ -341,6 +341,34 @@ public function getAvatarPathAttribute($avatar) return Storage::disk('s3')->url($avatar); } + /** + * Public avatar URL for community pages. Falls back to a local placeholder + * when the stored path is empty or a known default (S3 defaults are often inaccessible). + */ + public function communityAvatarUrl(?string $fallback = null): string + { + $fallback = $fallback ?: asset('images/default.png'); + $raw = $this->attributes['avatar_path'] ?? null; + + if ($raw === null || trim((string) $raw) === '') { + return $fallback; + } + + $normalized = strtolower(trim((string) $raw)); + $defaults = [ + 'avatars/default_avatar.png', + 'avatars/default.png', + 'images/default-avatar.png', + 'images/default.png', + ]; + + if (in_array($normalized, $defaults, true)) { + return $fallback; + } + + return $this->avatar_path; + } + /** * Get the path to the user's avatar. * diff --git a/resources/views/community.blade.php b/resources/views/community.blade.php index 60de1020a..24a238168 100755 --- a/resources/views/community.blade.php +++ b/resources/views/community.blade.php @@ -246,9 +246,10 @@ class="duration-500 transform -translate-x-6 min-w-4 group-hover:translate-x-0"
{{ $ambassador->fullName() }}
@@ -571,6 +572,7 @@ class="text-[#1C4DA1] text-2xl md:text-4xl leading-[44px] font-medium font-['Mon var markers = {}; var selectedMarker = null; var allTeachers = []; + var defaultAvatar = @json($default_avatar ?? asset('images/default.png')); // Function to populate teacher information in the right sidebar function populateTeacherInfo(teachers, city = null) { @@ -642,9 +644,10 @@ function populateTeacherInfo(teachers, city = null) {
- ${ teacher.avatar_path - ? `Avatar` - : '' } + `; }); @@ -709,21 +712,21 @@ function populateTeacherInfo(teachers, city = null) { mymap.setView([centerInfo.latitude, centerInfo.longitude], centerInfo.zoom); }); - // Populate the global teachers array from PHP data + // Populate the global teachers array from PHP data (already filtered by country) @foreach ($teachers->groupBy('city_id') as $cityId => $teachersInCity) @foreach ($teachersInCity as $teacher) allTeachers.push({ - firstname: "{{ $teacher->firstname }}", - lastname: "{{ $teacher->lastname }}", - email: "{{ $teacher->email }}", - country_iso: "{{ $teacher->country_iso }}", - twitter: "{{ $teacher->twitter }}", - website: "{{ $teacher->website }}", + firstname: @json($teacher->firstname), + lastname: @json($teacher->lastname), + email: @json($teacher->email), + country_iso: @json($teacher->country_iso), + twitter: @json($teacher->twitter), + website: @json($teacher->website), bio: @json($teacher->bio), - avatar_path: "{{ $teacher->avatar_path }}", - city: "{{ $teacher->city->city ?? 'N/A' }}", - latitude: "{{ $teacher->city->latitude ?? '' }}", - longitude: "{{ $teacher->city->longitude ?? '' }}", + avatar_path: @json($teacher->communityAvatarUrl($default_avatar)), + city: @json($teacher->city->city ?? 'N/A'), + latitude: @json($teacher->city->latitude ?? ''), + longitude: @json($teacher->city->longitude ?? ''), expertises: @json($teacher->expertises->pluck('name')->toArray()) }); @endforeach @@ -758,15 +761,15 @@ function populateTeacherInfo(teachers, city = null) { var teacherList = [ @foreach ($teachersInCity as $teacher) { - firstname: "{{ $teacher->firstname }}", - lastname: "{{ $teacher->lastname }}", - email: "{{ $teacher->email }}", - country_iso: "{{ $teacher->country_iso }}", - twitter: "{{ $teacher->twitter }}", - website: "{{ $teacher->website }}", + firstname: @json($teacher->firstname), + lastname: @json($teacher->lastname), + email: @json($teacher->email), + country_iso: @json($teacher->country_iso), + twitter: @json($teacher->twitter), + website: @json($teacher->website), bio: @json($teacher->bio), - avatar_path: "{{ $teacher->avatar_path }}", - city: "{{ $teacher->city->city ?? 'N/A' }}", + avatar_path: @json($teacher->communityAvatarUrl($default_avatar)), + city: @json($teacher->city->city ?? 'N/A'), expertises: @json($teacher->expertises->pluck('name')->toArray()) }, @endforeach diff --git a/tests/Feature/CommunityAmbassadorFilteringTest.php b/tests/Feature/CommunityAmbassadorFilteringTest.php index cb6b4971c..5e5cc1def 100644 --- a/tests/Feature/CommunityAmbassadorFilteringTest.php +++ b/tests/Feature/CommunityAmbassadorFilteringTest.php @@ -2,6 +2,9 @@ namespace Tests\Feature; +use App\City; +use App\Country; +use App\User; use Illuminate\Foundation\Testing\DatabaseMigrations; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -15,9 +18,9 @@ public function community_view_does_not_include_ambassador_without_bio_or_avatar { $this->seed('RolesAndPermissionsSeeder'); $this->seed('LeadingTeacherRoleSeeder'); - $fr = \App\Country::factory()->create(['iso' => 'FR']); + $fr = Country::factory()->create(['iso' => 'FR']); - $bad = \App\User::factory()->create([ + $bad = User::factory()->create([ 'country_iso' => $fr->iso, 'bio' => null, 'avatar_path' => null, @@ -28,8 +31,52 @@ public function community_view_does_not_include_ambassador_without_bio_or_avatar $res->assertViewHas('ambassadors', function ($paginator) use ($bad) { $collection = $paginator->getCollection(); - return !$collection->contains('id', $bad->id); + + return ! $collection->contains('id', $bad->id); }); } -} + #[Test] + public function community_leading_teachers_are_filtered_by_selected_country(): void + { + $this->seed('RolesAndPermissionsSeeder'); + $this->seed('LeadingTeacherRoleSeeder'); + + $lt = Country::factory()->create(['iso' => 'LT']); + $fr = Country::factory()->create(['iso' => 'FR']); + $vilnius = City::factory()->create([ + 'country_iso' => 'LT', + 'city' => 'Vilnius', + 'latitude' => 54.6833, + 'longitude' => 25.2833, + ]); + + $lithuanian = User::factory()->create([ + 'country_iso' => $lt->iso, + 'city_id' => $vilnius->id, + 'approved' => 1, + 'firstname' => 'Dovile', + 'lastname' => 'Testiene', + 'avatar_path' => null, + ])->assignRole('leading teacher'); + + $french = User::factory()->create([ + 'country_iso' => $fr->iso, + 'approved' => 1, + 'firstname' => 'Marie', + 'lastname' => 'Dupont', + ])->assignRole('leading teacher'); + + $res = $this->get('/community?country_iso=LT'); + $res->assertOk(); + + $res->assertViewHas('teachers', function ($teachers) use ($lithuanian, $french) { + return $teachers->contains('id', $lithuanian->id) + && ! $teachers->contains('id', $french->id); + }); + + $res->assertSee('Dovile', false); + $res->assertDontSee('Marie', false); + $res->assertSee(asset('images/default.png'), false); + } +} diff --git a/tests/Unit/UserCommunityAvatarUrlTest.php b/tests/Unit/UserCommunityAvatarUrlTest.php new file mode 100644 index 000000000..2f063dbed --- /dev/null +++ b/tests/Unit/UserCommunityAvatarUrlTest.php @@ -0,0 +1,46 @@ +setRawAttributes(['avatar_path' => null]); + + $this->assertSame( + asset('images/default.png'), + $user->communityAvatarUrl() + ); + } + + #[Test] + public function default_s3_avatar_uses_local_placeholder(): void + { + $user = new User; + $user->setRawAttributes(['avatar_path' => 'avatars/default_avatar.png']); + + $this->assertSame( + asset('images/default.png'), + $user->communityAvatarUrl() + ); + } + + #[Test] + public function real_avatar_keeps_storage_url(): void + { + $user = new User; + $user->setRawAttributes(['avatar_path' => 'avatars/200/photo.jpg']); + + $this->assertStringContainsString( + 'avatars/200/photo.jpg', + $user->communityAvatarUrl() + ); + } +}