Files
hossein-por-shop/frontend/components/cart/index/CartItem.vue
T
2025-04-18 19:50:25 +03:30

282 lines
9.7 KiB
Vue

<script setup lang="ts">
// imports
import { useImage } from "@vueuse/core";
import { useToast } from "~/composables/global/useToast";
import useDeleteCartItem from "~/composables/api/orders/useDeleteCartItem";
import { QUERY_KEYS } from "~/constants";
import useAddCartItem from "~/composables/api/orders/useAddCartItem";
// types
type Props = {
data: CartItem;
};
// props
const props = defineProps<Props>();
const { data } = toRefs(props);
// state
const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast();
const counter = ref(data.value.quantity);
const debouncedCounter = refDebounced(counter, 500);
const { isLoading: cartImageIsLoading } = useImage({
src: data.value.product.image,
});
// queries
const { mutateAsync: deleteCartItem, isPending: deleteCartItemIsPending } = useDeleteCartItem();
const { mutateAsync: addCartItem } = useAddCartItem();
// methods
const invalidateCart = () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
};
const handleDeleteFromCart = () => {
deleteCartItem(
{ id: data.value.id },
{
onSuccess: () => {
invalidateCart();
},
onError: () => {
addToast({
message: "خطایی در حذف محصول از سبد رخ داد",
options: {
status: "error",
},
});
},
}
);
};
const handleIncreaseQuantity = () => {
if (counter.value == data.value.product.in_stock) {
addToast({
message: "به حداکثر موجودی انبار رسیده اید",
options: {
status: "error",
description: `تعداد موجودی ${data.value.product.in_stock} عدد میباشد`,
},
});
} else {
counter.value++;
}
};
const handleDecreaseQuantity = () => {
if (counter.value == 1) {
handleDeleteFromCart();
} else if (counter.value > 1) {
counter.value--;
}
};
// watch
watch(
() => debouncedCounter.value,
(nv) => {
addCartItem(
{ id: data.value.product.id, quantity: nv },
{
onSuccess: () => {
invalidateCart();
queryClient.refetchQueries({ queryKey: [QUERY_KEYS.product, data.value.product.id] });
},
onError: () => {
invalidateCart();
addToast({
message: `خطایی در تغییر تعداد محصول ${data.value.product.title} رخ داد`,
options: {
status: "error",
},
});
},
}
);
}
);
</script>
<template>
<li
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-slate-200 rounded-xl bg-slate-50 overflow-hidden relative"
>
<div class="flex items-start justify-start w-full gap-2.5 lg:gap-4">
<div
v-if="!cartImageIsLoading"
class="size-[4rem] lg:size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
>
<NuxtImg
:src="data.product.image"
class="object-cover size-full"
alt="product"
/>
</div>
<Skeleton
v-else
class="!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
/>
<div class="flex flex-col w-full gap-3 lg:gap-4">
<div class="flex items-center justify-between gap-3">
<span class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600">
{{ data.product.category }}
</span>
<div
v-if="data.discount > 0"
class="text-white bg-blue-500 px-3 lg:px-4 py-1.5 lg:py-2 text-[10px] lg:text-xs rounded-full flex items-center gap-1"
>
<Icon
name="bi:percent"
class="size-4"
/>
{{ data.discount }}
تخفیف
</div>
</div>
<span class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black">
{{ data.product.title }}
</span>
<div class="flex items-center justify-start gap-1.5">
<div
v-if="!!data.product.color"
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm flex-center gap-1.5"
>
<span> رنگ </span>
<span
class="size-3 lg:!size-4 shadow-black/30 shadow-inner rounded-full"
:style="{
backgroundColor: `${data.product.color}`,
}"
>
</span>
</div>
<span
v-if="data.product.product_attributes.length > 0"
v-for="(variant, index) in data.product.product_attributes"
:index="index"
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm"
>
{{ variant.value }}
</span>
</div>
<div class="items-center justify-between hidden w-full lg:flex -mt-1">
<div class="flex items-center">
<button
@click="handleIncreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button>
<div class="size-10 flex-center">{{ counter }}</div>
<button
@click="handleDecreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
v-if="counter == 1"
:name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
class="**:fill-red-700"
/>
<Icon
v-else
name="bi:dash"
class="**:stroke-slate-800"
/>
</button>
</div>
<div class="flex items-end gap-2">
<div class="flex flex-col">
<span
v-if="data.discount > 0"
class="typo-p-sm relative flex-center w-fit line-through"
>
{{ data.price }}
</span>
<span class="typo-p-xl relative flex-center w-fit font-medium">
{{ data.final_price }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between w-full lg:hidden">
<div class="flex items-center">
<button
@click="handleIncreaseQuantity"
class="border size-7 p-1 lg:size-10 flex-center rounded-50 border-slate-300"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button>
<div class="size-10 text-sm flex-center">
{{ counter }}
</div>
<button
@click="handleDecreaseQuantity"
class="border size-7 lg:size-10 p-1 flex-center rounded-50 border-slate-300"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
v-if="counter == 1"
:name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
class="**:fill-red-700"
/>
<Icon
v-else
name="bi:dash"
class="**:stroke-slate-800"
/>
</button>
</div>
<div class="flex flex-col">
<span
v-if="data.discount > 0"
class="typo-p-xs relative flex-center w-fit line-through"
>
{{ data.price }}
</span>
<span class="typo-p-md relative flex-center w-fit font-medium">
{{ data.final_price }}
</span>
</div>
</div>
</li>
</template>
<style scoped></style>