Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
@utility btn-xl {
|
||||
@apply typo-label-lg py-4 px-6 gap-3;
|
||||
@apply typo-label-lg py-4 px-5 lg:px-6 gap-3;
|
||||
svg {
|
||||
@apply size-[20px]
|
||||
@apply size-[20px];
|
||||
}
|
||||
}
|
||||
|
||||
@utility btn-lg {
|
||||
@apply typo-label-md py-3 px-4 gap-2.5;
|
||||
@apply typo-label-md py-3 px-3 lg:px-4 gap-2.5;
|
||||
svg {
|
||||
@apply size-[20px]
|
||||
@apply size-[20px];
|
||||
}
|
||||
}
|
||||
|
||||
@utility btn-md {
|
||||
@apply typo-label-sm py-[10px] px-[14px] gap-2;
|
||||
@apply typo-label-sm py-[6px] lg:py-[10px] px-[10px] lg:px-[14px] gap-2;
|
||||
svg {
|
||||
@apply size-[16px]
|
||||
@apply size-[16px];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
@apply text-white bg-black border-[1.5px] border-transparent;
|
||||
@apply btn-lg;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-white border-black text-black;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-black;
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
&:disabled {
|
||||
@apply bg-slate-100 text-slate-400;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-slate-400;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
@apply text-black bg-slate-100;
|
||||
@apply btn-lg;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-black;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
&:disabled {
|
||||
@apply bg-slate-100 text-slate-400;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-slate-400;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@
|
||||
@apply text-black border-[1.5px] border-slate-200;
|
||||
@apply btn-lg;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-black;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
&:disabled {
|
||||
@apply text-slate-300;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-slate-300;
|
||||
}
|
||||
}
|
||||
@@ -90,14 +90,14 @@
|
||||
@apply text-black bg-white;
|
||||
@apply btn-lg;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-black;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-500;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-slate-500;
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@
|
||||
&:disabled {
|
||||
@apply text-slate-400;
|
||||
|
||||
svg[class~=iconify] path {
|
||||
svg[class~="iconify"] path {
|
||||
@apply stroke-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@
|
||||
@import "./fonts/yekan-bakh.css";
|
||||
|
||||
@theme {
|
||||
|
||||
/* CONTAINER */
|
||||
|
||||
--app-container-padding: 20px;
|
||||
--app-container-padding: 1rem;
|
||||
|
||||
/* COLORS */
|
||||
--color-slate-50: hsl(210, 40%, 98%);
|
||||
@@ -282,7 +281,7 @@
|
||||
/* CONTAINER */
|
||||
|
||||
@utility container {
|
||||
@apply mx-auto px-[var(--app-container-padding)];
|
||||
@apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)];
|
||||
}
|
||||
|
||||
@layer {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// state
|
||||
|
||||
const counter = ref(1);
|
||||
|
||||
// methods
|
||||
|
||||
const handleDeleteFromCart = () => {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-slate-200 rounded-xl bg-slate-50"
|
||||
>
|
||||
<div class="flex items-center justify-start w-full gap-2.5 lg:gap-4">
|
||||
<div
|
||||
class="size-[10rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src="/img/product-1.jpg"
|
||||
class="object-cover size-full"
|
||||
alt="product"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
<span class="font-semibold typo-sub-h-md text-slate-600">
|
||||
موبایل
|
||||
</span>
|
||||
|
||||
<span class="font-semibold typo-sub-h-xl text-black">
|
||||
فشارسنج بازویی امرن Omron M3
|
||||
</span>
|
||||
|
||||
<div class="items-center justify-between hidden w-full lg:flex">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
@click="counter++"
|
||||
class="border size-10 flex-center rounded-100 border-slate-300"
|
||||
>
|
||||
<Icon name="bi:plus" class="**:stroke-slate-800" />
|
||||
</button>
|
||||
|
||||
<div class="size-10 flex-center">{{ counter }}</div>
|
||||
|
||||
<button
|
||||
@click="
|
||||
counter > 1 ? counter-- : handleDeleteFromCart
|
||||
"
|
||||
class="border size-10 flex-center rounded-100 border-slate-300"
|
||||
>
|
||||
<Icon
|
||||
v-if="counter == 1"
|
||||
name="bi:trash"
|
||||
class="**:fill-red-700"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="bi:dash"
|
||||
class="**:stroke-slate-800"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="typo-p-lg text-black font-semibold">
|
||||
۲,۸۹۱,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full lg:hidden">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="border size-10 flex-center rounded-100 border-slate-400"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="stroke-slate-800"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.33334 8H12.6667"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M8 3.33325V12.6666"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="size-10 text-[1.125rem] flex-center">1</div>
|
||||
|
||||
<button
|
||||
class="border size-10 flex-center rounded-100 border-slate-400"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="stroke-status-error-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 4H14"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12.6667 4V13.3333C12.6667 14 12 14.6667 11.3333 14.6667H4.66668C4.00001 14.6667 3.33334 14 3.33334 13.3333V4"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M5.33334 3.99992V2.66659C5.33334 1.99992 6.00001 1.33325 6.66668 1.33325H9.33334C10 1.33325 10.6667 1.99992 10.6667 2.66659V3.99992"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="text-[1.125rem] text-slate-900 font-semibold">
|
||||
۲,۸۹۱,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
+2
-4
@@ -24,8 +24,6 @@ const emit = defineEmits(["select"]);
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const modalIsShow = ref(false);
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
// queries
|
||||
@@ -78,7 +76,7 @@ const handleDeleteAddress = (id: number) => {
|
||||
</div>
|
||||
<span class="flex items-center justify-between w-full gap-3">
|
||||
<div
|
||||
class="flex items-center gap-3 lg:text-[1.125rem] font-semibold text-slate-900"
|
||||
class="flex items-center gap-3 typo-sub-h-lg font-semibold text-slate-900"
|
||||
>
|
||||
{{ !!address ? address.name : "آدرس" }}
|
||||
<span
|
||||
@@ -114,7 +112,7 @@ const handleDeleteAddress = (id: number) => {
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end w-full lg:w-3/12">
|
||||
<AddressModal :address="address" v-model:isShow="modalIsShow" />
|
||||
<AddressModal :address="address" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
+8
-25
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useCreateOrUpdateAddress, {
|
||||
type CreateOrUpdateAddressRequest,
|
||||
} from "~/composables/api/account/useCreateOrUpdateAddress";
|
||||
import useCreateOrUpdateAddress from "~/composables/api/account/useCreateOrUpdateAddress";
|
||||
import useGetAccount from "~/composables/api/account/useGetAccount";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
@@ -12,23 +10,13 @@ import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
type Props = {
|
||||
address?: Address;
|
||||
isShow: boolean;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
"update:address": [value: File];
|
||||
"update:isShow": [value: boolean];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { address, isShow } = toRefs(props);
|
||||
|
||||
// emits
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const { address } = toRefs(props);
|
||||
|
||||
// computed
|
||||
|
||||
@@ -40,7 +28,9 @@ const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const addressData = ref<CreateOrUpdateAddressRequest>({
|
||||
const isShow = ref(false);
|
||||
|
||||
const addressData = ref({
|
||||
id: address.value?.id ?? undefined,
|
||||
province: address.value?.province ?? "",
|
||||
city: address.value?.city ?? "",
|
||||
@@ -64,13 +54,6 @@ const {
|
||||
isPending: createAddressIsPending,
|
||||
} = useCreateOrUpdateAddress(isEditing);
|
||||
|
||||
// computed
|
||||
|
||||
const visible = computed({
|
||||
get: () => isShow.value ?? false,
|
||||
set: (value: boolean) => emit("update:isShow", value),
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -86,7 +69,7 @@ const closeModal = () => {
|
||||
for_me: "بله",
|
||||
};
|
||||
}
|
||||
visible.value = false;
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const addNew = () => {
|
||||
@@ -138,7 +121,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model="visible"
|
||||
v-model="isShow"
|
||||
:title="!!address ? 'ویرایش آدرس' : 'افزودن آدرس'"
|
||||
:icon="!!address ? 'bi:pen' : 'ci:plus'"
|
||||
:iconSize="!!address ? '20' : '32'"
|
||||
@@ -151,7 +134,7 @@ watch(
|
||||
size="md"
|
||||
class="rounded-full"
|
||||
>
|
||||
<span class="font-bold whitespace-pre">
|
||||
<span class="whitespace-pre">
|
||||
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
|
||||
</span>
|
||||
</Button>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
|
||||
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const discountCode = ref("");
|
||||
|
||||
// queries
|
||||
|
||||
const { data: cart, isLoading: cartIsLoading } = useGetOrdersCart();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const {
|
||||
mutateAsync: submitDiscountCode,
|
||||
isPending: submitDiscountCodeIsPending,
|
||||
} = useSubmitDiscountCode();
|
||||
|
||||
// computed
|
||||
|
||||
const nextPage: ComputedRef<{ name: string; label: string } | undefined> =
|
||||
computed(() => route.meta.nextPage);
|
||||
|
||||
// methods
|
||||
|
||||
const handleSubmitDiscountCode = () => {
|
||||
submitDiscountCode(
|
||||
{ code: discountCode.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در ثبت کد تخفیف رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
discountCode.value = "";
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-slate-50 sticky top-44 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
|
||||
>
|
||||
<div
|
||||
class="w-full flex items-center justify-between p-5 border-b border-slate-200"
|
||||
>
|
||||
<span class="typo-sub-h-xl text-black">فاکتور خرید</span>
|
||||
<Icon name="ci:cart" class="**:stroke-black" size="24" />
|
||||
</div>
|
||||
|
||||
<div v-if="cartIsLoading" class="flex flex-col p-5 gap-4">
|
||||
<Skeleton
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
class="w-full !h-7"
|
||||
:class="{
|
||||
'!h-12': [4, 5].includes(i),
|
||||
'!rounded-full': i == 5,
|
||||
'!rounded-lg': i != 5,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col p-5 gap-4">
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-800"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm"> جمع سبد خرید: </span>
|
||||
|
||||
<span class="max-w-1/2 text-sm">
|
||||
{{ cart?.cart_total }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-800"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm"> مالیات ارزش افزوده: </span>
|
||||
|
||||
<span class="max-w-1/2 text-sm"> {{ cart?.tax }} </span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!!cart?.discount_code"
|
||||
class="flex items-center justify-between w-full text-status-error-primary text-red-700"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm"> تخفیف: </span>
|
||||
|
||||
<span class="max-w-1/2 text-sm">
|
||||
{{ cart?.discount_code.amount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-900"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm"> جمع کل: </span>
|
||||
|
||||
<span class="max-w-1/2 text-sm">
|
||||
{{ cart?.final_price }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="discountCode"
|
||||
placeholder="کد تخفیف"
|
||||
class="!py-2 !pe-2 ps-2.5"
|
||||
>
|
||||
<template #endItem>
|
||||
<button
|
||||
@click="handleSubmitDiscountCode"
|
||||
class="text-xs px-3 rounded-[7px] py-1.5 text-white bg-black hover:invert border border-white transition-all"
|
||||
>
|
||||
ثبت
|
||||
</button>
|
||||
</template>
|
||||
</Input>
|
||||
|
||||
<NuxtLink :to="{ name: nextPage?.name }">
|
||||
<Button start-icon="bi:arrow-right" class="w-full rounded-full">
|
||||
{{ nextPage?.label }}
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<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, 700);
|
||||
|
||||
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.id, quantity: nv },
|
||||
{
|
||||
onSuccess: () => {
|
||||
invalidateCart();
|
||||
},
|
||||
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"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
class="absolute -top-5 -left-5 rotate-[135deg] size-28"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-start w-full gap-2.5 lg:gap-4">
|
||||
<Skeleton
|
||||
v-if="cartImageIsLoading"
|
||||
class="!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
:src="data.product.image"
|
||||
class="object-cover size-full"
|
||||
alt="product"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<span class="font-semibold typo-sub-h-sm text-slate-600">
|
||||
{{ data.product.category }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-start gap-3">
|
||||
<span class="font-semibold typo-sub-h-xl text-black">
|
||||
{{ data.product.title }}
|
||||
</span>
|
||||
<div
|
||||
v-if="data.product.discount > 0"
|
||||
class="text-white bg-blue-500 px-4 py-2 text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
<Icon name="bi:percent" class="size-4" />
|
||||
{{ data.product.discount }}
|
||||
% تخفیف
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-sm flex-center gap-1.5"
|
||||
>
|
||||
<span> رنگ </span>
|
||||
<span
|
||||
class="!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-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.product.discount > 0"
|
||||
class="typo-p-sm relative flex-center w-fit line-through"
|
||||
>
|
||||
{{ data.product.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{ data.product.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-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 text-[1.125rem] flex-center">1</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>
|
||||
|
||||
<span class="text-[1.125rem] text-slate-900 font-semibold">
|
||||
{{ data.product.price }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useDeleteCartAll from "~/composables/api/orders/useDeleteCartAll";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const isShow = ref(false);
|
||||
|
||||
// queries
|
||||
|
||||
const { mutateAsync: deleteCartAll, isPending: deleteCartAllIsPending } =
|
||||
useDeleteCartAll();
|
||||
|
||||
// methods
|
||||
|
||||
const handleSubmit = () => {
|
||||
deleteCartAll(undefined, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
|
||||
isShow.value = false;
|
||||
addToast({
|
||||
message: "سبد با موفقیت حذف شد",
|
||||
options: {
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push({ name: "index" });
|
||||
}, 1000);
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در حذف سبد رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model="isShow"
|
||||
title="ورژن جدید"
|
||||
contectClass="!w-[90vw] lg:!w-[35vw]"
|
||||
@close="isShow = false"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button class="rounded-full" end-icon="bi:trash" size="md">
|
||||
حذف همه
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="w-full flex flex-col text-start gap-3 py-5" dir="rtl">
|
||||
<p>آیا از حذف تمام محصولات از سبد خرید اطمینان دارید؟</p>
|
||||
<p>این فرایند غیر قابل بازگشت است</p>
|
||||
</div>
|
||||
|
||||
<div class="py-6 border-t border-slate-200 flex gap-3">
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
class="rounded-full px-10"
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="deleteCartAllIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else>بله</span>
|
||||
</Button>
|
||||
<DialogClose aria-label="Close">
|
||||
<Button
|
||||
variant="outlined"
|
||||
class="rounded-full px-10"
|
||||
size="md"
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -85,7 +85,7 @@ watch(
|
||||
<ComboboxSeparator v-if="index !== 0" class="h-6" />
|
||||
|
||||
<ComboboxLabel
|
||||
class="flex items-center justify-between px-[1.2rem] w-full text-md text-black font-bold bg-slate-200/50 leading-[25px] py-3 rounded-lg"
|
||||
class="flex items-center justify-between px-[1.2rem] w-full text-md text-black bg-slate-200/50 leading-[25px] py-3 rounded-lg"
|
||||
>
|
||||
<span>
|
||||
{{ group.name }}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import useGetAccount from "~/composables/api/account/useGetAccount";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
|
||||
import { NAV_LINKS } from "~/constants";
|
||||
|
||||
// state
|
||||
@@ -13,12 +14,17 @@ const isSideDrawerOpen = ref(false);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: account } = useGetAccount();
|
||||
const { data: account, suspense: accountSuspense } = useGetAccount();
|
||||
|
||||
accountSuspense();
|
||||
|
||||
const { data: cart, suspense: cartSuspense } = useGetOrdersCart();
|
||||
|
||||
cartSuspense();
|
||||
|
||||
// computed
|
||||
|
||||
const isHomePage = computed(() => route.path === "/");
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,10 +37,12 @@ const isHomePage = computed(() => route.path === "/");
|
||||
class="size-full flex items-center justify-between container h-[65px] lg:h-[85px] shrink-0 grow-0"
|
||||
>
|
||||
<button class="md:hidden" @click="isSideDrawerOpen = true">
|
||||
<Icon name="humbleicons:bars" size="28"/>
|
||||
<Icon name="humbleicons:bars" size="28" />
|
||||
</button>
|
||||
<div class="max-md:hidden flex items-center gap-8 lg:gap-16">
|
||||
<div class="flex items-center justify-end gap-4 lg:gap-[1.5rem]">
|
||||
<div
|
||||
class="flex items-center justify-end gap-4 lg:gap-[1.5rem]"
|
||||
>
|
||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||
<NuxtLink
|
||||
:to="{ name: 'profile' }"
|
||||
@@ -71,10 +79,19 @@ const isHomePage = computed(() => route.path === "/");
|
||||
</Tooltip>
|
||||
<Tooltip title="سبد خرید">
|
||||
<NuxtLink to="/cart" class="flex-center">
|
||||
<Icon
|
||||
name="ci:cart"
|
||||
class="**:stroke-black size-5 lg:size-6"
|
||||
/>
|
||||
<button class="relative">
|
||||
<Icon
|
||||
name="ci:cart"
|
||||
class="**:stroke-black size-5 lg:size-6"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="cart?.items.length! > 0"
|
||||
class="size-4 shrink-0 absolute -bottom-2 -right-2 flex-center rounded-sm text-white bg-red-600 text-xs animate-pulse"
|
||||
>
|
||||
{{ cart?.items.length }}
|
||||
</span>
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -112,6 +129,5 @@ const isHomePage = computed(() => route.path === "/");
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<SideDrawer v-model="isSideDrawerOpen"/>
|
||||
|
||||
</template>
|
||||
<SideDrawer v-model="isSideDrawerOpen" />
|
||||
</template>
|
||||
|
||||
@@ -29,20 +29,20 @@ const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const value = computed({
|
||||
get: () => modelValue.value ?? "",
|
||||
set: (value) => emit("update:modelValue", value)
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
return [
|
||||
"flex items-center text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100",
|
||||
"flex items-center text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-2.5 lg:py-3.5 selection:bg-slate-100 rounded-100",
|
||||
{
|
||||
"input-solid": variant.value === "solid",
|
||||
"input-outlined": variant.value === "outlined",
|
||||
"input-effects": !error.value,
|
||||
[variant.value === "solid"
|
||||
? "input-solid-error"
|
||||
: "input-outlined-error"]: error.value
|
||||
}
|
||||
: "input-outlined-error"]: error.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
@@ -54,12 +54,10 @@ const classes = computed(() => {
|
||||
<input
|
||||
v-model="value"
|
||||
ref="inputRef"
|
||||
class="outline-none flex-1 text-sm placeholder-slate-400"
|
||||
class="outline-none flex-1 text-xs lg:text-sm placeholder-slate-400"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
||||
<slot name="endItem" />
|
||||
</div>
|
||||
<!-- <Tooltip :title="message" class="w-full">
|
||||
</Tooltip> -->
|
||||
</template>
|
||||
|
||||
@@ -26,27 +26,21 @@ const emit = defineEmits<Emits>();
|
||||
|
||||
// computed
|
||||
|
||||
const isShow = ref(modelValue.value);
|
||||
|
||||
watch(
|
||||
() => isShow.value,
|
||||
(nv) => {
|
||||
if (!nv) {
|
||||
emit("update:modelValue", false);
|
||||
const isShow = computed({
|
||||
get: () => modelValue.value ?? false,
|
||||
set: (value) => {
|
||||
emit("update:modelValue", value);
|
||||
if (!value) {
|
||||
emit("close", null);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-model:open="isShow"
|
||||
@update:open="
|
||||
(state) => {
|
||||
!state ? (isShow = false) : null;
|
||||
}
|
||||
"
|
||||
@update:open="(state) => (isShow = state)"
|
||||
>
|
||||
<DialogTrigger :class="!$slots['trigger'] ? 'hidden' : ''">
|
||||
<slot name="trigger" />
|
||||
|
||||
@@ -41,10 +41,10 @@ watch(
|
||||
<Transition name="fade-right">
|
||||
<div
|
||||
v-show="isSideShow"
|
||||
class="hidden md:flex w-1/3 bg-white h-full rounded-e-[1.5rem] overflow-hidden fixed top-0 right-0 min-md:flex-col"
|
||||
class="flex flex-col w-full lg:w-1/3 bg-white h-full lg:rounded-e-[1.5rem] overflow-hidden fixed top-0 right-0 min-md:flex-col"
|
||||
>
|
||||
<div
|
||||
class="w-full flex justify-between items-center py-[2.5rem] px-[3rem]"
|
||||
class="w-full flex justify-between items-center py-[1.5rem] lg:py-[2.5rem] px-[1.5rem] lg:px-[3rem]"
|
||||
>
|
||||
<span class="typo-h-5">
|
||||
{{ title }}
|
||||
@@ -63,7 +63,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="size-full flex flex-col grow overflow-y-auto p-[3rem] pt-0"
|
||||
class="size-full flex flex-col grow overflow-y-auto py-[1.5rem] lg:py-[2.5rem] px-[1.5rem] lg:px-[3rem] pt-0"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,8 @@ const in_stock = ref(JSON.parse(params.in_stock ?? false));
|
||||
|
||||
const sliderValueDebounced = refDebounced(sliderValue, 1000);
|
||||
|
||||
const filtersSuccessMessage = ref<{ title: string; status: string } | null>("");
|
||||
|
||||
// queries
|
||||
|
||||
const filters = computed(() => {
|
||||
@@ -46,7 +48,8 @@ const { data: categories, suspense } = useGetCategories();
|
||||
|
||||
await suspense();
|
||||
|
||||
const { isPending: productsIsPending } = useGetProducts(filters);
|
||||
const { isPending: productsIsPending, status: productsStatus } =
|
||||
useGetProducts(filters);
|
||||
|
||||
// computed
|
||||
|
||||
@@ -92,6 +95,26 @@ watch(
|
||||
params.in_stock = newInStock;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => productsStatus.value,
|
||||
(nv) => {
|
||||
if (nv == "success") {
|
||||
filtersSuccessMessage.value = {
|
||||
title: "فیلتر اعمال شد",
|
||||
status: nv,
|
||||
};
|
||||
} else if (nv == "error") {
|
||||
filtersSuccessMessage.value = {
|
||||
title: "خطایی در اعمال فیلتر رخ داد",
|
||||
status: nv,
|
||||
};
|
||||
}
|
||||
setTimeout(() => {
|
||||
filtersSuccessMessage.value = null;
|
||||
}, 4000);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -188,22 +211,50 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:disabled="productsIsPending"
|
||||
variant="solid"
|
||||
@click="resetFilters"
|
||||
class="rounded-full py-4 !cursor-pointer disabled:pointer-events-none"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<span v-if="productsIsPending" class="flex-center gap-3">
|
||||
در حال دریافت اطلاعات
|
||||
<Icon name="svg-spinners:3-dots-bounce" size="20" />
|
||||
</span>
|
||||
<span v-else class="flex-center gap-3">
|
||||
بازنشانی به پیش فرض
|
||||
<Icon name="ci:close" size="20" />
|
||||
</span>
|
||||
<div class="w-full flex flex-col items-center gap-5">
|
||||
<Transition
|
||||
enter-active-class="animate__animated animate__fadeInUp animate__faster"
|
||||
leave-active-class="animate__animated animate__fadeOutDown animate__faster"
|
||||
>
|
||||
<div
|
||||
v-if="!!filtersSuccessMessage"
|
||||
class="w-full flex-center gap-0.5"
|
||||
:class="
|
||||
filtersSuccessMessage.status == 'success'
|
||||
? 'text-success-500'
|
||||
: 'text-danger-500'
|
||||
"
|
||||
>
|
||||
<span class="text-sm">{{
|
||||
filtersSuccessMessage.title
|
||||
}}</span>
|
||||
<Icon
|
||||
:name="
|
||||
filtersSuccessMessage.status == 'success'
|
||||
? 'bi:check'
|
||||
: 'bi:x'
|
||||
"
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="productsIsPending"
|
||||
variant="solid"
|
||||
@click="resetFilters"
|
||||
class="w-full rounded-full py-4 !cursor-pointer disabled:pointer-events-none"
|
||||
>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<span v-if="productsIsPending" class="flex-center gap-3">
|
||||
در حال دریافت اطلاعات
|
||||
<Icon name="svg-spinners:3-dots-bounce" size="20" />
|
||||
</span>
|
||||
<span v-else class="flex-center gap-3">
|
||||
بازنشانی به پیش فرض
|
||||
<Icon name="ci:close" size="20" />
|
||||
</span>
|
||||
</Transition>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
@@ -21,7 +20,9 @@ const selectedColor = ref(product.value!.colors[0]);
|
||||
|
||||
// provide / inject
|
||||
|
||||
const { selectedVariant, changeSelectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
const { selectedVariant, changeSelectedVariant } = inject(
|
||||
"productVariant"
|
||||
) as ProductVariantProvideType;
|
||||
|
||||
// computed
|
||||
|
||||
@@ -31,24 +32,37 @@ const sanitizedProductDescription = computed(() => {
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => selectedVariantId.value, (newId) => {
|
||||
const newVariant = product.value!.variants.find(variant => variant.id === newId)!;
|
||||
changeSelectedVariant(newVariant);
|
||||
});
|
||||
watch(
|
||||
() => selectedVariantId.value,
|
||||
(newId) => {
|
||||
const newVariant = product.value!.variants.find(
|
||||
(variant) => variant.id === newId
|
||||
)!;
|
||||
changeSelectedVariant(newVariant);
|
||||
}
|
||||
);
|
||||
|
||||
watch(() => selectedColor.value, (newValue) => {
|
||||
const filteredVariants = product.value!.variants.filter(v => v.color === newValue);
|
||||
selectedVariantId.value = filteredVariants[0].id;
|
||||
selectedVariant.value = filteredVariants[0];
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
|
||||
watch(() => selectedVariant.value, (newValue) => {
|
||||
selectedQuantity.value = 1;
|
||||
selectedSlide.value = newValue.images[0].id;
|
||||
});
|
||||
watch(
|
||||
() => selectedColor.value,
|
||||
(newValue) => {
|
||||
const filteredVariants = product.value!.variants.filter(
|
||||
(v) => v.color === newValue
|
||||
);
|
||||
selectedVariantId.value = filteredVariants[0].id;
|
||||
selectedVariant.value = filteredVariants[0];
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => selectedVariant.value,
|
||||
(newValue) => {
|
||||
selectedQuantity.value = 1;
|
||||
selectedSlide.value = newValue.images[0].id;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,26 +74,31 @@ watch(() => selectedVariant.value, (newValue) => {
|
||||
/>
|
||||
<div class="flex-1 flex flex-col gap-3 mt-12">
|
||||
<span class="typo-label-sm"> سامسونگ </span>
|
||||
<h1 class="typo-h-2"> {{ product!.name }} </h1>
|
||||
<h1 class="typo-h-2">{{ product!.name }}</h1>
|
||||
<div class="flex w-full items-center justify-between h-[85px]">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="typo-p-lg relative flex-center w-fit"
|
||||
:class="'after:w-full after:h-[2px] after:bg-black after:absolute'"
|
||||
class="typo-p-lg relative flex-center w-fit line-through"
|
||||
>
|
||||
{{ selectedVariant.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-2xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{ selectedVariant.discount > 0 ? selectedVariant.price : selectedVariant.price }}
|
||||
{{
|
||||
selectedVariant.discount > 0
|
||||
? selectedVariant.price
|
||||
: selectedVariant.price
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedVariant.discount > 0"
|
||||
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1">
|
||||
<Icon name="material-symbols:percent" class="size-4" />
|
||||
<div
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
<Icon name="bi:percent" class="size-4" />
|
||||
{{ selectedVariant.discount }}
|
||||
درصد تخفیف
|
||||
</div>
|
||||
@@ -93,17 +112,15 @@ watch(() => selectedVariant.value, (newValue) => {
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="typo-p-lg">
|
||||
تنوع رنگی :
|
||||
</span>
|
||||
<span class="typo-p-lg"> تنوع رنگی : </span>
|
||||
<div class="flex items-center gap-4 py-4">
|
||||
<ColorCircle
|
||||
v-for="color in product!.colors"
|
||||
:key="color"
|
||||
@click="selectedColor = color"
|
||||
selectable
|
||||
:selected="selectedColor === color "
|
||||
:style="{backgroundColor: color}"
|
||||
:selected="selectedColor === color"
|
||||
:style="{ backgroundColor: color }"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
@@ -111,7 +128,11 @@ watch(() => selectedVariant.value, (newValue) => {
|
||||
|
||||
<div class="flex items-center gap-6 flex-wrap">
|
||||
<ProductVariant
|
||||
@click="variant.in_stock > 0 ? selectedVariantId = variant.id : undefined"
|
||||
@click="
|
||||
variant.in_stock > 0
|
||||
? (selectedVariantId = variant.id)
|
||||
: undefined
|
||||
"
|
||||
v-for="variant in product!.variants.filter(p => p.color === selectedColor)"
|
||||
:key="variant.id"
|
||||
:variantDetail="variant"
|
||||
@@ -120,7 +141,6 @@ watch(() => selectedVariant.value, (newValue) => {
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-6 mt-10">
|
||||
|
||||
<RemainQuantity
|
||||
:maxQuantity="selectedVariant.in_stock"
|
||||
:quantity="selectedQuantity"
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
isSelected: boolean;
|
||||
variantDetail: ProductVariant;
|
||||
}
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
isSelected ? 'border-blue-500' : 'border-slate-300',
|
||||
variantDetail.in_stock > 0 ? 'cursor-pointer' : '!border-slate-100'
|
||||
variantDetail.in_stock > 0 ? 'cursor-pointer' : '!border-slate-100',
|
||||
]"
|
||||
class="transition-all min-w-[350px] w-full duration-100 p-4 rounded-150 border-[2px] flex gap-4"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
:class="[
|
||||
isSelected ? 'ring-blue-500 bg-blue-500' : 'ring-slate-300 bg-slate-300',
|
||||
variantDetail.in_stock > 0 ? '' : '!ring-slate-100 !bg-slate-300-100'
|
||||
isSelected
|
||||
? 'ring-blue-500 bg-blue-500'
|
||||
: 'ring-slate-300 bg-slate-300',
|
||||
variantDetail.in_stock > 0
|
||||
? ''
|
||||
: '!ring-slate-100 !bg-slate-300-100',
|
||||
]"
|
||||
class="size-3 mt-2 ring-2 ring-offset-2 rounded-full "
|
||||
>
|
||||
|
||||
</div>
|
||||
class="size-3 mt-2 ring-2 ring-offset-2 rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="w-full flex justify-between items-center gap-2">
|
||||
@@ -39,29 +39,32 @@ defineProps<Props>();
|
||||
</span>
|
||||
<div
|
||||
v-if="variantDetail.discount > 0"
|
||||
:class="variantDetail.in_stock > 0 ? 'bg-blue-500' :'bg-slate-400/60'"
|
||||
:class="
|
||||
variantDetail.in_stock > 0
|
||||
? 'bg-blue-500'
|
||||
: 'bg-slate-400/60'
|
||||
"
|
||||
class="text-white mb-1 px-3 py-1 text-xs rounded-full w-fit flex items-center justify-center gap-1"
|
||||
>
|
||||
<template v-if="variantDetail.in_stock > 0">
|
||||
<Icon name="material-symbols:percent" class="size-3.5" />
|
||||
<Icon name="bi:percent" class="size-3.5" />
|
||||
<span class="mt-px">
|
||||
{{ variantDetail.discount }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<span v-else class="mt-px">
|
||||
اتمام موجودی
|
||||
</span>
|
||||
<span v-else class="mt-px"> اتمام موجودی </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex items-center flex-wrap gap-3 max-w-[400px] mt-4">
|
||||
|
||||
<!-- <div-->
|
||||
<!-- class="flex items-center gap-2 text-sm rounded-full border border-slate-400 px-4 h-[40px]"-->
|
||||
<!-- >-->
|
||||
<!-- <span>رنگ</span>-->
|
||||
<!-- <ColorCircle class="size-[22px]" :style="{backgroundColor:variantDetail.color}" />-->
|
||||
<!-- </div>-->
|
||||
<div
|
||||
class="w-full flex items-center flex-wrap gap-3 max-w-[400px] mt-4"
|
||||
>
|
||||
<!-- <div-->
|
||||
<!-- class="flex items-center gap-2 text-sm rounded-full border border-slate-400 px-4 h-[40px]"-->
|
||||
<!-- >-->
|
||||
<!-- <span>رنگ</span>-->
|
||||
<!-- <ColorCircle class="size-[22px]" :style="{backgroundColor:variantDetail.color}" />-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div
|
||||
v-for="attribute in variantDetail.product_attributes"
|
||||
@@ -70,8 +73,7 @@ defineProps<Props>();
|
||||
<span>{{ attribute.attribute_type.name }}</span>
|
||||
<span>{{ attribute.value }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<Button
|
||||
end-icon="ci:filter"
|
||||
variant="outlined"
|
||||
class="rounded-full"
|
||||
class="max-lg:size-11 rounded-full max-lg:aspect-square"
|
||||
>
|
||||
فیلتر محصولات
|
||||
<span class="hidden lg:block"> فیلتر محصولات </span>
|
||||
</Button>
|
||||
</template>
|
||||
<FilterProducts />
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type AddCartItemRequest = {
|
||||
id: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
const useAddCartItem = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleAddCartItem = async (params: AddCartItemRequest) => {
|
||||
const { data } = await axios.post(
|
||||
`${API_ENDPOINTS.orders.cart.add_one}/${params.id}`,
|
||||
{
|
||||
quantity: params.quantity,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (itemData: AddCartItemRequest) =>
|
||||
handleAddCartItem(itemData),
|
||||
});
|
||||
};
|
||||
|
||||
export default useAddCartItem;
|
||||
@@ -0,0 +1,25 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
const useDeleteCartAll = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleDeleteCartAll = async () => {
|
||||
const { data } = await axios.delete(
|
||||
API_ENDPOINTS.orders.cart.delete_all
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => handleDeleteCartAll(),
|
||||
});
|
||||
};
|
||||
|
||||
export default useDeleteCartAll;
|
||||
@@ -0,0 +1,32 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type DeleteCartItemRequest = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
const useDeleteCartItem = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleDeleteCartItem = async (id: number) => {
|
||||
const { data } = await axios.delete(
|
||||
`${API_ENDPOINTS.orders.cart.delete_one}/${id}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: DeleteCartItemRequest) =>
|
||||
handleDeleteCartItem(data.id),
|
||||
});
|
||||
};
|
||||
|
||||
export default useDeleteCartItem;
|
||||
@@ -5,7 +5,7 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetOrdersCartResponse = Order[];
|
||||
export type GetOrdersCartResponse = Cart;
|
||||
|
||||
const useGetOrdersCart = () => {
|
||||
// state
|
||||
@@ -16,7 +16,7 @@ const useGetOrdersCart = () => {
|
||||
|
||||
const handleGetOrdersCart = async () => {
|
||||
const { data } = await axios.get<GetOrdersCartResponse>(
|
||||
API_ENDPOINTS.orders.get_cart
|
||||
API_ENDPOINTS.orders.cart.get_all
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type SubmitDiscountCodeRequest = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
const useSubmitDiscountCode = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleSubmitDiscountCode = async (
|
||||
params: SubmitDiscountCodeRequest
|
||||
) => {
|
||||
const { data } = await axios.post(
|
||||
API_ENDPOINTS.orders.cart.add_discount,
|
||||
{
|
||||
...params,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (discountData: SubmitDiscountCodeRequest) =>
|
||||
handleSubmitDiscountCode(discountData),
|
||||
});
|
||||
};
|
||||
|
||||
export default useSubmitDiscountCode;
|
||||
@@ -43,8 +43,14 @@ export const API_ENDPOINTS = {
|
||||
create_message: "/tickets/message/create",
|
||||
},
|
||||
orders: {
|
||||
get_all: "/order/list",
|
||||
get_cart: "/order/cart",
|
||||
get_all: "/order/all",
|
||||
cart: {
|
||||
get_all: "/order/cart",
|
||||
delete_one: "/order/cart/item",
|
||||
delete_all: "/order/cart/all",
|
||||
add_one: "/order/cart/item",
|
||||
add_discount: "/order/cart/discount",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -91,8 +97,4 @@ export const NAV_LINKS = [
|
||||
title: "ارتباط با ما",
|
||||
path: "/contact-us",
|
||||
},
|
||||
{
|
||||
title: "امکانات",
|
||||
path: "#",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,7 +7,6 @@ const route = useRoute();
|
||||
|
||||
const pageTitle = computed(() => route.meta.pageTitle);
|
||||
const prevPage = computed(() => route.meta.prevPage);
|
||||
const nextPage = computed(() => route.meta.nextPage);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +16,7 @@ const nextPage = computed(() => route.meta.nextPage);
|
||||
>
|
||||
<Header />
|
||||
<main
|
||||
class="w-full overflow-x-hidden container flex flex-col gap-[5rem]"
|
||||
class="w-full overflow-x-hidden container flex flex-col gap-[5rem] max-w-[80vw]"
|
||||
>
|
||||
<div class="w-full flex flex-col">
|
||||
<div
|
||||
@@ -35,7 +34,7 @@ const nextPage = computed(() => route.meta.nextPage);
|
||||
name="bi:arrow-right"
|
||||
class="**:stroke-cyan-400"
|
||||
/>
|
||||
<span class="font-bold text-cyan-400">
|
||||
<span class="text-cyan-400">
|
||||
{{ prevPage?.label }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
@@ -58,90 +57,12 @@ const nextPage = computed(() => route.meta.nextPage);
|
||||
<NuxtPage />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col bg-slate-50 sticky top-44 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
|
||||
>
|
||||
<div
|
||||
class="w-full flex items-center justify-between p-5 border-b border-slate-200"
|
||||
>
|
||||
<span class="typo-sub-h-xl text-black"
|
||||
>فاکتور خرید</span
|
||||
>
|
||||
<Icon
|
||||
name="ci:cart"
|
||||
class="**:stroke-black"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col p-5 gap-[1rem]">
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-800"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
جمع سبد خرید:
|
||||
</span>
|
||||
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
۳,۲۹۱,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-status-error-primary"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
تخفیف:
|
||||
</span>
|
||||
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
۹۰۰,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full text-slate-900"
|
||||
>
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
جمع کل:
|
||||
</span>
|
||||
|
||||
<span class="max-w-1/2 text-sm lg:text-[1rem]">
|
||||
۲,۳۹۱,۰۰۰ تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label
|
||||
v-if="route.name == 'cart-checkout'"
|
||||
class="flex items-center w-full group gap-2 p-3 text-sm transition-all border text-slate-600 focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-cyan-500 bg-slate-50 border-slate-200 rounded-100"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="کد تخفیف"
|
||||
class="w-full border-none focus:border-none focus:outline-none placeholder:text-slate-600 h-[22px]"
|
||||
/>
|
||||
<button
|
||||
class="ring ring-offset-[-4px] active:ring-offset-2 transition-all duration-75 font-bold text-cyan-500 rounded-50"
|
||||
>
|
||||
ثبت
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<NuxtLink :to="{ name: nextPage?.name }">
|
||||
<Button
|
||||
start-icon="bi:arrow-right"
|
||||
class="w-full rounded-full mt-2"
|
||||
>
|
||||
{{ nextPage?.label }}
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<CartSummary />
|
||||
</div>
|
||||
</div>
|
||||
<ProductsSlider title="دیگران این محصولات را هم خریدهاند" />
|
||||
</main>
|
||||
<div class="w-full flex-col flex">
|
||||
<div class="w-full flex-col flex mt-20">
|
||||
<ServiceHighlights />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
||||
dir="rtl"
|
||||
>
|
||||
|
||||
<Header />
|
||||
<main class="w-full overflow-x-hidden">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
<div class="w-full flex-col flex">
|
||||
<div class="w-full flex-col flex mt-20">
|
||||
<ServiceHighlights />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -17,15 +17,14 @@ await suspense();
|
||||
>
|
||||
<Header />
|
||||
<main
|
||||
class="w-full overflow-x-hidden container flex items-start gap-[2rem]"
|
||||
class="w-full overflow-x-hidden container flex items-start gap-8 lg:gap-6"
|
||||
>
|
||||
<ProfileSidebar />
|
||||
<div class="w-9/12">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</main>
|
||||
<div class="w-full flex-col flex">
|
||||
<ServiceHighlights />
|
||||
<div class="w-full flex-col flex mt-20">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const { data: cart, suspense } = useGetOrdersCart();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
await suspense();
|
||||
|
||||
if (cart.value?.items.length! > 1) {
|
||||
return;
|
||||
} else {
|
||||
addToast({
|
||||
message: "سبد خرید شما خالی است",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
return navigateTo("/");
|
||||
}
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
definePageMeta({
|
||||
layout: "cart",
|
||||
middleware: "check-is-logged-in",
|
||||
middleware: ["check-is-logged-in", "check-has-cart-item"],
|
||||
pageTitle: "ثبت سفارش",
|
||||
prevPage: { name: "cart-delivery", label: "انتخاب آدرس" },
|
||||
nextPage: { name: "checkout", label: "پرداخت" },
|
||||
@@ -100,9 +100,7 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
|
||||
|
||||
<div class="h-7 flex-center col-span-full lg:hidden">
|
||||
<button class="gap-2 flex-center">
|
||||
<span class="text-sm font-bold text-black">
|
||||
مشاهده بیشتر
|
||||
</span>
|
||||
<span class="text-sm text-black"> مشاهده بیشتر </span>
|
||||
<Icon name="bi:chevron-down" class="**:stroke-black" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
|
||||
|
||||
definePageMeta({
|
||||
layout: "cart",
|
||||
middleware: "check-is-logged-in",
|
||||
middleware: ["check-is-logged-in", "check-has-cart-item"],
|
||||
pageTitle: "انتخاب آدرس",
|
||||
prevPage: { name: "cart", label: "سبد خرید" },
|
||||
nextPage: { name: "cart-checkout", label: "تسویه حساب" },
|
||||
@@ -182,9 +182,7 @@ const handleSelectAddress = (address: Address) => {
|
||||
|
||||
<div class="h-7 flex-center col-span-full lg:hidden">
|
||||
<button class="gap-2 flex-center">
|
||||
<span class="text-sm font-bold text-black">
|
||||
مشاهده بیشتر
|
||||
</span>
|
||||
<span class="text-sm text-black"> مشاهده بیشتر </span>
|
||||
<Icon name="bi:chevron-down" class="**:stroke-black" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
|
||||
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
layout: "cart",
|
||||
middleware: "check-is-logged-in",
|
||||
middleware: ["check-is-logged-in", 'check-has-cart-item'],
|
||||
pageTitle: "سبد خرید",
|
||||
prevPage: { name: "index", label: "بازگشت به خانه" },
|
||||
nextPage: { name: "cart-delivery", label: "انتخاب آدرس" },
|
||||
});
|
||||
|
||||
// queries
|
||||
|
||||
const { data: cart, isLoading: cartIsLoading } = useGetOrdersCart();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-4 lg:gap-6">
|
||||
<CartItem v-for="i in 3" />
|
||||
<div class="w-full flex flex-col gap-4 lg:gap-6">
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-3 px-5 py-4 rounded-xl bg-slate-50 border border-slate-200"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="cartIsLoading"
|
||||
class="!w-36 !h-[43px] !rounded-lg"
|
||||
/>
|
||||
|
||||
<div v-else class="flex items-center w-full gap-3 lg:w-1/2">
|
||||
<p class="font-semibold lg:text-lg text-black">
|
||||
{{ cart?.items.length }} مرسوله
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Skeleton
|
||||
v-if="cartIsLoading"
|
||||
class="!w-28 !h-[43px] !rounded-full"
|
||||
/>
|
||||
|
||||
<DeleteCartAllModal v-else />
|
||||
</div>
|
||||
|
||||
<ul v-if="cartIsLoading" class="w-full flex flex-col gap-4 lg:gap-6">
|
||||
<Skeleton
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="w-full !h-[12rem] !rounded-xl"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!cart?.items.length" class="flex flex-grow w-full">
|
||||
<Placeholder
|
||||
title="محصولی در سبد خرید یافت نشد :("
|
||||
icon="bi:cart"
|
||||
/>
|
||||
</div>
|
||||
<ul v-else class="w-full flex flex-col gap-4 lg:gap-6">
|
||||
<CartItem
|
||||
v-for="(item, index) in cart?.items"
|
||||
:key="index"
|
||||
:data="item"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+22
-16
@@ -59,33 +59,36 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="w-full container flex flex-col">
|
||||
<div class="w-full flex justify-end items-end py-[5rem]">
|
||||
<div
|
||||
class="w-full flex flex-col lg:flex-row justify-end items-end py-[3.5rem] lg:py-[5rem] gap-10 lg:gap-5"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-start gap-[1.5rem] text-black w-full"
|
||||
class="flex flex-col items-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full"
|
||||
>
|
||||
<div class="flex-center gap-[.75rem]">
|
||||
<span>خانه</span>
|
||||
<span>/</span>
|
||||
<span>محصولات</span>
|
||||
<span>/</span>
|
||||
<span>همه</span>
|
||||
<span class="text-xs lg:text-sm">خانه</span>
|
||||
<span class="text-xs lg:text-sm">/</span>
|
||||
<span class="text-xs lg:text-sm">محصولات</span>
|
||||
<span class="text-xs lg:text-sm">/</span>
|
||||
<span class="text-xs lg:text-sm">همه</span>
|
||||
</div>
|
||||
<h1 class="typo-h-3">لیست محصولات</h1>
|
||||
<h1 class="typo-h-5 lg:typo-h-4">لیست محصولات</h1>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-end gap-4">
|
||||
<div
|
||||
class="w-full flex items-center justify-between lg:justify-end gap-4"
|
||||
>
|
||||
<Input
|
||||
placeholder="جست و جو محصول ..."
|
||||
v-model="search"
|
||||
variant="outlined"
|
||||
class="rounded-full w-8/12"
|
||||
class="rounded-full w-full lg:w-8/12"
|
||||
>
|
||||
<template #endItem>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon
|
||||
class="translate-y-[-1px]"
|
||||
class="translate-y-[-1px] text-[20px] lg:text-[24px]"
|
||||
name="ci:search"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -94,7 +97,7 @@ watch(
|
||||
<FilterButton />
|
||||
<template #fallback>
|
||||
<Skeleton
|
||||
class="!w-[10.35rem] !h-[3.35rem] !rounded-full"
|
||||
class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-full"
|
||||
/>
|
||||
</template>
|
||||
</Suspense>
|
||||
@@ -102,19 +105,22 @@ watch(
|
||||
</div>
|
||||
<ul
|
||||
v-if="productsIsLoading"
|
||||
class="w-full grid grid-cols-3 gap-[1.5rem]"
|
||||
class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[1.5rem]"
|
||||
>
|
||||
<Skeleton
|
||||
v-for="i in 9"
|
||||
:key="i"
|
||||
class="w-full !h-[31.25rem] !rounded-2xl"
|
||||
class="w-full !h-[25rem] lg:!h-[22.5rem] !rounded-2xl"
|
||||
/>
|
||||
</ul>
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!products?.length" class="flex flex-grow w-full">
|
||||
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
|
||||
</div>
|
||||
<ul v-else class="w-full grid grid-cols-3 gap-[1.5rem]">
|
||||
<ul
|
||||
v-else
|
||||
class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[1.5rem]"
|
||||
>
|
||||
<li v-for="(product, index) in products" :key="index">
|
||||
<ProductCard
|
||||
:id="product.id"
|
||||
|
||||
@@ -154,9 +154,7 @@ const paginationData = computed(() => {
|
||||
|
||||
<NuxtLink :to="{ name: 'profile-tickets-new' }">
|
||||
<Button end-icon="bi:plus" size="md" class="rounded-full">
|
||||
<span class="font-bold whitespace-pre">
|
||||
تیکت جدید
|
||||
</span>
|
||||
<span class="whitespace-pre"> تیکت جدید </span>
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
+34
-32
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import { helpers, required } from "@vuelidate/validators";
|
||||
@@ -22,7 +21,7 @@ type LoginInfo = {
|
||||
|
||||
definePageMeta({
|
||||
layout: "none",
|
||||
middleware: ["check-is-not-logged-in"]
|
||||
middleware: ["check-is-not-logged-in"],
|
||||
});
|
||||
|
||||
// state
|
||||
@@ -43,13 +42,13 @@ const formRules = computed(() => {
|
||||
phoneValidator: helpers.withMessage(
|
||||
"شماره تلفن وارد شده معتبر نمی باشد",
|
||||
helpers.regex(/^[1-9][0-9]{9}$/)
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const loginInfo = ref<LoginInfo>({
|
||||
phone: ""
|
||||
phone: "",
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, loginInfo);
|
||||
@@ -58,16 +57,16 @@ const {
|
||||
timer: otpBlockerTimePassed,
|
||||
start: startOtpBlocker,
|
||||
reset: resetOtpBlocker,
|
||||
isPending: isResendOtpBlocked
|
||||
isPending: isResendOtpBlocked,
|
||||
} = useTimer({
|
||||
duration: 5000
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
const { mutateAsync: sendOtp, isPending: sendOtpIsPending } = useOtp();
|
||||
const {
|
||||
mutateAsync: signIn,
|
||||
isPending: signInIsPending,
|
||||
status: signInStatus
|
||||
status: signInStatus,
|
||||
} = useSignIn();
|
||||
|
||||
// computed
|
||||
@@ -76,14 +75,14 @@ const sendOtpHandler = async () => {
|
||||
if (!sendOtpIsPending.value) {
|
||||
try {
|
||||
await sendOtp({
|
||||
phone: `0${loginInfo.value.phone}`
|
||||
phone: `0${loginInfo.value.phone}`,
|
||||
});
|
||||
|
||||
addToast({
|
||||
message: "کد برای شما ارسال شد",
|
||||
options: {
|
||||
status: "success"
|
||||
}
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
|
||||
showOtp.value = true;
|
||||
@@ -91,8 +90,8 @@ const sendOtpHandler = async () => {
|
||||
addToast({
|
||||
message: "مشکلی پیش آمده",
|
||||
options: {
|
||||
status: "error"
|
||||
}
|
||||
status: "error",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -109,7 +108,7 @@ const handleLogin = async () => {
|
||||
try {
|
||||
const response = await signIn({
|
||||
otp: otpCode.value.join(""),
|
||||
phone: `0${loginInfo.value.phone}`
|
||||
phone: `0${loginInfo.value.phone}`,
|
||||
});
|
||||
|
||||
updateToken(response.access);
|
||||
@@ -120,8 +119,8 @@ const handleLogin = async () => {
|
||||
addToast({
|
||||
message: "با موفقیت وارد شدید",
|
||||
options: {
|
||||
status: "success"
|
||||
}
|
||||
status: "success",
|
||||
},
|
||||
});
|
||||
|
||||
navigateTo("/");
|
||||
@@ -148,7 +147,6 @@ const resetForm = () => {
|
||||
otpCode.value = [];
|
||||
showOtp.value = false;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -157,11 +155,10 @@ const resetForm = () => {
|
||||
class="bg-[url(/img/pattern-1.png)] -z-10 size-full absolute opacity-70"
|
||||
:style="{
|
||||
backgroundSize: 150,
|
||||
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)'
|
||||
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)',
|
||||
}"
|
||||
/>
|
||||
<div class="flex items-center justify-center flex-col size-full">
|
||||
|
||||
<img
|
||||
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
||||
src="/img/heymlz-seat.gif"
|
||||
@@ -169,7 +166,7 @@ const resetForm = () => {
|
||||
/>
|
||||
|
||||
<div
|
||||
class="max-w-[600px] w-full p-6 h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-xl"
|
||||
class="max-w-[600px] w-full p-6 h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-2xl"
|
||||
>
|
||||
<h1 class="typo-h-5 mt-8">شماره خود را وارد کنید</h1>
|
||||
|
||||
@@ -189,7 +186,9 @@ const resetForm = () => {
|
||||
name="twemoji:flag-iran"
|
||||
size="24"
|
||||
/>
|
||||
<span class="text-slate-500 typo-label-sm"> +98 </span>
|
||||
<span class="text-slate-500 typo-label-sm">
|
||||
+98
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Input>
|
||||
@@ -198,12 +197,12 @@ const resetForm = () => {
|
||||
v-else
|
||||
v-model="otpCode"
|
||||
:status="
|
||||
signInStatus === 'success'
|
||||
? 'success'
|
||||
: signInStatus === 'error'
|
||||
? 'error'
|
||||
: 'idle'
|
||||
"
|
||||
signInStatus === 'success'
|
||||
? 'success'
|
||||
: signInStatus === 'error'
|
||||
? 'error'
|
||||
: 'idle'
|
||||
"
|
||||
:disabled="signInIsPending || sendOtpIsPending"
|
||||
:autofocus="true"
|
||||
@complete="handleLogin"
|
||||
@@ -237,17 +236,20 @@ const resetForm = () => {
|
||||
@click="resendOtp"
|
||||
:loading="signInIsPending || sendOtpIsPending"
|
||||
:disabled="
|
||||
signInIsPending ||
|
||||
isResendOtpBlocked ||
|
||||
sendOtpIsPending
|
||||
"
|
||||
signInIsPending ||
|
||||
isResendOtpBlocked ||
|
||||
sendOtpIsPending
|
||||
"
|
||||
>
|
||||
ارسال مجدد کد
|
||||
{{ isResendOtpBlocked ? otpBlockerTimePassed : "" }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<NuxtLink to="/" class="flex items-center gap-2 justify-center mt-6">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="flex items-center gap-2 justify-center mt-6"
|
||||
>
|
||||
<span> بازگشت به فروشگاه </span>
|
||||
<Icon name="ci:left-rotation" size="24" />
|
||||
</NuxtLink>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Vendored
+42
-1
@@ -88,7 +88,10 @@ declare global {
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">
|
||||
type ProductListItem = Pick<
|
||||
Product,
|
||||
"id" | "variants" | "name" | "rating" | "slug" | "category" | "colors"
|
||||
>;
|
||||
|
||||
type Article = {
|
||||
id: number;
|
||||
@@ -176,6 +179,44 @@ declare global {
|
||||
address: Address | null;
|
||||
};
|
||||
|
||||
type DiscountCode = {
|
||||
code: string;
|
||||
percent: number;
|
||||
amount: string;
|
||||
};
|
||||
|
||||
type CartItem = {
|
||||
id: number;
|
||||
product: {
|
||||
title: string;
|
||||
product_attributes: {
|
||||
id: number;
|
||||
attribute_type: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
value: string;
|
||||
}[];
|
||||
category: string;
|
||||
in_stock: number;
|
||||
price: string;
|
||||
discount: number;
|
||||
color: string;
|
||||
image: string;
|
||||
discount_amount: string;
|
||||
final_price: string;
|
||||
};
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
type Cart = {
|
||||
discount_code: DiscountCode;
|
||||
items: CartItem[];
|
||||
cart_total: string;
|
||||
tax: string;
|
||||
final_price: string;
|
||||
};
|
||||
|
||||
type ServerFile = {
|
||||
id: number;
|
||||
file_link: string;
|
||||
|
||||
Reference in New Issue
Block a user