Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2025-03-22 23:44:21 +03:30
23 changed files with 322 additions and 65 deletions
@@ -112,7 +112,9 @@ const handleDeleteAddress = (id: number) => {
</div>
<div class="flex items-center justify-end w-full lg:w-3/12">
<AddressModal :address="address" />
<ClientOnly>
<AddressModal :address="address" />
</ClientOnly>
</div>
</div>
</button>
@@ -33,7 +33,7 @@ const onSwiper = (swiper: SwiperClass) => {
</script>
<template>
<section class="w-full flex flex-col gap-10 md:gap-[4rem]">
<section class="w-full flex flex-col gap-10 md:gap-[4rem] lg:container">
<div class="w-full flex justify-between items-center max-lg:container">
<span class="text-black typo-h-6 md:typo-h-5 lg:typo-h-4">
{{ title }}
@@ -91,11 +91,11 @@ const onSwiper = (swiper: SwiperClass) => {
:breakpoints="{
640: {
centeredSlides: true,
slidesPerView: 2.5,
slidesPerView: 3,
},
1024: {
centeredSlides: false,
slidesPerView: 3,
slidesPerView: 4,
},
}"
>
@@ -0,0 +1,27 @@
<script setup lang="ts"></script>
<template>
<li
class="w-full rounded-xl border border-slate-200 bg-slate-50 p-4 flex items-center justify-between"
>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<Icon name="bi:info-circle" size="20" />
<h3 class="typo-sub-h-lg font-semibold">تغییر وضعیت سفارش</h3>
|
<span class="typo-p-xs text-cyan-500 font-semibold">
۲۳ تیر
</span>
</div>
<p class="typo-p-sm text-slate-700 text-justify">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با
استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله
در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد
نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد،
کتابهای زیادی در شصت.
</p>
</div>
</li>
</template>
<style scoped></style>
@@ -0,0 +1,34 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
// types
export type SubscribeNotificationRequest = {
body: PushSubscriptionJSON;
};
const useSubscribeNotification = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleSubscribeNotification = async (
params: SubscribeNotificationRequest
) => {
const { data } = await axios.post(API_ENDPOINTS.account.subscribe, {
...params.body,
});
return data;
};
return useMutation({
mutationFn: (subscribeData: SubscribeNotificationRequest) =>
handleSubscribeNotification(subscribeData),
});
};
export default useSubscribeNotification;
@@ -1,5 +1,4 @@
// composables/usePersianTimeAgo.ts
import { ref, onMounted, onUnmounted } from "vue";
import { formatDistance, toDate } from "date-fns-jalali";
import { faIR } from "date-fns-jalali/locale";
+1
View File
@@ -14,6 +14,7 @@ export const API_ENDPOINTS = {
delete: "/accounts/address/delete",
},
update: "/accounts/profile",
subscribe: "/accounts/subscribe",
},
product: {
comments: "/products/comments",
+2 -2
View File
@@ -12,7 +12,7 @@ const prevPage = computed(() => route.meta.prevPage as { name: string, label: st
// queries
const { data: cart, isPending: cartIsPending, suspense } = useGetCartOrders();
const { data: cart, isLoading: cartIsLoading, suspense } = useGetCartOrders();
await suspense();
@@ -72,7 +72,7 @@ const hasCartItem = computed(
>
<NuxtPage />
</div>
<CartSummary v-if="hasCartItem && !cartIsPending" />
<CartSummary v-if="hasCartItem && !cartIsLoading" />
</div>
</div>
<ProductsSlider title="دیگر محصولات" />
+1 -1
View File
@@ -17,7 +17,7 @@ await suspense();
>
<Header />
<main
class="w-full overflow-x-hidden container flex items-start gap-8 lg:gap-6"
class="w-full overflow-x-hidden container flex items-start gap-8 lg:gap-6 min-h-svh"
>
<ProfileSidebar />
<div class="w-9/12">
+1
View File
@@ -44,6 +44,7 @@
"vue-skeletor": "^1.0.6",
"vue3-marquee": "^4.2.2",
"vue3-persian-datetime-picker": "^1.2.2",
"web-push": "^3.6.7",
"workbox-window": "^7.3.0"
},
"devDependencies": {
+1 -3
View File
@@ -15,9 +15,7 @@ definePageMeta({
// queries
const { data: cart, isLoading: cartIsLoading, suspense } = useGetCartOrders();
await suspense();
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
// computed
+117 -2
View File
@@ -5,12 +5,127 @@ definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
});
// imports
import { usePushNotifications } from "~/composables/global/usePushNotifications";
import useSubscribeNotification from "~/composables/api/notifications/useSubscribeNotification";
// state
const params = useUrlSearchParams("history");
const subscribe = ref(false);
const sortFilters = ref([
{
title: "جدید ترین",
value: "created_at",
},
{
title: "قدیمی ترین",
value: "-created_at",
},
]);
// queries
const {
isSupported,
subscribe: notificationSubsribe,
unsubscribe: notificationUnSubsribe,
subscription,
} = usePushNotifications();
const { isPending: subscribeNotificationIsPending } =
useSubscribeNotification();
// watch
watch(
() => subscribe.value,
(nv) => {
if (!!subscription && nv) {
notificationSubsribe();
} else {
notificationUnSubsribe();
}
}
);
</script>
<template>
<div class="w-full flex flex-col gap-5">
<section class="w-full flex flex-col gap-5">
<ProfilePageTitle title="اعلان های شما" icon="bi:bell" />
</div>
<div
v-if="isSupported"
class="w-fill flex items-center justify-between p-5 pt-0 border-b border-slate-200"
>
<div class="flex items-start justify-start gap-3">
<Icon name="bi:bell" size="20" />
<div class="flex flex-col gap-1 pb-0.5">
<span class="text-sm"> دریافت مستقیم اعلانات </span>
<span class="text-xs text-slate-500">
اعلانات حساب شما به صورت مستقیم به دستگاه شما ارسال می
شود
</span>
</div>
</div>
<div class="flex items-center justify-end gap-3">
<Switch v-model="subscribe" />
<Icon
v-if="subscribeNotificationIsPending"
name="svg-spinners:180-ring-with-bg"
size="20"
/>
</div>
</div>
<div class="w-full flex items-center justify-between px-5">
<span> 1 اعلان </span>
<div class="flex items-center justify-start gap-3">
<span class="text-sm">فیلتر بر اساس</span>
<Select
v-model="params.sort!"
triggerRootClass="!py-2.5"
class="w-[6rem]"
>
<template #content>
<SelectGroup>
<SelectItem
v-for="(category, index) in sortFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
:value="category.value"
>
<SelectItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
>
{{ category.title }}
</SelectItemText>
</SelectItem>
</SelectGroup>
</template>
</Select>
</div>
</div>
<div class="w-full flex flex-col gap-5">
<Notification v-for="i in 3" />
</div>
<!-- <div v-if="data?.count > 7" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
</div> -->
</section>
</template>
<style scoped></style>
+26
View File
@@ -0,0 +1,26 @@
import webPush from "web-push";
export default defineEventHandler(() => {
// Generate once and set in .env for production
if (process.env.NODE_ENV === "production") {
if (
!process.env.VAPID_PRIVATE_KEY ||
!process.env.NUXT_PUBLIC_VAPID_KEY
) {
throw createError({
statusCode: 500,
statusMessage: "VAPID keys not configured in production",
});
}
return { publicKey: process.env.NUXT_PUBLIC_VAPID_KEY };
}
// Development: Generate and reuse
if (!process.env.VAPID_PRIVATE_KEY) {
const vapidKeys = webPush.generateVAPIDKeys();
process.env.VAPID_PRIVATE_KEY = vapidKeys.privateKey;
process.env.NUXT_PUBLIC_VAPID_KEY = vapidKeys.publicKey;
}
return { publicKey: process.env.NUXT_PUBLIC_VAPID_KEY };
});