This commit is contained in:
Parsa Nazer
2025-01-14 22:03:02 +03:30
19 changed files with 372 additions and 60 deletions
+17 -11
View File
@@ -1,17 +1,16 @@
<script setup lang="ts">
// types
import { useImageColor } from "~/composables/useImageColor";
import { useImageColor } from "~/composables/global/useImageColor";
type Props = {
id: number,
id: number;
category: string;
count: number;
description: string;
picture: string;
darkLayer?: boolean;
}
};
// props
@@ -21,7 +20,6 @@ const { id } = toRefs(props);
// state
const { colorObject } = useImageColor(`#category-image-${id.value}`);
</script>
<template>
@@ -37,10 +35,15 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
class="bg-linear-to-t from-black/50 to-transparent to-40% absolute z-10 size-full"
/>
<div class="absolute z-20 bottom-0 p-6 flex items-end justify-between w-full">
<div
class="absolute z-20 bottom-0 p-6 flex items-end justify-between w-full"
>
<div
:class="(colorObject?.isLight && !darkLayer) ? 'text-black' : 'text-white'"
:class="
colorObject?.isLight && !darkLayer
? 'text-black'
: 'text-white'
"
class="flex flex-col gap-2"
>
<div class="typo-s-h-md">
@@ -56,9 +59,12 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
size="24"
name="ci:arrow-left"
class="mb-1"
:class="(colorObject?.isLight && !darkLayer) ? '**:stroke-black' : '**:stroke-white'"
:class="
colorObject?.isLight && !darkLayer
? '**:stroke-black'
: '**:stroke-white'
"
/>
</div>
</div>
</template>
</template>
+13 -14
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
// import
import useGetAccount from "~/composables/api/account/useGetAccount";
import { useAuth } from "~/composables/api/auth/useAuth";
// types
@@ -19,24 +19,24 @@ const { logout } = useAuth();
const nav_links = ref<NavLink[]>([
{
title: "فروشگاه",
path: "#"
path: "#",
},
{
title: "دسته بندی ها",
path: "#"
path: "#",
},
{
title: "جستجو",
path: "#"
path: "#",
},
{
title: "ارتباط با ما",
path: "#"
path: "#",
},
{
title: "امکانات",
path: "#"
}
path: "#",
},
]);
</script>
@@ -45,16 +45,15 @@ const nav_links = ref<NavLink[]>([
<div
class="size-full flex items-center justify-between container py-[2.25rem]"
>
<div v-if="!!account" class="w-2/12 flex items-center justify-start">
<div
v-if="!!account"
class="w-2/12 flex items-center justify-start"
>
<span class="size-[2rem] bg-black rounded-full"></span>
<button @click="() => logout(true)">
خروج از وبسایت
</button>
<button @click="() => logout(true)">خروج از وبسایت</button>
</div>
<div v-else class="text-black">
KIR
</div>
<div v-else class="text-black">KIR</div>
<nav
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
+59 -9
View File
@@ -1,18 +1,68 @@
<script setup lang="ts"></script>
<script setup lang="ts">
// types
type Props = {
total: number;
items: {
type: "page" | "not-page";
value: number;
}[];
};
// props
defineProps<Props>();
</script>
<template>
<PaginationRoot>
<PaginationList v-slot="{ items }">
<PaginationFirst />
<PaginationPrev />
<PaginationRoot
:total="100"
:sibling-count="1"
:items-per-page="10"
show-edges
>
<PaginationList
v-slot="{ items }"
class="flex items-center gap-1 text-stone-700 dark:text-white"
>
<PaginationFirst
class="w-9 h-9 flex items-center justify-center bg-transparent hover:bg-white dark:hover:bg-stone-700/70 transition disabled:opacity-50 rounded-lg"
>
<Icon name="ci:double-arrow-left" />
</PaginationFirst>
<PaginationPrev
class="w-9 h-9 flex items-center justify-center bg-transparent hover:bg-white dark:hover:bg-stone-700/70 transition mr-4 disabled:opacity-50 rounded-lg"
>
<Icon name="ci:chevron-left" />
</PaginationPrev>
<template v-for="(page, index) in items">
<PaginationListItem v-if="page.type === 'page'" :key="index" />
<PaginationEllipsis v-else :key="page.type" :index="index">
<PaginationListItem
v-if="page.type === 'page'"
:key="index"
class="w-9 h-9 border dark:border-stone-800 rounded-lg data-[selected]:!bg-white data-[selected]:shadow-sm data-[selected]:text-blackA11 hover:bg-white dark:hover:bg-stone-700/70 transition"
:value="page.value"
>
{{ page.value }}
</PaginationListItem>
<PaginationEllipsis
v-else
:key="page.type"
:index="index"
class="w-9 h-9 flex items-center justify-center"
>
&#8230;
</PaginationEllipsis>
</template>
<PaginationNext />
<PaginationLast />
<PaginationNext
class="w-9 h-9 flex items-center justify-center bg-transparent hover:bg-white dark:hover:bg-stone-700/70 transition ml-4 disabled:opacity-50 rounded-lg"
>
<Icon name="ci:chevron-right" />
</PaginationNext>
<PaginationLast
class="w-9 h-9 flex items-center justify-center bg-transparent hover:bg-white dark:hover:bg-stone-700/70 transition disabled:opacity-50 rounded-lg"
>
<Icon name="ci:double-arrow-right" />
</PaginationLast>
</PaginationList>
</PaginationRoot>
</template>
@@ -1,11 +1,11 @@
<script setup>
// imports
import { useToast } from "~/composables/global/useToast";
// state
import ToastBox from "~/components/ui/ToastContainer/ToastBox.vue";
const { toasts } = useToast();
</script>
<template>
@@ -16,4 +16,4 @@ const { toasts } = useToast();
:message="toast.message"
:options="toast.options"
/>
</template>
</template>
@@ -7,7 +7,11 @@ import { PRODUCT_RANGE } from "~/constants";
const params = useUrlSearchParams("history");
const sort_filter = ref(["جدیدترین ها", "گران ترین ها", "ارزان ترین ها"]);
const sort_filter = ref([
{ title: "جدیدترین ها", value: "newest" },
{ title: "گران ترین ها", value: "price" },
{ title: "ارزان ترین ها", value: "-price" },
]);
const options = [
{
@@ -63,15 +67,15 @@ const resetFilters = () => {
<button
v-for="(sort, index) in sort_filter"
:key="index"
@click="params.sort = sort"
@click="params.sort = sort.value"
:class="
params.sort == sort
params.sort == sort.value
? 'bg-black text-white'
: 'bg-slate-100'
"
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-full text-sm"
>
{{ sort }}
{{ sort.title }}
</button>
</div>
</div>
@@ -2,6 +2,7 @@
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
import { useAuth } from "~/composables/api/auth/useAuth";
// types
@@ -12,6 +13,7 @@ const useGetAccount = () => {
// state
const { $axios: axios } = useNuxtApp();
const { token } = useAuth();
// methods
@@ -21,6 +23,7 @@ const useGetAccount = () => {
};
return useQuery({
enabled: !!token.value,
queryKey: [QUERY_KEYS.account],
queryFn: () => handleGetAccount()
});
-10
View File
@@ -1,5 +1,3 @@
import useGetAccount from "~/composables/api/account/useGetAccount";
export const useAuth = () => {
// state
@@ -17,14 +15,6 @@ export const useAuth = () => {
if (reload) window.location.reload();
};
// watch
watch(() => token.value, (newValue) => {
token.value = newValue;
}, {
immediate: true
});
return { token, updateToken, logout };
};
@@ -0,0 +1,115 @@
// imports
import { QueryClient, useMutation } from "@tanstack/vue-query";
import type { InfiniteData } from "@tanstack/vue-query";
import { API_ENDPOINTS, MUTATION_KEYS, QUERY_KEYS } from "~/constants";
// types
export type CreateChatMessageRequest = {
productId: string | number;
new_message: string;
};
export type CreateChatMessageResponse = Chat[]
const useCreateChatMessage = (queryClient: QueryClient) => {
// state
const { $axios: axios } = useNuxtApp();
// method
const handleCreateChatMessage = async (variables: CreateChatMessageRequest) => {
const { data } = await axios.post<CreateChatMessageResponse>(`${API_ENDPOINTS.chat.new_message}/${variables.productId}`, variables);
return data;
};
return useMutation({
mutationKey: [MUTATION_KEYS.create_chat],
mutationFn: (variables: CreateChatMessageRequest) => handleCreateChatMessage(variables),
onMutate: (newMessage) => {
const prevData = queryClient.getQueriesData({ queryKey: [QUERY_KEYS.chat] });
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
const lastPage = oldData!.pages[oldData!.pages.length - 1];
return {
pages: [
{
count: lastPage.count,
next: lastPage.next,
previous: lastPage.previous,
results: [
{
id: Date.now(),
content: newMessage.new_message,
sender: "user"
}
]
},
...oldData!.pages
],
pageParams: [
...oldData!.pageParams,
{
limit: 10,
offset: 0
}
]
};
});
return { prevData: prevData ? prevData[0][1] : undefined };
},
onSuccess: (response) => {
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
if (oldData) {
const lastPage = oldData!.pages[oldData!.pages.length - 1];
return {
pages: [
{
count: lastPage.count,
next: lastPage.next,
previous: lastPage.previous,
results: {
...response[0],
id: Date.now()
}
},
...oldData!.pages
],
pageParams: [
...oldData!.pageParams,
{
limit: 10,
offset: 0
}
]
};
}
return oldData;
});
},
onError: (err, newMessage, context) => {
if (context) {
queryClient.setQueryData(
[QUERY_KEYS.chat],
context.prevData
);
}
},
onSettled: (newMessage) => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.chat] });
}
});
};
export default useCreateChatMessage;
@@ -0,0 +1,62 @@
// imports
import { useInfiniteQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetBranchResponse = ApiPaginated<Chat>;
const useGetBranch = (
productId: string | number,
enabled: Ref<boolean>
) => {
// state
const { $axios: axios } = useNuxtApp();
// method
const handleGetChat = async ({ productId, limit, offset }: {
productId: number | string,
limit: number,
offset: number
}) => {
const { data } = await axios.get<GetBranchResponse>(`${API_ENDPOINTS.chat.messages}/${productId}`, {
params: {
offset,
limit
},
headers: {
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`
}
});
return data;
};
return useInfiniteQuery({
enabled,
queryKey: [QUERY_KEYS.chat],
initialPageParam: {
limit: 10,
offset: 0
},
queryFn: ({ pageParam }) => handleGetChat({
limit: pageParam.limit,
offset: pageParam.offset,
productId: productId
}),
getNextPageParam: (lastPage, pages) => {
if (!lastPage.next) return undefined;
return {
limit: 10,
offset: pages.length * 10
};
}
});
};
export default useGetBranch;
@@ -0,0 +1,60 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetProductsResponse = Product[];
export type GetProductsFilters = {
search: string | undefined;
sort: string | undefined;
categories: string[] | undefined;
price_range: number[] | undefined;
has_discount: boolean | undefined;
in_stock: boolean | undefined;
};
// composable
const useGetProducts = (
filters: GetProductsFilters,
page: ComputedRef<number>
) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetProducts = async ({
filters,
page,
}: {
filters: GetProductsFilters;
page: number;
}) => {
const { data } = await axios.get<GetProductsResponse>(
`${API_ENDPOINTS.products.get_all}`,
{
params: {
...filters,
page,
offest: page * 10 - 10,
limit: 10,
},
}
);
return data;
};
return useQuery({
staleTime: 60 * 1000,
queryKey: [QUERY_KEYS.products, filters, page],
queryFn: () => handleGetProducts({ filters, page: page.value }),
});
};
export default useGetProducts;
+6 -2
View File
@@ -1,10 +1,10 @@
export const API_ENDPOINTS = {
account: {
profile : "/accounts/profile",
profile: "/accounts/profile",
send_otp: "/accounts/send_otp",
},
product: {
get : "/products",
get: "/products",
},
auth: {
signin: "/token",
@@ -14,11 +14,15 @@ export const API_ENDPOINTS = {
messages: "/chat/product",
new_message: "/chat/product",
},
products: {
get_all: "/products",
},
};
export const QUERY_KEYS = {
chat: "chat",
product: "product",
products: "products",
account: "account",
};
+1 -1
View File
@@ -1,7 +1,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
ssr: false,
ssr: true,
devtools: { enabled: false },
css: ["~/assets/css/tailwind.css", "swiper/css"],
+18 -1
View File
@@ -1,11 +1,28 @@
<script setup lang="ts">
// import
import useGetProducts, {
type GetProductsFilters,
} from "~/composables/api/products/useGetProducts";
import { PRODUCT_RANGE } from "~/constants";
// state
const params = useUrlSearchParams("history");
const route = useRoute();
const params: GetProductsFilters & { page: number } =
useUrlSearchParams("history");
// computed
const page = computed(() => (route.query["page"] ? +route.query["page"] : 1));
// queries
const { data: products, isLoading: productsIsLoading } = useGetProducts(
params,
page
);
// life-cycle
+3 -1
View File
@@ -5,10 +5,12 @@
import { helpers, required } from "@vuelidate/validators";
import { useVuelidate } from "@vuelidate/core";
import useOtp from "~/composables/api/auth/useOtp";
import { useTimer } from "~/composables/useTimer";
import useSignIn from "~/composables/api/auth/useSignIn";
import { definePageMeta } from "#imports";
import useGetAccount from "~/composables/api/account/useGetAccount";
import { useAuth } from "~/composables/api/auth/useAuth";
import { useToast } from "~/composables/global/useToast";
import { useTimer } from "~/composables/global/useTimer";
// types
+3 -3
View File
@@ -1,13 +1,13 @@
import axiosOriginal from "axios";
import { useAuth } from "~/composables/api/auth/useAuth";
import { API_ENDPOINTS } from "~/constants";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const { logout, token } = useAuth();
const { token } = useAuth();
const axios = axiosOriginal.create({
baseURL: config.public.API_BASE_URL
baseURL: config.public.API_BASE_URL,
});
axios.interceptors.request.use((config) => {