This commit is contained in:
Parsa Nazer
2025-03-19 20:08:58 +03:30
25 changed files with 522 additions and 253 deletions
+1 -1
View File
@@ -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"
+11 -13
View File
@@ -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,10 +103,10 @@ 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"
class="h-5 lg:h-6 "
fill="none"
viewBox="0 0 220 40"
>
+28 -9
View File
@@ -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]"
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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>
+14 -14
View File
@@ -32,7 +32,9 @@ let scrollTrigger: ScrollTrigger;
// methods
const onSwiper = (swiper: SwiperClass) => {
showLoadingOverlay.value = false;
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"
+79 -88
View File
@@ -6,7 +6,7 @@ import useHomeData from "~/composables/api/home/useHomeData";
// state
const { data : homeData } = useHomeData();
const { data: homeData } = useHomeData();
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
@@ -17,71 +17,60 @@ 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");
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(() => {
scrollTrigger.refresh()
}, 1000);
showcaseElements.forEach((element, index) => {
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(() => {
@@ -92,33 +81,35 @@ 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]"
>
<NuxtLink
v-for="slide in homeData!.show_case_slider"
:key="slide.id"
:to="slide.link"
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
>
<div class="w-full h-[125svh] bg-black">
<NuxtLink
v-for="slide in homeData!.show_case_slider"
:key="slide.id"
:to="slide.link"
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
>
<img
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"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
}"
alt=""
/>
<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">
{{ slide.title }}
</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">
{{ slide.description }}
</p>
</div>
</NuxtLink>
</div>
<img
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"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
}"
alt=""
/>
<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">
{{ slide.title }}
</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">
{{ slide.description }}
</p>
</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"
/>
</div>
<Avatar
:src="currentProfile"
alt=""
class="!size-32"
/>
<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>
+9
View File
@@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(() => {
const config = useRuntimeConfig();
if (config.public.DEBUG === "true") {
return;
} else {
return navigateTo("/");
}
});
+27 -26
View File
@@ -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
}
}
});
+1
View File
@@ -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 -1
View File
@@ -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" />
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+163
View File
@@ -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>
+3 -4
View File
@@ -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

+6
View File
@@ -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
View File
@@ -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);
}
+20
View File
@@ -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";