merage
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@ dist
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
.logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
|
||||
@@ -47,7 +47,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<div class="typo-s-h-md">
|
||||
تمام دسته ها
|
||||
{{ category }}
|
||||
<span class="typo-p-xs -translate-y-1 inline-block mr-1">
|
||||
24
|
||||
</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot class="relative" dir="rtl" :multiple="true" v-model="value">
|
||||
<ComboboxRoot class="relative" dir="rtl" v-model="value">
|
||||
<ComboboxAnchor
|
||||
class="w-full inline-flex items-center justify-between rounded-full border border-slate-200 hover:border-slate-300 focus:border-slate-800 px-[1rem] text-sm leading-none py-3.5 gap-[5px] bg-slate-50 text-black hover:bg-white/90 transition-all data-[placeholder]:text-black/80 typo-label-sm outline-none"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,7 +53,13 @@ const nav_links = ref<NavLink[]>([
|
||||
<button @click="() => logout(true)">خروج از وبسایت</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-black">KIR</div>
|
||||
<button
|
||||
@click="navigateTo('/signin')"
|
||||
class="cursor-pointer"
|
||||
v-else
|
||||
>
|
||||
وارد شوید
|
||||
</button>
|
||||
|
||||
<nav
|
||||
class="flex-center gap-[2.5rem] w-8/12 typo-label-sm text-slate-500"
|
||||
|
||||
@@ -28,7 +28,7 @@ const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const classes = computed(() => {
|
||||
return [
|
||||
"flex items-center cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3 rounded-100",
|
||||
"flex items-center justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3 rounded-100",
|
||||
{
|
||||
"input-solid": variant.value === "solid",
|
||||
"input-outlined": variant.value === "outlined",
|
||||
@@ -56,7 +56,7 @@ const onInput = (e: any) => {
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
ref="inputRef"
|
||||
class="outline-none w-max"
|
||||
class="outline-none flex-1"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { GetProductsFilters } from "~/composables/api/products/useGetProducts";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
total: number;
|
||||
items: {
|
||||
type: "page" | "not-page";
|
||||
type: string;
|
||||
value: number;
|
||||
}[];
|
||||
};
|
||||
@@ -12,34 +14,48 @@ type Props = {
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
// state
|
||||
|
||||
const params: GetProductsFilters = inject("params");
|
||||
|
||||
const page = ref(Number(params.page) ?? 1);
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => page.value,
|
||||
(newPage) => {
|
||||
params.page = newPage;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationRoot
|
||||
:total="100"
|
||||
:total="total"
|
||||
:sibling-count="1"
|
||||
:items-per-page="10"
|
||||
:items-per-page="9"
|
||||
show-edges
|
||||
v-model:page="page"
|
||||
>
|
||||
<PaginationList
|
||||
v-slot="{ items }"
|
||||
class="flex items-center gap-1 text-stone-700 dark:text-white"
|
||||
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<PaginationLast
|
||||
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
>
|
||||
<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="bi:chevron-double-right" class="**:stroke-black" />
|
||||
</PaginationLast>
|
||||
<PaginationNext
|
||||
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed 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>
|
||||
<Icon name="bi:chevron-right" class="**:stroke-black" />
|
||||
</PaginationNext>
|
||||
|
||||
<template v-for="(page, index) in items">
|
||||
<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"
|
||||
class="w-9 h-9 border border-stone-800 rounded-lg data-[selected]:!bg-black data-[selected]:text-white data-[selected]:shadow-sm hover:bg-stone-700/20 transition"
|
||||
:value="page.value"
|
||||
>
|
||||
{{ page.value }}
|
||||
@@ -53,16 +69,17 @@ defineProps<Props>();
|
||||
…
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
<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"
|
||||
|
||||
<PaginationPrev
|
||||
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition mr-4 disabled:opacity-50 disabled:cursor-not-allowed 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="bi:chevron-left" class="**:stroke-black" />
|
||||
</PaginationPrev>
|
||||
<PaginationFirst
|
||||
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-stone-700/20 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
>
|
||||
<Icon name="ci:double-arrow-right" />
|
||||
</PaginationLast>
|
||||
<Icon name="bi:chevron-double-left" class="**:stroke-black" />
|
||||
</PaginationFirst>
|
||||
</PaginationList>
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@ watch(
|
||||
</Transition>
|
||||
<Transition name="fade-right">
|
||||
<div
|
||||
v-if="isSideShow"
|
||||
v-show="isSideShow"
|
||||
id="side-content"
|
||||
class="hidden md:flex w-1/3 bg-white h-full rounded-e-[1.5rem] overflow-hidden absolute top-0 min-md:flex-col"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
// import
|
||||
|
||||
import type { ToastOptions } from "~/composables/useToast";
|
||||
import type { ToastOptions } from "~/composables/global/useToast";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
// type
|
||||
|
||||
|
||||
@@ -42,23 +42,23 @@ defineProps<Props>();
|
||||
</Tag>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex justify-between items-center"
|
||||
class="absolute inset-x-0 bottom-0 pb-6 px-6 flex flex-row-reverse justify-between items-end"
|
||||
>
|
||||
<span class="typo-p-md"> ${{ price }} </span>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<span class="typo-p-md"> {{ price }} </span>
|
||||
<div class="flex flex-col gap-2 items-start">
|
||||
<span class="typo-p-md">
|
||||
{{ brand }}
|
||||
</span>
|
||||
<span class="typo-sub-h-md">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<!-- <div class="flex items-center gap-2 mt-1">
|
||||
<ColorCircle
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetProducts, {
|
||||
type GetProductsFilters,
|
||||
} from "~/composables/api/products/useGetProducts";
|
||||
import { useParams } from "~/composables/global/useParams";
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const params = useUrlSearchParams("history");
|
||||
const params: GetProductsFilters = inject("params");
|
||||
|
||||
const sort_filter = ref([
|
||||
{ title: "جدیدترین ها", value: "newest" },
|
||||
@@ -13,6 +17,13 @@ const sort_filter = ref([
|
||||
{ title: "ارزان ترین ها", value: "-price" },
|
||||
]);
|
||||
|
||||
const sliderValue = ref([PRODUCT_RANGE.min, PRODUCT_RANGE.max]);
|
||||
|
||||
const has_discount = ref(JSON.parse(params.has_discount) ?? false);
|
||||
const in_stock = ref(JSON.parse(params.in_stock) ?? false);
|
||||
|
||||
const sliderValueDebounced = refDebounced(sliderValue, 1000);
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: "میوه",
|
||||
@@ -42,15 +53,47 @@ const options = [
|
||||
},
|
||||
];
|
||||
|
||||
const { isPending: productsIsPending } = useGetProducts(params);
|
||||
|
||||
// methods
|
||||
|
||||
const resetFilters = () => {
|
||||
params.sort = null;
|
||||
params.categories = [];
|
||||
params.range = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
|
||||
params.has_discount = false;
|
||||
params.in_stock = false;
|
||||
params.search = "";
|
||||
params.sort = "newest";
|
||||
sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
|
||||
has_discount.value = false;
|
||||
in_stock.value = false;
|
||||
params.category = "";
|
||||
};
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => sliderValueDebounced.value,
|
||||
(newValue) => {
|
||||
params.price_gte = newValue[0];
|
||||
params.price_lte = newValue[1];
|
||||
}
|
||||
);
|
||||
|
||||
watchOnce(
|
||||
() => [params.price_gte, params.price_lte],
|
||||
([newGte, newLte]) => {
|
||||
sliderValue.value[0] = newGte;
|
||||
sliderValue.value[1] = newLte;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [has_discount.value, in_stock.value],
|
||||
([newHasDiscount, newInStock]) => {
|
||||
params.has_discount = newHasDiscount;
|
||||
params.in_stock = newInStock;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -87,14 +130,15 @@ const resetFilters = () => {
|
||||
<Icon name="ci:grid" size="24" />
|
||||
دسته بندی
|
||||
</div>
|
||||
<ComboBox :options="options" v-model="params.categories" />
|
||||
<div class="w-full flex flex-wrap gap-2 px-[1rem]">
|
||||
<ComboBox :options="options" v-model="params.category" />
|
||||
<div
|
||||
v-if="params.category"
|
||||
class="w-full flex flex-wrap gap-2 px-[1rem]"
|
||||
>
|
||||
<span
|
||||
v-for="(sort_param, index) in params.categories"
|
||||
:key="index"
|
||||
class="py-1 px-3 cursor-pointer text-nowrap bg-slate-100 rounded-full text-sm"
|
||||
>
|
||||
{{ sort_param }}
|
||||
{{ params.category }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,12 +151,12 @@ const resetFilters = () => {
|
||||
محدوده قیمت
|
||||
</div>
|
||||
<SliderRoot
|
||||
v-model="params.range"
|
||||
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="1"
|
||||
:step="1000"
|
||||
>
|
||||
<SliderTrack
|
||||
class="bg-black/10 relative grow rounded-full h-[3px]"
|
||||
@@ -132,8 +176,8 @@ const resetFilters = () => {
|
||||
<span class="text-sm text-black">حداقل</span>
|
||||
<span class="text-sm text-black">
|
||||
{{
|
||||
"range" in params
|
||||
? params.range[0].toLocaleString()
|
||||
"price_gte" in params
|
||||
? params.price_gte.toLocaleString()
|
||||
: PRODUCT_RANGE.min
|
||||
}}
|
||||
</span>
|
||||
@@ -142,8 +186,8 @@ const resetFilters = () => {
|
||||
<span class="text-sm text-black">حداکثر</span>
|
||||
<span class="text-sm text-black">
|
||||
{{
|
||||
"range" in params
|
||||
? params.range[1].toLocaleString()
|
||||
"price_lte" in params
|
||||
? params.price_lte.toLocaleString()
|
||||
: PRODUCT_RANGE.max
|
||||
}}
|
||||
</span>
|
||||
@@ -154,7 +198,7 @@ const resetFilters = () => {
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<span class="text-black">فقط کالاهای تخفیف دار</span>
|
||||
<SwitchRoot
|
||||
v-model="params.has_discount"
|
||||
v-model="has_discount"
|
||||
class="w-[3rem] h-[1.8rem] flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-stone-800 border border-slate-300 data-[state=checked]:border-stone-700 rounded-full relative transition-all focus-within:outline-none"
|
||||
>
|
||||
<SwitchThumb
|
||||
@@ -167,7 +211,7 @@ const resetFilters = () => {
|
||||
<span class="text-black">فقط کالاهای موجود</span>
|
||||
|
||||
<SwitchRoot
|
||||
v-model="params.in_stock"
|
||||
v-model="in_stock"
|
||||
class="w-[3rem] h-[1.8rem] flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-stone-800 border border-slate-300 data-[state=checked]:border-stone-700 rounded-full relative transition-all focus-within:outline-none"
|
||||
>
|
||||
<SwitchThumb
|
||||
@@ -178,12 +222,21 @@ const resetFilters = () => {
|
||||
</div>
|
||||
|
||||
<Button
|
||||
end-icon="ci:close"
|
||||
:disabled="productsIsPending"
|
||||
variant="solid"
|
||||
@click="resetFilters"
|
||||
class="rounded-full py-4 !cursor-pointer"
|
||||
class="rounded-full py-4 !cursor-pointer disabled:pointer-events-none"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -61,6 +61,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
:key="slide.id"
|
||||
>
|
||||
<CategoryCard
|
||||
:id="slide.id"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
|
||||
@@ -7,6 +7,7 @@ import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
||||
import { useIsMutating } from "@tanstack/vue-query";
|
||||
import { MUTATION_KEYS } from "~/constants";
|
||||
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// provide-inject
|
||||
|
||||
@@ -14,6 +15,8 @@ const { isOpen } = inject("isOpen") as any;
|
||||
|
||||
// state
|
||||
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string | number;
|
||||
|
||||
@@ -126,7 +129,9 @@ whenever(
|
||||
>
|
||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||
|
||||
<template v-if="isLoggedIn">
|
||||
<Transition name="zoom" mode="out-in">
|
||||
|
||||
<div
|
||||
v-if="!isChatPending"
|
||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||
@@ -168,6 +173,7 @@ whenever(
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||
@@ -175,6 +181,10 @@ whenever(
|
||||
<AiLoading />
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<div class="text-black p-4.5 size-full flex justify-center items-center" v-else>
|
||||
Please sign in first
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
||||
import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
// state
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ export const useAuth = () => {
|
||||
// state
|
||||
|
||||
const token = useCookie("token");
|
||||
const refreshToken = useCookie("refresh-token");
|
||||
|
||||
// method
|
||||
|
||||
@@ -10,11 +11,20 @@ export const useAuth = () => {
|
||||
token.value = newToken;
|
||||
};
|
||||
|
||||
const updateRefreshToken = (newToken: string) => {
|
||||
refreshToken.value = newToken;
|
||||
};
|
||||
|
||||
const logout = (reload ?: boolean) => {
|
||||
token.value = undefined;
|
||||
refreshToken.value = undefined;
|
||||
if (reload) window.location.reload();
|
||||
};
|
||||
|
||||
return { token, updateToken, logout };
|
||||
// computed
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
|
||||
return { token, refreshToken, updateRefreshToken, updateToken, logout, isLoggedIn };
|
||||
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type RefreshAuthRequest = {
|
||||
refresh: string,
|
||||
};
|
||||
|
||||
export type RefreshAuthResponse = {
|
||||
access: string,
|
||||
refresh: string,
|
||||
};
|
||||
|
||||
|
||||
const useRefreshAuth = () => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleRefreshAuth = async (variables: RefreshAuthRequest) => {
|
||||
const { data } = await axios.post<RefreshAuthResponse>(`${API_ENDPOINTS.auth.refresh}/`, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: RefreshAuthRequest) => handleRefreshAuth(variables)
|
||||
});
|
||||
};
|
||||
|
||||
export default useRefreshAuth;
|
||||
@@ -0,0 +1,30 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type VerifyRequest = {
|
||||
token: string,
|
||||
};
|
||||
|
||||
const useVerify = () => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleVerify = async (variables: VerifyRequest) => {
|
||||
const { data } = await axios.post(`${API_ENDPOINTS.auth.verify}`, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: VerifyRequest) => handleVerify(variables)
|
||||
});
|
||||
};
|
||||
|
||||
export default useVerify;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useInfiniteQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// types
|
||||
|
||||
@@ -16,6 +17,8 @@ const useGetBranch = (
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
// method
|
||||
|
||||
const handleGetChat = async ({ productId, limit, offset }: {
|
||||
@@ -37,7 +40,7 @@ const useGetBranch = (
|
||||
};
|
||||
|
||||
return useInfiniteQuery({
|
||||
enabled,
|
||||
enabled: isLoggedIn,
|
||||
queryKey: [QUERY_KEYS.chat],
|
||||
initialPageParam: {
|
||||
limit: 10,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetCategoriesResponse = { categories: Category[] };
|
||||
|
||||
const useGetCategories = () => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetCategories = async () => {
|
||||
const { data } = await axios.get<GetCategoriesResponse>(`${API_ENDPOINTS.products.categories}`);
|
||||
return data.categories;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.categories],
|
||||
queryFn: () => handleGetCategories()
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetCategories;
|
||||
@@ -5,44 +5,53 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetProductsResponse = Product[];
|
||||
export type GetProductsResponse = ApiPaginated<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;
|
||||
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 useGetProducts = (
|
||||
filters: GetProductsFilters,
|
||||
page: ComputedRef<number>
|
||||
) => {
|
||||
const useGetProducts = (params?: GetProductsFilters) => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
const {
|
||||
search,
|
||||
sort,
|
||||
in_stock,
|
||||
has_discount,
|
||||
category,
|
||||
price_gte,
|
||||
price_lte,
|
||||
page,
|
||||
} = toRefs(params as GetProductsFilters);
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetProducts = async ({
|
||||
filters,
|
||||
page,
|
||||
}: {
|
||||
filters: GetProductsFilters;
|
||||
page: number;
|
||||
}) => {
|
||||
const handleGetProducts = async (params?: GetProductsFilters) => {
|
||||
const { data } = await axios.get<GetProductsResponse>(
|
||||
`${API_ENDPOINTS.products.get_all}`,
|
||||
{
|
||||
params: {
|
||||
...filters,
|
||||
page,
|
||||
offest: page * 10 - 10,
|
||||
limit: 10,
|
||||
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,
|
||||
offest: params?.page! * 9 - 9,
|
||||
limit: 9,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -52,8 +61,18 @@ const useGetProducts = (
|
||||
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [QUERY_KEYS.products, filters, page],
|
||||
queryFn: () => handleGetProducts({ filters, page: page.value }),
|
||||
queryKey: [
|
||||
QUERY_KEYS.products,
|
||||
search,
|
||||
sort,
|
||||
in_stock,
|
||||
has_discount,
|
||||
category,
|
||||
price_gte,
|
||||
price_lte,
|
||||
page,
|
||||
],
|
||||
queryFn: () => handleGetProducts(params),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ export const API_ENDPOINTS = {
|
||||
get: "/products",
|
||||
},
|
||||
auth: {
|
||||
refresh: "/token/refresh",
|
||||
verify: "/accounts/verify",
|
||||
signin: "/token",
|
||||
logout: "/accounts/logout",
|
||||
},
|
||||
@@ -16,6 +18,7 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
products: {
|
||||
get_all: "/products",
|
||||
categories: "/products/categories",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +27,7 @@ export const QUERY_KEYS = {
|
||||
product: "product",
|
||||
products: "products",
|
||||
account: "account",
|
||||
categories: "categories",
|
||||
};
|
||||
|
||||
export const MUTATION_KEYS = {
|
||||
|
||||
@@ -1,3 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
import useRefreshAuth from "~/composables/api/auth/useRefreshAuth";
|
||||
import useVerify from "~/composables/api/auth/useVerify";
|
||||
|
||||
// state
|
||||
|
||||
const { mutateAsync: refreshAuth } = useRefreshAuth();
|
||||
const { token, refreshToken, updateToken, updateRefreshToken, logout } = useAuth();
|
||||
const { mutateAsync: verify } = useVerify();
|
||||
|
||||
// lifecycle
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
if (!!token.value) {
|
||||
|
||||
// 1.1 - token is there
|
||||
|
||||
try {
|
||||
|
||||
await verify({
|
||||
token: token.value
|
||||
});
|
||||
|
||||
// 2.1 - token is valid, finish
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// 2.2 - token is there, but not valid, try to refresh token
|
||||
|
||||
if (!!refreshToken.value) {
|
||||
|
||||
// 3.1 - refresh token is there, try to refresh
|
||||
|
||||
try {
|
||||
const refreshResponse = await refreshAuth({ refresh: refreshToken.value });
|
||||
|
||||
// 4.1 - token is refreshed successfully, finish
|
||||
|
||||
updateToken(refreshResponse.access);
|
||||
updateRefreshToken(refreshResponse.refresh);
|
||||
} catch (e) {
|
||||
|
||||
// 4.2 - cant refreshing token, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
} else {
|
||||
|
||||
// 3.2 - refresh token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// 1.2 - token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { token } = useAuth();
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
if (token.value) {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { token, logout } = useAuth();
|
||||
|
||||
if (!!token.value) {
|
||||
return;
|
||||
} else {
|
||||
return navigateTo("/signin");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
|
||||
const { token } = useAuth();
|
||||
|
||||
@@ -5,6 +5,10 @@ export default defineNuxtConfig({
|
||||
devtools: { enabled: false },
|
||||
css: ["~/assets/css/tailwind.css", "swiper/css"],
|
||||
|
||||
routeRules: {
|
||||
"/products": { prerender: false, ssr: false },
|
||||
},
|
||||
|
||||
postcss: {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
@@ -44,6 +48,7 @@ export default defineNuxtConfig({
|
||||
"@nuxt/icon",
|
||||
"reka-ui/nuxt",
|
||||
"@vueuse/nuxt",
|
||||
"@formkit/auto-animate/nuxt"
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
|
||||
Generated
+15
-1
@@ -7,6 +7,7 @@
|
||||
"name": "nuxt-app",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@nuxt/icon": "^1.9.1",
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@tanstack/vue-query": "^5.62.2",
|
||||
@@ -24,7 +25,8 @@
|
||||
"universal-cookie": "^7.2.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vue-scrollto": "^2.20.0"
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-skeletor": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
||||
@@ -1007,6 +1009,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@formkit/auto-animate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.2.tgz",
|
||||
"integrity": "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/collections": {
|
||||
"version": "1.0.492",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.492.tgz",
|
||||
@@ -10774,6 +10782,12 @@
|
||||
"bezier-easing": "2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-skeletor": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-skeletor/-/vue-skeletor-1.0.6.tgz",
|
||||
"integrity": "sha512-ER4vHlFSXCW3ixK2DlczUE6CZliHsn4d2TvZ9/26C6Oq8zoyEY23BsqweMPtF8QULSz1+G5m2New1BwKNVOZhQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@nuxt/icon": "^1.9.1",
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@tanstack/vue-query": "^5.62.2",
|
||||
@@ -29,7 +30,8 @@
|
||||
"universal-cookie": "^7.2.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vue-scrollto": "^2.20.0"
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-skeletor": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0-beta.5",
|
||||
|
||||
@@ -1,68 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import useGetCategories from "~/composables/api/product/useGetCategories";
|
||||
|
||||
// state
|
||||
|
||||
const { data: categories, suspense } = useGetCategories();
|
||||
|
||||
const search = ref("");
|
||||
const debouncedSearch = refDebounced(search, 300);
|
||||
|
||||
// computed
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
if (debouncedSearch.value.length > 0) {
|
||||
return categories.value!.filter(cat => cat.name.includes(debouncedSearch.value));
|
||||
}
|
||||
|
||||
return categories.value!;
|
||||
});
|
||||
|
||||
// lifecycle
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
const response = await suspense();
|
||||
|
||||
if (response.isError) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Error in categories page prefetch`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="mt-20">
|
||||
<div class="mt-20 flex gap-6 justify-between items-center">
|
||||
<span class="typo-h-3 text-black">دسته بندی ها</span>
|
||||
<Input
|
||||
class="max-w-[400px] w-full"
|
||||
variant="outlined"
|
||||
placeholder="جستجو..."
|
||||
v-model="search"
|
||||
>
|
||||
<template #endItem>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon class="translate-y-[-1px]" name="ci:search" size="24" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4 w-full mt-12">
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="filteredCategories.length !== 0"
|
||||
v-auto-animate
|
||||
class="grid grid-cols-3 gap-4 w-full mt-12"
|
||||
>
|
||||
<CategoryCard
|
||||
:id="1"
|
||||
category="یک دسته بندی تست"
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:id="category.id"
|
||||
:category="category.name"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
dark-layer
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="2"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-2.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="3"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-3.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="8"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-4.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="4"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-5.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="5"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="6"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-2.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="7"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-3.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex w-full mt-12"
|
||||
>
|
||||
<div
|
||||
class="flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-100 flex-center"
|
||||
>
|
||||
<Icon name="bi:search" size="50" class="**:fill-gray-500" />
|
||||
<span class="text-lg text-gray-500">
|
||||
دسته بندی یافت نشد :(
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
+65
-22
@@ -8,31 +8,48 @@ import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
const params: GetProductsFilters = useUrlSearchParams("history", {
|
||||
initialValue: {
|
||||
search: "",
|
||||
sort: "newest",
|
||||
price_gte: PRODUCT_RANGE.min,
|
||||
price_lte: PRODUCT_RANGE.max,
|
||||
in_stock: false,
|
||||
has_discount: false,
|
||||
category: "",
|
||||
page: "1",
|
||||
},
|
||||
});
|
||||
|
||||
const params: GetProductsFilters & { page: number } =
|
||||
useUrlSearchParams("history");
|
||||
const search = ref(params.search ?? "");
|
||||
const searchDebounced = refDebounced(search, 1000);
|
||||
|
||||
// computed
|
||||
// provide / inject
|
||||
|
||||
const page = computed(() => (route.query["page"] ? +route.query["page"] : 1));
|
||||
provide("params", params);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: products, isLoading: productsIsLoading } = useGetProducts(
|
||||
params,
|
||||
page
|
||||
);
|
||||
const { data, isLoading: productsIsLoading } = useGetProducts(params);
|
||||
|
||||
// life-cycle
|
||||
|
||||
onMounted(() => {
|
||||
if (!("range" in params)) {
|
||||
params.range = [];
|
||||
params.range[0] = PRODUCT_RANGE.min;
|
||||
params.range[1] = PRODUCT_RANGE.max;
|
||||
}
|
||||
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>
|
||||
@@ -54,24 +71,50 @@ onMounted(() => {
|
||||
<div class="w-full flex items-center justify-end gap-4">
|
||||
<Input
|
||||
placeholder="جست و جو محصول ..."
|
||||
v-model="search"
|
||||
class="bg-slate-50 !border-slate-200 hover:border-slate-300 focus:!border-slate-800 !rounded-full w-8/12"
|
||||
/>
|
||||
<FilterButton />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="w-full grid grid-cols-3 gap-[1.5rem]">
|
||||
<li v-for="i in 9" :key="i">
|
||||
<ul
|
||||
v-if="productsIsLoading"
|
||||
class="w-full grid grid-cols-3 gap-[1.5rem]"
|
||||
>
|
||||
<Skeleton
|
||||
v-for="i in 9"
|
||||
:key="i"
|
||||
class="w-full !h-[31.25rem] !rounded-2xl"
|
||||
/>
|
||||
</ul>
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!products?.length" class="flex flex-grow px-5 w-full">
|
||||
<div
|
||||
class="flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-100 flex-center"
|
||||
>
|
||||
<Icon name="bi:search" size="50" class="**:fill-gray-500" />
|
||||
<span class="text-lg text-gray-500">
|
||||
محصولی یافت نشد :(
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul v-else class="w-full grid grid-cols-3 gap-[1.5rem]">
|
||||
<li v-for="(product, index) in products" :key="index">
|
||||
<ProductCard
|
||||
brand="Samsung"
|
||||
title="Galaxy S20 Ultra"
|
||||
:title="product.name"
|
||||
picture="/assets/img/product-1.jpg"
|
||||
:colors="['#0000ff', '#00ff00', 'red']"
|
||||
:price="599"
|
||||
:rate="2.4"
|
||||
:price="product.price"
|
||||
:rate="product.rating"
|
||||
tag="New"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -28,11 +28,10 @@ definePageMeta({
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const { updateToken } = useAuth();
|
||||
const { updateToken, updateRefreshToken } = useAuth();
|
||||
|
||||
const { refetch: refetchAccount } = useGetAccount();
|
||||
|
||||
|
||||
const showOtp = ref(false);
|
||||
const otpCode = ref([]);
|
||||
|
||||
@@ -107,6 +106,8 @@ const handleLogin = async () => {
|
||||
});
|
||||
|
||||
updateToken(response.access);
|
||||
updateRefreshToken(response.refresh);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await refetchAccount();
|
||||
|
||||
addToast({
|
||||
@@ -116,9 +117,7 @@ const handleLogin = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
navigateTo("/");
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
otpCode.value = [];
|
||||
addToast({ message: "مشکلی پیش آمده" });
|
||||
|
||||
@@ -4,10 +4,10 @@ import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig();
|
||||
const { token } = useAuth();
|
||||
const { token, logout } = useAuth();
|
||||
|
||||
const axios = axiosOriginal.create({
|
||||
baseURL: config.public.API_BASE_URL,
|
||||
baseURL: config.public.API_BASE_URL
|
||||
});
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
@@ -25,9 +25,9 @@ export default defineNuxtPlugin(() => {
|
||||
return response;
|
||||
}, function(error) {
|
||||
|
||||
if (error.status === 401) {
|
||||
logout();
|
||||
}
|
||||
// if (error.status === 401) {
|
||||
// logout();
|
||||
// }
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Skeletor } from 'vue-skeletor';
|
||||
import 'vue-skeletor/dist/vue-skeletor.css';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.component("Skeleton", Skeletor)
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
import fs from "fs/promises";
|
||||
|
||||
type LogType = {
|
||||
title: string;
|
||||
status?: "success" | "error" | "info" | "warning";
|
||||
message?: string,
|
||||
details?: any
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private static formatToMarkdown(log: LogType) {
|
||||
const date = new Date();
|
||||
let month = "" + (date.getMonth() + 1);
|
||||
let day = "" + date.getDate();
|
||||
let year = date.getFullYear();
|
||||
let hour = date.getHours();
|
||||
let minutes = date.getMinutes();
|
||||
let seconds = date.getSeconds();
|
||||
|
||||
if (month.length < 2) {
|
||||
month = "0" + month;
|
||||
}
|
||||
|
||||
if (day.length < 2) {
|
||||
day = "0" + day;
|
||||
}
|
||||
|
||||
let markdownContent = "";
|
||||
|
||||
let icon = "ℹ️";
|
||||
|
||||
switch (log.status) {
|
||||
case "info":
|
||||
icon = "ℹ️";
|
||||
break;
|
||||
case "error":
|
||||
icon = "‼️";
|
||||
break;
|
||||
case "warning":
|
||||
icon = "⚠️";
|
||||
break;
|
||||
case "success":
|
||||
icon = "✅";
|
||||
break;
|
||||
default :
|
||||
icon = "ℹ️";
|
||||
break;
|
||||
}
|
||||
|
||||
markdownContent += `# ${icon} ${log.title} \n`;
|
||||
markdownContent += `## ${[year, month, day].join("-")} ${hour}:${minutes}:${seconds} \n`;
|
||||
|
||||
if (log.message) {
|
||||
markdownContent += `**Message:** ${log.message}\n`;
|
||||
}
|
||||
if (log.details) {
|
||||
markdownContent += `**Details:**\n\n\`\`\`json\n${JSON.stringify(log.details, null, 2)}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
markdownContent += "<br></br>\n\n";
|
||||
markdownContent += "---\n";
|
||||
|
||||
return markdownContent;
|
||||
}
|
||||
|
||||
public static async log(info: LogType) {
|
||||
const formattedLog = this.formatToMarkdown(info);
|
||||
|
||||
try {
|
||||
await fs.appendFile(".logs/log.md", formattedLog);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Logger;
|
||||
Vendored
+11
@@ -32,4 +32,15 @@ declare global {
|
||||
"meta_rating": number | null
|
||||
}
|
||||
|
||||
type Category = {
|
||||
"id": number,
|
||||
"name": string,
|
||||
"slug": string,
|
||||
"icon": string,
|
||||
"meta_title": string,
|
||||
"meta_description": string,
|
||||
"parent": number,
|
||||
"children": "string"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user