This commit is contained in:
marzban-dev
2025-05-21 17:48:10 +03:30
12 changed files with 546 additions and 18 deletions
@@ -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,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>