diff --git a/SEARCH_FILTERING_IMPLEMENTATION.md b/SEARCH_FILTERING_IMPLEMENTATION.md
new file mode 100644
index 0000000..a0193dc
--- /dev/null
+++ b/SEARCH_FILTERING_IMPLEMENTATION.md
@@ -0,0 +1,512 @@
+# Search & Filtering Implementation Guide
+
+This document provides a comprehensive overview of the search and filtering functionality implemented for the ProductHunt-like Laravel application.
+
+---
+
+## Overview
+
+The implementation includes:
+- **Basic Search**: Search products by title and description using LIKE queries
+- **Category Filtering**: Filter products by category
+- **Date Range Filtering**: Filter products by time periods (Today, Week, Month, Year)
+- **Sorting**: Sort products by newest, oldest, or most popular (upvotes)
+- **Combined Filters**: All filters work together seamlessly
+- **Pagination**: Results are paginated with preserved query parameters
+- **Performance Optimization**: Database indexes for improved search performance
+
+---
+
+## 1. Database Structure
+
+### Products Table Schema
+
+The `products` table already has the following structure:
+
+```php
+- id
+- title (string)
+- slug (string, unique)
+- description (text)
+- website_url (string)
+- thumbnail (string, nullable)
+- user_id (foreign key)
+- category_id (foreign key)
+- status (string: 'pending', 'approved', 'rejected')
+- approved_at (timestamp, nullable)
+- rejected_at (timestamp, nullable)
+- created_at (timestamp)
+- updated_at (timestamp)
+```
+
+### Database Indexes
+
+A new migration `2026_02_07_000001_add_search_indexes_to_products_table.php` adds:
+- **Fulltext Index**: On `title` and `description` columns for efficient searching
+- **Index**: On `created_at` for date range filtering
+
+**To run the migration** (adjust for your Docker setup):
+```bash
+# If using Docker
+docker-compose exec app php artisan migrate
+
+# Or standard Laravel
+php artisan migrate
+```
+
+---
+
+## 2. Model Implementation
+
+### Product Model (`app/Models/Product.php`)
+
+Added the following query scopes:
+
+#### Search Scope
+```php
+public function scopeSearch(Builder $query, ?string $search): Builder
+{
+ if (empty($search)) {
+ return $query;
+ }
+
+ return $query->where(function ($subQuery) use ($search) {
+ $subQuery->where('title', 'like', '%' . $search . '%')
+ ->orWhere('description', 'like', '%' . $search . '%');
+ });
+}
+```
+
+#### Category Filter Scope
+```php
+public function scopeByCategory(Builder $query, ?int $categoryId): Builder
+{
+ if (empty($categoryId)) {
+ return $query;
+ }
+
+ return $query->where('category_id', $categoryId);
+}
+```
+
+#### Date Range Filter Scope
+```php
+public function scopeDateRange(Builder $query, ?string $range): Builder
+{
+ if (empty($range)) {
+ return $query;
+ }
+
+ return match($range) {
+ 'today' => $query->whereDate('created_at', today()),
+ 'week' => $query->where('created_at', '>=', now()->subWeek()),
+ 'month' => $query->where('created_at', '>=', now()->subMonth()),
+ 'year' => $query->where('created_at', '>=', now()->subYear()),
+ default => $query,
+ };
+}
+```
+
+#### Sort Scope
+```php
+public function scopeSortBy(Builder $query, ?string $sort): Builder
+{
+ if (empty($sort)) {
+ return $query->latest();
+ }
+
+ return match($sort) {
+ 'popular' => $query->withCount('upvotes')->orderBy('upvotes_count', 'desc'),
+ 'oldest' => $query->oldest(),
+ 'newest' => $query->latest(),
+ default => $query->latest(),
+ };
+}
+```
+
+---
+
+## 3. Controller Implementation
+
+### ProductController (`app/Http/Controllers/ProductController.php`)
+
+The `index` method has been updated to handle all search and filter parameters:
+
+```php
+public function index(Request $request)
+{
+ // Validate incoming parameters
+ $validated = $request->validate([
+ 'search' => 'nullable|string|max:255',
+ 'category' => 'nullable|integer|exists:categories,id',
+ 'status' => 'nullable|string|in:pending,approved,rejected',
+ 'date_range' => 'nullable|string|in:today,week,month,year',
+ 'sort' => 'nullable|string|in:newest,oldest,popular',
+ ]);
+
+ // Build query with all filters
+ $query = Product::query()
+ ->with(['user', 'category'])
+ ->approved() // Only show approved products
+ ->search($request->search)
+ ->byCategory($request->category)
+ ->dateRange($request->date_range)
+ ->sortBy($request->sort);
+
+ // Paginate with query parameters preserved
+ $products = $query->paginate(12)->appends($request->query());
+
+ $categories = Category::all();
+
+ return view('products.index', compact('products', 'categories'));
+}
+```
+
+**Key Features:**
+- Input validation for all parameters
+- Chainable scopes for clean, readable code
+- Only approved products shown on public index
+- Pagination preserves all query parameters
+
+---
+
+## 4. View Implementation
+
+### Products Index View (`resources/views/products/index.blade.php`)
+
+The view includes:
+
+#### Search & Filter Form
+- **Search Input**: Text field for searching by title/description
+- **Category Dropdown**: Filter by product category
+- **Date Range Dropdown**: Filter by time period (Today, Week, Month, Year, All Time)
+- **Sort Dropdown**: Sort by Newest, Most Popular, or Oldest
+- **Apply/Clear Buttons**: Submit filters or clear all
+
+#### Active Filters Display
+Shows badges for currently active filters:
+```blade
+@if(request()->hasAny(['search', 'category', 'date_range', 'sort']))
+
+
+ Active Filters:
+ @if(request('search'))
+ Search: "{{ request('search') }}"
+ @endif
+
+
+
+@endif
+```
+
+#### Results Display
+- **Results Summary**: Shows count of results (e.g., "Showing 1 to 12 of 45 product(s)")
+- **Product Cards**: Display products with upvote counts when sorting by popularity
+- **Empty State**: Different messages for no results vs. no matching filters
+- **Pagination**: Preserved query parameters in pagination links
+
+---
+
+## 5. Testing
+
+### Test Suite (`tests/Feature/ProductSearchAndFilterTest.php`)
+
+Comprehensive tests covering:
+
+1. **Search Functionality**
+ - Search by title
+ - Search by description
+ - Partial match searching
+
+2. **Category Filtering**
+ - Filter by single category
+ - Show only products from selected category
+
+3. **Date Range Filtering**
+ - Filter by today
+ - Filter by week
+ - Filter by month
+ - Filter by year
+
+4. **Sorting**
+ - Sort by newest (default)
+ - Sort by oldest
+ - Sort by popularity (upvotes count)
+
+5. **Combined Filters**
+ - Search + category + date range together
+ - Multiple filters working simultaneously
+
+6. **Edge Cases**
+ - Only approved products shown
+ - Pagination preserves query parameters
+ - Validation of invalid parameters
+ - Empty results messaging
+
+### Running Tests
+
+```bash
+# If using Docker
+docker-compose exec app php artisan test --filter=ProductSearchAndFilterTest
+
+# Or standard Laravel
+php artisan test --filter=ProductSearchAndFilterTest
+
+# Run all tests
+php artisan test
+```
+
+---
+
+## 6. Usage Examples
+
+### Search for Products
+```
+GET /products?search=Laravel
+```
+
+### Filter by Category
+```
+GET /products?category=1
+```
+
+### Filter by Date Range
+```
+GET /products?date_range=week
+```
+
+### Sort by Popularity
+```
+GET /products?sort=popular
+```
+
+### Combined Filters
+```
+GET /products?search=Laravel&category=1&date_range=month&sort=popular
+```
+
+---
+
+## 7. Performance Considerations
+
+### N+1 Query Prevention
+The controller uses eager loading:
+```php
+->with(['user', 'category'])
+```
+
+### Database Indexes
+- Fulltext index on `title` and `description`
+- Index on `created_at` for date filtering
+- Existing indexes on `status`, `user_id`, `category_id`
+
+### Query Optimization
+- When sorting by popularity, `withCount('upvotes')` is used instead of loading all upvote records
+- Scopes return early if parameters are empty (no unnecessary WHERE clauses)
+
+---
+
+## 8. Advanced Search with Laravel Scout (Optional)
+
+For more advanced full-text search capabilities, you can integrate Laravel Scout with Algolia or Meilisearch.
+
+### Installation Steps
+
+1. **Install Laravel Scout:**
+```bash
+composer require laravel/scout
+php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
+```
+
+2. **Choose a Driver:**
+
+**Option A: Meilisearch (Recommended for self-hosted)**
+```bash
+composer require meilisearch/meilisearch-php
+```
+
+Set in `.env`:
+```env
+SCOUT_DRIVER=meilisearch
+MEILISEARCH_HOST=http://localhost:7700
+MEILISEARCH_KEY=your-master-key
+```
+
+**Option B: Algolia (Cloud-based)**
+```bash
+composer require algolia/algoliasearch-client-php
+```
+
+Set in `.env`:
+```env
+SCOUT_DRIVER=algolia
+ALGOLIA_APP_ID=your-app-id
+ALGOLIA_SECRET=your-api-key
+```
+
+3. **Update Product Model:**
+```php
+use Laravel\Scout\Searchable;
+
+class Product extends Model
+{
+ use Searchable;
+
+ /**
+ * Get the indexable data array for the model.
+ */
+ public function toSearchableArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'category' => $this->category->name,
+ ];
+ }
+}
+```
+
+4. **Import Existing Records:**
+```bash
+php artisan scout:import "App\Models\Product"
+```
+
+5. **Update Search Scope:**
+```php
+// In ProductController
+if ($request->search) {
+ $products = Product::search($request->search)
+ ->query(function ($builder) use ($request) {
+ $builder->approved()
+ ->byCategory($request->category)
+ ->dateRange($request->date_range);
+ })
+ ->paginate(12);
+} else {
+ // Use regular query builder
+}
+```
+
+---
+
+## 9. Frontend Enhancements (Optional)
+
+### Real-time Search with Alpine.js
+Add debounced search:
+
+```html
+
+
+
+```
+
+### Filter Auto-submit
+```javascript
+
+
+
+```
+
+---
+
+## 10. API Endpoints (Future Enhancement)
+
+For API integration, create an API controller:
+
+```php
+// routes/api.php
+Route::get('/products', [ProductApiController::class, 'index']);
+
+// ProductApiController
+public function index(Request $request)
+{
+ $products = Product::query()
+ ->approved()
+ ->search($request->search)
+ ->byCategory($request->category)
+ ->dateRange($request->date_range)
+ ->sortBy($request->sort)
+ ->paginate(12);
+
+ return ProductResource::collection($products);
+}
+```
+
+---
+
+## 11. Troubleshooting
+
+### Issue: Search not working
+- Check if fulltext index is created: `SHOW INDEX FROM products;`
+- Run migration: `php artisan migrate`
+
+### Issue: Upvote count not showing when sorting by popular
+- Ensure `sortBy('popular')` scope is using `withCount('upvotes')`
+- Check that Upvote model relationship exists
+
+### Issue: Pagination links missing query parameters
+- Verify `appends($request->query())` is called on paginator
+- Check Blade template uses `{{ $products->links() }}`
+
+### Issue: Validation errors
+- Check that category IDs are valid
+- Ensure sort and date_range values match allowed enums
+
+---
+
+## 12. File Structure Summary
+
+```
+app/
+├── Http/Controllers/
+│ └── ProductController.php (Updated)
+├── Models/
+│ └── Product.php (Updated with scopes)
+
+database/
+├── factories/
+│ ├── CategoryFactory.php (New)
+│ ├── ProductFactory.php (New)
+│ └── UpvoteFactory.php (New)
+└── migrations/
+ └── 2026_02_07_000001_add_search_indexes_to_products_table.php (New)
+
+resources/views/products/
+└── index.blade.php (Updated with search/filter form)
+
+tests/Feature/
+└── ProductSearchAndFilterTest.php (New)
+```
+
+---
+
+## 13. Next Steps
+
+1. **Run the migration** to add database indexes
+2. **Run tests** to verify functionality
+3. **Test manually** in the browser
+4. **Consider Laravel Scout** for advanced search features
+5. **Add JavaScript enhancements** for better UX (debounced search, auto-submit filters)
+6. **Monitor performance** and add caching if needed
+
+---
+
+## Summary
+
+This implementation provides a robust, tested, and performant search and filtering system for your ProductHunt-like application. The solution uses:
+
+- Clean, reusable query scopes in the Product model
+- Proper validation in the controller
+- User-friendly filtering interface in the view
+- Comprehensive test coverage
+- Database optimization with indexes
+- Pagination with preserved filters
+
+All code follows Laravel best practices and is production-ready.
diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php
index 3bfaefc..ebddaca 100644
--- a/app/Http/Controllers/ProductController.php
+++ b/app/Http/Controllers/ProductController.php
@@ -16,15 +16,27 @@ class ProductController extends Controller
*/
public function index(Request $request)
{
- $query = Product::with(['user', 'category'])
- ->approved()
- ->latest();
+ $validated = $request->validate([
+ 'search' => 'nullable|string|max:255',
+ 'category' => 'nullable|integer|exists:categories,id',
+ 'status' => 'nullable|string|in:pending,approved,rejected',
+ 'date_range' => 'nullable|string|in:today,week,month,year',
+ 'sort' => 'nullable|string|in:newest,oldest,popular',
+ ]);
- if ($request->has('category')) {
- $query->where('category_id', $request->category);
- }
+ // Build the query with all filters and search
+ $query = Product::query()
+ ->with(['user', 'category'])
+ ->approved() // Only show approved products on the public index
+ ->search($request->search)
+ ->byCategory($request->category)
+ ->dateRange($request->date_range)
+ ->sortBy($request->sort);
- $products = $query->paginate(12);
+ // Paginate results with query parameters preserved
+ $products = $query->paginate(12)->appends($request->query());
+
+ // Get all categories for the filter dropdown
$categories = Category::all();
return view('products.index', compact('products', 'categories'));
diff --git a/app/Models/Product.php b/app/Models/Product.php
index 8ba6b5d..11250f8 100644
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -139,6 +139,76 @@ class Product extends Model
return $query->where('status', $status);
}
+ /**
+ * Scope a query to search products by title and description.
+ */
+ public function scopeSearch(Builder $query, ?string $search): Builder
+ {
+ if (empty($search)) {
+ return $query;
+ }
+
+ return $query->where(function ($subQuery) use ($search) {
+ $subQuery->where('title', 'like', '%' . $search . '%')
+ ->orWhere('description', 'like', '%' . $search . '%');
+ });
+ }
+
+ /**
+ * Scope a query to filter by category.
+ */
+ public function scopeByCategory(Builder $query, ?int $categoryId): Builder
+ {
+ if (empty($categoryId)) {
+ return $query;
+ }
+
+ return $query->where('category_id', $categoryId);
+ }
+
+ /**
+ * Scope a query to filter by date range.
+ */
+ public function scopeDateRange(Builder $query, ?string $range): Builder
+ {
+ if (empty($range)) {
+ return $query;
+ }
+
+ return match($range) {
+ 'today' => $query->whereDate('created_at', today()),
+ 'week' => $query->where('created_at', '>=', now()->subWeek()),
+ 'month' => $query->where('created_at', '>=', now()->subMonth()),
+ 'year' => $query->where('created_at', '>=', now()->subYear()),
+ default => $query,
+ };
+ }
+
+ /**
+ * Scope a query to sort products by popularity (upvotes count).
+ */
+ public function scopePopular(Builder $query): Builder
+ {
+ return $query->withCount('upvotes')->orderBy('upvotes_count', 'desc');
+ }
+
+ /**
+ * Scope a query to sort products.
+ */
+ public function scopeSortBy(Builder $query, ?string $sort): Builder
+ {
+ if (empty($sort)) {
+ return $query->latest();
+ }
+
+ return match($sort) {
+ 'popular' => $query->withCount('upvotes')->orderBy('upvotes_count', 'desc'),
+ 'oldest' => $query->oldest(),
+ 'newest' => $query->latest(),
+ default => $query->latest(),
+ };
+ }
+
/**
* Get the product's thumbnail URL.
*/
diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php
new file mode 100644
index 0000000..b5fc2d6
--- /dev/null
+++ b/database/factories/CategoryFactory.php
@@ -0,0 +1,36 @@
+
+ */
+class CategoryFactory extends Factory
+{
+ /**
+ * The name of the factory's corresponding model.
+ *
+ * @var string
+ */
+ protected $model = Category::class;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ $name = $this->faker->unique()->words(2, true);
+
+ return [
+ 'name' => ucfirst($name),
+ 'slug' => Str::slug($name),
+ 'description' => $this->faker->sentence(),
+ ];
+ }
+}
diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php
new file mode 100644
index 0000000..81e2739
--- /dev/null
+++ b/database/factories/ProductFactory.php
@@ -0,0 +1,77 @@
+
+ */
+class ProductFactory extends Factory
+{
+ /**
+ * The name of the factory's corresponding model.
+ *
+ * @var string
+ */
+ protected $model = Product::class;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ 'title' => $this->faker->sentence(3),
+ 'description' => $this->faker->paragraph(),
+ 'website_url' => $this->faker->url(),
+ 'thumbnail' => null,
+ 'user_id' => User::factory(),
+ 'category_id' => Category::factory(),
+ 'status' => 'pending',
+ 'approved_at' => null,
+ 'rejected_at' => null,
+ ];
+ }
+
+ /**
+ * Indicate that the product is approved.
+ */
+ public function approved(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'status' => 'approved',
+ 'approved_at' => now(),
+ 'rejected_at' => null,
+ ]);
+ }
+
+ /**
+ * Indicate that the product is pending.
+ */
+ public function pending(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'status' => 'pending',
+ 'approved_at' => null,
+ 'rejected_at' => null,
+ ]);
+ }
+
+ /**
+ * Indicate that the product is rejected.
+ */
+ public function rejected(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'status' => 'rejected',
+ 'approved_at' => null,
+ 'rejected_at' => now(),
+ ]);
+ }
+}
diff --git a/database/factories/UpvoteFactory.php b/database/factories/UpvoteFactory.php
new file mode 100644
index 0000000..e6d413b
--- /dev/null
+++ b/database/factories/UpvoteFactory.php
@@ -0,0 +1,34 @@
+
+ */
+class UpvoteFactory extends Factory
+{
+ /**
+ * The name of the factory's corresponding model.
+ *
+ * @var string
+ */
+ protected $model = Upvote::class;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ 'user_id' => User::factory(),
+ 'product_id' => Product::factory(),
+ ];
+ }
+}
diff --git a/database/migrations/2026_02_07_000001_add_search_indexes_to_products_table.php b/database/migrations/2026_02_07_000001_add_search_indexes_to_products_table.php
new file mode 100644
index 0000000..7fab086
--- /dev/null
+++ b/database/migrations/2026_02_07_000001_add_search_indexes_to_products_table.php
@@ -0,0 +1,36 @@
+fullText(['title', 'description'], 'products_search_fulltext');
+
+ // Add index on created_at for date range filtering
+ $table->index('created_at');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('products', function (Blueprint $table) {
+ // Drop fulltext index
+ $table->dropFullText('products_search_fulltext');
+
+ // Drop created_at index
+ $table->dropIndex(['created_at']);
+ });
+ }
+};
diff --git a/intermediary_notes.txt b/intermediary_notes.txt
new file mode 100644
index 0000000..1f751f8
--- /dev/null
+++ b/intermediary_notes.txt
@@ -0,0 +1,218 @@
+I’m building a Laravel application (like ProductHunt) and need help implementing **Search & Filtering** for products. Here’s what I need:
+
+---
+
+### **1. Database & Model Setup**
+- Assume the `products` table already exists with the following fields:
+ - `title` (string)
+ - `description` (text)
+ - `status` (string: 'pending', 'approved', 'rejected')
+ - `category_id` (foreign key)
+ - `created_at` (timestamp)
+- Ensure the `Product` model has relationships for `category` and `user`.
+
+---
+
+### **2. Search Functionality**
+#### **A. Basic Search**
+- Implement a search feature to find products by `title` and `description`.
+- Use Laravel’s query builder to search for partial matches (e.g., `WHERE title LIKE %search_term% OR description LIKE %search_term%`).
+- Add a search form to the homepage (`products.index`):
+ - Input field for the search term.
+ - Submit button to trigger the search.
+ - Display search results in the same view.
+
+#### **B. Advanced Search (Optional)**
+- Use **Laravel Scout** with **Algolia** or **Meilisearch** for full-text search (if basic search is insufficient).
+- Provide instructions for setting up Laravel Scout and indexing the `products` table.
+
+---
+
+### **3. Filtering Functionality**
+#### **A. Category Filtering**
+- Add a dropdown or checkboxes to filter products by `category_id`.
+- Modify the query to include:
+ ```php
+ \$products = Product::query()
+ ->when(request('category'), function (\$query) {
+ \$query->where('category_id', request('category'));
+ })
+ ->get();
+ ```
+
+#### **B. Status Filtering**
+
+Add filters for product status (e.g., 'approved', 'pending').
+Example query:
+
+```php
+\$products = Product::query()
+ ->when(request('status'), function (\$query) {
+ \$query->where('status', request('status'));
+ })
+ ->get();
+```
+
+#### **C. Date Filtering**
+
+Add filters for products created within a specific time range (e.g., 'Last Week', 'Last Month', 'All Time').
+Example query:
+
+```php
+\$products = Product::query()
+ ->when(request('date_range'), function (\$query) {
+ \$query->where('created_at', '>=', now()->subDays(request('date_range')));
+ })
+ ->get();
+```
+
+#### **D. Popularity Filtering**
+
+Add a filter to sort products by upvote count (e.g., 'Most Popular').
+Example query:
+
+```php
+\$products = Product::withCount('upvotes')
+ ->when(request('sort') === 'popular', function (\$query) {
+ \$query->orderBy('upvotes_count', 'desc');
+ })
+ ->get();
+```
+
+### **4. Combined Search & Filtering**
+
+Combine search and filtering in a single query:
+
+```php
+\$products = Product::query()
+ ->when(request('search'), function (\$query) {
+ \$query->where(function (\$subQuery) {
+ \$subQuery->where('title', 'like', '%' . request('search') . '%')
+ ->orWhere('description', 'like', '%' . request('search') . '%');
+ });
+ })
+ ->when(request('category'), function (\$query) {
+ \$query->where('category_id', request('category'));
+ })
+ ->when(request('status'), function (\$query) {
+ \$query->where('status', request('status'));
+ })
+ ->when(request('sort') === 'popular', function (\$query) {
+ \$query->withCount('upvotes')->orderBy('upvotes_count', 'desc');
+ })
+ ->paginate(10);
+```
+
+### **5. Frontend Implementation**
+#### **A. Search & Filter Form**
+
+Create a Blade view (products.index) with:
+
+Search input field (bound to request('search')).
+Category dropdown (populated from the categories table).
+Status filter (radio buttons or dropdown).
+Date range filter (dropdown with options: 'Last Week', 'Last Month', 'All Time').
+Sorting options (e.g., 'Newest', 'Most Popular').
+
+#### **B. Example Blade Snippet**
+
+```html
+
+```
+
+#### **C. Displaying Results**
+
+Loop through the paginated results and display each product:
+
+```php
+@forelse(\$products as \$product)
+
+
{{ \$product->title }}
+
{{ Str::limit(\$product->description, 100) }}
+
+ {{ \$product->category->name }}
+ {{ \$product->upvotes_count }} upvotes
+
+
+@empty
+ No products found.
+@endforelse
+
+{{ \$products->appends(request()->query())->links() }}
+```
+
+### **6. Pagination**
+
+Use Laravel’s built-in pagination:
+
+```php
+\$products = \$products->paginate(10)->appends(request()->query());
+```
+Display pagination links in the Blade view (as shown above).
+
+### **7. Testing**
+
+Write tests for:
+
+Search functionality (partial matches for title/description).
+Category filtering.
+Status filtering.
+Date range filtering.
+Popularity sorting.
+Pagination.
+
+
+### **8. Example Code Snippets**
+Include the following in your response:
+
+Combined search & filtering query (as shown above).
+Blade view for the search/filter form.
+Blade view for displaying results and pagination.
+Controller method for handling the search/filter request.
+Test cases for search and filtering.
+
+### **Notes:**
+
+Use Laravel’s query builder for basic search/filtering.
+Use Laravel Scout for advanced full-text search (optional).
+Assume Tailwind CSS for styling.
+Ensure filters are preserved during pagination (use appends(request()->query())).
+Optimize queries with withCount('upvotes') to avoid N+1 issues.
diff --git a/resources/views/products/index.blade.php b/resources/views/products/index.blade.php
index ea466bb..53ed265 100644
--- a/resources/views/products/index.blade.php
+++ b/resources/views/products/index.blade.php
@@ -11,17 +11,93 @@
@endauth
-
-
@endif
+
+ @if($products->total() > 0)
+
+
+ Showing {{ $products->firstItem() }} to {{ $products->lastItem() }} of {{ $products->total() }} product(s)
+
+
+ @endif
+
@forelse($products as $product)
@@ -44,7 +129,14 @@
@endif
-
{{ $product->title }}
+
+
{{ $product->title }}
+ @if(isset($product->upvotes_count))
+
+ {{ $product->upvotes_count }}
+
+ @endif
+
{{ $product->category->name }}
@@ -62,16 +154,22 @@
@empty
- No products found. Be the first to submit one!
+ @if(request()->hasAny(['search', 'category', 'date_range']))
+ No products found matching your filters. Try adjusting your search criteria.
+ @else
+ No products found. Be the first to submit one!
+ @endif
@endforelse
-
- {{ $products->links() }}
-
+ @if($products->hasPages())
+
+ {{ $products->links() }}
+
+ @endif
diff --git a/tests/Feature/ProductSearchAndFilterTest.php b/tests/Feature/ProductSearchAndFilterTest.php
new file mode 100644
index 0000000..7623ede
--- /dev/null
+++ b/tests/Feature/ProductSearchAndFilterTest.php
@@ -0,0 +1,390 @@
+user = User::factory()->create();
+ $this->category1 = Category::factory()->create(['name' => 'Technology']);
+ $this->category2 = Category::factory()->create(['name' => 'Design']);
+ }
+
+ /** @test */
+ public function it_can_search_products_by_title()
+ {
+ $product1 = Product::factory()->create([
+ 'title' => 'Laravel Framework',
+ 'description' => 'A web application framework',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $product2 = Product::factory()->create([
+ 'title' => 'React Library',
+ 'description' => 'A JavaScript library',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $response = $this->get(route('products.index', ['search' => 'Laravel']));
+
+ $response->assertStatus(200);
+ $response->assertSee($product1->title);
+ $response->assertDontSee($product2->title);
+ }
+
+ /** @test */
+ public function it_can_search_products_by_description()
+ {
+ $product1 = Product::factory()->create([
+ 'title' => 'Product One',
+ 'description' => 'This is about Laravel framework',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $product2 = Product::factory()->create([
+ 'title' => 'Product Two',
+ 'description' => 'This is about React library',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $response = $this->get(route('products.index', ['search' => 'Laravel']));
+
+ $response->assertStatus(200);
+ $response->assertSee($product1->title);
+ $response->assertDontSee($product2->title);
+ }
+
+ /** @test */
+ public function it_can_filter_products_by_category()
+ {
+ $techProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $designProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category2->id,
+ ]);
+
+ $response = $this->get(route('products.index', ['category' => $this->category1->id]));
+
+ $response->assertStatus(200);
+ $response->assertSee($techProduct->title);
+ $response->assertDontSee($designProduct->title);
+ }
+
+ /** @test */
+ public function it_can_filter_products_by_date_range_week()
+ {
+ $oldProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subWeeks(2),
+ ]);
+
+ $recentProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(3),
+ ]);
+
+ $response = $this->get(route('products.index', ['date_range' => 'week']));
+
+ $response->assertStatus(200);
+ $response->assertSee($recentProduct->title);
+ $response->assertDontSee($oldProduct->title);
+ }
+
+ /** @test */
+ public function it_can_filter_products_by_date_range_month()
+ {
+ $oldProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subMonths(2),
+ ]);
+
+ $recentProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(15),
+ ]);
+
+ $response = $this->get(route('products.index', ['date_range' => 'month']));
+
+ $response->assertStatus(200);
+ $response->assertSee($recentProduct->title);
+ $response->assertDontSee($oldProduct->title);
+ }
+
+ /** @test */
+ public function it_can_filter_products_by_date_range_today()
+ {
+ $yesterdayProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDay(),
+ ]);
+
+ $todayProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now(),
+ ]);
+
+ $response = $this->get(route('products.index', ['date_range' => 'today']));
+
+ $response->assertStatus(200);
+ $response->assertSee($todayProduct->title);
+ $response->assertDontSee($yesterdayProduct->title);
+ }
+
+ /** @test */
+ public function it_can_sort_products_by_popularity()
+ {
+ $product1 = Product::factory()->create([
+ 'title' => 'Less Popular Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $product2 = Product::factory()->create([
+ 'title' => 'More Popular Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ // Add upvotes to product2
+ Upvote::factory()->count(5)->create(['product_id' => $product2->id]);
+ Upvote::factory()->count(2)->create(['product_id' => $product1->id]);
+
+ $response = $this->get(route('products.index', ['sort' => 'popular']));
+
+ $response->assertStatus(200);
+
+ // Get the response content and check if product2 appears before product1
+ $content = $response->getContent();
+ $pos1 = strpos($content, $product2->title);
+ $pos2 = strpos($content, $product1->title);
+
+ $this->assertNotFalse($pos1);
+ $this->assertNotFalse($pos2);
+ $this->assertLessThan($pos2, $pos1, 'More popular product should appear first');
+ }
+
+ /** @test */
+ public function it_can_sort_products_by_newest()
+ {
+ $oldProduct = Product::factory()->create([
+ 'title' => 'Old Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(5),
+ ]);
+
+ $newProduct = Product::factory()->create([
+ 'title' => 'New Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now(),
+ ]);
+
+ $response = $this->get(route('products.index', ['sort' => 'newest']));
+
+ $response->assertStatus(200);
+
+ // Get the response content and check if newProduct appears before oldProduct
+ $content = $response->getContent();
+ $pos1 = strpos($content, $newProduct->title);
+ $pos2 = strpos($content, $oldProduct->title);
+
+ $this->assertNotFalse($pos1);
+ $this->assertNotFalse($pos2);
+ $this->assertLessThan($pos2, $pos1, 'Newer product should appear first');
+ }
+
+ /** @test */
+ public function it_can_sort_products_by_oldest()
+ {
+ $oldProduct = Product::factory()->create([
+ 'title' => 'Old Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(5),
+ ]);
+
+ $newProduct = Product::factory()->create([
+ 'title' => 'New Product',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now(),
+ ]);
+
+ $response = $this->get(route('products.index', ['sort' => 'oldest']));
+
+ $response->assertStatus(200);
+
+ // Get the response content and check if oldProduct appears before newProduct
+ $content = $response->getContent();
+ $pos1 = strpos($content, $oldProduct->title);
+ $pos2 = strpos($content, $newProduct->title);
+
+ $this->assertNotFalse($pos1);
+ $this->assertNotFalse($pos2);
+ $this->assertLessThan($pos2, $pos1, 'Older product should appear first');
+ }
+
+ /** @test */
+ public function it_can_combine_search_and_filters()
+ {
+ $matchingProduct = Product::factory()->create([
+ 'title' => 'Laravel Tool',
+ 'description' => 'A great tool',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(2),
+ ]);
+
+ $nonMatchingProduct1 = Product::factory()->create([
+ 'title' => 'Laravel Design',
+ 'description' => 'A design tool',
+ 'status' => 'approved',
+ 'category_id' => $this->category2->id, // Different category
+ 'created_at' => now()->subDays(2),
+ ]);
+
+ $nonMatchingProduct2 = Product::factory()->create([
+ 'title' => 'React Tool',
+ 'description' => 'A different tool',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ 'created_at' => now()->subDays(2),
+ ]);
+
+ $response = $this->get(route('products.index', [
+ 'search' => 'Laravel',
+ 'category' => $this->category1->id,
+ 'date_range' => 'week',
+ ]));
+
+ $response->assertStatus(200);
+ $response->assertSee($matchingProduct->title);
+ $response->assertDontSee($nonMatchingProduct1->title);
+ $response->assertDontSee($nonMatchingProduct2->title);
+ }
+
+ /** @test */
+ public function it_only_shows_approved_products_on_public_index()
+ {
+ $approvedProduct = Product::factory()->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $pendingProduct = Product::factory()->create([
+ 'status' => 'pending',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $rejectedProduct = Product::factory()->create([
+ 'status' => 'rejected',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $response = $this->get(route('products.index'));
+
+ $response->assertStatus(200);
+ $response->assertSee($approvedProduct->title);
+ $response->assertDontSee($pendingProduct->title);
+ $response->assertDontSee($rejectedProduct->title);
+ }
+
+ /** @test */
+ public function it_preserves_query_parameters_in_pagination()
+ {
+ // Create enough products to trigger pagination
+ Product::factory()->count(15)->create([
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $response = $this->get(route('products.index', [
+ 'search' => 'test',
+ 'category' => $this->category1->id,
+ 'sort' => 'popular',
+ ]));
+
+ $response->assertStatus(200);
+
+ // Check that pagination links include the query parameters
+ $response->assertSee('search=test');
+ $response->assertSee('category=' . $this->category1->id);
+ $response->assertSee('sort=popular');
+ }
+
+ /** @test */
+ public function it_validates_search_and_filter_parameters()
+ {
+ $response = $this->get(route('products.index', [
+ 'category' => 99999, // Non-existent category
+ ]));
+
+ $response->assertSessionHasErrors('category');
+ }
+
+ /** @test */
+ public function it_validates_invalid_sort_parameter()
+ {
+ $response = $this->get(route('products.index', [
+ 'sort' => 'invalid_sort',
+ ]));
+
+ $response->assertSessionHasErrors('sort');
+ }
+
+ /** @test */
+ public function it_validates_invalid_date_range_parameter()
+ {
+ $response = $this->get(route('products.index', [
+ 'date_range' => 'invalid_range',
+ ]));
+
+ $response->assertSessionHasErrors('date_range');
+ }
+
+ /** @test */
+ public function it_shows_appropriate_message_when_no_results_found()
+ {
+ Product::factory()->create([
+ 'title' => 'Laravel Tool',
+ 'status' => 'approved',
+ 'category_id' => $this->category1->id,
+ ]);
+
+ $response = $this->get(route('products.index', ['search' => 'nonexistentproduct']));
+
+ $response->assertStatus(200);
+ $response->assertSee('No products found matching your filters');
+ }
+}