This commit is contained in:
Parsa Nazer
2025-03-19 17:03:28 +03:30
8 changed files with 137 additions and 53 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
// imports
import useDeleteDiscountCode from "~/composables/api/orders/useDeleteDiscountCode";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
import { useToast } from "~/composables/global/useToast";
@@ -10,25 +11,31 @@ import { QUERY_KEYS } from "~/constants";
const route = useRoute();
const { $queryClient: queryClient } = useNuxtApp();
const discountCode = ref("");
const { addToast } = useToast();
// queries
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const { addToast } = useToast();
const discountCode = ref(cart.value?.discount_code?.code || "");
const {
mutateAsync: submitDiscountCode,
isPending: submitDiscountCodeIsPending,
} = useSubmitDiscountCode();
const {
mutateAsync: deleteDiscountCode,
isPending: deleteDiscountCodeIsPending,
} = useDeleteDiscountCode();
// computed
const nextPage: ComputedRef<{ name: string; label: string } | undefined> =
computed(() => route.meta.nextPage);
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
// methods
const handleSubmitDiscountCode = () => {
@@ -50,6 +57,24 @@ const handleSubmitDiscountCode = () => {
}
);
};
const handleDeleteDiscountCode = () => {
deleteDiscountCode(undefined, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
discountCode.value = "";
},
onError: () => {
addToast({
message: "خطایی در حذف کد تخفیف رخ داد",
options: {
status: "error",
},
});
discountCode.value = "";
},
});
};
</script>
<template>
@@ -94,7 +119,7 @@ const handleSubmitDiscountCode = () => {
</div>
<div
v-if="!!cart?.discount_code"
v-if="hasSubmittedDiscountCode"
class="flex items-center justify-between w-full text-status-error-primary text-red-700"
>
<span class="max-w-1/2 text-sm"> تخفیف: </span>
@@ -119,19 +144,33 @@ const handleSubmitDiscountCode = () => {
v-model="discountCode"
placeholder="کد تخفیف"
class="!py-3 !pe-2 ps-2.5 w-full !rounded-none !border-e-[0px] !rounded-s-100"
:disabled="hasSubmittedDiscountCode"
/>
<button
@click="handleSubmitDiscountCode"
@click="
hasSubmittedDiscountCode
? handleDeleteDiscountCode()
: handleSubmitDiscountCode()
"
class="text-xs px-5 rounded-e-100 py-1.5 text-white bg-black hover:invert border-[1.5px] border-black hover:border-white transition-all disabled:cursor-not-allowed"
:disabled="!discountCode.length"
:disabled="
!discountCode.length ||
submitDiscountCodeIsPending ||
deleteDiscountCodeIsPending
"
>
<Icon
v-if="submitDiscountCodeIsPending"
v-if="
submitDiscountCodeIsPending ||
deleteDiscountCodeIsPending
"
name="svg-spinners:180-ring-with-bg"
size="20"
class="**:fill-white"
/>
<span v-else> ثبت </span>
<span v-else>
{{ hasSubmittedDiscountCode ? "حذف" : "ثبت" }}
</span>
</button>
</div>
+16 -10
View File
@@ -140,11 +140,11 @@ watch(
{{ data.product.category }}
</span>
<div
v-if="data.product.discount > 0"
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.product.discount }}
{{ data.discount }}
تخفیف
</div>
</div>
@@ -227,15 +227,15 @@ watch(
<div class="flex items-end gap-2">
<div class="flex flex-col">
<span
v-if="data.product.discount > 0"
v-if="data.discount > 0"
class="typo-p-sm relative flex-center w-fit line-through"
>
{{ data.product.price }}
{{ data.price }}
</span>
<span
class="typo-p-xl relative flex-center w-fit font-medium"
>
{{ data.product.final_price }}
{{ data.final_price }}
</span>
</div>
</div>
@@ -279,11 +279,17 @@ watch(
</button>
</div>
<span
class="text-sm lg:text-[1.125rem] text-slate-900 font-semibold"
>
{{ data.product.price }}
</span>
<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>
+2 -1
View File
@@ -15,7 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false,
placeholder: "وارد نشده",
});
const { variant, error, modelValue } = toRefs(props);
const { variant, error, modelValue, disabled } = toRefs(props);
// emits
@@ -38,6 +38,7 @@ const classes = computed(() => {
{
"input-solid": variant.value === "solid",
"input-outlined": variant.value === "outlined",
"pointer-events-none opacity-80 text-slate-500": disabled.value,
"input-effects": !error.value,
[variant.value === "solid"
? "input-solid-error"
@@ -0,0 +1,25 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
const useDeleteDiscountCode = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleDeleteDiscountCode = async () => {
const { data } = await axios.delete(
API_ENDPOINTS.orders.cart.delete_discount
);
return data;
};
return useMutation({
mutationFn: () => handleDeleteDiscountCode(),
});
};
export default useDeleteDiscountCode;
+19
View File
@@ -0,0 +1,19 @@
export const usePWA = () => {
const isInstalledAsPWA = ref(false);
const checkPWAInstallation = () => {
const isStandalone = window.matchMedia(
"(display-mode: standalone)"
).matches;
const isIOSPWA = (window.navigator as any).standalone;
isInstalledAsPWA.value = isStandalone || isIOSPWA;
};
onMounted(() => {
checkPWAInstallation();
});
return {
isInstalledAsPWA,
};
};
+1
View File
@@ -50,6 +50,7 @@ export const API_ENDPOINTS = {
delete_all: "/order/cart/all",
add_one: "/order/cart/item",
add_discount: "/order/cart/discount",
delete_discount: "/order/cart/discount",
},
},
};
+22 -33
View File
@@ -1,50 +1,39 @@
import { Workbox } from "workbox-window";
import { usePWA } from "~/composables/global/usePwa";
export default defineNuxtPlugin(() => {
const updateAvailable = ref(false);
let wb: Workbox | null = null;
if ("serviceWorker" in navigator) {
const { isInstalledAsPWA } = usePWA();
if ("serviceWorker" in navigator && isInstalledAsPWA.value) {
wb = new Workbox("/sw.js");
const isStandalone = window.matchMedia(
"(display-mode: standalone)"
).matches;
const isIOSPWA = (window.navigator as any).standalone;
const isInstalledAsPWA = isStandalone || isIOSPWA;
// Listen for messages from the service worker
navigator.serviceWorker.addEventListener("message", (event) => {
if (
event.data &&
event.data.type === "VERSION_CHECK"
) {
checkForUpdate(event.data.version);
}
wb.addEventListener("waiting", () => {
checkForUpdate();
});
// Register the service worker and check if there's already a waiting one
wb.register().then((registration: any) => {
if (registration.waiting) {
checkForUpdate();
}
});
wb.register()
.then((registration: any) => {
if (registration.waiting) {
checkForUpdate();
}
})
.catch((error) => {
console.error("Service worker registration failed:", error);
});
}
// 🔹 Function to compare versions and show update modal if needed
const checkForUpdate = (newVersion?: string) => {
const currentVersion = localStorage.getItem("pwa_version");
if (newVersion && currentVersion !== newVersion) {
updateAvailable.value = true;
localStorage.setItem("pwa_version", newVersion);
}
const checkForUpdate = () => {
updateAvailable.value = true;
};
// 🔹 Function to apply the update
const handleUpdate = () => {
wb?.messageSW({ type: "SKIP_WAITING" });
window.location.reload();
if (wb) {
wb.messageSW({ type: "SKIP_WAITING" }).then(() => {
window.location.reload();
});
}
};
return {
+5 -1
View File
@@ -71,7 +71,7 @@ declare global {
discount: number;
color: string;
video: string | null;
cart_quantity : number;
cart_quantity: number;
};
type Product = {
@@ -215,6 +215,10 @@ declare global {
discount_amount: string;
final_price: string;
};
discount: number;
discount_amount: string;
price: string;
final_price: string;
quantity: number;
};