This commit is contained in:
marzban-dev
2025-03-22 23:43:49 +03:30
parent 852a09c298
commit 2106f3f40b
56 changed files with 198 additions and 161 deletions
+1 -1
View File
@@ -35,6 +35,6 @@ const closeModal = () => {
/>
</ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" />
</div>
</template>
@@ -109,7 +109,7 @@ watch(
(newValue) => {
if (!isEditing.value) {
addressData.value.phone =
newValue == "بله" ? account.value?.phone : "";
newValue == "بله" ? account.value!.phone : "";
}
},
{
@@ -21,18 +21,17 @@ const discountCode = ref(cart.value?.discount_code?.code || "");
const {
mutateAsync: submitDiscountCode,
isPending: submitDiscountCodeIsPending,
isPending: submitDiscountCodeIsPending
} = useSubmitDiscountCode();
const {
mutateAsync: deleteDiscountCode,
isPending: deleteDiscountCodeIsPending,
isPending: deleteDiscountCodeIsPending
} = useDeleteDiscountCode();
// computed
const nextPage: ComputedRef<{ name: string; label: string } | undefined> =
computed(() => route.meta.nextPage);
const nextPage = computed(() => route.meta.nextPage as { name: string; label: string } | undefined);
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
@@ -49,11 +48,11 @@ const handleSubmitDiscountCode = () => {
addToast({
message: "خطایی در ثبت کد تخفیف رخ داد",
options: {
status: "error",
},
status: "error"
}
});
discountCode.value = "";
},
}
}
);
};
@@ -68,11 +67,11 @@ const handleDeleteDiscountCode = () => {
addToast({
message: "خطایی در حذف کد تخفیف رخ داد",
options: {
status: "error",
},
status: "error"
}
});
discountCode.value = "";
},
}
});
};
</script>
@@ -44,7 +44,7 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
</div>
<div
v-if="cart?.items.length > 5"
v-if="cart && cart.items.length > 5"
class="h-7 flex-center col-span-full lg:hidden"
>
<button
+4 -3
View File
@@ -6,7 +6,7 @@ import { useImage } from "@vueuse/core";
// types
type Props = {
src: string;
src: string | null | undefined;
alt: string;
};
@@ -18,7 +18,7 @@ const { src } = toRefs(props);
// state
const { isLoading } = useImage({ src: src.value });
const { isLoading } = useImage({ src: src.value ?? '' });
</script>
<template>
@@ -26,11 +26,12 @@ const { isLoading } = useImage({ src: src.value });
class="flex-center size-full select-none rounded-full align-middle overflow-hidden inset-shadow-black/20 inset-shadow-sm"
>
<Skeleton
v-if="isLoading"
v-if="isLoading && !!src"
class="w-full !h-[110%] !rounded-full aspect-square"
/>
<template v-else>
<AvatarImage
v-if="!!src"
class="!size-full rounded-full object-cover"
:src="src"
:alt="alt"
+2 -2
View File
@@ -37,7 +37,7 @@ const value = ref<OptionChildren>();
watch(
() => value.value,
(newValue: OptionChildren) => {
(newValue) => {
if (!!newValue) {
emit("update:modelValue", newValue.id);
}
@@ -51,7 +51,7 @@ watch(
.flatMap((option) => option.children)
.find((child) => child.id == newValue);
value.value = target || null;
value.value = target || undefined;
},
{ immediate: true }
);
+4 -4
View File
@@ -35,8 +35,8 @@ const fileLimit = 1024 * 1024 * 2;
// methods
const onDrop = (files: File[] | null) => {
if (modelValue.value.length < 3) {
files?.forEach((file, index) => {
if (modelValue.value.length < 3 && files) {
files.forEach((file, index) => {
if (file.size > fileLimit) {
files.splice(index, 1);
addToast({
@@ -51,7 +51,7 @@ const onDrop = (files: File[] | null) => {
emit("update:modelValue", [...files.slice(0, 3)]);
} else {
if (modelValue.value.length + files.length <= 3) {
files?.forEach((item) => {
files.forEach((item) => {
emit("change", item);
resetFileDialog();
});
@@ -110,7 +110,7 @@ const removeAttachment = (id: number) => {
<div class="flex flex-col w-full h-max gap-5 pt-8">
<div
ref="dropZoneRef"
@click="openDialog"
@click="openDialog()"
class="bg-slate-50 relative flex-col-center w-full transition-all text-black/50 gap-3 h-[20rem] border border-dashed rounded-xl cursor-pointer select-none"
:class="{
'border-black': isOverDropZone,
@@ -57,7 +57,7 @@ onMounted(() => {
>
<img
id="loading-overlay-image"
src="/img/heymlz-loading-1.gif"
src="/img/heymlz/heymlz-loading-1.gif"
class="opacity-0 scale-70 absolute z-20"
alt=""
/>
@@ -11,22 +11,22 @@ type Highlight = {
const highlights = ref<Highlight[]>([
{
icon: "/img/footer-support.svg",
icon: "/img/heymlz/footer-support.svg",
title: "خدمات مشتری",
description: "پشتیبانی استثنایی، راه‌حل‌های پایدار برای شما",
},
{
icon: "/img/footer-send.svg",
icon: "/img/heymlz/footer-send.svg",
title: "ارسال سریع و رایگان",
description: "ارسال رایگان برای سفارش‌های بالای ۱۵۰ دلار",
},
{
icon: "/img/footer-share.svg",
icon: "/img/heymlz/footer-share.svg",
title: "معرفی به دوستان",
description: "ما را به دوستان خود معرفی کنید",
},
{
icon: "/img/footer-security.svg",
icon: "/img/heymlz/footer-security.svg",
title: "پرداخت امن",
description: "پرداخت شما به‌صورت امن پردازش می‌شود",
},
@@ -12,12 +12,12 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
<div class="w-full flex flex-col">
<AccordionRoot
class="w-full last:border-b last:border-slate-200"
:default-value="'item' + selectedVariant.details[0].detail_category"
:default-value="'item' + selectedVariant!.details[0].detail_category"
type="single"
:collapsible="true"
>
<AccordionItem
v-for="detailItem in selectedVariant.details"
v-for="detailItem in selectedVariant!.details"
:value="'item' + detailItem.detail_category"
class="overflow-hidden"
>
@@ -9,7 +9,7 @@ import { PRODUCT_RANGE } from "~/constants";
// state
const params: GetProductsFilters = inject("params");
const params = inject("params") as GetProductsFilters;
const sort_filter = ref([
{ title: "جدیدترین ها", value: "newest" },
@@ -22,12 +22,12 @@ const sliderValue = ref([
params.price_lte ?? PRODUCT_RANGE.max,
]);
const has_discount = ref(JSON.parse(params.has_discount ?? false));
const in_stock = ref(JSON.parse(params.in_stock ?? false));
const has_discount = ref(Boolean(params.has_discount) ?? false);
const in_stock = ref(Boolean(params.in_stock) ?? false);
const sliderValueDebounced = refDebounced(sliderValue, 1000);
const filtersSuccessMessage = ref<{ title: string; status: string } | null>("");
const filtersSuccessMessage = ref<{ title: string; status: string } | null>(null);
// queries
+1 -1
View File
@@ -32,7 +32,7 @@ const onSwiper = (swiper: SwiperClass) => {
</div>
<img
src="/img/gradient-2.png"
src="/img/categories-gradient.png"
class="animate-spin [animation-duration:16s] object-cover absolute size-full brightness-45 scale-115 aspect-square"
:style="{
maskImage: 'radial-gradient(black, transparent 50%)'
@@ -29,17 +29,17 @@ const {
isPending: isChatPending,
isFetchingNextPage: isNextChatPagePending,
hasNextPage: hasMoreChat,
fetchNextPage: loadMoreChat,
fetchNextPage: loadMoreChat
} = useGetChat(id, isOpen);
const isCreateMessagePending = useIsMutating({
mutationKey: [MUTATION_KEYS.create_chat],
mutationKey: [MUTATION_KEYS.create_chat]
});
const canLoadMoreChat = ref(false);
const isChatScrollLocked = useScrollLock(chatContainerEl);
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
behavior: "smooth",
behavior: "smooth"
});
useInfiniteScroll(
@@ -56,7 +56,7 @@ useInfiniteScroll(
distance: 10,
direction: "top",
throttle: 1000,
canLoadMore: () => canLoadMoreChat.value,
canLoadMore: () => canLoadMoreChat.value
}
);
@@ -116,7 +116,7 @@ whenever(
}, 2000);
},
{
once: true,
once: true
}
);
</script>
@@ -125,7 +125,7 @@ whenever(
<Transition name="fade-right-to-left">
<div
v-if="isOpen"
class="fixed right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
class="fixed z-50 right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
>
<CloseButton :disabled="!!isCreateMessagePending" />
@@ -187,6 +187,7 @@ whenever(
class="text-black p-4.5 size-full flex justify-center items-center"
v-else
>
<img class="size-[50px]" src="/img/heymlz/heymlz-idle.gif" alt="" />
Please sign in first
</div>
</div>
@@ -13,18 +13,18 @@ provide("isOpen", {
isOpen,
closeChat,
});
</script>
<template>
<button
v-if="!isOpen"
@click="isOpen = !isOpen"
class="cursor-pointer fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
>
<Icon
name="streamline:artificial-intelligence-spark"
class="**:stroke-white"
size="26"
class="**:stroke-white size-[26px]"
/>
</button>
@@ -80,7 +80,7 @@ onMounted(() => {
>
<img
v-if="!reverse"
src="/public/img/hero-bg.jpg"
src="/img/footer-bg.jpg"
class="size-full object-cover absolute"
alt="profile"
/>
@@ -1,8 +1,10 @@
<script lang="ts" setup>
// provide / inject
// import
import type { ProductVariantProvideType } from "~/pages/product";
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
// provide / inject
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
@@ -26,7 +28,7 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
class="w-full grid grid-cols-2 gap-y-[1.5rem] sm:gap-x-[3rem]"
>
<div
v-for="inPackItem in selectedVariant.in_pack_items"
v-for="inPackItem in selectedVariant!.in_pack_items"
class="w-full flex-col-center gap-[.75rem]"
>
<div
+1 -1
View File
@@ -3,7 +3,7 @@
// import
import useGetProduct from "~/composables/api/product/useGetProduct";
import type { ProductVariantProvideType } from "~/pages/product/types";
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
// state
@@ -6,7 +6,7 @@ import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
// types
type Props = {
data: Ticket;
data: Omit<Ticket, "messages">;
};
// props
@@ -2,7 +2,7 @@
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
import { sanitize } from "isomorphic-dompurify";
import sanitizeHtml from 'sanitize-html';
// types
@@ -27,8 +27,8 @@ const useGetArticle = (id: number | string | undefined) => {
select: (article) => {
const copyOfArticle = { ...article };
copyOfArticle.summery = sanitize(copyOfArticle.summery);
copyOfArticle.content = sanitize(copyOfArticle.content);
copyOfArticle.summery = sanitizeHtml(copyOfArticle.summery);
copyOfArticle.content = sanitizeHtml(copyOfArticle.content);
return copyOfArticle;
}
@@ -76,10 +76,9 @@ const useCreateChatMessage = (queryClient: QueryClient) => {
onSuccess: (response) => {
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>(
[QUERY_KEYS.chat],
(oldData) => {
(oldData : any) => {
if (oldData) {
const lastPage =
oldData!.pages[oldData!.pages.length - 1];
const lastPage = oldData!.pages[oldData!.pages.length - 1];
return {
pages: [
@@ -14,6 +14,7 @@ export type GetAllOrdersRequest = {
};
const useGetAllOrders = (params: ComputedRef<GetAllOrdersRequest>) => {
// state
const { $axios: axios } = useNuxtApp();
@@ -2,7 +2,7 @@
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
import { sanitize } from "isomorphic-dompurify";
import sanitizeHtml from 'sanitize-html';
// types
@@ -27,7 +27,7 @@ const useGetProduct = (id: string | number | undefined) => {
select: (product) => {
const copyOfProduct = { ...product };
copyOfProduct.description = sanitize(copyOfProduct.description);
copyOfProduct.description = sanitizeHtml(copyOfProduct.description);
copyOfProduct.variants = copyOfProduct.variants.sort((a, b) => b.in_stock - a.in_stock);
+1 -1
View File
@@ -14,7 +14,7 @@ export const useImageColor = (img: string) => {
try {
const color = await fac.getColorAsync(imageEl);
isPending.value = false;
colorObject.value = color;
colorObject.value = color
} catch (e) {
isPending.value = false;
}
+1 -1
View File
@@ -8,7 +8,7 @@ const route = useRoute();
// computed
const pageTitle = computed(() => route.meta.pageTitle);
const prevPage = computed(() => route.meta.prevPage);
const prevPage = computed(() => route.meta.prevPage as { name: string, label: string } | undefined);
// queries
+4
View File
@@ -105,6 +105,10 @@ export default defineNuxtConfig({
devOptions: { enabled: true, type: "module" }
},
typescript: {
typeCheck: false
},
runtimeConfig: {
public: {
API_BASE_URL: process.env.API_BASE_URL,
+8 -3
View File
@@ -6,6 +6,7 @@
"start": "node .output/server/index.mjs",
"build": "nuxt build",
"dev": "nuxt dev",
"lint": "nuxi typecheck",
"dev-network": "nuxi dev --host",
"dev-o": "nuxt dev -- -o",
"test": "vitest",
@@ -20,6 +21,7 @@
"@tanstack/vue-query": "^5.62.2",
"@tanstack/vue-query-devtools": "^5.62.3",
"@vite-pwa/nuxt": "^0.10.6",
"@vue/language-server": "^2.2.8",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.7.0",
@@ -30,10 +32,10 @@
"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",
"reka-ui": "^1.0.0-alpha.6",
"sanitize-html": "^2.15.0",
"swiper": "^11.2.4",
"universal-cookie": "^7.2.2",
"vue": "latest",
@@ -46,9 +48,12 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@types/masonry-layout": "^4.2.8",
"@types/node": "^22.13.11",
"@types/sanitize-html": "^2.13.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9"
"tailwindcss": "^4.0.9",
"typescript": "^5.8.2",
"vue-tsc": "^2.2.8"
}
}
+2 -1
View File
@@ -15,6 +15,7 @@ const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
const response = await suspense();
if (response.isError) {
console.log(response);
throw createError({
statusCode: 500,
statusMessage: `Landing error : ${response.error.message}`
@@ -41,7 +42,7 @@ onMounted(() => {
/>
<Categories class="mt-40" />
<Brands />
<MostRecentComments />
<!-- <MostRecentComments />-->
<ClientOnly>
<LatestStories class="mb-20" />
</ClientOnly>
+4 -4
View File
@@ -123,16 +123,16 @@ watch(
</div>
</ul>
<div v-else class="w-full h-max">
<div v-if="!products?.length" class="flex flex-grow w-full">
<div v-if="!products!.length" class="flex flex-grow w-full">
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
</div>
<ProductsGrid
:with-header="false"
:products="products"
:products="products!"
class="!p-0"
/>
<div v-if="data?.count > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
<div v-if="data && paginationData && data.count > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data.count" />
</div>
</div>
</div>
+3 -3
View File
@@ -109,7 +109,7 @@ const formRules = computed(() => {
};
});
const formValidator$ = useVuelidate(formRules, personalData);
const formValidator$ = useVuelidate(formRules, personalData as any);
// methods
@@ -195,7 +195,7 @@ const handleSubmit = (withValidation: boolean) => {
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
بایوگرافی درست میکنه :)
</span>
<span
<div
class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full"
>
<div class="rounded-full p-2">
@@ -208,7 +208,7 @@ const handleSubmit = (withValidation: boolean) => {
<span class="text-xs text-yellow-500"
>جزو ۳ مشتری برتر</span
>
</span>
</div>
</div>
</div>
@@ -12,7 +12,7 @@ definePageMeta({
// state
const params: GetAllOrdersRequest = useUrlSearchParams("history");
const params = useUrlSearchParams("history") as GetAllOrdersRequest;
const filters = computed(() => {
return {
@@ -91,12 +91,12 @@ const purchases = computed(() => {
return data.value?.results.flat();
});
const hasPurchases = computed(() => data.value?.count > 0);
const hasPurchases = computed(() => data.value && data.value.count > 0);
const hasFilters = computed(() =>
Object.keys(params)
.filter((key) => key != "page")
.some((key) => params[key] != undefined)
.some((key) => (params as any)[key] != undefined)
);
const paginationData = computed(() => {
@@ -246,8 +246,8 @@ const clearFilters = () => {
</template>
</Table>
<div v-if="data?.count > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
<div v-if="data && paginationData && data.count > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data.count" />
</div>
</div>
</div>
+4 -4
View File
@@ -14,7 +14,7 @@ definePageMeta({
// state
const params: GetAllTicketsRequest = useUrlSearchParams("history");
const params = useUrlSearchParams("history") as GetAllTicketsRequest;
const filters = computed(() => {
return {
@@ -72,12 +72,12 @@ const tickets = computed(() => {
return data.value?.results.flat();
});
const hasTickets = computed(() => data.value?.count > 0);
const hasTickets = computed(() => data.value && data.value.count > 0);
const hasFilters = computed(() =>
Object.keys(params)
.filter((key) => key != "page")
.some((key) => params[key] != undefined)
.some((key) => (params as any)[key] != undefined)
);
const paginationData = computed(() => {
@@ -227,7 +227,7 @@ const clearFilters = () => {
</template>
</Table>
<div v-if="data?.count > 7" class="w-full flex-center py-10">
<div v-if="data && paginationData && data.count > 7" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
</div>
</div>
+2 -2
View File
@@ -160,8 +160,8 @@ const resetForm = () => {
/>
<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"
class="aspect-square w-[450px] translate-y-[197px] animate-fade-in"
src="/img/heymlz/heymlz-seat-2.webp"
alt=""
/>
+9 -2
View File
@@ -22,7 +22,7 @@ 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");
const response = await axios.get<AxiosLogType[]>("http://localhost:3000/api/server-logger");
return response.data.reverse();
},
refetchInterval: 5000,
@@ -102,7 +102,14 @@ onMounted(() => {
<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">
<details
v-if="log.response && typeof log.response === 'string' && log.response.includes('<!DOCTYPE html>')"
class="text-white"
>
<summary class="cursor-pointer select-none">Preview :</summary>
<iframe class="w-full h-[500px]" :srcdoc="log.response"></iframe>
</details>
<details v-if="log.response" class="text-white">
<summary class="cursor-pointer select-none">Response :</summary>
<pre>
<code class="language-json">

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before

Width:  |  Height:  |  Size: 10 MiB

After

Width:  |  Height:  |  Size: 10 MiB

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 943 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

+64 -64
View File
@@ -1,64 +1,64 @@
import { precacheAndRoute } from "workbox-precaching";
// Precaching configuration for PWA assets
precacheAndRoute(self.__WB_MANIFEST);
// Version
const VERSION = "1.0.4";
// Service Worker Installation
self.addEventListener("install", (event) => {
self.skipWaiting();
});
// Service Worker Activation
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({ type: "window" });
// Notify all open clients about the version
clients.forEach((client) =>
client.postMessage({ type: "VERSION_CHECK", version: VERSION })
);
self.clients.claim();
console.log("Service Worker Activated (Version: " + VERSION + ")");
})()
);
});
// Push Notification Handler for Django Web Push
self.addEventListener("push", (event) => {
try {
const payload = event.data?.json() || {
title: "New Notification",
body: "You have a new message",
icon: "/logo-192x192.png",
data: { url: "/" },
};
event.waitUntil(
self.registration.showNotification(payload.title, {
body: payload.body,
icon: payload.icon || "/logo-192x192.png",
data: payload.data,
})
);
} catch (error) {
console.error("Push handling failed:", error);
}
});
// Notification Click Handler
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data?.url || "/"));
});
self.addEventListener("message", (event) => {
if (event.data === "SKIP_WAITING") {
self.skipWaiting();
}
});
// import { precacheAndRoute } from "workbox-precaching";
//
// // Precaching configuration for PWA assets
// precacheAndRoute(self.__WB_MANIFEST);
//
// // Version
//
// const VERSION = "1.0.4";
//
// // Service Worker Installation
// self.addEventListener("install", (event) => {
// self.skipWaiting();
// });
//
// // Service Worker Activation
// self.addEventListener("activate", (event) => {
// event.waitUntil(
// (async () => {
// const clients = await self.clients.matchAll({ type: "window" });
//
// // Notify all open clients about the version
// clients.forEach((client) =>
// client.postMessage({ type: "VERSION_CHECK", version: VERSION })
// );
//
// self.clients.claim();
// console.log("Service Worker Activated (Version: " + VERSION + ")");
// })()
// );
// });
//
// // Push Notification Handler for Django Web Push
// self.addEventListener("push", (event) => {
// try {
// const payload = event.data?.json() || {
// title: "New Notification",
// body: "You have a new message",
// icon: "/logo-192x192.png",
// data: { url: "/" },
// };
//
// event.waitUntil(
// self.registration.showNotification(payload.title, {
// body: payload.body,
// icon: payload.icon || "/logo-192x192.png",
// data: payload.data,
// })
// );
// } catch (error) {
// console.error("Push handling failed:", error);
// }
// });
//
// // Notification Click Handler
// self.addEventListener("notificationclick", (event) => {
// event.notification.close();
// event.waitUntil(clients.openWindow(event.notification.data?.url || "/"));
// });
//
// self.addEventListener("message", (event) => {
// if (event.data === "SKIP_WAITING") {
// self.skipWaiting();
// }
// });
+1 -1
View File
@@ -2,5 +2,5 @@ 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>[];
return JSON.parse(oldLogs) as Record<any, any>[];
});
+8 -3
View File
@@ -1,4 +1,5 @@
import fs from "fs/promises";
import { ensureFileExists } from "~/utils";
class Logger {
public static async axiosErrorLog(error: any) {
@@ -13,19 +14,23 @@ class Logger {
method: errorJson.config.method,
response: error?.response?.data,
requestHeaders: errorJson.config.headers,
responseHeaders: error.response.headers,
// responseHeaders: error.response.headers,
payload: errorJson.config.data ? JSON.parse(errorJson.config.data) : undefined,
params: errorJson.config.params ?? undefined,
date: nowDate.toString()
};
const logFilePath = ".logs/log.json";
try {
const oldLogs = await fs.readFile(".logs/log.json", "utf-8");
await ensureFileExists(logFilePath, "[]");
const oldLogs = await fs.readFile(logFilePath, "utf-8");
const oldLogsJson = JSON.parse(oldLogs) as Record<any, any>[];
oldLogsJson.push(logData);
await fs.writeFile(".logs/log.json", JSON.stringify(oldLogsJson));
await fs.writeFile(logFilePath, JSON.stringify(oldLogsJson));
} catch (e) {
console.error(e);
}
+3 -3
View File
@@ -21,8 +21,8 @@ declare global {
status: number,
code: string,
requestHeaders: Record<any, any>,
responseHeaders: Record<any, any>,
response?: Record<any, any>,
responseHeaders?: Record<any, any>,
response?: any,
payload?: Record<any, any>,
params?: Record<any, any>,
date: string
@@ -35,7 +35,7 @@ declare global {
};
type Account = {
profile_photo: File | null;
profile_photo: string | null;
first_name: string;
last_name: string;
phone: string;
+19
View File
@@ -1,3 +1,6 @@
import fs from "fs/promises";
import path from "path";
export const dateFormatter = (date: string | undefined) => {
const formattedDate = useTimeAgo(date!);
return formattedDate.value;
@@ -77,3 +80,19 @@ export const isImage = (name: string | undefined) => {
}
return false;
};
// Ensure Exist
export const ensureFileExists = async (filePath: string, initialContent = "") => {
try {
await fs.access(filePath);
} catch (error) {
const err = error as any;
if (err.code === "ENOENT") {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, initialContent, "utf-8");
} else {
throw err;
}
}
};
-7
View File
@@ -1,7 +0,0 @@
import { defineVitestConfig } from "@nuxt/test-utils/config";
export default defineVitestConfig({
test: {
environment: "nuxt",
},
});