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() + ); + } +}