Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -6,6 +6,6 @@ urlpatterns = [
|
||||
path('slider_category', ShowCaseProductsView.as_view(), name='category-products'),
|
||||
path('categories', AllCategories.as_view(), name='all-categories'),
|
||||
path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'),
|
||||
path('<int:pk>', ProductView.as_view(), name='product-detail'),
|
||||
path('<int:slug>', ProductView.as_view(), name='product-detail'),
|
||||
path('comments/<int:pk>', CommentView.as_view(), name='comment-views'),
|
||||
]
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<span class="hidden lg:block"> فیلتر محصولات </span>
|
||||
</Button>
|
||||
</template>
|
||||
<FilterProducts />
|
||||
<slot name="content" />
|
||||
</SideModal>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +27,7 @@ onMounted(() => {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: -25,
|
||||
zIndex: 1,
|
||||
top: 0,
|
||||
ease: "none",
|
||||
}
|
||||
@@ -34,6 +35,7 @@ onMounted(() => {
|
||||
opacity: 0,
|
||||
scale: 1,
|
||||
// rotateX: -25,
|
||||
zIndex: 1,
|
||||
top: 20,
|
||||
ease: "none",
|
||||
},
|
||||
@@ -41,6 +43,7 @@ onMounted(() => {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: 0,
|
||||
zIndex: 5,
|
||||
top: 0,
|
||||
ease: "none",
|
||||
},
|
||||
@@ -88,10 +91,9 @@ onUnmounted(() => {
|
||||
class="perspective-midrange relative z-[999]"
|
||||
>
|
||||
<div class="w-full min-h-[120svh] lg:min-h-[102svh] bg-black">
|
||||
<NuxtLink
|
||||
v-for="slide in homeData!.show_case_slider"
|
||||
:key="slide.id"
|
||||
:to="slide.link"
|
||||
<div
|
||||
v-for="(slide, index) in homeData!.show_case_slider"
|
||||
:key="index"
|
||||
class="showcase-slide origin-bottom absolute size-full bg-black flex items-center justify-center max-lg:-mt-16 lg:mt-5"
|
||||
>
|
||||
<NuxtImg
|
||||
@@ -112,7 +114,7 @@ onUnmounted(() => {
|
||||
{{ slide.description }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
:to="slide.link"
|
||||
:to="`/resellers/category/${slide.id}`"
|
||||
class="relative"
|
||||
>
|
||||
<NuxtImg
|
||||
@@ -128,7 +130,7 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetCategories from "~/composables/api/product/useGetCategories";
|
||||
import useGetCategories from "~/composables/api/categories/useGetCategories";
|
||||
import useGetProducts, { type GetProductsFilters } from "~/composables/api/products/useGetProducts";
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetResellersCategories from "~/composables/api/resellers/useGetResellersCategories";
|
||||
import useGetResellersProducts, {
|
||||
type GetResellersProductsFilters,
|
||||
} from "~/composables/api/resellers/useGetResellersProducts";
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const params = inject("params") as GetResellersProductsFilters;
|
||||
|
||||
const currentCategory = computed({
|
||||
get: () => {
|
||||
return Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined;
|
||||
},
|
||||
set: (newValue) => {
|
||||
router.push({ path: `/resellers/category/${newValue}`, query: { ...route.query } });
|
||||
},
|
||||
});
|
||||
|
||||
const sort_filter = ref([
|
||||
{ title: "جدیدترین ها", value: "newest" },
|
||||
{ title: "گران ترین ها", value: "price" },
|
||||
{ title: "ارزان ترین ها", value: "-price" },
|
||||
]);
|
||||
|
||||
const sliderValue = ref([params.price_gte ?? PRODUCT_RANGE.min, params.price_lte ?? PRODUCT_RANGE.max]);
|
||||
|
||||
const has_discount = ref(Boolean(params.has_discount) ?? false);
|
||||
const in_stock = ref(Boolean(params.in_stock) ?? false);
|
||||
|
||||
const sliderValueDebounced = refDebounced(sliderValue, 1000);
|
||||
|
||||
const filtersSuccessMessage = ref<{ title: string; status: string } | null>(null);
|
||||
|
||||
// queries
|
||||
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
sort: params.sort ?? "newest",
|
||||
search: params.search ?? "",
|
||||
price_gte: params.price_gte ?? PRODUCT_RANGE.min,
|
||||
price_lte: params.price_lte ?? PRODUCT_RANGE.max,
|
||||
in_stock: params.in_stock ?? false,
|
||||
has_discount: params.has_discount ?? false,
|
||||
category: currentCategory.value,
|
||||
page: params.page ?? 1,
|
||||
};
|
||||
});
|
||||
|
||||
const { data: categories, suspense } = useGetResellersCategories();
|
||||
|
||||
await suspense();
|
||||
|
||||
const { isPending: productsIsPending, status: productsStatus } = useGetResellersProducts(filters);
|
||||
|
||||
// computed
|
||||
|
||||
const allCategories = computed(() => {
|
||||
return categories.value!.map((category) => {
|
||||
return category.title;
|
||||
});
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
const resetFilters = () => {
|
||||
params.search = "";
|
||||
params.sort = "";
|
||||
sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
|
||||
has_discount.value = false;
|
||||
in_stock.value = false;
|
||||
|
||||
router.push({ path: `/resellers/`, query: { ...route.query } });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => sliderValueDebounced.value,
|
||||
(newValue) => {
|
||||
params.price_gte = newValue[0];
|
||||
params.price_lte = newValue[1];
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [has_discount.value, in_stock.value],
|
||||
([newHasDiscount, newInStock]) => {
|
||||
params.has_discount = newHasDiscount;
|
||||
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>
|
||||
<div class="size-full flex flex-col gap-14 justify-between">
|
||||
<div class="w-full flex flex-col gap-8 lg:gap-10">
|
||||
<div class="flex flex-col items-center w-full gap-5">
|
||||
<div class="flex items-center justify-start gap-2 max-lg:text-sm w-full">
|
||||
<Icon
|
||||
name="ci:filter-list"
|
||||
class="text-xl"
|
||||
/>
|
||||
ترتیب بر اساس
|
||||
</div>
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<button
|
||||
v-for="(sort, index) in sort_filter"
|
||||
:key="index"
|
||||
@click="params.sort = sort.value"
|
||||
:class="params.sort == sort.value ? 'bg-black text-white' : 'bg-slate-100'"
|
||||
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-md text-xs lg:text-sm"
|
||||
>
|
||||
{{ sort.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
<div class="flex items-center justify-start gap-2 max-lg:text-sm w-full">
|
||||
<Icon
|
||||
name="ci:grid"
|
||||
class="text-xl"
|
||||
/>
|
||||
دسته بندی
|
||||
</div>
|
||||
<Select
|
||||
v-model="currentCategory"
|
||||
:options="allCategories"
|
||||
placeholder="انتخاب کنید"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
<div class="flex items-center justify-start gap-2 max-lg:text-sm w-full">
|
||||
<Icon
|
||||
name="ci:scan-box"
|
||||
class="text-xl"
|
||||
/>
|
||||
محدوده قیمت
|
||||
</div>
|
||||
<SliderRoot
|
||||
v-model="sliderValue"
|
||||
class="relative flex items-center select-none touch-none h-5"
|
||||
dir="rtl"
|
||||
:min="PRODUCT_RANGE.min"
|
||||
:max="PRODUCT_RANGE.max"
|
||||
:step="1000"
|
||||
>
|
||||
<SliderTrack class="bg-black/10 relative grow rounded-full h-[3px]">
|
||||
<SliderRange class="absolute bg-black rounded-full h-full" />
|
||||
</SliderTrack>
|
||||
<SliderThumb
|
||||
v-for="thumb in Object.keys(PRODUCT_RANGE)"
|
||||
:key="thumb"
|
||||
class="block w-5 h-5 bg-white border-2 border-slate-400 rounded-[10px] hover:bg-slate-100 focus:outline-none focus:shadow-[0_0_0_5px] focus:shadow-black/60"
|
||||
/>
|
||||
</SliderRoot>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-center gap-2 text-xs lg:text-sm">
|
||||
<span class="text-black">حداقل</span>
|
||||
<span class="text-black">
|
||||
{{ sliderValue[0].toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-center gap-2 text-xs lg:text-sm">
|
||||
<span class="text-black">حداکثر</span>
|
||||
<span class="text-black">
|
||||
{{ sliderValue[1].toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<span class="text-black max-lg:text-sm">فقط کالاهای تخفیف دار</span>
|
||||
|
||||
<Switch v-model="has_discount" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<span class="text-black max-lg:text-sm">فقط کالاهای موجود</span>
|
||||
|
||||
<Switch v-model="in_stock" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-max rounded-full py-1.5 px-3 text-xs border flex-center gap-0.5 z-[2]"
|
||||
:class="
|
||||
filtersSuccessMessage.status == 'success'
|
||||
? 'text-success-600 bg-success-100 border-success-600'
|
||||
: ' text-danger-600 bg-danger-100 border-danger-600'
|
||||
"
|
||||
>
|
||||
<span class="text-sm">{{ filtersSuccessMessage.title }}</span>
|
||||
<Icon
|
||||
:name="filtersSuccessMessage.status == 'success' ? 'bi:check' : 'bi:x'"
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
<Button
|
||||
:disabled="productsIsPending"
|
||||
variant="solid"
|
||||
@click="resetFilters"
|
||||
class="w-full rounded-full py-4 !cursor-pointer disabled:pointer-events-none z-[3]"
|
||||
>
|
||||
<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>
|
||||
@@ -0,0 +1,37 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetResellersCategoriesResponse = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
image: string;
|
||||
}[];
|
||||
|
||||
const useGetResellersCategories = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetResellersCategories = async () => {
|
||||
const { data } = await axios.get<GetResellersCategoriesResponse>(
|
||||
`${API_ENDPOINTS.resellers_products.categories}`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.resellers_categories],
|
||||
queryFn: () => handleGetResellersCategories(),
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetResellersCategories;
|
||||
@@ -0,0 +1,55 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetResellersProductsResponse = ApiPaginated<ProductListItem>;
|
||||
|
||||
export type GetResellersProductsFilters = {
|
||||
search?: string | undefined;
|
||||
sort?: string | undefined;
|
||||
category?: string | undefined;
|
||||
price_gte: number;
|
||||
price_lte: number;
|
||||
has_discount?: boolean | undefined;
|
||||
in_stock?: boolean | undefined;
|
||||
page: number;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetResellersProducts = (params?: ComputedRef<GetResellersProductsFilters>) => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetResellersProducts = async (params?: GetResellersProductsFilters) => {
|
||||
const { data } = await axios.get<GetResellersProductsResponse>(`${API_ENDPOINTS.resellers_products.get_all}`, {
|
||||
params: {
|
||||
sort: params?.sort,
|
||||
in_stock: params?.in_stock,
|
||||
search: params?.search,
|
||||
has_discount: params?.has_discount,
|
||||
category: params?.category,
|
||||
price_gte: params?.price_gte,
|
||||
price_lte: params?.price_lte,
|
||||
offset: Number(params?.page) * 15 - 15,
|
||||
limit: 15,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [QUERY_KEYS.resellers_products, params],
|
||||
queryFn: () => handleGetResellersProducts(params?.value),
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetResellersProducts;
|
||||
@@ -35,6 +35,10 @@ export const API_ENDPOINTS = {
|
||||
get_all: "/products",
|
||||
categories: "/products/categories",
|
||||
},
|
||||
resellers_products: {
|
||||
get_all: "/products/slider_category",
|
||||
categories: "/products/slider_categories",
|
||||
},
|
||||
tickets: {
|
||||
get_all: "/tickets",
|
||||
create: "/tickets/create",
|
||||
@@ -71,8 +75,10 @@ export const QUERY_KEYS = {
|
||||
chat: "chat",
|
||||
product: "product",
|
||||
products: "products",
|
||||
resellers_products: "resellers_products",
|
||||
account: "account",
|
||||
categories: "categories",
|
||||
resellers_categories: "resellers_categories",
|
||||
addresses: "addresses",
|
||||
tickets: "tickets",
|
||||
ticket: "ticket",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
// import
|
||||
|
||||
import useGetCategories from "~/composables/api/product/useGetCategories";
|
||||
import useGetCategories from "~/composables/api/categories/useGetCategories";
|
||||
|
||||
// state
|
||||
|
||||
|
||||
@@ -75,13 +75,6 @@ watch(
|
||||
<div class="w-full container flex flex-col">
|
||||
<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-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full">
|
||||
<!-- <div class="flex-center gap-[.75rem]">
|
||||
<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-5 lg:typo-h-4">لیست محصولات</h1>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +95,11 @@ watch(
|
||||
</template>
|
||||
</Input>
|
||||
<Suspense>
|
||||
<FilterButton />
|
||||
<FilterButton>
|
||||
<template #content>
|
||||
<FilterProducts />
|
||||
</template>
|
||||
</FilterButton>
|
||||
<template #fallback>
|
||||
<Skeleton class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-xl" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
// import
|
||||
|
||||
import useGetResellersProducts, {
|
||||
type GetResellersProductsFilters,
|
||||
} from "~/composables/api/resellers/useGetResellersProducts";
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
useSeoMeta({
|
||||
title: "محصولات",
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
validate: (route) => {
|
||||
if (Array.isArray(route.params.slug)) {
|
||||
return route.params.slug.length === 2 && route.params.slug[0] === "category";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const params: GetResellersProductsFilters = useUrlSearchParams("history", {
|
||||
removeFalsyValues: true,
|
||||
removeNullishValues: true,
|
||||
});
|
||||
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
sort: params.sort ?? "newest",
|
||||
search: params.search ?? "",
|
||||
price_gte: params.price_gte ?? PRODUCT_RANGE.min,
|
||||
price_lte: params.price_lte ?? PRODUCT_RANGE.max,
|
||||
in_stock: params.in_stock ?? false,
|
||||
has_discount: params.has_discount ?? false,
|
||||
category: Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined,
|
||||
page: params.page ?? 1,
|
||||
};
|
||||
});
|
||||
|
||||
const search = ref(params.search ?? "");
|
||||
const searchDebounced = refDebounced(search, 1000);
|
||||
|
||||
// provide / inject
|
||||
|
||||
provide("params", params);
|
||||
|
||||
// queries
|
||||
|
||||
const { data, isLoading: productsIsLoading } = useGetResellersProducts(filters);
|
||||
|
||||
const products = computed(() => {
|
||||
return data.value?.results.flat();
|
||||
});
|
||||
|
||||
const paginationData = computed(() => {
|
||||
return data!.value?.results.map((_, i: number) => {
|
||||
return { type: "page", value: i };
|
||||
});
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => searchDebounced.value,
|
||||
(newValue) => {
|
||||
params.search = newValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full container flex flex-col">
|
||||
<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-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full">
|
||||
<h1 class="typo-h-5 lg:typo-h-4">لیست محصولات</h1>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-between lg:justify-end gap-4">
|
||||
<Input
|
||||
placeholder="جست و جو محصول ..."
|
||||
v-model="search"
|
||||
variant="outlined"
|
||||
class="!rounded-xl w-full lg:w-8/12"
|
||||
>
|
||||
<template #endItem>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon
|
||||
class="translate-y-[-1px] text-[20px] lg:text-[24px]"
|
||||
name="ci:search"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Input>
|
||||
<Suspense>
|
||||
<FilterButton>
|
||||
<template #content>
|
||||
<FilterResellersProducts />
|
||||
</template>
|
||||
</FilterButton>
|
||||
<template #fallback>
|
||||
<Skeleton class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-xl" />
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
v-if="productsIsLoading"
|
||||
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-y-8 gap-5 sm:gap-8 w-full"
|
||||
>
|
||||
<div
|
||||
class="w-full flex flex-col gap-3"
|
||||
v-for="i in 10"
|
||||
:key="i"
|
||||
>
|
||||
<Skeleton
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'!h-[11.9rem] lg:!h-[17.25rem] !rounded-2xl': i == 1,
|
||||
'!h-[1.4rem] lg:!h-[1.5rem] !rounded-sm': [2, 3].includes(i),
|
||||
'!w-1/2 lg:!w-full': i == 2,
|
||||
'lg:!w-1/2': i == 3,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
<ProductsGrid
|
||||
:with-header="false"
|
||||
:products="products!"
|
||||
class="!p-0"
|
||||
/>
|
||||
<div
|
||||
v-if="data && paginationData && data.count > 15"
|
||||
class="w-full flex-center py-10 mt-5 lg:mt-10"
|
||||
>
|
||||
<Pagination
|
||||
:items="paginationData"
|
||||
:total="data.count"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user