diff --git a/app/Livewire/Home/Users.php b/app/Livewire/Home/Search.php similarity index 64% rename from app/Livewire/Home/Users.php rename to app/Livewire/Home/Search.php index 8d585a24f..8ece7ce61 100644 --- a/app/Livewire/Home/Users.php +++ b/app/Livewire/Home/Search.php @@ -5,18 +5,23 @@ namespace App\Livewire\Home; use App\Livewire\Concerns\Followable; +use App\Models\Question; use App\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection as SupportCollection; use Illuminate\Support\Facades\Cache; use Illuminate\View\View; +use Livewire\Attributes\Locked; use Livewire\Attributes\Url; use Livewire\Component; -final class Users extends Component +final class Search extends Component { use Followable; + private const int MIN_CONTENT_SEARCH_QUERY_LENGTH = 1; + /** * The component's search query. */ @@ -28,18 +33,72 @@ final class Users extends Component */ public bool $focusInput = false; + /** + * Indicates if the search result is for the welcome page. + */ + #[Locked] + public bool $welcomeSearch = false; + /** * Renders the component. */ public function render(): View { - return view('livewire.home.users', [ - 'users' => $this->query !== '' - ? $this->usersByQuery() + return view('livewire.home.search', [ + 'results' => $this->query !== '' + ? $this->searchByQuery() : $this->defaultUsers(), ]); } + /** + * Returns the users and questions by query. + * + * @return \Illuminate\Support\Collection + */ + private function searchByQuery(): SupportCollection + { + $users = $this->usersByQuery(); + + // Only search for questions if the query is long enough. + $questions = (mb_strlen($this->query) >= self::MIN_CONTENT_SEARCH_QUERY_LENGTH) + ? $this->questionsByQuery() + : collect(); + + return $this->welcomeSearch + ? $this->welcomeSearchResults($users, $questions) + : $users->merge($questions); + } + + /** + * Returns the users and questions by query. + * + * @param Collection $users + * @param Collection|SupportCollection $questions + * @return SupportCollection + */ + private function welcomeSearchResults(Collection $users, Collection|SupportCollection $questions): SupportCollection + { + if ($questions->isEmpty()) { + return collect()->merge($users); + } + + // With few matching users, fill results with questions, to reach 10. + if ($users->count() <= 6) { + return collect() + ->merge($users) + ->merge($questions->take(10 - $users->count())); + } + + // Otherwise take up to 4 questions and users enough to reach 10 results. + $questions = $questions->take(4); + $users = $users->take(10 - $questions->count()); + + return collect() + ->merge($users) + ->merge($questions); + } + /** * Returns the users by query, ordered by the number of questions received. * @@ -68,6 +127,24 @@ private function usersByQuery(): Collection ->get(); } + /** + * Returns the questions by query, ordered by the number of likes received. + * + * @return Collection + */ + private function questionsByQuery(): Collection + { + return Question::query() + ->withCount('likes') + ->orderBy('likes_count', 'desc') + ->with(['to', 'from', 'likes']) + ->whereAny(['question', 'answer'], 'like', "%{$this->query}%") + ->where('is_reported', false) + ->where('is_ignored', false) + ->limit(10) + ->get(); + } + /** * Returns the default users, ordered by the number of questions received. * diff --git a/resources/views/about.blade.php b/resources/views/about.blade.php index 5e5dff700..fc48cdae9 100644 --- a/resources/views/about.blade.php +++ b/resources/views/about.blade.php @@ -104,7 +104,7 @@ class="mt-12 max-w-4xl text-center font-mona text-3xl font-light md:text-4xl"
- +
diff --git a/resources/views/components/found-avatar-with-name.blade.php b/resources/views/components/found-avatar-with-name.blade.php new file mode 100644 index 000000000..a907353dd --- /dev/null +++ b/resources/views/components/found-avatar-with-name.blade.php @@ -0,0 +1,39 @@ +@props(['user']) + + +
+ {{ $user->username }} +
+ +
+
+

+ {{ $user->name }} +

+ + @if ($user->is_verified && $user->is_company_verified) + + @elseif ($user->is_verified) + + @endif +
+ +

+ {{ '@'.$user->username }} +

+
+
\ No newline at end of file diff --git a/resources/views/components/home-menu.blade.php b/resources/views/components/home-menu.blade.php index b7117a094..ffddfdd94 100644 --- a/resources/views/components/home-menu.blade.php +++ b/resources/views/components/home-menu.blade.php @@ -42,8 +42,8 @@ class="h-6 w-6 xsm:mr-2" - +
diff --git a/resources/views/livewire/home/search.blade.php b/resources/views/livewire/home/search.blade.php new file mode 100644 index 000000000..a355d3209 --- /dev/null +++ b/resources/views/livewire/home/search.blade.php @@ -0,0 +1,90 @@ + diff --git a/resources/views/livewire/home/users.blade.php b/resources/views/livewire/home/users.blade.php deleted file mode 100644 index 3ae1a628c..000000000 --- a/resources/views/livewire/home/users.blade.php +++ /dev/null @@ -1,80 +0,0 @@ -
-
-
- - - - -
-
- - @if ($users->isEmpty()) -
-

No users found.

-
- @else -
- -
- @endif -
diff --git a/routes/web.php b/routes/web.php index 8afb65024..4216db0de 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,7 +22,7 @@ Route::redirect('/for-you', '/following')->name('home.for_you'); Route::view('/following', 'home/following')->name('home.following'); Route::view('/trending', 'home/trending-questions')->name('home.trending'); -Route::view('/users', 'home/users')->name('home.users'); +Route::view('/search', 'home/search')->name('home.search'); Route::get('/hashtag/{hashtag}', HashtagController::class)->name('hashtag.show'); diff --git a/tests/Http/Home/SearchTest.php b/tests/Http/Home/SearchTest.php new file mode 100644 index 000000000..b70dc5eb1 --- /dev/null +++ b/tests/Http/Home/SearchTest.php @@ -0,0 +1,13 @@ +get(route('home.search')); + + $response->assertOk() + ->assertSee('Search') + ->assertSeeLivewire(Search::class); +}); diff --git a/tests/Http/Home/UsersTest.php b/tests/Http/Home/UsersTest.php deleted file mode 100644 index 7592b67e8..000000000 --- a/tests/Http/Home/UsersTest.php +++ /dev/null @@ -1,13 +0,0 @@ -get(route('home.users')); - - $response->assertOk() - ->assertSee('Search') - ->assertSeeLivewire(Users::class); -}); diff --git a/tests/Unit/Livewire/Home/UsersTest.php b/tests/Unit/Livewire/Home/SearchTest.php similarity index 69% rename from tests/Unit/Livewire/Home/UsersTest.php rename to tests/Unit/Livewire/Home/SearchTest.php index f1a533a6e..64f3a8155 100644 --- a/tests/Unit/Livewire/Home/UsersTest.php +++ b/tests/Unit/Livewire/Home/SearchTest.php @@ -2,16 +2,19 @@ declare(strict_types=1); -use App\Livewire\Home\Users; +use App\Livewire\Home\Search; use App\Models\Link; +use App\Models\Question; use App\Models\User; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Livewire\Livewire; -test('lists no users when there are no users', function () { - $component = Livewire::test(Users::class); +test('lists no result when there are no users', function () { + $component = Livewire::test(Search::class); - $component->assertSee('No users found.'); + $component->assertSee('No matching users or content found.'); }); test('lists by default users with GitHub or Twitter links', function () { @@ -19,7 +22,7 @@ 'url' => 'twitter.com/nunomaduro', ]); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); $users = User::all(); expect($users->count())->toBe(3); @@ -43,7 +46,7 @@ 'email_verified_at' => now(), ]); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); $component->assertDontSee('Nuno Maduro') ->assertDontSee('Taylor Otwell'); @@ -91,7 +94,7 @@ 'answer' => 'Livewire', ]); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); $component->set('query', 'Artisan'); @@ -126,7 +129,7 @@ ->hasQuestionsReceived(1, ['answer' => 'this is an answer']) ->create(); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); $component->assertSee('Nuno Maduro') ->assertSee('Punyapal Shah'); @@ -149,7 +152,7 @@ ->hasQuestionsReceived(1, ['answer' => 'this is an answer']) ->create(['name' => 'Adam Lee']); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); foreach (range(1, 50) as $index) { $component->refresh(); @@ -167,7 +170,7 @@ Cache::forget('top-50-users'); - Livewire::test(Users::class); + Livewire::test(Search::class); $this->assertTrue(Cache::has('top-50-users')); @@ -186,7 +189,7 @@ Cache::forget('top-50-users'); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); $this->assertTrue(Cache::has('top-50-users')); @@ -231,16 +234,16 @@ ]) ->create(); - $component = Livewire::test(Users::class); + $component = Livewire::test(Search::class); - $component->viewData('users')->each(function (User $user): void { + $component->viewData('results')->each(function (User $user): void { expect($user)->not->toHaveKey('is_follower'); expect($user)->not->toHaveKey('is_following'); }); $component->set('query', 'un'); - $component->viewData('users')->each(function (User $user): void { + $component->viewData('results')->each(function (User $user): void { expect($user)->not->toHaveKey('is_follower'); expect($user)->not->toHaveKey('is_following'); }); @@ -249,15 +252,87 @@ $component->set('query', ''); - $component->viewData('users')->each(function (User $user): void { + $component->viewData('results')->each(function (User $user): void { expect($user->is_follower)->toBeBool(); expect($user->is_following)->toBeBool(); }); $component->set('query', 'un'); - $component->viewData('users')->each(function (User $user): void { + $component->viewData('results')->each(function (User $user): void { expect($user->is_follower)->toBeBool(); expect($user->is_following)->toBeBool(); }); }); + +test('search for questions when query at least 3 characters', function () { + User::factory()->create([ + 'name' => 'Nuno Maduro', + 'email_verified_at' => now(), + ]); + + Question::factory()->create([ + 'content' => 'How to start?', + 'answer' => 'Hello world!', + ]); + + $component = Livewire::test(Search::class); + + $component->assertDontSee('Nuno Maduro') + ->assertDontSee('Hello world'); + + $component->set('query', 'Hello'); + + $component->assertDontSee('Nuno Maduro') + ->assertSee('Hello world'); +}); + +test('returns up to 4 questions in welcome search with enough matching users', function () { + User::factory(10)->create([ + 'name' => 'Nuno Maduro', + 'email_verified_at' => now(), + ]); + + Question::factory(5)->create([ + 'content' => 'Who created Pest?', + 'answer' => 'Nuno Maduro', + ]); + + $component = Livewire::test(Search::class, ['welcomeSearch' => true]); + + $component->set('query', 'Nuno'); + + $component->assertViewHas('results', function (Collection $results) { + $counts = $results->countBy(function (Model $result) { + return $result::class; + }); + + return $counts->get(Question::class) === 4 + && $counts->get(User::class) === 6; + }); +}); + +test('returns more questions in welcome search with less than 6 matching users', function () { + User::factory(5)->create([ + 'name' => 'Nuno Maduro', + 'email_verified_at' => now(), + ]); + + Question::factory(10)->create([ + 'content' => 'Who created Pest?', + 'answer' => 'Nuno Maduro', + ]); + + $component = Livewire::test(Search::class, ['welcomeSearch' => true]); + + $component->set('query', 'Nuno'); + + $component->assertViewHas('results', function (Collection $results) { + $counts = $results->countBy(function (Model $result) { + return $result::class; + }); + + return $counts->get(User::class) === 5 + && $counts->get(Question::class) === 5; + }); +});