Skip to content

Commit 7f80d31

Browse files
committed
fix: enhance cart functionality with quantity adjustment and status handling
1 parent 2ce5b11 commit 7f80d31

File tree

3 files changed

+180
-69
lines changed

3 files changed

+180
-69
lines changed

app/components/Cart.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
const { cart, handleRemoveFromCart, removeFromCartButtonStatus } = useCart();
2+
const { cart, handleRemoveFromCart, itemStatus } = useCart();
33
const { order } = useCheckout();
44
</script>
55

@@ -29,9 +29,13 @@ const { order } = useCheckout();
2929
</div>
3030
</div>
3131
<button
32-
@click="handleRemoveFromCart(product.key)"
33-
class="absolute md:opacity-0 group-hover:opacity-100 top-2 right-2 md:-top-1 md:-right-1 transition bg-red-700 flex p-1 items-center justify-center rounded-full hover:bg-red-500 active:scale-95">
34-
<UIcon :name="removeFromCartButtonStatus === 'remove' ? 'i-iconamoon-trash-light' : 'i-svg-spinners-90-ring-with-bg'" size="18" class="text-white" />
32+
@click="handleRemoveFromCart(product.key)"
33+
:disabled="itemStatus[product.key] === 'loading'"
34+
class="absolute md:opacity-0 group-hover:opacity-100 top-2 right-2 md:-top-1 md:-right-1 transition bg-red-700 flex p-1 items-center justify-center rounded-full hover:bg-red-500 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed">
35+
<UIcon
36+
:name="itemStatus[product.key] === 'loading' ? 'i-svg-spinners-90-ring-with-bg' : 'i-iconamoon-trash-light'"
37+
size="18"
38+
class="text-white" />
3539
</button>
3640
</div>
3741
</div>

app/composables/useCart.js

Lines changed: 120 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,122 @@
11
export const useCart = () => {
2-
const cart = useState('cart', () => []);
3-
const addToCartButtonStatus = ref('add');
4-
const removeFromCartButtonStatus = ref('remove');
5-
const { push } = useNotivue();
6-
7-
const handleAddToCart = productId => {
8-
addToCartButtonStatus.value = 'loading';
9-
10-
$fetch('/api/cart/add', {
11-
method: 'POST',
12-
body: { productId },
13-
})
14-
.then(res => {
15-
updateCart([...cart.value, res.addToCart.cartItem]);
16-
addToCartButtonStatus.value = 'added';
17-
setTimeout(() => (addToCartButtonStatus.value = 'add'), 2000);
18-
})
19-
.catch(err => {
20-
addToCartButtonStatus.value = 'add';
21-
const errorMessage = err.response.errors[0].message
22-
.replace(/<a[^>]*>(.*?)<\/a>/g, '')
23-
.replace(/&mdash;/g, '—')
24-
.trim();
25-
push.error(errorMessage);
26-
});
27-
};
28-
29-
const handleRemoveFromCart = key => {
30-
removeFromCartButtonStatus.value = 'loading';
31-
$fetch('/api/cart/update', {
32-
method: 'POST',
33-
body: { items: [{ key, quantity: 0 }] },
34-
}).then(() => {
35-
removeFromCartButtonStatus.value = 'remove';
36-
updateCart(cart.value.filter(item => item.key !== key));
2+
const cart = useState('cart', () => []);
3+
const itemStatus = useState('itemStatus', () => ({}));
4+
const { push } = useNotivue();
5+
6+
const clearStatusAfterDelay = (key, delay = 2000) => {
7+
setTimeout(() => {
8+
if (itemStatus.value[key]) {
9+
itemStatus.value[key] = 'idle';
10+
}
11+
}, delay);
12+
};
13+
14+
const updateCartCache = (newCart) => {
15+
cart.value = newCart;
16+
if (process.client) {
17+
localStorage.setItem('cart', JSON.stringify(newCart));
18+
}
19+
};
20+
21+
const handleAddToCart = (productId) => {
22+
itemStatus.value[productId] = 'loading';
23+
24+
const existingItem = cart.value.find(item => item.variation?.node.databaseId === productId);
25+
if (existingItem) {
26+
increaseQuantity(existingItem);
27+
return;
28+
}
29+
30+
$fetch('/api/cart/add', {
31+
method: 'POST',
32+
body: { productId },
33+
})
34+
.then(res => {
35+
const newItem = res.addToCart.cartItem;
36+
updateCartCache([...(cart.value || []), newItem]);
37+
38+
const statusKey = newItem.key;
39+
itemStatus.value[statusKey] = 'added';
40+
clearStatusAfterDelay(statusKey);
41+
42+
if (statusKey !== productId) {
43+
delete itemStatus.value[productId];
44+
}
45+
})
46+
.catch(err => {
47+
itemStatus.value[productId] = 'idle';
48+
const errorMessage = err.response?.errors?.[0]?.message?.replace(/<a[^>]*>(.*?)<\/a>/g, '').trim() || 'Could not add item.';
49+
push.error({ message: errorMessage });
50+
});
51+
};
52+
53+
const updateItemQuantity = (item, newQuantity) => {
54+
const statusKey = item.key;
55+
itemStatus.value[statusKey] = 'loading';
56+
57+
if (newQuantity <= 0) {
58+
handleRemoveFromCart(item.key);
59+
return;
60+
}
61+
62+
$fetch('/api/cart/update', {
63+
method: 'POST',
64+
body: { items: [{ key: item.key, quantity: newQuantity }] },
65+
})
66+
.then(() => {
67+
const isIncreasing = newQuantity > item.quantity;
68+
const isDecreasing = newQuantity < item.quantity;
69+
70+
const newCart = cart.value.map(cartItem =>
71+
cartItem.key === item.key ? { ...cartItem, quantity: newQuantity } : cartItem
72+
);
73+
updateCartCache(newCart);
74+
75+
if (isIncreasing) {
76+
itemStatus.value[statusKey] = 'increased';
77+
} else if (isDecreasing) {
78+
itemStatus.value[statusKey] = 'decreased';
79+
}
80+
81+
clearStatusAfterDelay(statusKey);
82+
})
83+
.catch(err => {
84+
itemStatus.value[statusKey] = 'idle';
85+
const errorMessage = err.response?.errors?.[0]?.message?.replace(/<a[^>]*>(.*?)<\/a>/g, '').trim() || 'Could not update quantity.';
86+
push.error({ message: errorMessage });
87+
});
88+
};
89+
90+
const increaseQuantity = item => updateItemQuantity(item, item.quantity + 1);
91+
const decreaseQuantity = item => updateItemQuantity(item, item.quantity - 1);
92+
93+
const handleRemoveFromCart = key => {
94+
itemStatus.value[key] = 'loading';
95+
$fetch('/api/cart/update', {
96+
method: 'POST',
97+
body: { items: [{ key, quantity: 0 }] },
98+
}).then(() => {
99+
updateCartCache(cart.value.filter(item => item.key !== key));
100+
delete itemStatus.value[key];
101+
}).catch(() => {
102+
itemStatus.value[key] = 'idle';
103+
push.error({ message: 'Could not remove item from cart.' });
104+
});
105+
};
106+
107+
onMounted(() => {
108+
if (process.client) {
109+
const storedCart = localStorage.getItem('cart');
110+
if (storedCart) cart.value = JSON.parse(storedCart);
111+
}
37112
});
38-
};
39-
40-
const updateCart = newCart => {
41-
cart.value = newCart;
42-
localStorage.setItem('cart', JSON.stringify(newCart));
43-
};
44-
45-
onMounted(() => {
46-
const storedCart = localStorage.getItem('cart');
47-
if (storedCart) cart.value = JSON.parse(storedCart);
48-
});
49-
50-
return {
51-
cart,
52-
handleAddToCart,
53-
addToCartButtonStatus,
54-
handleRemoveFromCart,
55-
removeFromCartButtonStatus,
56-
};
57-
};
113+
114+
return {
115+
cart,
116+
itemStatus,
117+
handleAddToCart,
118+
increaseQuantity,
119+
decreaseQuantity,
120+
handleRemoveFromCart,
121+
};
122+
};

app/pages/product/[id].vue

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,20 @@ useHead(() => ({
107107
],
108108
}));
109109
110-
const { handleAddToCart, addToCartButtonStatus } = useCart();
110+
const {
111+
cart,
112+
itemStatus,
113+
handleAddToCart,
114+
increaseQuantity,
115+
decreaseQuantity,
116+
} = useCart();
117+
118+
const cartItem = computed(() => {
119+
if (!selectedVariation.value || !cart.value) {
120+
return null;
121+
}
122+
return cart.value.find(item => item.variation?.node.databaseId === selectedVariation.value.databaseId);
123+
});
111124
</script>
112125
113126
<template>
@@ -206,18 +219,47 @@ const { handleAddToCart, addToCartButtonStatus } = useCart();
206219
</label>
207220
</div>
208221
209-
<div class="flex">
222+
<div class="flex gap-2">
223+
<div v-if="cartItem" class="flex items-center justify-center w-full h-12 rounded-md border-2 border-neutral-300 dark:border-neutral-700">
224+
<button
225+
@click="decreaseQuantity(cartItem)"
226+
:disabled="itemStatus[cartItem.key] === 'loading'"
227+
class="px-5 text-2xl font-bold hover:bg-neutral-100 dark:hover:bg-neutral-800 h-full rounded-l-md disabled:opacity-50 disabled:cursor-not-allowed">-</button>
228+
229+
<div class="flex-grow text-center font-semibold">
230+
<UIcon v-if="itemStatus[cartItem.key] === 'loading'" name="i-svg-spinners-90-ring-with-bg" size="22"/>
231+
<span v-else-if="itemStatus[cartItem.key] === 'added'">
232+
Added to cart
233+
</span>
234+
<span v-else-if="itemStatus[cartItem.key] === 'increased'">
235+
Increased to {{ cartItem.quantity }}
236+
</span>
237+
<span v-else-if="itemStatus[cartItem.key] === 'decreased'">
238+
Decreased to {{ cartItem.quantity }}
239+
</span>
240+
<span v-else>
241+
{{ cartItem.quantity }} in cart
242+
</span>
243+
</div>
244+
245+
<button
246+
@click="increaseQuantity(cartItem)"
247+
:disabled="itemStatus[cartItem.key] === 'loading'"
248+
class="px-5 text-2xl font-bold hover:bg-neutral-100 dark:hover:bg-neutral-800 h-full rounded-r-md disabled:opacity-50 disabled:cursor-not-allowed">+</button>
249+
</div>
250+
210251
<button
211-
@click="handleAddToCart(selectedVariation.databaseId)"
212-
:disabled="addToCartButtonStatus !== 'add'"
213-
class="button-bezel w-full h-12 rounded-md relative tracking-wide font-semibold text-white text-sm flex justify-center items-center">
214-
<Transition name="slide-up">
215-
<div v-if="addToCartButtonStatus === 'add'" class="absolute">Add to Cart</div>
216-
<UIcon v-else-if="addToCartButtonStatus === 'loading'" class="absolute" name="i-svg-spinners-90-ring-with-bg" size="22" />
217-
<div v-else-if="addToCartButtonStatus === 'added'" class="absolute">Added to Cart!</div>
252+
v-else
253+
@click="handleAddToCart(selectedVariation.databaseId)"
254+
:disabled="itemStatus[selectedVariation.databaseId] === 'loading'"
255+
class="button-bezel w-full h-12 rounded-md relative tracking-wide font-semibold text-white text-sm flex justify-center items-center disabled:opacity-50 disabled:cursor-not-allowed">
256+
<Transition name="slide-up" mode="out-in">
257+
<div v-if="!itemStatus[selectedVariation.databaseId] || itemStatus[selectedVariation.databaseId] === 'idle'" class="absolute">Add to cart</div>
258+
<UIcon v-else-if="itemStatus[selectedVariation.databaseId] === 'loading'" class="absolute" name="i-svg-spinners-90-ring-with-bg" size="22"/>
259+
<div v-else-if="itemStatus[selectedVariation.databaseId] === 'added'" class="absolute">Added</div>
218260
</Transition>
219261
</button>
220-
<ButtonWishlist :product="product" />
262+
<ButtonWishlist :product="product"/>
221263
</div>
222264
</div>
223265
<div class="px-3 lg:px-0">

0 commit comments

Comments
 (0)