Skip to content

Commit 081479b

Browse files
committed
feat: wip product variant selector on customer end with add to cart option
1 parent 0c8138f commit 081479b

File tree

13 files changed

+313
-25
lines changed

13 files changed

+313
-25
lines changed

app/Http/Controllers/Admin/MediaController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public function storeFolder(Request $request)
185185
if ($request->ajax()) {
186186
return response()->json($response);
187187
}
188+
188189
return redirect()->back()->with($success, $message);
189190
}
190191

app/Http/Controllers/Frontend/CartController.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@ public function add(Request $request)
1717
{
1818
$productId = $request->input('product_id');
1919
$quantity = $request->input('quantity', 1);
20+
$variantId = $request->input('variant_id') ?? null;
2021

21-
$this->service->add($productId, $quantity);
22-
22+
$result = $this->service->add($productId, $quantity, $variantId);
2323
$carts = session()->get('cart') ?? [];
24+
if (!$result['success']) {
25+
return response()->json([
26+
'success' => false,
27+
'message' => $result['message'],
28+
'data' => $carts
29+
], 422);
30+
}
31+
2432
return response()->json(['success' => true,'message' => 'Successfully added to cart!','data' => $carts]);
2533
}
2634

app/Http/Controllers/Frontend/ProductController.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
use Illuminate\Http\Request;
77
use App\Services\CartService;
88
use App\Http\Controllers\Controller;
9+
use App\Models\Attribute;
10+
use App\Models\AttributeValue;
11+
use App\Models\ProductVariant;
12+
use Illuminate\Support\Facades\DB;
13+
use Illuminate\Support\Facades\Storage;
914

1015
class ProductController extends Controller
1116
{
@@ -14,4 +19,44 @@ public function index()
1419
$products = Product::with('category')->paginate(10);
1520
return view('frontend.products.index', compact('products'));
1621
}
22+
23+
public function show($slug)
24+
{
25+
$product = Product::where('slug', $slug)->with([
26+
'category',
27+
])->firstOrFail();
28+
$variantIds = ProductVariant::where('product_id', $product->id)->pluck('id');
29+
30+
$attributeValueIds = DB::table('product_variant_values')
31+
->whereIn('product_variant_id', $variantIds)
32+
->pluck('attribute_value_id')
33+
->unique();
34+
35+
$attributeIds = AttributeValue::whereIn('id', $attributeValueIds)
36+
->pluck('attribute_id')
37+
->unique();
38+
39+
$attributes = Attribute::with(['values' => function ($query) use ($attributeValueIds) {
40+
$query->whereIn('id', $attributeValueIds);
41+
}])
42+
->whereIn('id', $attributeIds)
43+
->get();
44+
45+
$product->load('variants.attributeValues');
46+
$product->variants->transform(function ($variant) {
47+
$variant->attribute_value_ids = $variant->attributeValues->pluck('id')->sort()->values()->all();
48+
return $variant;
49+
});
50+
51+
$variants = $product->variants->map(function($v) {
52+
return [
53+
'id' => $v->id,
54+
'price' => $v->price,
55+
'sku' => $v->sku,
56+
'image' => Storage::disk('public')->url($v->image),
57+
'attribute_value_ids' => $v->attributeValues->pluck('id')->sort()->values()->all(),
58+
];
59+
})->toArray();
60+
return view('frontend.products.show', compact('product', 'attributes','variants'));
61+
}
1762
}

app/Http/Requests/ProductRequest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public function rules(): array
2929
return [
3030
'title' => 'required|string|max:120|unique:products,title,' . $id,
3131
'description' => 'nullable|string|max:200',
32+
'slug' => [
33+
'required',
34+
'string',
35+
'regex:/^[a-z0-9-]+$/',
36+
],
3237
'sku' => 'required|string|max:200|unique:products,sku,' . $id,
3338
'category_id' => 'required',
3439
'type' => 'required',

app/Services/CartService.php

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,74 @@
44
namespace App\Services;
55

66
use App\Models\Product;
7+
use App\Models\ProductVariant;
78

89
class CartService
910
{
10-
public function add($productId, $quantity = 1)
11+
public function add($productId, $quantity = 1, $variantId = null)
1112
{
12-
$product = Product::findOrFail($productId);
13-
1413
$cart = session()->get('cart', []);
1514

16-
if (isset($cart['items'][$productId])) {
17-
$cart['items'][$productId]['quantity'] += $quantity;
15+
// Use unique key for product + variant combo
16+
$key = $productId . '_' . ($variantId ?? 'default');
17+
18+
$currentQty = $cart['items'][$key]['quantity'] ?? 0;
19+
$newQuantity = $currentQty + $quantity;
20+
21+
$exceeded = false;
22+
if ($newQuantity > 10) {
23+
$newQuantity = 10;
24+
$exceeded = true;
25+
}
26+
27+
if (isset($cart['items'][$key])) {
28+
// Update quantity only
29+
$cart['items'][$key]['quantity'] = $newQuantity;
1830
} else {
19-
$cart['items'][$productId] = [
31+
$product = Product::findOrFail($productId);
32+
33+
$price = $product->price;
34+
$sku = $product->sku;
35+
$image = $product->image ? asset($product->image) : 'https://placehold.co/400';
36+
37+
if ($variantId) {
38+
$variant = ProductVariant::find($variantId);
39+
if ($variant) {
40+
if ($variant->price !== null) {
41+
$price = $variant->price;
42+
}
43+
if ($variant->sku) {
44+
$sku = $variant->sku;
45+
}
46+
if ($variant->image ?? false) {
47+
$image = asset($variant->image);
48+
}
49+
}
50+
}
51+
52+
$cart['items'][$key] = [
2053
'product_id' => $product->id,
21-
'title' => $product->title,
22-
'price' => $product->price,
23-
'image' => $product->image ? asset($product->image) : 'https://placehold.co/400',
24-
'sku' => $product->sku,
25-
'quantity' => $quantity,
54+
'variant_id' => $variantId,
55+
'title' => $product->title,
56+
'price' => $price,
57+
'image' => $image,
58+
'sku' => $sku,
59+
'quantity' => $newQuantity,
2660
'attributes' => [],
2761
];
2862
}
63+
2964
$cart = $this->updateTotal($cart);
3065
session()->put('cart', $cart);
66+
67+
return [
68+
'success' => !$exceeded,
69+
'message' => $exceeded ? 'Maximum quantity for this product is 10.' : 'Successfully added to cart!',
70+
];
3171
}
3272

33-
public function update($productId, $quantity){
73+
public function update($productId, $quantity)
74+
{
3475
$cart = session()->get('cart');
3576
if (isset($cart['items'][$productId])) {
3677
$cart['items'][$productId]['quantity'] = $quantity;
@@ -52,7 +93,8 @@ public function remove($productId)
5293
return $cart;
5394
}
5495

55-
private function updateTotal($cart){
96+
private function updateTotal($cart)
97+
{
5698
$total = 0;
5799
foreach ($cart['items'] as $key => $item) {
58100
// Skip if key is 'total' itself to avoid issues

database/factories/ProductFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class ProductFactory extends Factory
1919
public function definition(): array
2020
{
2121
return [
22-
'title' => $this->faker->sentence(3),
22+
'title' => $title = $this->faker->sentence(3),
23+
'slug' => Str::slug($title), // create slug from title
2324
'type' => 'variable',
2425
'sku' => strtoupper(Str::random(10)),
2526
'price' => null, // price comes from variants

database/migrations/2025_06_23_185956_create_products_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function up(): void
1515
$table->id();
1616
$table->string('title');
1717
$table->string('sku')->unique();
18+
$table->string('slug')->unique();
1819
$table->float('price')->nullable();
1920
$table->string('image')->nullable();
2021
$table->text('description')->nullable();

resources/views/admin/products/create.blade.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@
5252
</small>
5353
</div>
5454
</div>
55+
<div class="mb-3 row">
56+
<label class="col-3 col-form-label required">Product slug</label>
57+
<div class="col">
58+
<input type="text" class="form-control" aria-describedby="emailHelp"
59+
placeholder="Product slug" name="slug" value="{{ old('slug') }}">
60+
<small class="form-hint">
61+
@error('slug')
62+
<div class="text-danger mt-2">{{ $message }}</div>
63+
@enderror
64+
</small>
65+
</div>
66+
</div>
5567
<div class="mb-3 row">
5668
<div class="col-3 col-form-label required">Product Image</div>
5769
<div class="col">

resources/views/admin/products/edit.blade.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left">
6868
</small>
6969
</div>
7070
</div>
71+
<div class="mb-3 row">
72+
<label class="col-3 col-form-label required">Product slug</label>
73+
<div class="col">
74+
<input type="text" class="form-control" aria-describedby="emailHelp"
75+
placeholder="Product slug" name="slug" value="{{ $product->slug }}">
76+
<small class="form-hint">
77+
@error('slug')
78+
<div class="text-danger mt-2">{{ $message }}</div>
79+
@enderror
80+
</small>
81+
</div>
82+
</div>
7183
<div class="mb-3 row">
7284
<div class="col-3 col-form-label required">Product Image</div>
7385
<div class="col">

resources/views/frontend/carts/sidebar.blade.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
<dl class="mt-0.5 space-y-px text-[10px] text-gray-600">
1515
<div>
16-
<dt class="inline" x-text="`Price: $${item.price.toFixed(2)}`"></dt>
17-
{{-- <dd class="inline">XXS</dd> --}}
16+
<dt class="inline" x-text="`Price: $${parseFloat(item.price).toFixed(2)}`"></dt>
17+
<br>
18+
<dd class="inline" x-text="`Sku: ${item.sku}`"></dd>
1819
</div>
1920

2021
{{-- <div>

0 commit comments

Comments
 (0)