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>
|
||||
<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
|
||||
v-if="isLoading"
|
||||
|
||||
@@ -32,7 +32,7 @@ const isHomePage = computed(() => route.path === "/");
|
||||
<div
|
||||
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" />
|
||||
</button>
|
||||
<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="حساب کاربری">
|
||||
<NuxtLink
|
||||
: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
|
||||
class="!size-[1.6rem]"
|
||||
class="!size-7"
|
||||
:src="account.profile_photo"
|
||||
:alt="
|
||||
account.first_name && account.last_name
|
||||
@@ -66,7 +66,7 @@ const isHomePage = computed(() => route.path === "/");
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
<Tooltip title="محصولات">
|
||||
<NuxtLink to="/products" class="flex-center">
|
||||
<NuxtLink to="/products" class="flex-center header-navbar-item">
|
||||
<Icon
|
||||
name="ci:search"
|
||||
class="**:stroke-black size-4.5 lg:size-[21px]"
|
||||
@@ -78,22 +78,20 @@ const isHomePage = computed(() => route.path === "/");
|
||||
<button class="relative">
|
||||
<Icon
|
||||
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"
|
||||
class="size-4 shrink-0 absolute -bottom-2 -right-2 flex-center rounded-sm text-white bg-red-600 text-xs animate-pulse"
|
||||
>
|
||||
{{ cart?.items.length }}
|
||||
</span>
|
||||
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"
|
||||
/>
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<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
|
||||
v-for="(link, index) in NAV_LINKS"
|
||||
@@ -105,7 +103,7 @@ const isHomePage = computed(() => route.path === "/");
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="header-navbar-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 lg:h-6 "
|
||||
|
||||
@@ -3,29 +3,33 @@
|
||||
// state
|
||||
|
||||
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) => {
|
||||
if (!value) {
|
||||
const timeline = gsap.timeline();
|
||||
|
||||
const imageElement = document.querySelector("#loading-overlay-image") as HTMLImageElement;
|
||||
|
||||
imageElement.src = "/img/heymlz-loading-2.gif";
|
||||
|
||||
timeline
|
||||
.to("#loading-overlay", {
|
||||
scale: 1
|
||||
})
|
||||
.to("#loading-overlay", {
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
delay: 2.5
|
||||
})
|
||||
.to("#loading-overlay", {
|
||||
opacity: 0,
|
||||
y: "20%",
|
||||
delay: 2,
|
||||
onComplete: () => {
|
||||
shouldRenderLoadingOverlay.value = false;
|
||||
isWindowScrollLocked.value = false;
|
||||
disableLoadingOverlay.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -33,6 +37,16 @@ watch(() => showLoadingOverlay.value, (value) => {
|
||||
once: true
|
||||
});
|
||||
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
isWindowScrollLocked.value = true;
|
||||
|
||||
const newImage = new Image();
|
||||
newImage.src = "/img/heymlz-loading-2.gif";
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,7 +55,12 @@ watch(() => showLoadingOverlay.value, (value) => {
|
||||
id="loading-overlay"
|
||||
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
|
||||
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]"
|
||||
|
||||
@@ -49,10 +49,10 @@ const closeSideDrawer = () => {
|
||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
||||
<NuxtLink
|
||||
: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
|
||||
class="!size-[1.6rem]"
|
||||
class="!size-7"
|
||||
:src="account.profile_photo"
|
||||
:alt="
|
||||
account.first_name && account.last_name
|
||||
|
||||
@@ -51,9 +51,10 @@ const { data: account } = useGetAccount();
|
||||
<template #trigger>
|
||||
<button
|
||||
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
|
||||
class="!size-7"
|
||||
:src="account.profile_photo"
|
||||
:alt="
|
||||
account.first_name && account.last_name
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
type Props = {
|
||||
modelValue: number;
|
||||
max: number;
|
||||
disable: boolean;
|
||||
}
|
||||
|
||||
// props
|
||||
@@ -48,6 +49,7 @@ const onInput = (e: any) => {
|
||||
|
||||
<template>
|
||||
<NumberFieldRoot
|
||||
:disabled="disable"
|
||||
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
|
||||
v-model="currentQuantity"
|
||||
:min="1"
|
||||
@@ -58,7 +60,7 @@ const onInput = (e: any) => {
|
||||
</NumberFieldIncrement>
|
||||
<NumberFieldInput
|
||||
@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">
|
||||
<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
|
||||
|
||||
const onSwiper = (swiper: SwiperClass) => {
|
||||
setTimeout(() => {
|
||||
showLoadingOverlay.value = false;
|
||||
}, 1000);
|
||||
swiper_instance.value = swiper;
|
||||
};
|
||||
|
||||
@@ -70,11 +72,9 @@ const initializeGsapAnimation = () => {
|
||||
}, {
|
||||
value: 1.2
|
||||
}, "=")
|
||||
.fromTo("#header-navbar", {
|
||||
background: "transparent",
|
||||
.fromTo(".header-navbar-item", {
|
||||
filter: "invert(100%)"
|
||||
}, {
|
||||
background: "transparent",
|
||||
filter: "invert(0%)"
|
||||
}, "=")
|
||||
.fromTo("#header-navbar", {
|
||||
@@ -96,7 +96,9 @@ const initializeGsapAnimation = () => {
|
||||
|
||||
const resetTimelineForMobile = () => {
|
||||
gsap.to("#header-navbar", {
|
||||
background: "white",
|
||||
background: "white"
|
||||
});
|
||||
gsap.to(".header-navbar-item", {
|
||||
filter: "invert(0%)"
|
||||
});
|
||||
gsap.set(".header-slider-item", {
|
||||
@@ -113,6 +115,7 @@ onMounted(() => {
|
||||
initializeGsapAnimation();
|
||||
|
||||
scrollTrigger = ScrollTrigger.create({
|
||||
anticipatePin: 1,
|
||||
trigger: "#header-slider-container",
|
||||
animation: gsapTimeline,
|
||||
scrub: 1,
|
||||
@@ -124,27 +127,23 @@ onMounted(() => {
|
||||
|
||||
const calculateOnResize = () => {
|
||||
if (window.innerWidth > 768) {
|
||||
gsap.to("#header-navbar", {
|
||||
background: "transparent",
|
||||
filter: "invert(100%)"
|
||||
});
|
||||
scrollTrigger.enable();
|
||||
} else {
|
||||
resetTimelineForMobile();
|
||||
scrollTrigger.disable();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
calculateOnResize()
|
||||
calculateOnResize();
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
calculateOnResize()
|
||||
calculateOnResize();
|
||||
}, 200);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
calculateOnResize()
|
||||
calculateOnResize();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -213,7 +212,8 @@ onUnmounted(() => {
|
||||
<div class="flex items-center gap-4 lg:gap-8">
|
||||
<div
|
||||
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
|
||||
@click="isMuted = !isMuted"
|
||||
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
|
||||
|
||||
@@ -17,37 +17,28 @@ let scrollTrigger: ScrollTrigger;
|
||||
|
||||
onMounted(() => {
|
||||
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");
|
||||
|
||||
setTimeout(() => {
|
||||
showcaseElements.forEach((element, index) => {
|
||||
|
||||
gsapTimeline.fromTo(element, index === 0 ? {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: -25,
|
||||
y: 0,
|
||||
top: 0,
|
||||
ease: "none"
|
||||
} : {
|
||||
opacity: 0,
|
||||
scale: 0.97,
|
||||
scale: 1,
|
||||
// rotateX: -25,
|
||||
y: 20,
|
||||
top: 20,
|
||||
ease: "none"
|
||||
}, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
// rotateX: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
ease: "none"
|
||||
}, index === 0 ? "-=0%" : undefined);
|
||||
|
||||
@@ -56,32 +47,30 @@ onMounted(() => {
|
||||
opacity: 0,
|
||||
scale: 1.03,
|
||||
// rotateX: 25,
|
||||
y: -20,
|
||||
top: -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",
|
||||
anticipatePin: 1,
|
||||
// markers: true,
|
||||
end: "bottom top"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
scrollTrigger.refresh()
|
||||
scrollTrigger.update();
|
||||
scrollTrigger.refresh();
|
||||
}, 1000);
|
||||
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -92,10 +81,11 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<section
|
||||
id="products-showcase-container"
|
||||
class="perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
|
||||
class="perspective-midrange relative z-[99999]"
|
||||
>
|
||||
<div class="w-full h-[125svh] bg-black">
|
||||
<NuxtLink
|
||||
v-for="slide in homeData!.show_case_slider"
|
||||
:key="slide.id"
|
||||
@@ -121,4 +111,5 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -71,7 +71,7 @@ await suspense();
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<Avatar
|
||||
class="!size-[3rem]"
|
||||
class="!size-12"
|
||||
:src="account!.profile_photo"
|
||||
:alt="
|
||||
account?.first_name && account?.last_name
|
||||
|
||||
@@ -134,15 +134,11 @@ onFileDialogChange((files: any) => {
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-col-center gap-5">
|
||||
<div
|
||||
class="size-32 border border-slate-200 rounded-full"
|
||||
>
|
||||
<Avatar
|
||||
:src="currentProfile"
|
||||
:alt="''"
|
||||
class="size-full"
|
||||
alt=""
|
||||
class="!size-32"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
class="rounded-full w-[8rem]"
|
||||
@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: [
|
||||
"~/assets/css/tailwind.css",
|
||||
"swiper/css",
|
||||
"animate.css/animate.min.css",
|
||||
"animate.css/animate.min.css"
|
||||
],
|
||||
|
||||
routeRules: {
|
||||
"/products": { prerender: false, ssr: false },
|
||||
"/products": { prerender: false, ssr: false }
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
title: "فروشگاه هی ملز",
|
||||
title: "فروشگاه هی ملز"
|
||||
},
|
||||
pageTransition: {
|
||||
enterActiveClass:
|
||||
"animate__animated animate__fadeIn animate__faster",
|
||||
leaveActiveClass:
|
||||
"animate__animated animate__fadeOut animate__faster",
|
||||
mode: "out-in",
|
||||
mode: "out-in"
|
||||
},
|
||||
layoutTransition: {
|
||||
enterActiveClass:
|
||||
"animate__animated animate__fadeIn animate__faster",
|
||||
leaveActiveClass:
|
||||
"animate__animated animate__fadeOut animate__faster",
|
||||
mode: "out-in",
|
||||
},
|
||||
mode: "out-in"
|
||||
}
|
||||
},
|
||||
|
||||
postcss: {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
autoprefixer: {}
|
||||
}
|
||||
},
|
||||
|
||||
components: [
|
||||
{
|
||||
path: "~/components",
|
||||
pathPrefix: false,
|
||||
},
|
||||
pathPrefix: false
|
||||
}
|
||||
],
|
||||
|
||||
icon: {
|
||||
@@ -52,9 +52,9 @@ export default defineNuxtConfig({
|
||||
customCollections: [
|
||||
{
|
||||
prefix: "ci",
|
||||
dir: "./public/icons",
|
||||
},
|
||||
],
|
||||
dir: "./public/icons"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
modules: [
|
||||
@@ -65,15 +65,15 @@ export default defineNuxtConfig({
|
||||
"DM Sans": "100..900",
|
||||
Inter: "100..900",
|
||||
download: true,
|
||||
inject: false,
|
||||
},
|
||||
},
|
||||
inject: false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@nuxt/icon",
|
||||
"reka-ui/nuxt",
|
||||
"@vueuse/nuxt",
|
||||
"@formkit/auto-animate/nuxt",
|
||||
"@vite-pwa/nuxt",
|
||||
"@vite-pwa/nuxt"
|
||||
],
|
||||
|
||||
pwa: {
|
||||
@@ -88,26 +88,27 @@ export default defineNuxtConfig({
|
||||
{
|
||||
src: "/logo/logo-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
type: "image/png"
|
||||
},
|
||||
{
|
||||
src: "/logo/logo-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
type: "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
navigateFallback: "/",
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
skipWaiting: true
|
||||
},
|
||||
devOptions: { enabled: true, type: "module" },
|
||||
devOptions: { enabled: true, type: "module" }
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
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",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.7",
|
||||
"highlight.js": "^11.11.1",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jalali-ts": "^8.0.0",
|
||||
"nuxt": "^3.15.4",
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProductsGrid from "~/components/global/ProductsGrid.vue";
|
||||
// state
|
||||
|
||||
const { data: homeData, suspense } = useHomeData();
|
||||
const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
|
||||
|
||||
// ssr
|
||||
|
||||
@@ -20,11 +21,17 @@ if (response.isError) {
|
||||
});
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<LoadingOverlay />
|
||||
<LoadingOverlay v-if="!disableLoadingOverlay" />
|
||||
<Hero class="mb-20 max-md:mt-[80px]" />
|
||||
<Preview />
|
||||
<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="size-32 shrink-0 rounded-full border border-slate-200 flex-center relative"
|
||||
class="relative shrink-0 rounded-full flex-center"
|
||||
>
|
||||
<Avatar
|
||||
class="!size-32"
|
||||
:src="account!.profile_photo"
|
||||
:alt="
|
||||
account?.first_name && account?.last_name
|
||||
|
||||
@@ -123,7 +123,7 @@ const handleLogin = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
navigateTo("/");
|
||||
window.location.href = "/";
|
||||
} catch (e) {
|
||||
otpCode.value = [];
|
||||
addToast({ message: "مشکلی پیش آمده" });
|
||||
@@ -158,7 +158,7 @@ const resetForm = () => {
|
||||
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
|
||||
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
||||
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;
|
||||
},
|
||||
async function(error) {
|
||||
await Logger.axiosErrorLog(error);
|
||||
|
||||
// if (error.status === 401) {
|
||||
// logout();
|
||||
// }
|
||||
if (config.public.DEBUG === "true" && import.meta.server) {
|
||||
await Logger.axiosErrorLog(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";
|
||||
|
||||
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`;
|
||||
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) {
|
||||
const errorJson = error.toJSON();
|
||||
|
||||
const logData : LogType = {
|
||||
title : error?.message,
|
||||
message : `${error?.config?.method?.toUpperCase()} ${error?.config?.url}`,
|
||||
details : error,
|
||||
}
|
||||
const nowDate = new Date();
|
||||
|
||||
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 {
|
||||
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) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
Vendored
+20
@@ -8,6 +8,26 @@ declare global {
|
||||
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 = {
|
||||
id: number;
|
||||
sender: "ai" | "user";
|
||||
|
||||
Reference in New Issue
Block a user