update frontend
This commit is contained in:
@@ -113,11 +113,15 @@ const onSwiper = (swiper: SwiperClass) => {
|
|||||||
</Swiper>
|
</Swiper>
|
||||||
</div>
|
</div>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="w-full grid grid-cols-3 sm:grid-cols-4 gap-6">
|
<div class="w-full flex sm:grid justify-between sm:grid-cols-4 gap-6">
|
||||||
<div class="bg-neutral-100 !size-full !rounded-2xl !aspect-square" />
|
<div
|
||||||
<div class="bg-neutral-100 !size-full !rounded-2xl !aspect-square" />
|
class="bg-neutral-100 items-stretch w-25 sm:size-full rounded-e-2xl sm:rounded-2xl sm:aspect-square"
|
||||||
<div class="bg-neutral-100 !size-full !rounded-2xl !aspect-square" />
|
/>
|
||||||
<div class="bg-neutral-100 !size-full !rounded-2xl !aspect-square" />
|
<div class="bg-neutral-100 size-full rounded-2xl aspect-square" />
|
||||||
|
<div
|
||||||
|
class="bg-neutral-100 items-stretch w-25 sm:size-full rounded-s-2xl sm:rounded-2xl sm:aspect-square"
|
||||||
|
/>
|
||||||
|
<div class="bg-neutral-100 size-full rounded-2xl aspect-square max-sm:hidden" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
rate: number
|
rate: number;
|
||||||
}
|
haveRate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// props
|
// props
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="typo-p-sm font-normal translate-y-px">
|
<span class="typo-p-sm font-normal translate-y-px">
|
||||||
|
<template v-if="haveRate">
|
||||||
{{ rate }}
|
{{ rate }}
|
||||||
|
</template>
|
||||||
|
<template v-else> ( بدون نظر ) </template>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<span class="typo-p-md text-black">
|
|
||||||
اشتراک گذاری:
|
|
||||||
</span>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<NuxtLink>
|
|
||||||
<Icon name="ci:instagram" class="**:stroke-slate-500 size-6" />
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink>
|
|
||||||
<Icon name="ci:facebook" class="**:stroke-slate-500 size-6" />
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink>
|
|
||||||
<Icon name="ci:tiktok" class="**:stroke-slate-500 size-6" />
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink>
|
|
||||||
<Icon name="ci:youtube" class="**:stroke-slate-500 size-6" />
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
@@ -72,6 +72,7 @@ const changeSlide = (id: number) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
|
<ClientOnly>
|
||||||
<Swiper
|
<Swiper
|
||||||
:slides-per-view="3"
|
:slides-per-view="3"
|
||||||
:space-between="20"
|
:space-between="20"
|
||||||
@@ -111,6 +112,14 @@ const changeSlide = (id: number) => {
|
|||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
<template #fallback>
|
||||||
|
<div class="w-full grid grid-cols-3 gap-6">
|
||||||
|
<div class="bg-neutral-100 size-full rounded-2xl aspect-square" />
|
||||||
|
<div class="bg-neutral-100 size-full rounded-2xl aspect-square" />
|
||||||
|
<div class="bg-neutral-100 size-full rounded-2xl aspect-square" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="slides.length > 3"
|
v-if="slides.length > 3"
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const limitedComments = computed(() => {
|
|||||||
v-if="!!comments"
|
v-if="!!comments"
|
||||||
class="bg-slate-50"
|
class="bg-slate-50"
|
||||||
>
|
>
|
||||||
<div class="flex relative gap-8 my-42 container max-lg:flex-col">
|
<div class="flex relative gap-8 my-24 sm:my-42 container max-lg:flex-col">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 flex flex-col gap-6 lg:min-w-100 h-fit bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
class="sticky top-0 flex flex-col gap-6 lg:min-w-100 h-fit bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-full container pt-20 flex flex-col gap-y-[1.5rem]">
|
<section class="w-full container sm:pt-20 flex flex-col gap-y-6">
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<span class="text-black max-lg:hidden typo-h-4 mb-4"> جزئیات محصول </span>
|
<span class="text-black max-lg:hidden typo-h-4 mb-4"> جزئیات محصول </span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||||
|
import useSaveProduct from "~/composables/api/product/useSaveProduct";
|
||||||
|
import { useToast } from "~/composables/global/useToast";
|
||||||
|
import { QUERY_KEYS } from "~/constants";
|
||||||
|
|
||||||
|
// states
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const id = route.params.id as string | undefined;
|
||||||
|
|
||||||
|
const { $queryClient: queryClient } = useNuxtApp();
|
||||||
|
|
||||||
|
const { token } = useAuth();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
|
||||||
|
const { mutateAsync: saveProduct, isPending: isSaveProductPending } = useSaveProduct();
|
||||||
|
const { data: product, refetch: refetchProduct, isFetching: isFetchingPending } = useGetProduct(id);
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const saveProductHandler = async () => {
|
||||||
|
if (!!token.value) {
|
||||||
|
await saveProduct({ product_slug: product.value!.slug });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.product] });
|
||||||
|
} else {
|
||||||
|
addToast({
|
||||||
|
options: { status: "info" },
|
||||||
|
message: "برای ذخیره کردن لطفا وارد شوید.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="saveProductHandler"
|
||||||
|
:disabled="isSaveProductPending || isFetchingPending"
|
||||||
|
class="px-2 sm:px-3 py-2 flex items-center gap-2 bg-slate-50 border-slate-200 border rounded-lg flex-center"
|
||||||
|
>
|
||||||
|
<span class="typo-label-sm max-sm:hidden"> ذخیره </span>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="isSaveProductPending || isFetchingPending"
|
||||||
|
name="ci:svg-spinners-180-ring-with-bg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
:class="product?.added_to_favorites ? '**:fill-blue-400' : ''"
|
||||||
|
:name="product?.added_to_favorites ? 'bi-bookmark-fill' : 'ci:bi-bookmark'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import { useToast } from "~/composables/global/useToast";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
product: Product;
|
||||||
|
};
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const { product } = toRefs(props);
|
||||||
|
|
||||||
|
// states
|
||||||
|
|
||||||
|
const { addToast } = useToast();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const shareProduct = async () => {
|
||||||
|
const shareData = {
|
||||||
|
title: product.value.name,
|
||||||
|
text: `لینک اشتراک گذاری محصول ${product.value.name}`,
|
||||||
|
url: window.location.href,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Native share
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share(shareData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Share canceled or failed", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback → copy link
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareData.url);
|
||||||
|
|
||||||
|
addToast({
|
||||||
|
message: "لینک کالا کپی شد !",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Clipboard failed", error);
|
||||||
|
addToast({
|
||||||
|
options: { status: "error" },
|
||||||
|
message: "کپی لینک کالا با خطا مواجه شد !",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="shareProduct"
|
||||||
|
class="px-2 py-2 flex items-center gap-2 bg-slate-50 border-slate-200 border rounded-lg flex-center"
|
||||||
|
>
|
||||||
|
<span class="typo-label-sm max-sm:hidden"> ارسال </span>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
name="ci:bi-share"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -5,8 +5,6 @@ import useGetProduct from "~/composables/api/product/useGetProduct";
|
|||||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||||
import useAddCartItem from "~/composables/api/orders/useAddCartItem";
|
import useAddCartItem from "~/composables/api/orders/useAddCartItem";
|
||||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
import useSaveProduct from "~/composables/api/product/useSaveProduct";
|
|
||||||
import { QUERY_KEYS } from "~/constants";
|
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
@@ -14,11 +12,9 @@ const route = useRoute();
|
|||||||
const id = route.params.id as string | undefined;
|
const id = route.params.id as string | undefined;
|
||||||
|
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const { $queryClient: queryClient } = useNuxtApp();
|
|
||||||
|
|
||||||
const { data: product, refetch: refetchProduct, isFetching: isFetchingPending } = useGetProduct(id);
|
const { data: product, refetch: refetchProduct, isFetching: isFetchingPending } = useGetProduct(id);
|
||||||
const { mutateAsync: addCartItem, isPending: isAddCartItemPending } = useAddCartItem();
|
const { mutateAsync: addCartItem, isPending: isAddCartItemPending } = useAddCartItem();
|
||||||
const { mutateAsync: saveProduct, isPending: isSaveProductPending } = useSaveProduct();
|
|
||||||
|
|
||||||
const selectedVariantId = ref(product.value!.variants[0].id);
|
const selectedVariantId = ref(product.value!.variants[0].id);
|
||||||
const selectedQuantity = ref(1);
|
const selectedQuantity = ref(1);
|
||||||
@@ -40,11 +36,6 @@ const addItemToCart = async () => {
|
|||||||
await refetchProduct();
|
await refetchProduct();
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveProductHandler = async () => {
|
|
||||||
await saveProduct({ product_slug: product.value!.slug });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.product] });
|
|
||||||
};
|
|
||||||
|
|
||||||
// watch
|
// watch
|
||||||
|
|
||||||
watch([selectedVariantId, product], ([selectedVariantId, product]) => {
|
watch([selectedVariantId, product], ([selectedVariantId, product]) => {
|
||||||
@@ -77,31 +68,32 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex max-lg:flex-col lg:gap-12 xl:gap-16 container pt-[5rem] pb-28">
|
<div class="flex max-lg:flex-col lg:gap-12 xl:gap-16 container pt-8 sm:pt-20 pb-28">
|
||||||
<div class="flex flex-col gap-3 lg:hidden">
|
<div class="flex flex-col gap-3 lg:hidden">
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<NuxtLink
|
||||||
|
to="#"
|
||||||
|
class="typo-label-sm"
|
||||||
|
>
|
||||||
|
محصولات
|
||||||
|
</NuxtLink>
|
||||||
|
<Icon
|
||||||
|
name="ci:bi-chevron-left"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="#"
|
to="#"
|
||||||
class="typo-label-sm"
|
class="typo-label-sm"
|
||||||
>
|
>
|
||||||
{{ product!.category.name }}
|
{{ product!.category.name }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
</div>
|
||||||
@click="saveProductHandler"
|
|
||||||
:disabled="isSaveProductPending || isFetchingPending || !token"
|
|
||||||
class="size-10 bg-slate-50 border-slate-200 border rounded-lg flex-center"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
v-if="isSaveProductPending || isFetchingPending"
|
|
||||||
name="ci:svg-spinners-180-ring-with-bg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
<div class="flex items-center gap-2">
|
||||||
v-else
|
<ShareButton :product="product!" />
|
||||||
:class="product?.added_to_favorites ? '**:fill-blue-400' : ''"
|
<SaveButton />
|
||||||
:name="product?.added_to_favorites ? 'bi-bookmark-fill' : 'ci:bi-bookmark'"
|
</div>
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2">
|
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2">
|
||||||
{{ product!.name }}
|
{{ product!.name }}
|
||||||
@@ -136,7 +128,10 @@ watch(
|
|||||||
<span class="max-sm:hidden"> تخفیف درصد </span>
|
<span class="max-sm:hidden"> تخفیف درصد </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Rating :rate="product!.rating" />
|
<Rating
|
||||||
|
:rate="product!.rating"
|
||||||
|
:have-rate="product!.rating !== 0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -146,28 +141,28 @@ watch(
|
|||||||
/>
|
/>
|
||||||
<div class="lg:w-1/2 flex flex-col gap-3 mt-12">
|
<div class="lg:w-1/2 flex flex-col gap-3 mt-12">
|
||||||
<div class="flex items-center justify-between w-full max-lg:hidden">
|
<div class="flex items-center justify-between w-full max-lg:hidden">
|
||||||
|
<div class="flex items-center gap-2.5">
|
||||||
|
<NuxtLink
|
||||||
|
to="#"
|
||||||
|
class="typo-label-sm"
|
||||||
|
>
|
||||||
|
محصولات
|
||||||
|
</NuxtLink>
|
||||||
|
<Icon
|
||||||
|
name="ci:bi-chevron-left"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="#"
|
to="#"
|
||||||
class="typo-label-sm"
|
class="typo-label-sm"
|
||||||
>
|
>
|
||||||
{{ product!.category.name }}
|
{{ product!.category.name }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<button
|
</div>
|
||||||
@click="saveProductHandler"
|
<div class="flex items-center gap-2">
|
||||||
:disabled="isSaveProductPending || isFetchingPending || !token"
|
<ShareButton :product="product!" />
|
||||||
class="size-10 bg-slate-50 border-slate-200 border rounded-lg flex-center"
|
<SaveButton />
|
||||||
>
|
</div>
|
||||||
<Icon
|
|
||||||
v-if="isSaveProductPending || isFetchingPending"
|
|
||||||
name="ci:svg-spinners-180-ring-with-bg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
v-else
|
|
||||||
:class="product?.added_to_favorites ? '**:fill-blue-400' : ''"
|
|
||||||
:name="product?.added_to_favorites ? 'bi-bookmark-fill' : 'ci:bi-bookmark'"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="typo-h-4 xl:typo-h-3 max-lg:hidden">
|
<h1 class="typo-h-4 xl:typo-h-3 max-lg:hidden">
|
||||||
{{ product!.name }}
|
{{ product!.name }}
|
||||||
@@ -205,12 +200,14 @@ watch(
|
|||||||
|
|
||||||
<Rating
|
<Rating
|
||||||
:rate="product!.rating"
|
:rate="product!.rating"
|
||||||
|
:have-rate="product!.rating !== 0"
|
||||||
class="sm:hidden"
|
class="sm:hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Rating
|
<Rating
|
||||||
:rate="product!.rating"
|
:rate="product!.rating"
|
||||||
|
:have-rate="product!.rating !== 0"
|
||||||
class="max-sm:hidden"
|
class="max-sm:hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,8 +335,6 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InfoCard />
|
<InfoCard />
|
||||||
|
|
||||||
<Share />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductDescription
|
<ProductDescription
|
||||||
|
|||||||
@@ -73,6 +73,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<ProductComments :product="product!" />
|
<ProductComments :product="product!" />
|
||||||
|
|
||||||
<ChatButton :showChatButton="showChatButton" />
|
<!-- <ChatButton :showChatButton="showChatButton" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><!-- Icon from Bootstrap Icons by The Bootstrap Authors - https://github.com/twbs/icons/blob/main/LICENSE.md --><path fill="currentColor" d="M13.5 1a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.5 2.5 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5m-8.5 4a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m11 5.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3"/></svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
Reference in New Issue
Block a user