Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -23,7 +23,7 @@ const { isLoading } = useImage({ src: src.value });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AvatarRoot
|
<AvatarRoot
|
||||||
class="flex-center size-full select-none rounded-full align-middle overflow-hidden"
|
class="flex-center size-full select-none rounded-full align-middle overflow-hidden inset-shadow-black/20 inset-shadow-sm"
|
||||||
>
|
>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const isHomePage = computed(() => route.path === "/");
|
|||||||
<div
|
<div
|
||||||
class="size-full flex items-center justify-between container h-[65px] lg:h-[85px] shrink-0 grow-0"
|
class="size-full flex items-center justify-between container h-[65px] lg:h-[85px] shrink-0 grow-0"
|
||||||
>
|
>
|
||||||
<button class="md:hidden" @click="isSideDrawerOpen = true">
|
<button class="md:hidden header-navbar-item" @click="isSideDrawerOpen = true">
|
||||||
<Icon name="humbleicons:bars" size="28" />
|
<Icon name="humbleicons:bars" size="28" />
|
||||||
</button>
|
</button>
|
||||||
<div class="max-md:hidden flex items-center gap-8 lg:gap-16">
|
<div class="max-md:hidden flex items-center gap-8 lg:gap-16">
|
||||||
@@ -42,10 +42,10 @@ const isHomePage = computed(() => route.path === "/");
|
|||||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="{ name: 'profile' }"
|
:to="{ name: 'profile' }"
|
||||||
class="!size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full border-[1.2px] border-black"
|
class="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
class="!size-[1.6rem]"
|
class="!size-7"
|
||||||
:src="account.profile_photo"
|
:src="account.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account.first_name && account.last_name
|
account.first_name && account.last_name
|
||||||
@@ -66,7 +66,7 @@ const isHomePage = computed(() => route.path === "/");
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="محصولات">
|
<Tooltip title="محصولات">
|
||||||
<NuxtLink to="/products" class="flex-center">
|
<NuxtLink to="/products" class="flex-center header-navbar-item">
|
||||||
<Icon
|
<Icon
|
||||||
name="ci:search"
|
name="ci:search"
|
||||||
class="**:stroke-black size-4.5 lg:size-[21px]"
|
class="**:stroke-black size-4.5 lg:size-[21px]"
|
||||||
@@ -78,22 +78,20 @@ const isHomePage = computed(() => route.path === "/");
|
|||||||
<button class="relative">
|
<button class="relative">
|
||||||
<Icon
|
<Icon
|
||||||
name="ci:cart"
|
name="ci:cart"
|
||||||
class="**:stroke-black size-5 lg:size-6"
|
class="**:stroke-black size-5 lg:size-6 header-navbar-item"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<div
|
||||||
v-if="cart?.items.length! > 0"
|
v-if="cart?.items.length! > 0"
|
||||||
class="size-4 shrink-0 absolute -bottom-2 -right-2 flex-center rounded-sm text-white bg-red-600 text-xs animate-pulse"
|
class="size-2 shrink-0 absolute -bottom-1.5 -right-1.5 rounded-full bg-red-600 after:size-[125%] after:absolute after:bg-red-600 flex-center after:rounded-full after:animate-ping"
|
||||||
>
|
/>
|
||||||
{{ cart?.items.length }}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="flex-center gap-6 lg:gap-[2.5rem] typo-label-xs lg:typo-label-sm font-light text-black/80"
|
class="flex-center gap-6 lg:gap-[2.5rem] typo-label-xs lg:typo-label-sm font-light text-black/80 header-navbar-item"
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(link, index) in NAV_LINKS"
|
v-for="(link, index) in NAV_LINKS"
|
||||||
@@ -105,10 +103,10 @@ const isHomePage = computed(() => route.path === "/");
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="header-navbar-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 lg:h-6"
|
class="h-5 lg:h-6 "
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 220 40"
|
viewBox="0 0 220 40"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,29 +3,33 @@
|
|||||||
// state
|
// state
|
||||||
|
|
||||||
const { $gsap: gsap } = useNuxtApp();
|
const { $gsap: gsap } = useNuxtApp();
|
||||||
|
const showLoadingOverlay = useState("showLoadingOverlay");
|
||||||
|
const disableLoadingOverlay = useState("disableLoadingOverlay");
|
||||||
|
const shouldRenderLoadingOverlay = ref(true);
|
||||||
|
|
||||||
const showLoadingOverlay = useState('showLoadingOverlay');
|
const isWindowScrollLocked = useScrollLock(window);
|
||||||
|
|
||||||
// lifecycle
|
// watch
|
||||||
|
|
||||||
watch(() => showLoadingOverlay.value, (value) => {
|
watch(() => showLoadingOverlay.value, (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
const timeline = gsap.timeline();
|
const timeline = gsap.timeline();
|
||||||
|
|
||||||
|
const imageElement = document.querySelector("#loading-overlay-image") as HTMLImageElement;
|
||||||
|
|
||||||
|
imageElement.src = "/img/heymlz-loading-2.gif";
|
||||||
|
|
||||||
timeline
|
timeline
|
||||||
.to("#loading-overlay", {
|
.to("#loading-overlay", {
|
||||||
scale: 1
|
scale: 1
|
||||||
})
|
})
|
||||||
.to("#loading-overlay", {
|
.to("#loading-overlay", {
|
||||||
scale: 0.8,
|
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
delay: 2.5
|
delay: 2,
|
||||||
})
|
|
||||||
.to("#loading-overlay", {
|
|
||||||
opacity: 0,
|
|
||||||
y: "20%",
|
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
shouldRenderLoadingOverlay.value = false;
|
shouldRenderLoadingOverlay.value = false;
|
||||||
|
isWindowScrollLocked.value = false;
|
||||||
|
disableLoadingOverlay.value = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -33,6 +37,16 @@ watch(() => showLoadingOverlay.value, (value) => {
|
|||||||
once: true
|
once: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
isWindowScrollLocked.value = true;
|
||||||
|
|
||||||
|
const newImage = new Image();
|
||||||
|
newImage.src = "/img/heymlz-loading-2.gif";
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,7 +55,12 @@ watch(() => showLoadingOverlay.value, (value) => {
|
|||||||
id="loading-overlay"
|
id="loading-overlay"
|
||||||
class="fixed inset-0 size-full z-9999 flex-center bg-black"
|
class="fixed inset-0 size-full z-9999 flex-center bg-black"
|
||||||
>
|
>
|
||||||
<img id="loading-overlay-image" src="/video/loading-2.gif" class="opacity-0 scale-70 absolute z-20" alt="" />
|
<img
|
||||||
|
id="loading-overlay-image"
|
||||||
|
src="/img/heymlz-loading-1.gif"
|
||||||
|
class="opacity-0 scale-70 absolute z-20"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
id="loading-overlay-gradient"
|
id="loading-overlay-gradient"
|
||||||
class="opacity-0 scale-x-0 w-[1000px] h-[70px] bg-linear-to-r from-blue-500 via-violet-500 to-purple-500 blur-[150px] rounded-[100px]"
|
class="opacity-0 scale-x-0 w-[1000px] h-[70px] bg-linear-to-r from-blue-500 via-violet-500 to-purple-500 blur-[150px] rounded-[100px]"
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ const closeSideDrawer = () => {
|
|||||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="{ name: 'profile' }"
|
:to="{ name: 'profile' }"
|
||||||
class="!size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full border-[1.2px] border-black"
|
class="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
class="!size-[1.6rem]"
|
class="!size-7"
|
||||||
:src="account.profile_photo"
|
:src="account.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account.first_name && account.last_name
|
account.first_name && account.last_name
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ const { data: account } = useGetAccount();
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
v-if="!!account"
|
v-if="!!account"
|
||||||
class="size-[1.6rem] flex items-center justify-center relative overflow-hidden rounded-full border border-black"
|
class="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
class="!size-7"
|
||||||
:src="account.profile_photo"
|
:src="account.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account.first_name && account.last_name
|
account.first_name && account.last_name
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
modelValue: number;
|
modelValue: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
disable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// props
|
// props
|
||||||
@@ -48,6 +49,7 @@ const onInput = (e: any) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NumberFieldRoot
|
<NumberFieldRoot
|
||||||
|
:disabled="disable"
|
||||||
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
||||||
v-model="currentQuantity"
|
v-model="currentQuantity"
|
||||||
:min="1"
|
:min="1"
|
||||||
@@ -58,7 +60,7 @@ const onInput = (e: any) => {
|
|||||||
</NumberFieldIncrement>
|
</NumberFieldIncrement>
|
||||||
<NumberFieldInput
|
<NumberFieldInput
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
class="field-sizing-content bg-transparent w-[30px] text-center outline-none typo-label-md text-black"
|
class="field-sizing-content w-[30px] bg-transparent text-center outline-none typo-label-md text-black"
|
||||||
/>
|
/>
|
||||||
<NumberFieldDecrement class="cursor-pointer">
|
<NumberFieldDecrement class="cursor-pointer">
|
||||||
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
// import
|
||||||
|
|
||||||
|
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||||
|
import useAddCartItem from "~/composables/api/orders/useAddCartItem";
|
||||||
|
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||||
|
|
||||||
|
// provide / inject
|
||||||
|
|
||||||
|
const { selectedVariant } = inject(
|
||||||
|
"productVariant"
|
||||||
|
) as ProductVariantProvideType;
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const id = route.params.id as string | undefined;
|
||||||
|
|
||||||
|
const { refetch: refetchProduct } = useGetProduct(id);
|
||||||
|
const { mutateAsync: addCartItem, isPending: isAddCartItemPending } = useAddCartItem();
|
||||||
|
|
||||||
|
const timer = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const quantity = ref(1);
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const onInput = (e: any) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
if (value > 0 && value <= selectedVariant.value!.in_stock) {
|
||||||
|
quantity.value = value;
|
||||||
|
} else {
|
||||||
|
quantity.value = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => quantity.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (timer.value) clearTimeout(timer.value);
|
||||||
|
timer.value = setTimeout(async () => {
|
||||||
|
await addCartItem({ id: selectedVariant.value!.id, quantity: newValue });
|
||||||
|
await refetchProduct();
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(selectedVariant, (newValue) => {
|
||||||
|
quantity.value = newValue!.cart_quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
quantity.value = selectedVariant.value!.cart_quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NumberFieldRoot
|
||||||
|
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
||||||
|
v-model="quantity"
|
||||||
|
:min="1"
|
||||||
|
:max="selectedVariant!.in_stock"
|
||||||
|
>
|
||||||
|
<NumberFieldIncrement class="cursor-pointer">
|
||||||
|
<Icon name="ci:plus" class="**:stroke-slate-500 size-5" />
|
||||||
|
</NumberFieldIncrement>
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
:class="isAddCartItemPending ? 'opacity-100' : 'opacity-0'"
|
||||||
|
class="w-[40px] h-[25px] flex-center transition-all absolute bg-white"
|
||||||
|
>
|
||||||
|
<Icon :name="'svg-spinners:180-ring-with-bg'" class="size-[25px]" />
|
||||||
|
</div>
|
||||||
|
<NumberFieldInput
|
||||||
|
@input="onInput"
|
||||||
|
:class="!isAddCartItemPending ? 'opacity-100' : 'opacity-0'"
|
||||||
|
class="transition-all field-sizing-content w-[40px] h-[25px] bg-transparent text-center outline-none typo-label-md text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NumberFieldDecrement class="cursor-pointer">
|
||||||
|
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
||||||
|
</NumberFieldDecrement>
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</template>
|
||||||
@@ -32,7 +32,9 @@ let scrollTrigger: ScrollTrigger;
|
|||||||
// methods
|
// methods
|
||||||
|
|
||||||
const onSwiper = (swiper: SwiperClass) => {
|
const onSwiper = (swiper: SwiperClass) => {
|
||||||
showLoadingOverlay.value = false;
|
setTimeout(() => {
|
||||||
|
showLoadingOverlay.value = false;
|
||||||
|
}, 1000);
|
||||||
swiper_instance.value = swiper;
|
swiper_instance.value = swiper;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,11 +72,9 @@ const initializeGsapAnimation = () => {
|
|||||||
}, {
|
}, {
|
||||||
value: 1.2
|
value: 1.2
|
||||||
}, "=")
|
}, "=")
|
||||||
.fromTo("#header-navbar", {
|
.fromTo(".header-navbar-item", {
|
||||||
background: "transparent",
|
|
||||||
filter: "invert(100%)"
|
filter: "invert(100%)"
|
||||||
}, {
|
}, {
|
||||||
background: "transparent",
|
|
||||||
filter: "invert(0%)"
|
filter: "invert(0%)"
|
||||||
}, "=")
|
}, "=")
|
||||||
.fromTo("#header-navbar", {
|
.fromTo("#header-navbar", {
|
||||||
@@ -96,7 +96,9 @@ const initializeGsapAnimation = () => {
|
|||||||
|
|
||||||
const resetTimelineForMobile = () => {
|
const resetTimelineForMobile = () => {
|
||||||
gsap.to("#header-navbar", {
|
gsap.to("#header-navbar", {
|
||||||
background: "white",
|
background: "white"
|
||||||
|
});
|
||||||
|
gsap.to(".header-navbar-item", {
|
||||||
filter: "invert(0%)"
|
filter: "invert(0%)"
|
||||||
});
|
});
|
||||||
gsap.set(".header-slider-item", {
|
gsap.set(".header-slider-item", {
|
||||||
@@ -113,6 +115,7 @@ onMounted(() => {
|
|||||||
initializeGsapAnimation();
|
initializeGsapAnimation();
|
||||||
|
|
||||||
scrollTrigger = ScrollTrigger.create({
|
scrollTrigger = ScrollTrigger.create({
|
||||||
|
anticipatePin: 1,
|
||||||
trigger: "#header-slider-container",
|
trigger: "#header-slider-container",
|
||||||
animation: gsapTimeline,
|
animation: gsapTimeline,
|
||||||
scrub: 1,
|
scrub: 1,
|
||||||
@@ -124,27 +127,23 @@ onMounted(() => {
|
|||||||
|
|
||||||
const calculateOnResize = () => {
|
const calculateOnResize = () => {
|
||||||
if (window.innerWidth > 768) {
|
if (window.innerWidth > 768) {
|
||||||
gsap.to("#header-navbar", {
|
|
||||||
background: "transparent",
|
|
||||||
filter: "invert(100%)"
|
|
||||||
});
|
|
||||||
scrollTrigger.enable();
|
scrollTrigger.enable();
|
||||||
} else {
|
} else {
|
||||||
resetTimelineForMobile();
|
resetTimelineForMobile();
|
||||||
scrollTrigger.disable();
|
scrollTrigger.disable();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
calculateOnResize()
|
calculateOnResize();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
calculateOnResize()
|
calculateOnResize();
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
calculateOnResize()
|
calculateOnResize();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -213,7 +212,8 @@ onUnmounted(() => {
|
|||||||
<div class="flex items-center gap-4 lg:gap-8">
|
<div class="flex items-center gap-4 lg:gap-8">
|
||||||
<div
|
<div
|
||||||
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center">
|
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center">
|
||||||
<div class="size-full scale-75 bg-white absolute rounded-full animate-ping" />
|
<div
|
||||||
|
class="size-full scale-75 bg-white absolute rounded-full animate-ping" />
|
||||||
<button
|
<button
|
||||||
@click="isMuted = !isMuted"
|
@click="isMuted = !isMuted"
|
||||||
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
|
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import useHomeData from "~/composables/api/home/useHomeData";
|
|||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { data : homeData } = useHomeData();
|
const { data: homeData } = useHomeData();
|
||||||
|
|
||||||
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
|
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
|
||||||
|
|
||||||
@@ -17,71 +17,60 @@ let scrollTrigger: ScrollTrigger;
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
gsapTimeline = gsap.timeline();
|
gsapTimeline = gsap.timeline();
|
||||||
|
|
||||||
gsapTimeline
|
|
||||||
.fromTo("#header-navbar", {
|
|
||||||
background: "white",
|
|
||||||
filter: "invert(0%)"
|
|
||||||
}, {
|
|
||||||
background: "transparent",
|
|
||||||
filter: "invert(100%)"
|
|
||||||
});
|
|
||||||
|
|
||||||
const showcaseElements = gsap.utils.toArray<HTMLElement>(".showcase-slide");
|
const showcaseElements = gsap.utils.toArray<HTMLElement>(".showcase-slide");
|
||||||
|
|
||||||
showcaseElements.forEach((element, index) => {
|
|
||||||
|
|
||||||
gsapTimeline.fromTo(element, index === 0 ? {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
// rotateX: -25,
|
|
||||||
y: 0,
|
|
||||||
ease: "none"
|
|
||||||
} : {
|
|
||||||
opacity: 0,
|
|
||||||
scale: 0.97,
|
|
||||||
// rotateX: -25,
|
|
||||||
y: 20,
|
|
||||||
ease: "none"
|
|
||||||
}, {
|
|
||||||
opacity: 1,
|
|
||||||
scale: 1,
|
|
||||||
// rotateX: 0,
|
|
||||||
y: 0,
|
|
||||||
ease: "none"
|
|
||||||
}, index === 0 ? "-=0%" : undefined);
|
|
||||||
|
|
||||||
if (index < showcaseElements.length - 1) {
|
|
||||||
gsapTimeline.to(element, {
|
|
||||||
opacity: 0,
|
|
||||||
scale: 1.03,
|
|
||||||
// rotateX: 25,
|
|
||||||
y: -20,
|
|
||||||
ease: "none"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
gsapTimeline.to("#header-navbar", {
|
|
||||||
background: "white",
|
|
||||||
filter: "invert(0%)"
|
|
||||||
});
|
|
||||||
|
|
||||||
scrollTrigger = ScrollTrigger.create({
|
|
||||||
trigger: "#products-showcase-container",
|
|
||||||
animation: gsapTimeline,
|
|
||||||
scrub: 1,
|
|
||||||
pin: true,
|
|
||||||
start: "top top",
|
|
||||||
// markers: true,
|
|
||||||
end: "bottom top"
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollTrigger.refresh()
|
showcaseElements.forEach((element, index) => {
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
|
gsapTimeline.fromTo(element, index === 0 ? {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
// rotateX: -25,
|
||||||
|
top: 0,
|
||||||
|
ease: "none"
|
||||||
|
} : {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 1,
|
||||||
|
// rotateX: -25,
|
||||||
|
top: 20,
|
||||||
|
ease: "none"
|
||||||
|
}, {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
// rotateX: 0,
|
||||||
|
top: 0,
|
||||||
|
ease: "none"
|
||||||
|
}, index === 0 ? "-=0%" : undefined);
|
||||||
|
|
||||||
|
if (index < showcaseElements.length - 1) {
|
||||||
|
gsapTimeline.to(element, {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 1.03,
|
||||||
|
// rotateX: 25,
|
||||||
|
top: -20,
|
||||||
|
ease: "none"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollTrigger = ScrollTrigger.create({
|
||||||
|
trigger: "#products-showcase-container",
|
||||||
|
animation: gsapTimeline,
|
||||||
|
scrub: 1,
|
||||||
|
pin: true,
|
||||||
|
start: "top top",
|
||||||
|
anticipatePin: 1,
|
||||||
|
// markers: true,
|
||||||
|
end: "bottom top"
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollTrigger.update();
|
||||||
|
scrollTrigger.refresh();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -92,33 +81,35 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<section
|
||||||
id="products-showcase-container"
|
id="products-showcase-container"
|
||||||
class="perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
|
class="perspective-midrange relative z-[99999]"
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<div class="w-full h-[125svh] bg-black">
|
||||||
v-for="slide in homeData!.show_case_slider"
|
<NuxtLink
|
||||||
:key="slide.id"
|
v-for="slide in homeData!.show_case_slider"
|
||||||
:to="slide.link"
|
:key="slide.id"
|
||||||
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
:to="slide.link"
|
||||||
>
|
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
||||||
|
>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
|
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
|
||||||
:src="slide.image"
|
:src="slide.image"
|
||||||
:style="{
|
:style="{
|
||||||
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
|
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
|
||||||
}"
|
}"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col items-center justify-center gap-4 text-center absolute z-20 mt-20">
|
<div class="flex flex-col items-center justify-center gap-4 text-center absolute z-20 mt-20">
|
||||||
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
|
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
|
||||||
{{ slide.title }}
|
{{ slide.title }}
|
||||||
</span>
|
</span>
|
||||||
<p class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg">
|
<p class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg">
|
||||||
{{ slide.description }}
|
{{ slide.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +71,7 @@ await suspense();
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<Avatar
|
<Avatar
|
||||||
class="!size-[3rem]"
|
class="!size-12"
|
||||||
:src="account!.profile_photo"
|
:src="account!.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account?.first_name && account?.last_name
|
account?.first_name && account?.last_name
|
||||||
|
|||||||
@@ -134,15 +134,11 @@ onFileDialogChange((files: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex-col-center gap-5">
|
<div class="w-full flex-col-center gap-5">
|
||||||
<div
|
<Avatar
|
||||||
class="size-32 border border-slate-200 rounded-full"
|
:src="currentProfile"
|
||||||
>
|
alt=""
|
||||||
<Avatar
|
class="!size-32"
|
||||||
:src="currentProfile"
|
/>
|
||||||
:alt="''"
|
|
||||||
class="size-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full w-[8rem]"
|
class="rounded-full w-[8rem]"
|
||||||
@click="openFileDialog"
|
@click="openFileDialog"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
// import
|
||||||
|
|
||||||
|
import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
|
||||||
|
|
||||||
|
// type
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const { date } = toRefs(props);
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const timeAgo = usePersianTimeAgo(new Date(date.value));
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="px-3 pb-1 pt-1.5 rounded-lg bg-neutral-600 text-white" dir="rtl">
|
||||||
|
{{ timeAgo }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
if (config.public.DEBUG === "true") {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return navigateTo("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
+27
-26
@@ -6,45 +6,45 @@ export default defineNuxtConfig({
|
|||||||
css: [
|
css: [
|
||||||
"~/assets/css/tailwind.css",
|
"~/assets/css/tailwind.css",
|
||||||
"swiper/css",
|
"swiper/css",
|
||||||
"animate.css/animate.min.css",
|
"animate.css/animate.min.css"
|
||||||
],
|
],
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
"/products": { prerender: false, ssr: false },
|
"/products": { prerender: false, ssr: false }
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: "فروشگاه هی ملز",
|
title: "فروشگاه هی ملز"
|
||||||
},
|
},
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
enterActiveClass:
|
enterActiveClass:
|
||||||
"animate__animated animate__fadeIn animate__faster",
|
"animate__animated animate__fadeIn animate__faster",
|
||||||
leaveActiveClass:
|
leaveActiveClass:
|
||||||
"animate__animated animate__fadeOut animate__faster",
|
"animate__animated animate__fadeOut animate__faster",
|
||||||
mode: "out-in",
|
mode: "out-in"
|
||||||
},
|
},
|
||||||
layoutTransition: {
|
layoutTransition: {
|
||||||
enterActiveClass:
|
enterActiveClass:
|
||||||
"animate__animated animate__fadeIn animate__faster",
|
"animate__animated animate__fadeIn animate__faster",
|
||||||
leaveActiveClass:
|
leaveActiveClass:
|
||||||
"animate__animated animate__fadeOut animate__faster",
|
"animate__animated animate__fadeOut animate__faster",
|
||||||
mode: "out-in",
|
mode: "out-in"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: "~/components",
|
path: "~/components",
|
||||||
pathPrefix: false,
|
pathPrefix: false
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
@@ -52,9 +52,9 @@ export default defineNuxtConfig({
|
|||||||
customCollections: [
|
customCollections: [
|
||||||
{
|
{
|
||||||
prefix: "ci",
|
prefix: "ci",
|
||||||
dir: "./public/icons",
|
dir: "./public/icons"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
@@ -65,15 +65,15 @@ export default defineNuxtConfig({
|
|||||||
"DM Sans": "100..900",
|
"DM Sans": "100..900",
|
||||||
Inter: "100..900",
|
Inter: "100..900",
|
||||||
download: true,
|
download: true,
|
||||||
inject: false,
|
inject: false
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
"@nuxt/icon",
|
"@nuxt/icon",
|
||||||
"reka-ui/nuxt",
|
"reka-ui/nuxt",
|
||||||
"@vueuse/nuxt",
|
"@vueuse/nuxt",
|
||||||
"@formkit/auto-animate/nuxt",
|
"@formkit/auto-animate/nuxt",
|
||||||
"@vite-pwa/nuxt",
|
"@vite-pwa/nuxt"
|
||||||
],
|
],
|
||||||
|
|
||||||
pwa: {
|
pwa: {
|
||||||
@@ -88,26 +88,27 @@ export default defineNuxtConfig({
|
|||||||
{
|
{
|
||||||
src: "/logo/logo-192x192.png",
|
src: "/logo/logo-192x192.png",
|
||||||
sizes: "192x192",
|
sizes: "192x192",
|
||||||
type: "image/png",
|
type: "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/logo/logo-512x512.png",
|
src: "/logo/logo-512x512.png",
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png",
|
type: "image/png"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
navigateFallback: "/",
|
navigateFallback: "/",
|
||||||
clientsClaim: true,
|
clientsClaim: true,
|
||||||
skipWaiting: true,
|
skipWaiting: true
|
||||||
},
|
},
|
||||||
devOptions: { enabled: true, type: "module" },
|
devOptions: { enabled: true, type: "module" }
|
||||||
},
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
API_BASE_URL: "https://api.heymlz.com",
|
API_BASE_URL: process.env.API_BASE_URL,
|
||||||
},
|
DEBUG: process.env.DEBUG
|
||||||
},
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"date-fns-jalali": "^4.1.0-0",
|
"date-fns-jalali": "^4.1.0-0",
|
||||||
"fast-average-color": "^9.4.0",
|
"fast-average-color": "^9.4.0",
|
||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"isomorphic-dompurify": "^2.22.0",
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"jalali-ts": "^8.0.0",
|
"jalali-ts": "^8.0.0",
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ProductsGrid from "~/components/global/ProductsGrid.vue";
|
|||||||
// state
|
// state
|
||||||
|
|
||||||
const { data: homeData, suspense } = useHomeData();
|
const { data: homeData, suspense } = useHomeData();
|
||||||
|
const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
|
||||||
|
|
||||||
// ssr
|
// ssr
|
||||||
|
|
||||||
@@ -20,11 +21,17 @@ if (response.isError) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<LoadingOverlay />
|
<LoadingOverlay v-if="!disableLoadingOverlay" />
|
||||||
<Hero class="mb-20 max-md:mt-[80px]" />
|
<Hero class="mb-20 max-md:mt-[80px]" />
|
||||||
<Preview />
|
<Preview />
|
||||||
<ProductsShowcase class="mb-40" />
|
<ProductsShowcase class="mb-40" />
|
||||||
|
|||||||
@@ -165,9 +165,10 @@ const handleSubmit = (withValidation: boolean) => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-start gap-5 w-8/12">
|
<div class="flex items-center justify-start gap-5 w-8/12">
|
||||||
<div
|
<div
|
||||||
class="size-32 shrink-0 rounded-full border border-slate-200 flex-center relative"
|
class="relative shrink-0 rounded-full flex-center"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
class="!size-32"
|
||||||
:src="account!.profile_photo"
|
:src="account!.profile_photo"
|
||||||
:alt="
|
:alt="
|
||||||
account?.first_name && account?.last_name
|
account?.first_name && account?.last_name
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const handleLogin = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
navigateTo("/");
|
window.location.href = "/";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
otpCode.value = [];
|
otpCode.value = [];
|
||||||
addToast({ message: "مشکلی پیش آمده" });
|
addToast({ message: "مشکلی پیش آمده" });
|
||||||
@@ -158,7 +158,7 @@ const resetForm = () => {
|
|||||||
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)',
|
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center flex-col size-full">
|
<div class="flex items-center justify-center flex-col size-full translate-y-[-80px]">
|
||||||
<img
|
<img
|
||||||
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
||||||
src="/img/heymlz-seat.gif"
|
src="/img/heymlz-seat.gif"
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
// import
|
||||||
|
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import javascript from "highlight.js/lib/languages/javascript";
|
||||||
|
import "highlight.js/styles/atom-one-dark.css";
|
||||||
|
import LogDate from "~/components/server-logs/LogDate.vue";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
|
// meta
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware : "check-is-debug",
|
||||||
|
layout: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
const { data: serverLogs, isFetching, suspense } = useQuery({
|
||||||
|
queryKey: ["server-logs"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await axios.get("http://localhost:3000/api/server-logger");
|
||||||
|
return response.data.reverse();
|
||||||
|
},
|
||||||
|
refetchInterval: 5000,
|
||||||
|
staleTime: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
await suspense();
|
||||||
|
|
||||||
|
// computed
|
||||||
|
|
||||||
|
const logIcon = (status: number) => {
|
||||||
|
if (status >= 200 && status < 300) return "bi:check-circle-fill";
|
||||||
|
return "bi:x-circle-fill";
|
||||||
|
};
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
hljs.registerLanguage("json", javascript);
|
||||||
|
hljs.highlightAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-neutral-900 w-full min-h-svh py-32">
|
||||||
|
<div class="fixed top-10 right-1/2 translate-x-1/2 flex-center" v-if="isFetching">
|
||||||
|
<Icon
|
||||||
|
name="svg-spinners:180-ring-with-bg"
|
||||||
|
class="size-12 mb-1 **:fill-neutral-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full container flex flex-col gap-8">
|
||||||
|
<div
|
||||||
|
v-for="(log,index) in serverLogs"
|
||||||
|
:key="index"
|
||||||
|
class="border-2 p-5 rounded-xl log-item-animation"
|
||||||
|
:class="{
|
||||||
|
'bg-success-950/30 border-success-800' : log.status >= 200 && log.status < 300,
|
||||||
|
'bg-danger-950/30 border-danger-800' : log.status >= 400 && log.status < 600,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4 mt-4">
|
||||||
|
<Icon
|
||||||
|
:name="logIcon(log.status)"
|
||||||
|
class="size-8 mb-1"
|
||||||
|
:class="{
|
||||||
|
'**:fill-success-500' : log.status >= 200 && log.status < 300,
|
||||||
|
'**:fill-danger-500' : log.status >= 400 && log.status < 600,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<h3 class="text-white font-medium text-3xl">
|
||||||
|
{{ log.url }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 py-8">
|
||||||
|
<div
|
||||||
|
class="px-3 pb-1 pt-1.5 rounded-lg uppercase font-bold text-white"
|
||||||
|
:class="{
|
||||||
|
'bg-success-500' : log.status >= 200 && log.status < 300,
|
||||||
|
'bg-danger-500' : log.status >= 400 && log.status < 600,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ log.method }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-3 pb-1 pt-1.5 rounded-lg font-bold text-white"
|
||||||
|
:class="{
|
||||||
|
'bg-success-500' : log.status >= 200 && log.status < 300,
|
||||||
|
'bg-danger-500' : log.status >= 400 && log.status < 600,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ log.status }}
|
||||||
|
</div>
|
||||||
|
<LogDate :date="log.date" />
|
||||||
|
</div>
|
||||||
|
<details class="text-white">
|
||||||
|
<summary class="cursor-pointer select-none">Details :</summary>
|
||||||
|
<div class="flex flex-col gap-2 mt-2 ml-4">
|
||||||
|
<details class="text-white">
|
||||||
|
<summary class="cursor-pointer select-none">Response :</summary>
|
||||||
|
<pre>
|
||||||
|
<code class="language-json">
|
||||||
|
{{ log.response }}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
<details class="text-white">
|
||||||
|
<summary class="cursor-pointer select-none">Req Headers :</summary>
|
||||||
|
<pre class="whitespace-pre-line">
|
||||||
|
<code class="language-json">
|
||||||
|
{{ log.requestHeaders }}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
<details class="text-white">
|
||||||
|
<summary class="cursor-pointer select-none">Res Headers :</summary>
|
||||||
|
<pre>
|
||||||
|
<code class="language-json">
|
||||||
|
{{ log.responseHeaders }}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
<details v-if="log.payload" class="text-white">
|
||||||
|
<summary class="cursor-pointer select-none">Payload :</summary>
|
||||||
|
<pre>
|
||||||
|
<code class="language-json">
|
||||||
|
{{ log.payload }}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.log-item-animation {
|
||||||
|
animation-name: log-fade-in;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes log-fade-in {
|
||||||
|
from {
|
||||||
|
opacity : 0;
|
||||||
|
scale: 0.8;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity : 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -29,11 +29,10 @@ export default defineNuxtPlugin(() => {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async function(error) {
|
async function(error) {
|
||||||
await Logger.axiosErrorLog(error);
|
|
||||||
|
|
||||||
// if (error.status === 401) {
|
if (config.public.DEBUG === "true" && import.meta.server) {
|
||||||
// logout();
|
await Logger.axiosErrorLog(error);
|
||||||
// }
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 943 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,6 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const oldLogs = await fs.readFile(".logs/log.json", "utf-8");
|
||||||
|
return JSON.parse(oldLogs) as Record<any, any>[];
|
||||||
|
});
|
||||||
+20
-80
@@ -1,91 +1,31 @@
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
|
||||||
type LogType = {
|
|
||||||
title: string;
|
|
||||||
status?: "success" | "error" | "info" | "warning";
|
|
||||||
message?: string,
|
|
||||||
details?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
class Logger {
|
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`;
|
|
||||||
markdownContent += `## ${hour} : ${minutes} : ${seconds} \n`;
|
|
||||||
|
|
||||||
if (log.message) {
|
|
||||||
markdownContent += `**Message:**\n ${log.message}\n\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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async axiosErrorLog(error: any) {
|
public static async axiosErrorLog(error: any) {
|
||||||
|
const errorJson = error.toJSON();
|
||||||
|
|
||||||
const logData : LogType = {
|
const nowDate = new Date();
|
||||||
title : error?.message,
|
|
||||||
message : `${error?.config?.method?.toUpperCase()} ${error?.config?.url}`,
|
|
||||||
details : error,
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedLog = this.formatToMarkdown(logData);
|
const logData: AxiosLogType = {
|
||||||
|
url: errorJson.config.url,
|
||||||
|
code: errorJson.code!,
|
||||||
|
status: errorJson.status!,
|
||||||
|
method: errorJson.config.method,
|
||||||
|
response: error?.response?.data,
|
||||||
|
requestHeaders: errorJson.config.headers,
|
||||||
|
responseHeaders: error.response.headers,
|
||||||
|
payload: errorJson.config.data ? JSON.parse(errorJson.config.data) : undefined,
|
||||||
|
params: errorJson.config.params ?? undefined,
|
||||||
|
date: nowDate.toString()
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.appendFile(".logs/log.md", formattedLog);
|
const oldLogs = await fs.readFile(".logs/log.json", "utf-8");
|
||||||
|
const oldLogsJson = JSON.parse(oldLogs) as Record<any, any>[];
|
||||||
|
|
||||||
|
oldLogsJson.push(logData);
|
||||||
|
|
||||||
|
await fs.writeFile(".logs/log.json", JSON.stringify(oldLogsJson));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+20
@@ -8,6 +8,26 @@ declare global {
|
|||||||
results: T[];
|
results: T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LogType = {
|
||||||
|
title: string;
|
||||||
|
status?: "success" | "error" | "info" | "warning";
|
||||||
|
message?: string,
|
||||||
|
details?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type AxiosLogType = {
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
status: number,
|
||||||
|
code: string,
|
||||||
|
requestHeaders: Record<any, any>,
|
||||||
|
responseHeaders: Record<any, any>,
|
||||||
|
response?: Record<any, any>,
|
||||||
|
payload?: Record<any, any>,
|
||||||
|
params?: Record<any, any>,
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
type Chat = {
|
type Chat = {
|
||||||
id: number;
|
id: number;
|
||||||
sender: "ai" | "user";
|
sender: "ai" | "user";
|
||||||
|
|||||||
Reference in New Issue
Block a user