merge
This commit is contained in:
+6
-4
@@ -4,21 +4,23 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LoadingIndicator />
|
||||
|
||||
<NuxtRouteAnnouncer />
|
||||
|
||||
<NuxtLayout>
|
||||
<LoadingIndicator />
|
||||
|
||||
<ToastProvider>
|
||||
<NuxtPage />
|
||||
<div dir="ltr">
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
|
||||
</div>
|
||||
|
||||
<ToastContainer />
|
||||
<ToastViewport
|
||||
class="[--viewport-padding:_25px] fixed bottom-0 left-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none"
|
||||
/>
|
||||
</ToastProvider>
|
||||
</NuxtLayout>
|
||||
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const {} = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full flex flex-col justify-center h-[350px]">
|
||||
<div class="relative w-full flex flex-col justify-center min-h-[700px] h-[80svh]">
|
||||
<div class="-rotate-z-2 z-20">
|
||||
<div
|
||||
class="bg-warning-500 flex pr-20 gap-20 py-2 w-max animate-marquee-reverse"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
selected ?: boolean;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
// props
|
||||
@@ -14,7 +14,7 @@ defineProps<Props>();
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="size-[25px] rounded-full shadow-black/30 shadow-inner"
|
||||
:class="selected ? 'ring-black' : 'ring-transparent'"
|
||||
class="size-[25px] rounded-full transition-all ring-2 ring-offset-4 shadow-black/30 shadow-inner"
|
||||
:class="selected ? 'ring-blue-500' : 'ring-transparent'"
|
||||
/>
|
||||
</template>
|
||||
@@ -21,7 +21,7 @@ nuxtApp.hook("page:finish", () => {
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="h-[20px] flex items-center justify-center bg-black w-full left-0 top-0 fixed z-100"
|
||||
class="h-[20px] flex items-center justify-center bg-black w-full left-0 top-0 fixed z-9999"
|
||||
>
|
||||
<div class="absolute progress-indicator w-1/3 bg-white h-1 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// state
|
||||
|
||||
const { $gsap: gsap } = useNuxtApp();
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
const timeline = gsap.timeline();
|
||||
|
||||
timeline
|
||||
.to("#loading-overlay", {
|
||||
scale: 1
|
||||
})
|
||||
.to("#loading-overlay", {
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
delay: 5
|
||||
})
|
||||
.to("#loading-overlay", {
|
||||
opacity: 0,
|
||||
y: "20%"
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="loading-overlay" class="fixed inset-0 size-full z-9999 flex-center bg-black">
|
||||
<img id="loading-overlay-image" src="/video/loading-2.gif" class="opacity-0 scale-70 absolute z-20" alt="" />
|
||||
<div
|
||||
id="loading-overlay-gradient"
|
||||
class="opacity-0 scale-x-0 w-[1000px] h-[70px] bg-linear-to-r from-blue-500 via-violet-500 to-purple-500 blur-[150px] rounded-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
#loading-overlay-image {
|
||||
animation-name: loading-overlay-image-animation;
|
||||
animation-duration: 1s;
|
||||
animation-delay: 0.75s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
#loading-overlay-gradient {
|
||||
animation: 1.5s normal 0.5s 1 forwards loading-overlay-gradient-animation,
|
||||
1s ease-in-out 2s infinite alternate-reverse loading-overlay-gradient-pules-animation;
|
||||
}
|
||||
|
||||
@keyframes loading-overlay-image-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0.7;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-overlay-gradient-animation {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0 1 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0.9;
|
||||
scale: 1 1 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-overlay-gradient-pules-animation {
|
||||
from {
|
||||
opacity: 0.8;
|
||||
scale: 0.8 1 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0.9;
|
||||
scale: 1 1 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -91,9 +91,9 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
:id="product.id"
|
||||
brand="برند محصول"
|
||||
:title="product.name"
|
||||
:picture="product.image1"
|
||||
:colors="['white', 'black']"
|
||||
:price="product.price"
|
||||
:picture="product.variants[0].images[0].image"
|
||||
:colors="product.variants.map(v => v.color)"
|
||||
:price="product.variants[0].price"
|
||||
:rate="product.rating"
|
||||
:dark-layer="true"
|
||||
/>
|
||||
|
||||
@@ -10,49 +10,58 @@ type Props = {
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { modelValue } = toRefs(props);
|
||||
const { modelValue, max } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const timer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
// emit
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// state
|
||||
// computed
|
||||
|
||||
const currentQuantity = ref(modelValue.value);
|
||||
const currentQuantity = computed({
|
||||
get: () => modelValue.value ?? 0,
|
||||
set: (value: number) => {
|
||||
if (timer.value) clearTimeout(timer.value);
|
||||
timer.value = setTimeout(() => {
|
||||
emit("update:modelValue", value);
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
const onInput = (e: any) => {
|
||||
currentQuantity.value = Number(e.target.value);
|
||||
const value = Number(e.target.value);
|
||||
if (value > 0 && value <= max.value) {
|
||||
currentQuantity.value = value;
|
||||
} else {
|
||||
currentQuantity.value = 1;
|
||||
}
|
||||
};
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => currentQuantity.value, (newValue) => {
|
||||
emit("update:modelValue", newValue);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<NumberFieldRoot
|
||||
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
||||
v-model="currentQuantity"
|
||||
:min="1"
|
||||
:max="max"
|
||||
>
|
||||
<NumberFieldIncrement class="cursor-pointer">
|
||||
<Icon name="ci:plus" class="**:stroke-slate-500 size-5" />
|
||||
</NumberFieldIncrement>
|
||||
<NumberFieldInput
|
||||
@input="onInput"
|
||||
class="field-sizing-content bg-transparent outline-none typo-label-md text-black"
|
||||
/>
|
||||
<NumberFieldDecrement class="cursor-pointer">
|
||||
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
||||
</NumberFieldDecrement>
|
||||
</NumberFieldRoot>
|
||||
</div>
|
||||
<NumberFieldRoot
|
||||
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
||||
v-model="currentQuantity"
|
||||
:min="1"
|
||||
:max="max"
|
||||
>
|
||||
<NumberFieldIncrement class="cursor-pointer">
|
||||
<Icon name="ci:plus" class="**:stroke-slate-500 size-5" />
|
||||
</NumberFieldIncrement>
|
||||
<NumberFieldInput
|
||||
@input="onInput"
|
||||
class="field-sizing-content bg-transparent outline-none typo-label-md text-black"
|
||||
/>
|
||||
<NumberFieldDecrement class="cursor-pointer">
|
||||
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
||||
</NumberFieldDecrement>
|
||||
</NumberFieldRoot>
|
||||
</template>
|
||||
@@ -15,12 +15,12 @@ defineProps<Props>();
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<p class="typo-p-sm text-slate-500">
|
||||
سریع باش فقط
|
||||
<span class="text-black">
|
||||
<p class="typo-p-md text-slate-500">
|
||||
تعداد
|
||||
<span class="text-black font-bold">
|
||||
{{ maxQuantity }}
|
||||
</span>
|
||||
عدد از این محصول باقی مانده
|
||||
عدد از این محصول موجود است
|
||||
</p>
|
||||
<div class="h-2 rounded-full relative bg-slate-200 w-full">
|
||||
<div
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
|
||||
type Props = {
|
||||
selectedSlide: number;
|
||||
slides: {
|
||||
id: number;
|
||||
picture: string;
|
||||
}[]
|
||||
slides: ProductImage[]
|
||||
}
|
||||
|
||||
// props
|
||||
@@ -41,8 +38,8 @@ const changeSlide = (id: number) => {
|
||||
<img
|
||||
:key="selectedSlideDetail.id"
|
||||
class="size-full absolute object-contain"
|
||||
:src="selectedSlideDetail.picture"
|
||||
:alt="String(selectedSlideDetail.id)"
|
||||
:src="selectedSlideDetail.image"
|
||||
:alt="selectedSlideDetail.name"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -56,7 +53,7 @@ const changeSlide = (id: number) => {
|
||||
>
|
||||
<img
|
||||
class="absolute object-cover size-full"
|
||||
:src="slide.picture"
|
||||
:src="slide.image"
|
||||
:alt="String(slide.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// provide / inject
|
||||
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col">
|
||||
<AccordionRoot
|
||||
class="w-full last:border-b last:border-slate-200"
|
||||
default-value="item-1"
|
||||
:default-value="'item' + selectedVariant.details[0].detail_category"
|
||||
type="single"
|
||||
:collapsible="true"
|
||||
>
|
||||
<AccordionItem value="item-1" class="overflow-hidden">
|
||||
<AccordionItem
|
||||
v-for="detailItem in selectedVariant.details"
|
||||
:value="'item' + detailItem.detail_category"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<AccordionHeader
|
||||
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
|
||||
>
|
||||
<span class="typo-sub-h-md text-black">مشخصات</span>
|
||||
<span class="typo-sub-h-md text-black">{{ detailItem.detail_category }}</span>
|
||||
<AccordionTrigger class="group">
|
||||
<Icon
|
||||
name="ci:plus"
|
||||
@@ -26,97 +40,20 @@
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
|
||||
>
|
||||
<div
|
||||
v-for="i in 4"
|
||||
v-for="item in detailItem.detail"
|
||||
class="flex flex-col gap-y-[1.5rem]"
|
||||
>
|
||||
<span
|
||||
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
|
||||
>صفحه نمایش</span
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<ul class="list-disc w-full ps-5">
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
</li>
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2" class="overflow-hidden">
|
||||
<AccordionHeader
|
||||
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
|
||||
>
|
||||
<span class="typo-sub-h-md text-black">مشخصات</span>
|
||||
<AccordionTrigger class="group">
|
||||
<Icon
|
||||
name="ci:plus"
|
||||
size="24"
|
||||
class="group-data-[state=open]:rotate-45 transition-transform"
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent
|
||||
class="data-[state=open]:animate-slide-down pb-[1.5rem] data-[state=closed]:animate-slide-up overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
|
||||
>
|
||||
<div
|
||||
v-for="i in 4"
|
||||
class="flex flex-col gap-y-[1.5rem]"
|
||||
>
|
||||
<span
|
||||
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
|
||||
>صفحه نمایش</span
|
||||
>
|
||||
<ul class="list-disc w-full ps-5">
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
</li>
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3" class="overflow-hidden">
|
||||
<AccordionHeader
|
||||
class="border-t border-slate-200 py-[1.5rem] flex justify-between items-center"
|
||||
>
|
||||
<span class="typo-sub-h-md text-black">مشخصات</span>
|
||||
<AccordionTrigger class="group">
|
||||
<Icon
|
||||
name="ci:plus"
|
||||
size="24"
|
||||
class="group-data-[state=open]:rotate-45 transition-transform"
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent
|
||||
class="data-[state=open]:animate-slide-down pb-[1.5rem] data-[state=closed]:animate-slide-up overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[1rem]"
|
||||
>
|
||||
<div
|
||||
v-for="i in 4"
|
||||
class="flex flex-col gap-y-[1.5rem]"
|
||||
>
|
||||
<span
|
||||
class="typo-sub-h-lg text-black w-full pt-[1.5rem]"
|
||||
>صفحه نمایش</span
|
||||
>
|
||||
<ul class="list-disc w-full ps-5">
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
</li>
|
||||
<li class="text-slate-500 typo-p-md">
|
||||
روشنایی :3000mn
|
||||
<li
|
||||
v-for="detail in [ item.detail_text1, item.detail_text2, item.detail_text3, item.detail_text4 ]"
|
||||
class="text-slate-500 typo-p-md"
|
||||
>
|
||||
{{ detail }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -125,8 +62,4 @@
|
||||
</AccordionItem>
|
||||
</AccordionRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
</template>
|
||||
@@ -22,9 +22,12 @@ const { picture, price, title, color } = toRefs(props);
|
||||
<div class="relative size-[100px] rounded-100 overflow-hidden border-[0.5px] border-slate-200">
|
||||
<img :src="picture" :alt="title" class="object-cover absolute" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="typo-sub-h-md text-black">{{ title }}</span>
|
||||
<span class="typo-p-sm text-slate-500">{{ color }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="typo-p-sm text-slate-500">رنگ</span>
|
||||
<ColorCircle class="!size-5" :style="{backgroundColor: color}" />
|
||||
</div>
|
||||
<span class="typo-p-md text-black">{{ price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,15 +8,7 @@ import useHomeData from "~/composables/api/home/useHomeData";
|
||||
|
||||
// state
|
||||
|
||||
const { $gsap: gsap } = useNuxtApp();
|
||||
|
||||
const { data: homeData } = useHomeData();
|
||||
|
||||
const sectionTarget = ref(null);
|
||||
const isSectionInsideViewport = useElementVisibility(sectionTarget, {
|
||||
rootMargin: "0% 0px -100% 0px"
|
||||
});
|
||||
|
||||
const swiper_instance = ref<SwiperClass | null>(null);
|
||||
|
||||
// methods
|
||||
@@ -25,28 +17,6 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
swiper_instance.value = swiper;
|
||||
};
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => isSectionInsideViewport.value, (newValue) => {
|
||||
if (newValue) {
|
||||
gsap.fromTo("#header-navbar", {
|
||||
background: "white",
|
||||
filter: "invert(0%)"
|
||||
}, {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
});
|
||||
} else {
|
||||
gsap.fromTo("#header-navbar", {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
}, {
|
||||
background: "white",
|
||||
filter: "invert(0%)"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -59,6 +59,11 @@ onMounted(() => {
|
||||
|
||||
gsapTimeline = gsap.timeline();
|
||||
|
||||
gsap.to("#header-navbar", {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
});
|
||||
|
||||
gsapTimeline
|
||||
.fromTo(".header-slider-item", {
|
||||
borderRadius: 0,
|
||||
@@ -73,8 +78,10 @@ onMounted(() => {
|
||||
value: 1.2
|
||||
}, "=")
|
||||
.fromTo("#header-navbar", {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
}, {
|
||||
background: "transparent",
|
||||
filter: "invert(0%)"
|
||||
}, "=")
|
||||
.fromTo("#header-navbar", {
|
||||
@@ -99,9 +106,6 @@ onMounted(() => {
|
||||
}, {
|
||||
padding: "0px 40px"
|
||||
}, "=")
|
||||
.to(".header-slider-logo", {
|
||||
opacity: 0
|
||||
}, "-=150%");
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: "#header-slider-container",
|
||||
|
||||
@@ -10,16 +10,28 @@ const { data: homeData } = useHomeData();
|
||||
|
||||
const clipPathPercent = ref(49);
|
||||
|
||||
const activeSlideVideo = ref<"left" | "right" | "none">("none");
|
||||
|
||||
const draggableEl = ref<HTMLElement | null>(null);
|
||||
const previewContainerEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const { x: dragAxisX } = useDraggable(draggableEl, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
axis: "x",
|
||||
axis: "x"
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => clipPathPercent.value, (newValue) => {
|
||||
if (newValue > 80) {
|
||||
activeSlideVideo.value = "right";
|
||||
} else if (newValue < 20) {
|
||||
activeSlideVideo.value = "left";
|
||||
} else {
|
||||
activeSlideVideo.value = "none";
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => dragAxisX.value,
|
||||
(newValue) => {
|
||||
@@ -39,24 +51,45 @@ watch(
|
||||
<span class="typo-p-md text-slate-500">مقایسه محصولات</span>
|
||||
<span class="typo-h-3 text-black">
|
||||
تفاوت محصلات ما را ببینید
|
||||
{{ activeSlideVideo }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
ref="previewContainerEl"
|
||||
class="rounded-200 overflow-hidden h-[90svh] relative"
|
||||
>
|
||||
<img
|
||||
:src="homeData!.difreance_section.image1"
|
||||
class="select-none absolute size-full object-cover brightness-[95%]"
|
||||
:alt="homeData!.difreance_section.title1"
|
||||
/>
|
||||
<Transition name="fade">
|
||||
<img
|
||||
v-if="activeSlideVideo !== 'right'"
|
||||
:src="homeData!.difreance_section.image1"
|
||||
class="select-none absolute size-full object-cover brightness-[95%]"
|
||||
:alt="homeData!.difreance_section.title1"
|
||||
/>
|
||||
<video
|
||||
v-else
|
||||
autoplay
|
||||
muted
|
||||
src="/video/vid-3.mp4"
|
||||
class="select-none absolute size-full object-cover brightness-[95%]"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div class="absolute size-full right-0 w-full">
|
||||
<img
|
||||
:src="homeData!.difreance_section.image2"
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
:alt="homeData!.difreance_section.title2"
|
||||
/>
|
||||
<Transition name="fade">
|
||||
<img
|
||||
v-if="activeSlideVideo !== 'left'"
|
||||
:src="homeData!.difreance_section.image2"
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
:alt="homeData!.difreance_section.title2"
|
||||
/>
|
||||
<video
|
||||
v-else
|
||||
autoplay
|
||||
muted
|
||||
src="/video/vid-3.mp4"
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
/>
|
||||
</Transition>
|
||||
<div
|
||||
:style="{
|
||||
left: `${clipPathPercent}%`,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// state
|
||||
|
||||
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
|
||||
|
||||
const slides = [
|
||||
{
|
||||
id: 1,
|
||||
title: "موبایل iPhone 16 Pro Max",
|
||||
description: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم.",
|
||||
image: "/img/showcase-1.png"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "موبایل iPhone 16 Pro Max",
|
||||
description: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم.",
|
||||
image: "/img/showcase-2.png"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "موبایل iPhone 16 Pro Max",
|
||||
description: "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم.",
|
||||
image: "/img/showcase-1.png"
|
||||
}
|
||||
];
|
||||
|
||||
let gsapTimeline: gsap.core.Timeline;
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
gsapTimeline = gsap.timeline();
|
||||
|
||||
gsapTimeline
|
||||
.fromTo("#header-navbar", {
|
||||
background: "white",
|
||||
filter: "invert(0%)"
|
||||
}, {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
});
|
||||
|
||||
const showcaseElements = gsap.utils.toArray<HTMLElement>(".showcase-slide");
|
||||
|
||||
showcaseElements.forEach((element, index) => {
|
||||
|
||||
gsapTimeline.fromTo(element, index === 0 ? {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: -25,
|
||||
y: 0,
|
||||
ease: "none"
|
||||
} : {
|
||||
opacity: 0,
|
||||
scale: 0.97,
|
||||
// rotateX: -25,
|
||||
y: 20,
|
||||
ease: "none"
|
||||
}, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: 0,
|
||||
y: 0,
|
||||
ease: "none"
|
||||
}, index === 0 ? "-=0%" : undefined);
|
||||
|
||||
if (index < showcaseElements.length - 1) {
|
||||
gsapTimeline.to(element, {
|
||||
opacity: 0,
|
||||
scale: 1.03,
|
||||
// rotateX: 25,
|
||||
y: -20,
|
||||
ease: "none"
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
gsapTimeline.to("#header-navbar", {
|
||||
background: "white",
|
||||
filter: "invert(0%)"
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: "#products-showcase-container",
|
||||
animation: gsapTimeline,
|
||||
scrub: 1,
|
||||
pin: true,
|
||||
start: "top top",
|
||||
// markers: true,
|
||||
end: "bottom top"
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gsapTimeline.progress(1).pause();
|
||||
gsapTimeline.kill();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="products-showcase-container"
|
||||
class="mt-80 mb-40 perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
v-for="slide in slides"
|
||||
:key="slide.id"
|
||||
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="blur-[150px] w-[600px] h-[80px] bg-white/70 absolute z-10"
|
||||
/>
|
||||
<img
|
||||
class="w-[650px] z-20 mb-30"
|
||||
:src="slide.image"
|
||||
:style="{
|
||||
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
|
||||
}"
|
||||
alt=""
|
||||
/>
|
||||
<div class="flex flex-col items-center justify-center gap-4 text-center absolute z-20 mt-20">
|
||||
<span class="text-white typo-h-3">
|
||||
{{ slide.title }}
|
||||
</span>
|
||||
<p class="text-white max-w-[750px] typo-p-lg">
|
||||
{{ slide.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// provide / inject
|
||||
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full p-[5rem] flex flex-col gap-y-[1.5rem]">
|
||||
<div class="w-full flex">
|
||||
@@ -16,23 +26,27 @@
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[3rem]"
|
||||
>
|
||||
<div
|
||||
v-for="i in 4"
|
||||
v-for="inPackItem in selectedVariant.in_pack_items"
|
||||
class="w-full flex-col-center gap-[.75rem]"
|
||||
>
|
||||
<div
|
||||
class="size-[6.25rem] rounded-full border-slate-200 bg-white flex-center"
|
||||
>
|
||||
<Icon name="ci:flag" size="44" />
|
||||
<div class="size-11 relative">
|
||||
<img
|
||||
class="size-full absolute object-cover"
|
||||
:src="inPackItem.cover"
|
||||
:alt="inPackItem.item_title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-black typo-p-md">Headphones</span>
|
||||
<span class="text-black typo-p-md">
|
||||
{{ inPackItem.item_title}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
// state
|
||||
|
||||
@@ -12,47 +13,77 @@ const id = route.params.id as string | undefined;
|
||||
|
||||
const { data: product } = useGetProduct(id);
|
||||
|
||||
const quantity = ref(1);
|
||||
const selectedVariantId = ref(product.value!.variants[0].id);
|
||||
const selectedQuantity = ref(1);
|
||||
const selectedSlide = ref(product.value!.variants[0].images[0].id);
|
||||
|
||||
const selectedSlide = ref(0);
|
||||
const selectedColor = ref(product.value!.colors[0]);
|
||||
|
||||
// provide / inject
|
||||
|
||||
const { selectedVariant, changeSelectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
// computed
|
||||
|
||||
const slides = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
picture: product.value!.image1
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
picture: product.value?.image2 ?? product.value!.image1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
picture: product.value!.image3 ?? product.value!.image1
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const sanitizedProductDescription = computed(() => {
|
||||
return sanitize(product.value!.description);
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-12 container pt-[5rem]">
|
||||
<div class="flex gap-16 container pt-[5rem] pb-28">
|
||||
<Slider
|
||||
class="flex-1"
|
||||
v-model:selectedSlide="selectedSlide"
|
||||
:slides="slides"
|
||||
:slides="selectedVariant.images"
|
||||
/>
|
||||
<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>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span class="typo-p-2xl"> {{ product!.price }} </span>
|
||||
<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'"
|
||||
>
|
||||
{{ selectedVariant.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-2xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{ 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" />
|
||||
{{ selectedVariant.discount }}
|
||||
درصد تخفیف
|
||||
</div>
|
||||
</div>
|
||||
<Rating />
|
||||
</div>
|
||||
|
||||
@@ -61,11 +92,37 @@ const sanitizedProductDescription = computed(() => {
|
||||
v-html="sanitizedProductDescription"
|
||||
/>
|
||||
|
||||
<div class="w-full flex flex-col gap-6 mt-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<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"
|
||||
:selected="selectedColor === color "
|
||||
:style="{backgroundColor: color}"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 flex-wrap">
|
||||
<ProductVariant
|
||||
@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"
|
||||
:isSelected="selectedVariantId === variant.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-6 mt-10">
|
||||
|
||||
<RemainQuantity
|
||||
:maxQuantity="product!.in_stock"
|
||||
:quantity="quantity"
|
||||
:maxQuantity="selectedVariant.in_stock"
|
||||
:quantity="selectedQuantity"
|
||||
/>
|
||||
|
||||
<div class="w-full flex gap-3 flex-col">
|
||||
@@ -74,8 +131,8 @@ const sanitizedProductDescription = computed(() => {
|
||||
افزودن به سبد خرید
|
||||
</Button>
|
||||
<QuantityCounter
|
||||
v-model="quantity"
|
||||
:max="product!.in_stock"
|
||||
v-model="selectedQuantity"
|
||||
:max="selectedVariant.in_stock"
|
||||
/>
|
||||
</div>
|
||||
<Button class="w-full rounded-full" variant="outlined">
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<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'
|
||||
]"
|
||||
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'
|
||||
]"
|
||||
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">
|
||||
<span class="text-xl font-medium">
|
||||
{{ variantDetail.price }}
|
||||
</span>
|
||||
<div
|
||||
v-if="variantDetail.discount > 0"
|
||||
: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" />
|
||||
<span class="mt-px">
|
||||
{{ variantDetail.discount }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<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
|
||||
v-for="attribute in variantDetail.product_attributes"
|
||||
class="flex items-center gap-2 text-sm rounded-full border border-slate-400 px-4 h-[40px]"
|
||||
>
|
||||
<span>{{ attribute.attribute_type.name }}</span>
|
||||
<span>{{ attribute.value }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@
|
||||
// import
|
||||
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
// state
|
||||
|
||||
@@ -11,13 +12,14 @@ const id = route.params.id as string | undefined;
|
||||
|
||||
const { data: product } = useGetProduct(id);
|
||||
|
||||
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="product?.video" class="h-[110svh] w-full relative bg-black mt-[5rem]">
|
||||
<section v-if="selectedVariant?.video" class="h-[110svh] w-full relative bg-black mt-[5rem]">
|
||||
<video
|
||||
:src="product.video"
|
||||
:src="selectedVariant.video"
|
||||
class="object-cover absolute size-full"
|
||||
muted
|
||||
autoplay
|
||||
@@ -25,9 +27,9 @@ const { data: product } = useGetProduct(id);
|
||||
/>
|
||||
<div class="size-full absolute inset-0 bg-black/20" />
|
||||
<StickyCard
|
||||
color="آبی"
|
||||
:price="product!.price"
|
||||
picture="/img/product-6.webp"
|
||||
:color="selectedVariant.color!"
|
||||
:price="selectedVariant.price"
|
||||
:picture="selectedVariant.images[0].image"
|
||||
:title="product!.name"
|
||||
class="absolute right-10 bottom-10"
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
export type GetProductResponse = Product;
|
||||
|
||||
const useGetDoc = (id: string | number | undefined) => {
|
||||
const useGetProduct = (id: string | number | undefined) => {
|
||||
|
||||
// state
|
||||
|
||||
@@ -22,8 +22,15 @@ const useGetDoc = (id: string | number | undefined) => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.product, id],
|
||||
queryFn: () => handleGetProduct(id)
|
||||
queryFn: () => handleGetProduct(id),
|
||||
select: (product) => {
|
||||
const copyOfProduct = { ...product };
|
||||
|
||||
copyOfProduct.variants = copyOfProduct.variants.sort((a, b) => b.in_stock - a.in_stock);
|
||||
|
||||
return copyOfProduct;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetDoc;
|
||||
export default useGetProduct;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetProductsResponse = ApiPaginated<Product>;
|
||||
export type GetProductsResponse = ApiPaginated<ProductListItem>;
|
||||
|
||||
export type GetProductsFilters = {
|
||||
search?: string | undefined;
|
||||
@@ -40,8 +40,8 @@ const useGetProducts = (params?: ComputedRef<GetProductsFilters>) => {
|
||||
price_gte: params?.price_gte,
|
||||
price_lte: params?.price_lte,
|
||||
offset: Number(params?.page) * 9 - 9,
|
||||
limit: 9,
|
||||
},
|
||||
limit: 9
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ const useGetProducts = (params?: ComputedRef<GetProductsFilters>) => {
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [QUERY_KEYS.products, params],
|
||||
queryFn: () => handleGetProducts(params?.value),
|
||||
queryFn: () => handleGetProducts(params?.value)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ if (response.isError) {
|
||||
|
||||
<!-- This is for masonry js package -->
|
||||
|
||||
<ClientOnly>
|
||||
<ArticlesList :articles="articles!.results" />
|
||||
</ClientOnly>
|
||||
|
||||
<ArticlesList :articles="articles!.results" />
|
||||
|
||||
|
||||
<div class="w-full flex-center pt-24 pb-10">
|
||||
<Pagination :items="[]" :total="100" />
|
||||
|
||||
@@ -23,10 +23,12 @@ if (response.isError) {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<!-- <LoadingOverlay />-->
|
||||
<Hero />
|
||||
<Preview />
|
||||
<Categories />
|
||||
<ProductsShowcase />
|
||||
<ProductsSlider title="محصولات پرفروش" />
|
||||
<Categories />
|
||||
<Brands />
|
||||
<MostRecentComments />
|
||||
<LatestStories />
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import ChatButton from "~/components/product/ChatBox/ChatButton.vue";
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
import useGetComments from "~/composables/api/product/useGetComments";
|
||||
|
||||
// type
|
||||
|
||||
export type ProductVariantProvideType = {
|
||||
selectedVariant: Ref<ProductVariant>,
|
||||
changeSelectedVariant: (value: ProductVariant) => void
|
||||
}
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const id = route.params.id as string | undefined;
|
||||
@@ -12,6 +23,15 @@ const page = ref(1);
|
||||
const { suspense: suspenseProduct } = useGetProduct(id);
|
||||
const { suspense: suspenseComments } = useGetComments(id, page);
|
||||
|
||||
const selectedVariant = ref<ProductVariant>();
|
||||
|
||||
// provide / inject
|
||||
|
||||
provide("productVariant", {
|
||||
selectedVariant,
|
||||
changeSelectedVariant: (value: ProductVariant) => selectedVariant.value = value
|
||||
});
|
||||
|
||||
// ssr
|
||||
|
||||
const productResponse = await suspenseProduct();
|
||||
@@ -27,7 +47,7 @@ if (productResponse.isError || commentsResponse.isError) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-20">
|
||||
<div class="w-full flex flex-col ">
|
||||
<ProductHero />
|
||||
<ProductVideo />
|
||||
<ProductComments />
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
+65
-19
@@ -24,27 +24,73 @@ declare global {
|
||||
birth_date: string;
|
||||
};
|
||||
|
||||
type Product = {
|
||||
id: number;
|
||||
price: string;
|
||||
name: string;
|
||||
description: string;
|
||||
currency: string;
|
||||
video: string | null;
|
||||
image1: string;
|
||||
image2: string;
|
||||
image3: string;
|
||||
rating: number;
|
||||
view: number;
|
||||
sell: number;
|
||||
in_stock: number;
|
||||
discount: number;
|
||||
slug: string;
|
||||
meta_description: string | null;
|
||||
meta_keywords: null;
|
||||
meta_rating: number | null;
|
||||
type ProductVariantAttribute = {
|
||||
"id": number,
|
||||
"attribute_type": {
|
||||
"id": number,
|
||||
"name": string
|
||||
},
|
||||
"value": string
|
||||
}
|
||||
|
||||
type ProductImage = {
|
||||
"id": number,
|
||||
"name": string,
|
||||
"image": string
|
||||
}
|
||||
|
||||
|
||||
type ProductDetailItem = {
|
||||
"id": number,
|
||||
"title": string,
|
||||
"detail_text1": string,
|
||||
"detail_text2": string,
|
||||
"detail_text3": string,
|
||||
"detail_text4": string
|
||||
}
|
||||
|
||||
type ProductDetail = {
|
||||
"id": number,
|
||||
"detail": ProductDetailItem[],
|
||||
"detail_category": number
|
||||
}
|
||||
|
||||
type ProductInPackItem = {
|
||||
"id": number,
|
||||
"item_title": string,
|
||||
"cover": string
|
||||
}
|
||||
|
||||
type ProductVariant = {
|
||||
"id": number,
|
||||
"product_attributes": ProductVariantAttribute[],
|
||||
"price": string,
|
||||
"in_pack_items": ProductInPackItem[],
|
||||
"details": ProductDetail[],
|
||||
"images": ProductImage[],
|
||||
"in_stock": number,
|
||||
"discount": number,
|
||||
"color": string,
|
||||
"video": string | null
|
||||
};
|
||||
|
||||
type Product = {
|
||||
"id": number,
|
||||
"variants": ProductVariant[],
|
||||
"related_products": ProductListItem[],
|
||||
"name": string,
|
||||
"description": string,
|
||||
"rating": number,
|
||||
"slug": string,
|
||||
"meta_description": string | null,
|
||||
"meta_keywords": string | null,
|
||||
"meta_rating": number,
|
||||
"category": number,
|
||||
"colors": string[]
|
||||
}
|
||||
|
||||
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">
|
||||
|
||||
type Article = {
|
||||
id: number;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user