From 2e16d92fdd4efa1f3a487372976cfdee62ed77f0 Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 2 Mar 2026 14:22:17 +0530 Subject: [PATCH 1/4] fix: product images are not stored in public storage --- backend/app/Actions/UploadImageAction.php | 2 +- backend/app/Http/Controllers/ProductImagesController.php | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/app/Actions/UploadImageAction.php b/backend/app/Actions/UploadImageAction.php index cd11381..1c0e21b 100644 --- a/backend/app/Actions/UploadImageAction.php +++ b/backend/app/Actions/UploadImageAction.php @@ -8,7 +8,7 @@ { public function execute(UploadImageDTO $uploadImageDTO): void { - $path = $uploadImageDTO->image->store('public/images'); + $path = $uploadImageDTO->image->store('images', 'public'); $uploadImageDTO->product->images()->create([ 'path' => $path, ]); diff --git a/backend/app/Http/Controllers/ProductImagesController.php b/backend/app/Http/Controllers/ProductImagesController.php index 0ecd954..f906a45 100644 --- a/backend/app/Http/Controllers/ProductImagesController.php +++ b/backend/app/Http/Controllers/ProductImagesController.php @@ -16,11 +16,6 @@ public function store(UploadImageRequest $request, UploadImageAction $action) return response()->json(['message' => 'Image uploaded successfully']); } - public function show(ProductImages $productImages) - { - return $productImages; - } - public function destroy(ProductImages $productImages) { $productImages->delete(); From 3db5c079b9a2ea73878a776fbe7101ac5fb8b0f3 Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 2 Mar 2026 18:48:31 +0530 Subject: [PATCH 2/4] chore: add slug in products, fix image upload and product responses --- backend/app/Data/ProductDTO.php | 9 ++++++ .../Http/Controllers/ProductController.php | 7 ++++- .../app/Http/Requests/UploadImageRequest.php | 2 +- .../app/Http/Resources/ProductResource.php | 19 +++++++++++- backend/app/Models/Product.php | 11 +++++++ ...026_02_26_073626_create_products_table.php | 4 +-- ...3_02_111213_add_slug_to_products_table.php | 30 +++++++++++++++++++ 7 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 backend/database/migrations/2026_03_02_111213_add_slug_to_products_table.php diff --git a/backend/app/Data/ProductDTO.php b/backend/app/Data/ProductDTO.php index ea34b90..64bc2da 100644 --- a/backend/app/Data/ProductDTO.php +++ b/backend/app/Data/ProductDTO.php @@ -14,11 +14,14 @@ public function __construct( public int $id, public string $title, + public string $slug, public string $description, public int $actualPrice, public int $listPrice, public ProductCategoryDTO $category, public array $productImages, + public ?string $updatedAt = null, + public ?string $createdAt = null, ) {} /** @@ -29,12 +32,15 @@ public function toArray(): array return [ 'id' => $this->id, 'title' => $this->title, + 'slug' => $this->slug, 'description' => $this->description, 'actualPrice' => $this->actualPrice, 'listPrice' => $this->listPrice, 'category' => $this->category->toArray(), 'productImage' => array_map(fn (ProductImageDTO $productImage) => $productImage->toArray(), $this->productImages), + 'updatedAt' => $this->updatedAt, + 'createdAt' => $this->createdAt, ]; } @@ -43,11 +49,14 @@ public static function fromModel(Product $product): self return new self( id: $product->id, title: $product->title, + slug: $product->slug, description: $product->description, actualPrice: $product->actual_price, listPrice: $product->list_price, category: ProductCategoryDTO::fromModel($product->category), productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(), + updatedAt: $product->updated_at, + createdAt: $product->created_at, ); } } diff --git a/backend/app/Http/Controllers/ProductController.php b/backend/app/Http/Controllers/ProductController.php index 9081473..f58296a 100644 --- a/backend/app/Http/Controllers/ProductController.php +++ b/backend/app/Http/Controllers/ProductController.php @@ -23,7 +23,12 @@ public function store(CreateProductRequest $request) return Product::create($request->validated()); } - public function show(Product $product) {} + public function show(string $slug) + { + $product = Product::where('slug', $slug)->with(['category:id,name,slug', 'images:id,path,product_id'])->firstOrFail(); + + return new ProductResource(ProductDTO::fromModel($product)); + } public function update(Request $request, Product $product) {} diff --git a/backend/app/Http/Requests/UploadImageRequest.php b/backend/app/Http/Requests/UploadImageRequest.php index c5b523a..c214fa9 100644 --- a/backend/app/Http/Requests/UploadImageRequest.php +++ b/backend/app/Http/Requests/UploadImageRequest.php @@ -22,7 +22,7 @@ public function authorize(): bool public function rules(): array { return [ - 'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + 'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:10240', 'product_id' => 'required|exists:products,id', ]; } diff --git a/backend/app/Http/Resources/ProductResource.php b/backend/app/Http/Resources/ProductResource.php index ea6b9f8..e650cc7 100644 --- a/backend/app/Http/Resources/ProductResource.php +++ b/backend/app/Http/Resources/ProductResource.php @@ -3,8 +3,10 @@ namespace App\Http\Resources; use App\Data\ProductDTO; +use App\Data\ProductImageDTO; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Facades\Storage; /** * @property ProductDTO $resource @@ -13,6 +15,21 @@ class ProductResource extends JsonResource { public function toArray(Request $request): array { - return $this->resource->toArray(); + return [ + 'id' => $this->resource->id, + 'title' => $this->resource->title, + 'slug' => $this->resource->slug, + 'description' => $this->resource->description, + 'actualPrice' => $this->resource->actualPrice, + 'listPrice' => $this->resource->listPrice, + 'category' => [ + 'name' => $this->resource->category->name, + 'slug' => $this->resource->category->slug, + ], + 'productImages' => array_map(function (ProductImageDTO $productImage) { + return Storage::disk('public')->url($productImage->path); + }, $this->resource->productImages), + 'updatedAt' => $this->resource->updatedAt, + ]; } } diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php index c0725a3..82c3200 100644 --- a/backend/app/Models/Product.php +++ b/backend/app/Models/Product.php @@ -5,11 +5,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Str; class Product extends Model { protected $fillable = [ 'title', + 'slug', 'actual_price', 'list_price', 'description', @@ -25,4 +27,13 @@ public function images(): HasMany { return $this->hasMany(ProductImage::class, 'product_id', 'id'); } + + protected static function booted(): void + { + static::saving(function ($product) { + if (empty($product->slug) || $product->isDirty('title')) { + $product->slug = Str::slug($product->title); + } + }); + } } diff --git a/backend/database/migrations/2026_02_26_073626_create_products_table.php b/backend/database/migrations/2026_02_26_073626_create_products_table.php index 6dc7a2f..4692626 100644 --- a/backend/database/migrations/2026_02_26_073626_create_products_table.php +++ b/backend/database/migrations/2026_02_26_073626_create_products_table.php @@ -1,6 +1,6 @@ -string('slug')->unique()->after('title')->nullable(); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('slug'); + }); + } +}; From 8ef4383bd9f39694fac1b3f16ed73a1906563fdd Mon Sep 17 00:00:00 2001 From: kusowl Date: Thu, 5 Mar 2026 10:32:40 +0530 Subject: [PATCH 3/4] feature: endpoint to favorite product --- .ai/mcp/mcp.json | 0 .gitignore | 2 ++ .../FavouriteProductController.php | 35 +++++++++++++++++++ .../Resources/FavouriteProductResource.php | 23 ++++++++++++ backend/app/Models/User.php | 14 +++++++- ...133303_create_favourite_products_table.php | 26 ++++++++++++++ backend/routes/api.php | 4 +++ 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .ai/mcp/mcp.json create mode 100644 backend/app/Http/Controllers/FavouriteProductController.php create mode 100644 backend/app/Http/Resources/FavouriteProductResource.php create mode 100644 backend/database/migrations/2026_03_02_133303_create_favourite_products_table.php diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 9efe7dc..7353c22 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,8 @@ yarn-error.log testem.log /typings __screenshots__/ +*.cache +.php-cs-fixer.dist.php # System files .DS_Store diff --git a/backend/app/Http/Controllers/FavouriteProductController.php b/backend/app/Http/Controllers/FavouriteProductController.php new file mode 100644 index 0000000..80e2467 --- /dev/null +++ b/backend/app/Http/Controllers/FavouriteProductController.php @@ -0,0 +1,35 @@ +user(); + + $changes = $user->favoriteProducts()->toggle($product); + + // If changes has any item, that means a product has been attached. + $isFavorite = count($changes['attached']) > 0; + + return response()->json([ + 'message' => $isFavorite ? 'Product added to favorites' : 'Product removed from favorites', + 'isFavorite' => $isFavorite, + ]); + } +} diff --git a/backend/app/Http/Resources/FavouriteProductResource.php b/backend/app/Http/Resources/FavouriteProductResource.php new file mode 100644 index 0000000..4ad6a80 --- /dev/null +++ b/backend/app/Http/Resources/FavouriteProductResource.php @@ -0,0 +1,23 @@ + $this->id, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + + 'user_id' => $this->user_id, + 'product_id' => $this->product_id, + ]; + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index eb74b44..a0ab8b4 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -11,7 +11,9 @@ class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasFactory; + + use Notifiable; /** * The attributes that are mass assignable. @@ -50,4 +52,14 @@ protected function casts(): array 'role' => UserRoles::class, ]; } + + public function favoriteProducts() + { + return $this->belongsToMany(Product::class, 'favorite_products', 'user_id', 'product_id'); + } + + public function hasFavorited(Product $product): bool + { + return $this->favoriteProducts()->where('product_id', $product->id)->exists(); + } } diff --git a/backend/database/migrations/2026_03_02_133303_create_favourite_products_table.php b/backend/database/migrations/2026_03_02_133303_create_favourite_products_table.php new file mode 100644 index 0000000..0975051 --- /dev/null +++ b/backend/database/migrations/2026_03_02_133303_create_favourite_products_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignIdFor(User::class); + $table->foreignIdFor(Product::class); + $table->timestamps(); + $table->unique(['user_id', 'product_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('favorite_products'); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 5d8329d..311580f 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,7 @@ Date: Thu, 5 Mar 2026 13:32:49 +0530 Subject: [PATCH 4/4] chore: add isFavorite in produtcs response - refactor code to use query - add active column in products --- backend/app/Data/ProductDTO.php | 4 +++ .../FavouriteProductController.php | 3 +- .../Http/Controllers/ProductController.php | 8 +++-- .../app/Http/Resources/ProductResource.php | 1 + backend/app/Models/Product.php | 21 ++++++++++++++ backend/app/Queries/GetProductsQuery.php | 27 +++++++++++++++++ ...26_03_05_063756_add-active-in-products.php | 29 +++++++++++++++++++ 7 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 backend/app/Queries/GetProductsQuery.php create mode 100644 backend/database/migrations/2026_03_05_063756_add-active-in-products.php diff --git a/backend/app/Data/ProductDTO.php b/backend/app/Data/ProductDTO.php index 64bc2da..4190d09 100644 --- a/backend/app/Data/ProductDTO.php +++ b/backend/app/Data/ProductDTO.php @@ -22,6 +22,7 @@ public function __construct( public array $productImages, public ?string $updatedAt = null, public ?string $createdAt = null, + public ?bool $isFavorite = null ) {} /** @@ -41,6 +42,7 @@ public function toArray(): array $this->productImages), 'updatedAt' => $this->updatedAt, 'createdAt' => $this->createdAt, + 'isFavorite' => $this->isFavorite, ]; } @@ -57,6 +59,8 @@ public static function fromModel(Product $product): self productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(), updatedAt: $product->updated_at, createdAt: $product->created_at, + // this column is added by where exists query + isFavorite: $product->favorited_by_exists, ); } } diff --git a/backend/app/Http/Controllers/FavouriteProductController.php b/backend/app/Http/Controllers/FavouriteProductController.php index 80e2467..c7a99ac 100644 --- a/backend/app/Http/Controllers/FavouriteProductController.php +++ b/backend/app/Http/Controllers/FavouriteProductController.php @@ -7,6 +7,7 @@ use App\Models\Product; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; class FavouriteProductController extends Controller { @@ -23,7 +24,7 @@ public function toggle(Request $request, Product $product) $user = $request->user(); $changes = $user->favoriteProducts()->toggle($product); - + Log::info('hi again'); // If changes has any item, that means a product has been attached. $isFavorite = count($changes['attached']) > 0; diff --git a/backend/app/Http/Controllers/ProductController.php b/backend/app/Http/Controllers/ProductController.php index f58296a..c003c10 100644 --- a/backend/app/Http/Controllers/ProductController.php +++ b/backend/app/Http/Controllers/ProductController.php @@ -6,14 +6,16 @@ use App\Http\Requests\CreateProductRequest; use App\Http\Resources\ProductResource; use App\Models\Product; +use App\Queries\GetProductsQuery; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; class ProductController extends Controller { - public function index() + public function index(GetProductsQuery $getProductsQuery) { - $paginator = Product::query()->with(['category:id,name,slug', 'images:id,path,product_id'])->paginate(); - $paginatedDtos = $paginator->through(fn ($product) => ProductDTO::fromModel($product)); + $products = $getProductsQuery->get(Auth::user()); + $paginatedDtos = $products->through(fn ($product) => ProductDTO::fromModel($product)); return ProductResource::collection($paginatedDtos); } diff --git a/backend/app/Http/Resources/ProductResource.php b/backend/app/Http/Resources/ProductResource.php index e650cc7..9749042 100644 --- a/backend/app/Http/Resources/ProductResource.php +++ b/backend/app/Http/Resources/ProductResource.php @@ -30,6 +30,7 @@ public function toArray(Request $request): array return Storage::disk('public')->url($productImage->path); }, $this->resource->productImages), 'updatedAt' => $this->resource->updatedAt, + 'isFavorite' => $this->resource->isFavorite, ]; } } diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php index 82c3200..ef200b6 100644 --- a/backend/app/Models/Product.php +++ b/backend/app/Models/Product.php @@ -2,8 +2,11 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; @@ -28,6 +31,17 @@ public function images(): HasMany return $this->hasMany(ProductImage::class, 'product_id', 'id'); } + public function favoritedBy(): BelongsToMany + { + return $this->belongsToMany(User::class, 'favorite_products'); + } + + #[Scope] + protected function active(Builder $query): Builder + { + return $query->where('is_active', true); + } + protected static function booted(): void { static::saving(function ($product) { @@ -36,4 +50,11 @@ protected static function booted(): void } }); } + + protected function casts() + { + return [ + 'is_active' => 'boolean', + ]; + } } diff --git a/backend/app/Queries/GetProductsQuery.php b/backend/app/Queries/GetProductsQuery.php new file mode 100644 index 0000000..4d413c5 --- /dev/null +++ b/backend/app/Queries/GetProductsQuery.php @@ -0,0 +1,27 @@ +active() + ->when($user, function (Builder $query) use ($user) { + $query->withExists( + [ + 'favoritedBy' => fn (Builder $query) => $query->where('user_id', $user->id), + ] + ); + + }) + ->with(['category:id,name,slug', 'images:id,path,product_id']) + ->paginate(); + } +} diff --git a/backend/database/migrations/2026_03_05_063756_add-active-in-products.php b/backend/database/migrations/2026_03_05_063756_add-active-in-products.php new file mode 100644 index 0000000..d315a3b --- /dev/null +++ b/backend/database/migrations/2026_03_05_063756_add-active-in-products.php @@ -0,0 +1,29 @@ +boolean('is_active')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('is_active'); + }); + + } +};