Updated
This commit is contained in:
+16
-6
@@ -1,8 +1,22 @@
|
|||||||
# Todos
|
# Todos
|
||||||
|
|
||||||
[ ] Mega menu for categories in menu
|
[x] Mega menu for categories in menu
|
||||||
|
|
||||||
[ ] Marquee animation bug ( Replace with nuxt ui marquee )
|
[x] Marquee animation bug
|
||||||
|
|
||||||
|
[x] Add favorite button for products
|
||||||
|
|
||||||
|
[x] Change footer to a dynamic ray animation
|
||||||
|
|
||||||
|
[x] Create saved products tab in user panel
|
||||||
|
|
||||||
|
[x] Add save button to product page
|
||||||
|
|
||||||
|
[x] Add save button to product page
|
||||||
|
|
||||||
|
[x] Make mega menu responsive
|
||||||
|
|
||||||
|
[x] Fix mega menu bugs
|
||||||
|
|
||||||
[ ] Pause marquee on mouse hover
|
[ ] Pause marquee on mouse hover
|
||||||
|
|
||||||
@@ -14,8 +28,4 @@
|
|||||||
|
|
||||||
<s>Test showcase background with gradient</s>
|
<s>Test showcase background with gradient</s>
|
||||||
|
|
||||||
[ ] Add favorite button for products
|
|
||||||
|
|
||||||
[x] Change footer to a dynamic ray animation
|
|
||||||
|
|
||||||
[ ] Compress all large pictures and videos
|
[ ] Compress all large pictures and videos
|
||||||
@@ -26,7 +26,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :to="`/products/category/${slug}`">
|
<NuxtLink :to="{ name: 'products-slug', params: { slug: ['category', slug] } }">
|
||||||
<div class="group relative rounded-150 overflow-hidden w-full aspect-square bg-white brightness-[97%]">
|
<div class="group relative rounded-150 overflow-hidden w-full aspect-square bg-white brightness-[97%]">
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<video
|
<video
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<Button
|
<Button
|
||||||
end-icon="ci:filter"
|
end-icon="ci:filter"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
class="max-lg:size-11 rounded-xl max-lg:aspect-square lg:py-3.5"
|
class="max-lg:size-11 rounded-xl max-lg:aspect-square lg:py-3.5 relative"
|
||||||
>
|
>
|
||||||
<span class="hidden lg:block"> فیلتر محصولات </span>
|
<span class="hidden lg:block"> فیلتر محصولات </span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const isScrollLocked = useScrollLock(window);
|
|||||||
const navbarEl = ref<HTMLElement | null>(null);
|
const navbarEl = ref<HTMLElement | null>(null);
|
||||||
const { height: navbarHeight } = useElementSize(navbarEl);
|
const { height: navbarHeight } = useElementSize(navbarEl);
|
||||||
|
|
||||||
const { $gsap: gsap } = useNuxtApp();
|
const { $gsap: gsap, hooks } = useNuxtApp();
|
||||||
let gsapTimeline: gsap.core.Timeline;
|
let gsapTimeline: gsap.core.Timeline;
|
||||||
|
|
||||||
const prevNavbarStyle = ref({
|
const prevNavbarStyle = ref({
|
||||||
@@ -71,6 +71,12 @@ const updatePrevStyle = () => {
|
|||||||
prevNavbarStyle.value.itemFilter = getComputedStyle(headerNavbarItemEl).filter;
|
prevNavbarStyle.value.itemFilter = getComputedStyle(headerNavbarItemEl).filter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeAfterNavigate = () => {
|
||||||
|
hooks.hookOnce("page:finish", () => {
|
||||||
|
emit("update:isOpen", false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// watches
|
// watches
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -92,12 +98,9 @@ watch(isOpen, async (newValue) => {
|
|||||||
updatePrevStyle();
|
updatePrevStyle();
|
||||||
|
|
||||||
gsapTimeline
|
gsapTimeline
|
||||||
.to(
|
.to(".header-navbar-item", {
|
||||||
".header-navbar-item",
|
filter: "invert(0%)",
|
||||||
{
|
})
|
||||||
filter: "invert(0%)",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.to(
|
.to(
|
||||||
"#header-navbar",
|
"#header-navbar",
|
||||||
{
|
{
|
||||||
@@ -146,7 +149,7 @@ onMounted(() => {
|
|||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
:style="{ marginTop: `${navbarHeight}px` }"
|
:style="{ marginTop: `${navbarHeight}px` }"
|
||||||
class="fixed right-0 top-0 w-full z-999 bg-black/50 h-svh border-t border-slate-200 flex justify-center"
|
class="max-md:hidden fixed right-0 top-0 w-full z-1100 bg-black/50 h-svh border-t border-slate-200 flex justify-center"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div
|
<div
|
||||||
@@ -157,7 +160,7 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
v-for="(item, index) in items"
|
v-for="(item, index) in items"
|
||||||
:key="item.title"
|
:key="item.title"
|
||||||
class="text-black transition-all p-4 rounded-xl cursor-default text-start"
|
class="text-black transition-all p-4 rounded-xl cursor-default text-start max-lg:text-sm"
|
||||||
:class="index === selectedItem ? 'bg-blue-100 text-blue-500' : 'bg-slate-100'"
|
:class="index === selectedItem ? 'bg-blue-100 text-blue-500' : 'bg-slate-100'"
|
||||||
:id="`mega-menu-tab-${index}`"
|
:id="`mega-menu-tab-${index}`"
|
||||||
@mouseenter="menuTabMouseEnter(index)"
|
@mouseenter="menuTabMouseEnter(index)"
|
||||||
@@ -175,7 +178,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:key="selectedItem"
|
:key="selectedItem"
|
||||||
class="grid grid-cols-4 p-4 text-back w-full h-fit"
|
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4 text-back w-full h-fit"
|
||||||
:style="{
|
:style="{
|
||||||
// gridTemplateRows: 'repeat(5, auto)',
|
// gridTemplateRows: 'repeat(5, auto)',
|
||||||
// justifyContent: 'center',
|
// justifyContent: 'center',
|
||||||
@@ -183,10 +186,12 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="item in selectedItemSubItems"
|
v-for="item in selectedItemSubItems"
|
||||||
:to="'https://google.com'"
|
:to="{ name: 'products-slug', params: { slug: ['category', item.link] } }"
|
||||||
class="p-4 whitespace-nowrap h-fit text-slate-500 flex items-center justify-between hover:text-blue-500 hover:-translate-x-1.5 transition-all"
|
@click="closeAfterNavigate"
|
||||||
|
class="p-4 whitespace-nowrap h-fit text-slate-500 flex items-center justify-between hover:text-blue-500 hover:-translate-x-1.5 transition-all max-lg:text-sm"
|
||||||
>
|
>
|
||||||
<span class="truncate w-[90%]">
|
<span class="truncate w-[90%] flex items-center gap-2">
|
||||||
|
<span class="w-2 h-[3px] rounded-full bg-blue-400"> </span>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|||||||
@@ -15,25 +15,27 @@ withDefaults(defineProps<Props>(), {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-full flex flex-col gap-10 md:gap-[4rem] py-[5rem] container">
|
<section class="w-full flex flex-col gap-10 @min-[48rem]:gap-[4rem] py-[5rem] container @container">
|
||||||
<div
|
<div
|
||||||
v-if="withHeader"
|
v-if="withHeader"
|
||||||
class="w-full flex justify-between items-center"
|
class="w-full flex justify-between items-center"
|
||||||
>
|
>
|
||||||
<span class="text-black typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">
|
<span class="text-black typo-h-6 @max-[40rem]:text-xl @min-[48rem]:typo-h-5 @min-[64rem]:typo-h-4">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<NuxtLink to="/products">
|
<NuxtLink to="/products">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
class="rounded-full max-md:h-[38px] max-md:text-xs"
|
class="rounded-full @max-[48rem]:h-[38px] @max-[48rem]:text-xs"
|
||||||
end-icon="ci:arrow-left"
|
end-icon="ci:arrow-left"
|
||||||
>
|
>
|
||||||
نمایش همه
|
نمایش همه
|
||||||
</Button>
|
</Button>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<ul 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">
|
<ul
|
||||||
|
class="grid grid-cols-2 @min-[40rem]:grid-cols-3 @min-[64rem]:grid-cols-4 @min-[80rem]:grid-cols-5 gap-y-8 gap-5 @min-[40rem]:gap-8"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
class="w-full"
|
class="w-full"
|
||||||
v-for="product in products"
|
v-for="product in products"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useIsMutating } from "@tanstack/vue-query";
|
|||||||
import { MUTATION_KEYS } from "~/constants";
|
import { MUTATION_KEYS } from "~/constants";
|
||||||
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
||||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
import { useScroll } from "@vueuse/core";
|
||||||
|
|
||||||
// provide-inject
|
// provide-inject
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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
|
||||||
|
|
||||||
@@ -12,8 +14,11 @@ 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 { data: product, refetch: refetchProduct } = useGetProduct(id);
|
const { $queryClient: queryClient } = useNuxtApp();
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -35,6 +40,11 @@ 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]) => {
|
||||||
@@ -113,12 +123,30 @@ watch(
|
|||||||
:slides="selectedVariant!.images"
|
:slides="selectedVariant!.images"
|
||||||
/>
|
/>
|
||||||
<div class="lg:w-1/2 flex flex-col gap-3 lg:mt-12">
|
<div class="lg:w-1/2 flex flex-col gap-3 lg:mt-12">
|
||||||
<NuxtLink
|
<div class="flex items-center justify-between w-full">
|
||||||
to="#"
|
<NuxtLink
|
||||||
class="typo-label-sm max-lg:hidden"
|
to="#"
|
||||||
>
|
class="typo-label-sm max-lg:hidden"
|
||||||
{{ product!.category.name }}
|
>
|
||||||
</NuxtLink>
|
{{ product!.category.name }}
|
||||||
|
</NuxtLink>
|
||||||
|
<button
|
||||||
|
@click="saveProductHandler"
|
||||||
|
:disabled="isSaveProductPending || isFetchingPending"
|
||||||
|
class="size-10 bg-slate-50 border-slate-200 border rounded-lg flex-center"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="isSaveProductPending || isFetchingPending"
|
||||||
|
name="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' : 'bi:bookmark'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</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 }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ const profileLinks = ref([
|
|||||||
path: { name: "profile-purchases-and-orders" },
|
path: { name: "profile-purchases-and-orders" },
|
||||||
matchPattern: /^profile-purchases-and-orders/,
|
matchPattern: /^profile-purchases-and-orders/,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: "bi:bookmark",
|
||||||
|
title: "علاقهمندی ها",
|
||||||
|
path: { name: "profile-saved-products" },
|
||||||
|
matchPattern: /^profile-saved-products/,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: "bi:ticket",
|
icon: "bi:ticket",
|
||||||
title: "تیکت ها",
|
title: "تیکت ها",
|
||||||
@@ -95,26 +101,20 @@ const toggleSidebar = inject("toggleSidebar");
|
|||||||
:src="account!.profile_photo"
|
:src="account!.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account?.first_name && account?.last_name
|
account?.first_name && account?.last_name
|
||||||
? `${account?.first_name.charAt(
|
? `${account?.first_name.charAt(0)} ${account?.last_name.charAt(0)}`
|
||||||
0
|
|
||||||
)} ${account?.last_name.charAt(0)}`
|
|
||||||
: 'بدون نام کاربری'
|
: 'بدون نام کاربری'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="w-full flex flex-col gap-2 rounded-xl bg-slate-50 border border-slate-200 p-4">
|
||||||
class="w-full flex flex-col gap-2 rounded-xl bg-slate-50 border border-slate-200 p-4"
|
|
||||||
>
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(link, index) in profileLinks"
|
v-for="(link, index) in profileLinks"
|
||||||
:key="index"
|
:key="index"
|
||||||
:to="{ ...link.path }"
|
:to="{ ...link.path }"
|
||||||
:class="
|
:class="
|
||||||
isLinkActive(link)
|
isLinkActive(link) ? 'bg-black text-slate-100 **:fill-slate-100' : '**:fill-black hover:bg-gray-200'
|
||||||
? 'bg-black text-slate-100 **:fill-slate-100'
|
|
||||||
: '**:fill-black hover:bg-gray-200'
|
|
||||||
"
|
"
|
||||||
class="flex items-center justify-between transition-all rounded-lg py-3.5 lg:py-4 px-3"
|
class="flex items-center justify-between transition-all rounded-lg py-3.5 lg:py-4 px-3"
|
||||||
@click="toggleSidebar"
|
@click="toggleSidebar"
|
||||||
@@ -126,7 +126,10 @@ const toggleSidebar = inject("toggleSidebar");
|
|||||||
<span class="text-xs lg:text-sm">{{ link.title }}</span>
|
<span class="text-xs lg:text-sm">{{ link.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Icon name="bi:chevron-left" class="transition-all" />
|
<Icon
|
||||||
|
name="bi:chevron-left"
|
||||||
|
class="transition-all"
|
||||||
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<LogoutModal>
|
<LogoutModal>
|
||||||
@@ -141,9 +144,7 @@ const toggleSidebar = inject("toggleSidebar");
|
|||||||
class="**:fill-danger-500"
|
class="**:fill-danger-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs lg:text-sm">
|
<span class="text-xs lg:text-sm"> خروج از حساب </span>
|
||||||
خروج از حساب
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const useGetChat = (productId: string | number, enabled: Ref<boolean>) => {
|
|||||||
|
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
|
const isEnabled = computed(() => {
|
||||||
|
return enabled.value && isLoggedIn.value;
|
||||||
|
});
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
|
|
||||||
const handleGetChat = async ({
|
const handleGetChat = async ({
|
||||||
@@ -26,23 +30,20 @@ const useGetChat = (productId: string | number, enabled: Ref<boolean>) => {
|
|||||||
limit: number;
|
limit: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await axios.get<GetChatResponse>(
|
const { data } = await axios.get<GetChatResponse>(`${API_ENDPOINTS.chat.messages}/${productId}`, {
|
||||||
`${API_ENDPOINTS.chat.messages}/${productId}`,
|
params: {
|
||||||
{
|
offset,
|
||||||
params: {
|
limit,
|
||||||
offset,
|
},
|
||||||
limit,
|
headers: {
|
||||||
},
|
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`,
|
||||||
headers: {
|
},
|
||||||
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`,
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
enabled: isLoggedIn,
|
enabled: isEnabled,
|
||||||
queryKey: [QUERY_KEYS.chat],
|
queryKey: [QUERY_KEYS.chat],
|
||||||
initialPageParam: {
|
initialPageParam: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type SaveProductRequest = {
|
||||||
|
product_slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSaveProduct = () => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleSaveProduct = async (variables: SaveProductRequest) => {
|
||||||
|
const { data } = await axios.post(`${API_ENDPOINTS.product.save}`, variables);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: SaveProductRequest) => handleSaveProduct(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSaveProduct;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { useAppParams } from "~/composables/global/useAppParams";
|
||||||
|
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type GetSavedProductsResponse = ApiPaginated<ProductListItem>;
|
||||||
|
|
||||||
|
// composable
|
||||||
|
|
||||||
|
const useGetSavedProducts = () => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
const { page } = useAppParams();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleGetSavedProducts = async ({ signal }: { signal: AbortSignal }) => {
|
||||||
|
const { data } = await axios.get<GetSavedProductsResponse>(`${API_ENDPOINTS.products.saved}`, {
|
||||||
|
params: {
|
||||||
|
offset: Number(page.value) * 15 - 15,
|
||||||
|
limit: 15,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEYS.saved_products, page],
|
||||||
|
queryFn: ({ signal }) => handleGetSavedProducts({ signal }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetSavedProducts;
|
||||||
@@ -23,6 +23,7 @@ export const API_ENDPOINTS = {
|
|||||||
comments: "/products/comments",
|
comments: "/products/comments",
|
||||||
create_comment: "/products/comments",
|
create_comment: "/products/comments",
|
||||||
get: "/products",
|
get: "/products",
|
||||||
|
save: "/accounts/favorites/toggle",
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
refresh: "/token/refresh",
|
refresh: "/token/refresh",
|
||||||
@@ -37,6 +38,7 @@ export const API_ENDPOINTS = {
|
|||||||
products: {
|
products: {
|
||||||
get_all: "/products",
|
get_all: "/products",
|
||||||
categories: "/products/categories",
|
categories: "/products/categories",
|
||||||
|
saved: "/accounts/favorites",
|
||||||
},
|
},
|
||||||
resellers_products: {
|
resellers_products: {
|
||||||
get_all: "/products/slider_category",
|
get_all: "/products/slider_category",
|
||||||
@@ -78,6 +80,7 @@ export const QUERY_KEYS = {
|
|||||||
chat: "chat",
|
chat: "chat",
|
||||||
product: "product",
|
product: "product",
|
||||||
products: "products",
|
products: "products",
|
||||||
|
saved_products: "saved-products",
|
||||||
resellers_products: "resellers_products",
|
resellers_products: "resellers_products",
|
||||||
account: "account",
|
account: "account",
|
||||||
categories: "categories",
|
categories: "categories",
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ watch(
|
|||||||
dir="rtl"
|
dir="rtl"
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<main
|
<main class="w-full overflow-x-hidden container flex items-start gap-8 lg:gap-6 min-h-svh relative">
|
||||||
class="w-full overflow-x-hidden container flex items-start gap-8 lg:gap-6 min-h-svh relative"
|
|
||||||
>
|
|
||||||
<ProfileSidebar v-model:isShow="isSidebarShow" />
|
<ProfileSidebar v-model:isShow="isSidebarShow" />
|
||||||
<div class="w-full lg:w-9/12">
|
<div class="w-full lg:w-9/12">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|||||||
@@ -3,5 +3,3 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// imports
|
||||||
|
|
||||||
|
import useGetSavedProducts from "~/composables/api/products/useSavedProducts";
|
||||||
|
|
||||||
|
// meta
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: "پنل کاربری محصولات ذخیره شده",
|
||||||
|
});
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: "check-is-logged-in",
|
||||||
|
layout: "profile",
|
||||||
|
});
|
||||||
|
|
||||||
|
// states
|
||||||
|
|
||||||
|
const { data, isLoading: productsIsLoading } = useGetSavedProducts();
|
||||||
|
|
||||||
|
// computed
|
||||||
|
|
||||||
|
const products = computed(() => {
|
||||||
|
return data.value?.results.flat();
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationData = computed(() => {
|
||||||
|
return data.value?.results.map((_, i: number) => {
|
||||||
|
return { type: "page", value: i };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-col gap-5">
|
||||||
|
<ProfilePageTitle
|
||||||
|
title="علاقهمندی های شما"
|
||||||
|
icon="bi:bookmark"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProfileSection :title="`${paginationData?.length} محصول`">
|
||||||
|
<template #button>
|
||||||
|
<Button
|
||||||
|
end-icon="ci:chevron-left"
|
||||||
|
size="md"
|
||||||
|
class="rounded-full transition-all"
|
||||||
|
variant="solid"
|
||||||
|
@click="navigateTo({ name: 'products-slug' })"
|
||||||
|
>
|
||||||
|
<span class="whitespace-pre @max-[64rem]::text-xs"> همه محصولات </span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div class="w-full">
|
||||||
|
<ul
|
||||||
|
v-if="productsIsLoading"
|
||||||
|
class="grid grid-cols-2 @min-[40rem]:grid-cols-3 @min-[64rem]:grid-cols-4 @min-[80rem]:grid-cols-5 gap-y-8 gap-5 @min-[40rem]: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] @min-[64rem]:!h-[17.25rem] !rounded-2xl': i == 1,
|
||||||
|
'!h-[1.4rem] @min-[64rem]:!h-[1.5rem] !rounded-sm': [2, 3].includes(i),
|
||||||
|
'!w-1/2 @min-[64rem]:!w-full': i == 2,
|
||||||
|
'@min-[64rem]:!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 @min-[64rem]:mt-10"
|
||||||
|
>
|
||||||
|
<Pagination
|
||||||
|
:items="paginationData"
|
||||||
|
:total="data.count"
|
||||||
|
:per-page="15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ProfileSection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Vendored
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
export { };
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
type ApiPaginated<T> = {
|
type ApiPaginated<T> = {
|
||||||
@@ -104,6 +104,7 @@ declare global {
|
|||||||
meta_rating: number;
|
meta_rating: number;
|
||||||
category: SubCategory;
|
category: SubCategory;
|
||||||
colors: string[];
|
colors: string[];
|
||||||
|
added_to_favorites: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">;
|
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">;
|
||||||
@@ -235,7 +236,7 @@ declare global {
|
|||||||
discount_code: DiscountCode;
|
discount_code: DiscountCode;
|
||||||
items: CartItem[];
|
items: CartItem[];
|
||||||
cart_total: string;
|
cart_total: string;
|
||||||
items_discount_amount: string
|
items_discount_amount: string;
|
||||||
tax_amount: string;
|
tax_amount: string;
|
||||||
final_price: string;
|
final_price: string;
|
||||||
address: Address;
|
address: Address;
|
||||||
|
|||||||
Reference in New Issue
Block a user