diff --git a/app/Actions/AddRecentSearchAction.php b/app/Actions/AddRecentSearchAction.php new file mode 100644 index 0000000..c3d5e4a --- /dev/null +++ b/app/Actions/AddRecentSearchAction.php @@ -0,0 +1,30 @@ +recentSearches()->updateOrcreate($data); + $recentSearchCount = $user->recentSearches()->count(); + if ($recentSearchCount > 5) { + $user->recentSearches()->oldest()->limit(1)->delete(); + } + }); + } catch (\Throwable $e) { + Log::error('Error adding recent search', + [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTrace(), + ]); + } + } +} diff --git a/app/Http/Controllers/ExplorePageController.php b/app/Http/Controllers/ExplorePageController.php index fc79e14..81feea8 100644 --- a/app/Http/Controllers/ExplorePageController.php +++ b/app/Http/Controllers/ExplorePageController.php @@ -2,61 +2,61 @@ namespace App\Http\Controllers; +use App\Actions\AddRecentSearchAction; use App\Enums\ExplorePageFilters; use App\Enums\UserTypes; +use App\Http\Requests\ExploreSearchSortRequest; use App\Models\Deal; -use Illuminate\Http\Request; +use App\Models\DealCategory; +use App\Queries\ExplorePageDealsQuery; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; -use Illuminate\Validation\Rule; class ExplorePageController extends Controller { - public function __invoke(Request $request) - { - $sortBy = $request->validate([ - 'sortBy' => ['nullable', 'string', Rule::in(ExplorePageFilters::values())], - ]); - + public function __invoke( + ExploreSearchSortRequest $request, + ExplorePageDealsQuery $query, + AddRecentSearchAction $addRecentSearchAction + ) { return view('explore') ->with('profileLink', $this->profileLink()) - ->with('deals', $this->deals(ExplorePageFilters::tryFrom($sortBy['sortBy'] ?? null))); + ->with('categories', $this->categories()) + ->with('recentSearches', $this->recentSearches()) + ->with('deals', $this->deals($request, $query->builder(), $addRecentSearchAction)); } - protected function deals(?ExplorePageFilters $sortBy) + protected function deals(FormRequest $request, Builder $query, AddRecentSearchAction $action): LengthAwarePaginator { - $query = Deal::query() - ->select([ - 'id', 'title', 'description', 'image', 'active', 'slug', 'link', - 'deal_category_id', 'user_id', - ]) - // Select additional details - ->with([ - 'category:id,name', - 'broker' => function ($query) { - $query->select('id', 'name', 'email', 'role_type', 'role_id') - ->with('type'); - }, - ]) - // Select only admin-approved deals - ->withActiveDeals() - // Check if the current user interacted with the deal - ->withCurrentUserInteractions() - ->withLikePerDeal() - ->withRedirectionPerDeal(); + // Add a search query + if ($request->has('search') && $request->get('search') !== null) { + $query->tap(fn ($q) => (new Deal)->search($q, $request->search)); - // Add filters - if ($sortBy === ExplorePageFilters::Like) { - $query->orderBy('total_likes', 'desc'); - } elseif ($sortBy === ExplorePageFilters::Click) { - $query->orderBy('total_redirection', 'desc'); - } else { - $query->orderByRaw( - '((COALESCE(total_likes, 0) * 70.0)/100.0) + ((COALESCE(total_redirection, 0) * 30.0)/100.0) DESC' - ); + \Illuminate\Support\defer(function () use ($action, $request) { + $action->execute($request->user(), ['query' => $request->search]); + }); } - return $query->latest() - ->paginate(); + // Add category sorting filter + if ($request->has('category') && $request->get('category') !== null) { + $query->tap(fn ($q) => (new Deal)->filterByCategory($q, $request->category)); + } + + // Add sorting filters + if ($request->has('sortBy')) { + $query = match (ExplorePageFilters::tryFrom($request->sortBy)) { + ExplorePageFilters::Like => $query->orderBy('total_likes', 'desc'), + ExplorePageFilters::Click => $query->orderBy('total_redirection', 'desc'), + default => $query->orderByRaw( + '((COALESCE(total_likes, 0) * 70.0) / 100.0) + ((COALESCE(total_redirection, 0) * 30.0) / 100.0) DESC' + ) + }; + } + + return $query->latest()->paginate(); } /** @@ -65,11 +65,23 @@ protected function deals(?ExplorePageFilters $sortBy) * * @return string The URL for the user's dashboard. */ - protected function profileLink() + protected function profileLink(): string { $user = Auth::user(); if ($user->role === UserTypes::Broker->value) { return route('broker.profile.show', $user); } + + return ''; + } + + protected function categories(): Collection + { + return DealCategory::all(['id', 'name']); + } + + protected function recentSearches(): Collection + { + return Auth::user()->recentSearches()->latest()->pluck('query'); } } diff --git a/app/Http/Requests/ExploreSearchSortRequest.php b/app/Http/Requests/ExploreSearchSortRequest.php new file mode 100644 index 0000000..f5368da --- /dev/null +++ b/app/Http/Requests/ExploreSearchSortRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'sortBy' => ['nullable', 'string', Rule::in(ExplorePageFilters::values())], + 'search' => ['nullable', 'string', 'min:1', 'max:255'], + 'category' => ['nullable', 'exists:deal_categories,id'], + ]; + } +} diff --git a/app/Models/Deal.php b/app/Models/Deal.php index 9ceff4d..207db51 100644 --- a/app/Models/Deal.php +++ b/app/Models/Deal.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\InteractionType; +use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -27,18 +28,25 @@ public function interactions(): HasMany return $this->hasMany(Interaction::class); } + public function reports(): BelongsToMany + { + return $this->belongsToMany(Report::class); + } + /** - * Get deals that are active + * Scope a query to only include active deals */ - public function scopeWithActiveDeals(Builder $query): Builder + #[Scope] + public function WithActiveDeals(Builder $query): Builder { return $query->where('active', true); } /** - * Get if the current user has liked or favorite the deal + * Scope a query to determine if the current user has liked and favorite a deal */ - public function scopeWithCurrentUserInteractions(Builder $query): Builder + #[Scope] + public function WithCurrentUserInteractions(Builder $query): Builder { return $query->withExists([ 'interactions as is_liked' => function ($query) { @@ -52,7 +60,11 @@ public function scopeWithCurrentUserInteractions(Builder $query): Builder ]); } - public function scopeWithLikePerDeal(Builder $query): Builder + /** + * Scope a query to get total like count per deal + */ + #[Scope] + public function WithLikePerDeal(Builder $query): Builder { return $query->withCount([ 'interactions as total_likes' => function ($query) { @@ -61,7 +73,11 @@ public function scopeWithLikePerDeal(Builder $query): Builder ]); } - public function scopeWithRedirectionPerDeal(Builder $query): Builder + /** + * Scope a query to get click count per deal + */ + #[Scope] + public function WithRedirectionPerDeal(Builder $query): Builder { return $query->withSum([ 'interactions as total_redirection' => function ($query) { @@ -70,8 +86,21 @@ public function scopeWithRedirectionPerDeal(Builder $query): Builder ], 'count'); } - public function reports(): BelongsToMany + /** + * Scope a search in a query + */ + #[Scope] + public function search(Builder $query, string $search): Builder { - return $this->belongsToMany(Report::class); + return $query->where('title', 'LIKE', "%$search%"); + } + + /** + * Scope a category filter in a query + */ + #[Scope] + public function filterByCategory(Builder $query, string $category): Builder + { + return $query->where('deal_category_id', $category); } } diff --git a/app/Models/RecentSearch.php b/app/Models/RecentSearch.php new file mode 100644 index 0000000..c215570 --- /dev/null +++ b/app/Models/RecentSearch.php @@ -0,0 +1,16 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 09d60d8..3f67aab 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -73,4 +73,9 @@ public function interactions(): HasMany { return $this->hasMany(User::class); } + + public function recentSearches(): HasMany + { + return $this->hasMany(RecentSearch::class); + } } diff --git a/app/Queries/ExplorePageDealsQuery.php b/app/Queries/ExplorePageDealsQuery.php new file mode 100644 index 0000000..0f5f749 --- /dev/null +++ b/app/Queries/ExplorePageDealsQuery.php @@ -0,0 +1,35 @@ + + */ + public function builder(): Builder + { + return Deal::query() + ->select([ + 'id', 'title', 'description', 'image', 'active', 'slug', 'link', + 'deal_category_id', 'user_id', + ]) + // Select additional details + ->with([ + 'category:id,name', + 'broker' => function ($query) { + $query->select('id', 'name', 'email', 'role_type', 'role_id') + ->with('type'); + }, + ]) + // Select only admin-approved deals + ->tap(fn ($q) => (new Deal)->withActiveDeals($q)) + // Check if the current user interacted with the deal + ->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q)) + ->tap(fn ($q) => (new Deal)->withLikePerDeal($q)) + ->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q)); + } +} diff --git a/database/migrations/2026_01_20_064552_create_recent_searches_table.php b/database/migrations/2026_01_20_064552_create_recent_searches_table.php new file mode 100644 index 0000000..9bd3012 --- /dev/null +++ b/database/migrations/2026_01_20_064552_create_recent_searches_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignIdFor(User::class); + $table->string('query'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('recent_searches'); + } +}; diff --git a/resources/js/menu.js b/resources/js/menu.js index 9aa48f1..50b26c9 100644 --- a/resources/js/menu.js +++ b/resources/js/menu.js @@ -1,6 +1,7 @@ function showMenu(e){ const menu = e.nextElementSibling; - menu.classList.toggle('invisible'); + menu.classList.toggle('opacity-100'); + menu.classList.toggle('scale-100'); } document.showMenu = showMenu; diff --git a/resources/views/components/dashboard/user/listing.blade.php b/resources/views/components/dashboard/user/listing.blade.php index 7bcb3de..ee2ce87 100644 --- a/resources/views/components/dashboard/user/listing.blade.php +++ b/resources/views/components/dashboard/user/listing.blade.php @@ -3,6 +3,6 @@ @forelse($deals as $deal) @empty -

No Deals found till now !

+

No Deals found !

@endforelse diff --git a/resources/views/components/dashboard/user/recent-search/index.blade.php b/resources/views/components/dashboard/user/recent-search/index.blade.php new file mode 100644 index 0000000..7b3dcfc --- /dev/null +++ b/resources/views/components/dashboard/user/recent-search/index.blade.php @@ -0,0 +1,6 @@ +@props(['searches']) +
    + @foreach($searches as $search) + + @endforeach +
diff --git a/resources/views/components/dashboard/user/recent-search/recent-search-item.blade.php b/resources/views/components/dashboard/user/recent-search/recent-search-item.blade.php new file mode 100644 index 0000000..f9cad1f --- /dev/null +++ b/resources/views/components/dashboard/user/recent-search/recent-search-item.blade.php @@ -0,0 +1,7 @@ +@props(['item']) +
  • + + {{$item}} + + +
  • diff --git a/resources/views/components/explore/profile-menu.blade.php b/resources/views/components/explore/profile-menu.blade.php new file mode 100644 index 0000000..676afd6 --- /dev/null +++ b/resources/views/components/explore/profile-menu.blade.php @@ -0,0 +1,37 @@ +@props(['profileLink' => '']) +
    +
    + + +
    +
    + @csrf + @method('delete') + + + + +
    +
    diff --git a/resources/views/components/explore/search.blade.php b/resources/views/components/explore/search.blade.php new file mode 100644 index 0000000..93dd75f --- /dev/null +++ b/resources/views/components/explore/search.blade.php @@ -0,0 +1,14 @@ +@props(['recentSearches' => [], 'categories' => []]) + +
    +
    + + + + + +
    +
    +
    diff --git a/resources/views/components/explore/toggle-buttons.blade.php b/resources/views/components/explore/toggle-buttons.blade.php new file mode 100644 index 0000000..5a013e0 --- /dev/null +++ b/resources/views/components/explore/toggle-buttons.blade.php @@ -0,0 +1,22 @@ + + + + +

    All Deals

    +
    +
    + + + + +

    Most Liked

    +
    +
    + + + + +

    Most Clicked

    +
    +
    +
    diff --git a/resources/views/components/ui/button.blade.php b/resources/views/components/ui/button.blade.php index 2becb60..cdfdddf 100644 --- a/resources/views/components/ui/button.blade.php +++ b/resources/views/components/ui/button.blade.php @@ -1,4 +1,4 @@ -@props(['variant' => '', 'icon' => '', 'link' => '', 'external' => false]) +@props(['variant' => '', 'icon' => '', 'link' => '', 'external' => false, 'round' => false]) @php $variants = [ 'neutral' => 'bg-primary-600 text-white', @@ -7,13 +7,18 @@ ]; $variantClass = $variants[$variant] ?? ''; + $roundedClass = $round ? ' rounded-full p-3' : ' rounded-lg magnifying-glass px-4 py-2 '; + $variantClass.= $roundedClass; @endphp @if($link !== '') merge(['class' => "block px-4 py-2 rounded-lg font-medium hover:opacity-80 active:scale-80 transition-all ease-in-out duration-300 $variantClass", 'href' => $link])}}> + {{$attributes->merge([ + 'class' => "block px-4 py-2 font-medium hover:opacity-80 active:scale-80 transition-all ease-in-out duration-300 $variantClass", + 'href' => $link] + )}}>
    @if($icon !=='') @svg("heroicon-o-$icon", 'w-5 h-5') @@ -22,13 +27,14 @@
    @else - @endif diff --git a/resources/views/components/ui/input.blade.php b/resources/views/components/ui/input.blade.php index 89e2f86..cc7f5d9 100644 --- a/resources/views/components/ui/input.blade.php +++ b/resources/views/components/ui/input.blade.php @@ -1,7 +1,7 @@ @props(['label' => '', 'name' => '', 'placeholder' => '', 'type' => 'text', 'description' => '', 'required' => false, 'value' => '']) -
    merge(['class' => 'flex flex-col space-y-2'])}}> +
    merge(['class' => 'flex flex-col'])}}> @if($label !== '') -