Skip to content

Commit 1c64311

Browse files
committed
feat: update cart quantity, remove item, cart counter badge on header cart icon
1 parent 5a46ac0 commit 1c64311

File tree

5 files changed

+179
-85
lines changed

5 files changed

+179
-85
lines changed

app/Http/Controllers/Frontend/CartController.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,40 @@
88

99
class CartController extends Controller
1010
{
11+
public CartService $service;
12+
public function __construct(){
13+
$this->service = new CartService;
14+
}
1115
public function add(Request $request)
1216
{
1317
$productId = $request->input('product_id');
1418
$quantity = $request->input('quantity', 1);
1519

16-
app(CartService::class)->add($productId, $quantity);
20+
$this->service->add($productId, $quantity);
1721

1822
$carts = session()->get('cart') ?? [];
1923
return response()->json(['success' => true,'message' => 'Successfully added to cart!','data' => $carts]);
2024
}
25+
26+
public function update(Request $request)
27+
{
28+
$productId = $request->input('product_id');
29+
$quantity = $request->input('quantity');
30+
$cart = $this->service->update($productId, $quantity);
31+
return response()->json([
32+
'success' => true,
33+
'message' => 'Quantity updated',
34+
'data' => $cart,
35+
]);
36+
}
37+
38+
public function remove(Request $request)
39+
{
40+
$productId = $request->input('product_id');
41+
$cart = $this->service->remove($productId);
42+
return response()->json([
43+
'message' => 'Item removed successfully.',
44+
'data' => $cart,
45+
]);
46+
}
2147
}

app/Services/CartService.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,33 @@ public function add($productId, $quantity = 1)
2525
'quantity' => $quantity,
2626
];
2727
}
28+
$cart = $this->updateTotal($cart);
29+
session()->put('cart', $cart);
30+
}
31+
32+
public function update($productId, $quantity){
33+
$cart = session()->get('cart');
34+
if (isset($cart['items'][$productId])) {
35+
$cart['items'][$productId]['quantity'] = $quantity;
36+
}
37+
$cart = $this->updateTotal($cart);
38+
session()->put('cart', $cart);
39+
return $cart;
40+
}
2841

42+
public function remove($productId)
43+
{
44+
$cart = session()->get('cart', []);
45+
logger('Removing from cart:', ['productId' => $productId, 'cartKeys' => array_keys($cart['items'] ?? [])]);
46+
if (isset($cart['items'][$productId])) {
47+
unset($cart['items'][$productId]);
48+
}
49+
$cart = $this->updateTotal($cart);
50+
session()->put('cart', $cart);
51+
return $cart;
52+
}
53+
54+
private function updateTotal($cart){
2955
$total = 0;
3056
foreach ($cart['items'] as $key => $item) {
3157
// Skip if key is 'total' itself to avoid issues
@@ -35,7 +61,6 @@ public function add($productId, $quantity = 1)
3561
$total += $item['price'] * $item['quantity'];
3662
}
3763
$cart['attributes']['total'] = number_format($total, 2);
38-
39-
session()->put('cart', $cart);
64+
return $cart;
4065
}
4166
}

resources/views/frontend/app.blade.php

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
</main>
3131

3232

33-
<div x-init="console.log($store.toast)" x-cloak x-show="$store.toast.visible" x-transition @click="$store.toast.visible = false"
34-
class="toast fixed top-4 right-4 z-50 cursor-pointer">
33+
<div x-init="console.log($store.toast)" x-cloak x-show="$store.toast.visible" x-transition
34+
@click="$store.toast.visible = false" class="toast fixed bottom-4 right-4 z-50 cursor-pointer">
3535
<div :class="`alert ${$store.toast.type === 'success' ? 'alert-success' : 'alert-error'} text-white`">
3636
<span x-text="$store.toast.message"></span>
3737
</div>
@@ -52,13 +52,45 @@ class="toast fixed top-4 right-4 z-50 cursor-pointer">
5252
Alpine.store('cart', {
5353
items: @json(session()->get('cart')['items'] ?? []),
5454
attributes: @json(session()->get('cart')['attributes'] ?? []),
55+
total: @json(session()->get('cart')['attributes']['total'] ?? 0),
5556
reset(cart) {
57+
console.log(cart)
5658
this.items = cart.items ?? [];
5759
this.attributes = cart.attributes ?? [];
60+
this.total = cart.attributes.total ?? 0
61+
},
62+
63+
updateQuantity(productId, quantity) {
64+
axios.post('/cart/update', {
65+
product_id: productId,
66+
quantity: quantity
67+
})
68+
.then(response => {
69+
this.reset(response.data.data);
70+
Alpine.store('toast').show(true, response.data.message || 'Cart updated!');
71+
})
72+
.catch(error => {
73+
Alpine.store('toast').show(false, error.response?.data?.message ||
74+
'Update failed.');
75+
});
76+
},
77+
78+
removeItem(productId) {
79+
axios.post('/cart/remove', {
80+
product_id: productId
81+
})
82+
.then(response => {
83+
this.reset(response.data.data);
84+
Alpine.store('toast').show(true, response.data.message || 'Item removed!');
85+
})
86+
.catch(error => {
87+
Alpine.store('toast').show(false, error.response?.data?.message ||
88+
'Remove failed.');
89+
});
5890
}
5991
});
6092
61-
93+
6294
Alpine.store('toast', {
6395
visible: false,
6496
message: '',
@@ -83,11 +115,6 @@ class="toast fixed top-4 right-4 z-50 cursor-pointer">
83115
})
84116
.then(response => {
85117
Alpine.store('cart').reset(response.data.data);
86-
// Alpine.store('cart').addItem({
87-
// id: productId,
88-
// title: response.data.title || 'Product ' + productId,
89-
// quantity: 1
90-
// });
91118
Alpine.store('toast').show(true, response.data.message ||
92119
'Added to cart!');
93120
})
Lines changed: 80 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,104 @@
11
<div class="mt-4 space-y-6">
22
<div class="mt-8">
33
<ul class="space-y-4">
4+
<template x-if="Object.keys($store.cart.items).length === 0">
5+
<li class="text-gray-500 text-sm">Your cart is empty.</li>
6+
</template>
47
<template x-for="(item, key) in $store.cart.items" :key="key">
5-
<li class="flex items-center gap-4">
6-
<img :src="item.image"
7-
alt=""
8-
class="size-16 rounded-sm object-cover" />
8+
<li class="flex items-center gap-4">
9+
<img :src="item.image" alt="" class="size-16 rounded-sm object-cover" />
910

10-
<div>
11-
<h3 class="text-sm text-gray-900" x-text="item.title"></h3>
12-
13-
<dl class="mt-0.5 space-y-px text-[10px] text-gray-600">
14-
<div>
15-
<dt class="inline">Size:</dt>
16-
<dd class="inline">XXS</dd>
17-
</div>
11+
<div>
12+
<h3 class="text-sm text-gray-900" x-text="item.title"></h3>
1813

19-
<div>
20-
<dt class="inline">Color:</dt>
21-
<dd class="inline">White</dd>
22-
</div>
23-
</dl>
24-
</div>
14+
<dl class="mt-0.5 space-y-px text-[10px] text-gray-600">
15+
<div>
16+
<dt class="inline" x-text="`Price: $${item.price.toFixed(2)}`"></dt>
17+
{{-- <dd class="inline">XXS</dd> --}}
18+
</div>
2519

26-
<div class="flex flex-1 items-center justify-end gap-2">
27-
<form>
28-
<label for="Line1Qty" class="sr-only"> Quantity </label>
20+
{{-- <div>
21+
<dt class="inline">Color:</dt>
22+
<dd class="inline">White</dd>
23+
</div> --}}
24+
</dl>
25+
</div>
2926

30-
<input type="number" min="1" :value="item.quantity" id="Line1Qty"
31-
class="h-8 w-12 rounded-sm border-gray-200 bg-gray-50 p-0 text-center text-xs text-gray-600" />
32-
</form>
27+
<div class="flex flex-1 items-center justify-end gap-2">
28+
<form>
29+
<label for="Line1Qty" class="sr-only"> Quantity </label>
3330

34-
<button class="text-gray-600 transition hover:text-red-600">
35-
<span class="sr-only">Remove item</span>
31+
<select
32+
@change="$store.cart.updateQuantity(key, +$event.target.value)"
33+
class="h-8 w-14 rounded-sm border-gray-200 bg-gray-50 p-0 text-center text-xs text-gray-600">
34+
<template x-for="qty in 10" :key="qty">
35+
<option :value="qty" x-text="qty" :selected="qty === item.quantity"></option>
36+
</template>
37+
</select>
38+
</form>
3639

37-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
38-
stroke="currentColor" class="size-4">
39-
<path stroke-linecap="round" stroke-linejoin="round"
40-
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
41-
</svg>
42-
</button>
43-
</div>
44-
</li>
40+
<button class="text-gray-600 transition hover:text-red-600 cursor-pointer"
41+
@click="$store.cart.removeItem(key)"
42+
>
43+
<span class="sr-only">Remove item</span>
44+
45+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
46+
stroke-width="1.5" stroke="currentColor" class="size-4">
47+
<path stroke-linecap="round" stroke-linejoin="round"
48+
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
49+
</svg>
50+
</button>
51+
</div>
52+
</li>
4553
</template>
4654
</ul>
4755

4856
<div class="mt-8 flex justify-end border-t border-gray-100 pt-8">
49-
<div class="w-screen max-w-lg space-y-4">
50-
<dl class="space-y-0.5 text-sm text-gray-700">
51-
<div class="flex justify-between">
52-
<dt>Subtotal</dt>
53-
<dd x-text="$store.cart.attributes.total"></dd>
54-
</div>
57+
<template x-if="Object.keys($store.cart.items).length > 0">
58+
<div class="w-screen max-w-lg space-y-4">
59+
<dl class="space-y-0.5 text-sm text-gray-700">
60+
<div class="flex justify-between">
61+
<dt>Subtotal</dt>
62+
<dd x-text="$store.cart.total"></dd>
63+
</div>
5564

56-
{{-- <div class="flex justify-between">
57-
<dt>VAT</dt>
58-
<dd>£25</dd>
59-
</div>
65+
{{-- <div class="flex justify-between">
66+
<dt>VAT</dt>
67+
<dd>£25</dd>
68+
</div>
6069
61-
<div class="flex justify-between">
62-
<dt>Discount</dt>
63-
<dd>-£20</dd>
64-
</div> --}}
70+
<div class="flex justify-between">
71+
<dt>Discount</dt>
72+
<dd>-£20</dd>
73+
</div> --}}
6574

66-
<div class="flex justify-between !text-base font-medium">
67-
<dt>Total</dt>
68-
<dd x-text="$store.cart.attributes.total"></dd>
69-
</div>
70-
</dl>
75+
<div class="flex justify-between !text-base font-medium">
76+
<dt>Total</dt>
77+
<dd x-text="$store.cart.total"></dd>
78+
</div>
79+
</dl>
7180

72-
{{-- <div class="flex justify-end">
73-
<span
74-
class="inline-flex items-center justify-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-indigo-700">
75-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
76-
stroke="currentColor" class="-ms-1 me-1.5 size-4">
77-
<path stroke-linecap="round" stroke-linejoin="round"
78-
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
79-
</svg>
81+
{{-- <div class="flex justify-end">
82+
<span
83+
class="inline-flex items-center justify-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-indigo-700">
84+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
85+
stroke="currentColor" class="-ms-1 me-1.5 size-4">
86+
<path stroke-linecap="round" stroke-linejoin="round"
87+
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
88+
</svg>
8089
81-
<p class="text-xs whitespace-nowrap">2 Discounts Applied</p>
82-
</span>
83-
</div> --}}
90+
<p class="text-xs whitespace-nowrap">2 Discounts Applied</p>
91+
</span>
92+
</div> --}}
8493

85-
<div class="flex justify-end">
86-
<a href="#"
87-
class="block rounded-sm bg-gray-700 px-5 py-3 text-sm text-gray-100 transition hover:bg-gray-600">
88-
Checkout
89-
</a>
94+
<div class="flex justify-end">
95+
<a href="#"
96+
class="block rounded-sm bg-gray-700 px-5 py-3 text-sm text-gray-100 transition hover:bg-gray-600">
97+
Checkout
98+
</a>
99+
</div>
90100
</div>
91-
</div>
101+
</template>
92102
</div>
93103
</div>
94104
</div>

resources/views/frontend/partials/header.blade.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,19 @@ class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:bor
4444
<div x-data="{ isCartOpen: false }" x-transition x-cloak class="relative">
4545
<!-- 🛍️ Cart Button -->
4646
<button type="button" @click="isCartOpen = true"
47-
class="text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75">
48-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
49-
class="size-4">
47+
class="relative text-gray-500 transition hover:text-gray-500/75 dark:text-white dark:hover:text-white/75">
48+
49+
<!-- Cart Icon -->
50+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
5051
<path fill-rule="evenodd"
5152
d="M6 5v1H4.667a1.75 1.75 0 0 0-1.743 1.598l-.826 9.5A1.75 1.75 0 0 0 3.84 19H16.16a1.75 1.75 0 0 0 1.743-1.902l-.826-9.5A1.75 1.75 0 0 0 15.333 6H14V5a4 4 0 0 0-8 0Zm4-2.5A2.5 2.5 0 0 0 7.5 5v1h5V5A2.5 2.5 0 0 0 10 2.5ZM7.5 10a2.5 2.5 0 0 0 5 0V8.75a.75.75 0 0 1 1.5 0V10a4 4 0 0 1-8 0V8.75a.75.75 0 0 1 1.5 0V10Z"
5253
clip-rule="evenodd" />
5354
</svg>
55+
56+
<!-- Counter badge -->
57+
<span x-show="Object.keys($store.cart.items).length > 0"
58+
x-text="Object.keys($store.cart.items).length"
59+
class="absolute -top-2 -right-2 inline-flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] text-white"></span>
5460
</button>
5561

5662
<!-- 🛒 Cart Drawer -->
@@ -60,7 +66,7 @@ class="fixed right-0 top-0 z-50 h-full w-full max-w-sm overflow-y-auto border bo
6066

6167
<!-- ❌ Close Button -->
6268
<button @click="isCartOpen = false"
63-
class="absolute end-4 top-4 text-gray-600 transition hover:scale-110">
69+
class="absolute end-4 top-4 text-gray-600 transition hover:scale-110 cursor-pointer">
6470
<span class="sr-only">Close cart</span>
6571
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
6672
stroke-width="1.5" stroke="currentColor" class="size-5">

0 commit comments

Comments
 (0)