Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2025-03-27 15:38:14 +03:30
7 changed files with 207 additions and 107 deletions
+4
View File
@@ -152,6 +152,10 @@ class PushSubscription(models.Model):
def __str__(self): def __str__(self):
return f'{self.user} push' return f'{self.user} push'
class Meta:
verbose_name = 'اشتراک نوتیفیکیشن'
verbose_name_plural = 'اشتراک های نوتیفیکیشن'
def send_notif(self, title, body, icon): def send_notif(self, title, body, icon):
payload = { payload = {
"title": 'فروشگاه هی ملز', "title": 'فروشگاه هی ملز',
+7 -1
View File
@@ -178,16 +178,22 @@ class CommentModel(models.Model):
class AttributeType(models.Model): class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع اتربیوت', max_length=100) name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = 'نوع متغییر محصول'
verbose_name_plural = 'نوع های متغییر محصول'
class AttributeValue(models.Model): class AttributeValue(models.Model):
attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True) attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True) value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
class Meta: class Meta:
unique_together = ('attribute_type', 'value') unique_together = ('attribute_type', 'value')
verbose_name = 'مقدار متغییر محصول'
verbose_name_plural = 'مقدار های متغییر محصول'
def __str__(self): def __str__(self):
return f"{self.attribute_type}: {self.value}" return f"{self.attribute_type}: {self.value}"
+4
View File
@@ -20,6 +20,10 @@ class Attachment(models.Model):
self.name = self.file.name self.name = self.file.name
super(Attachment, self).save(*args, **kwargs) super(Attachment, self).save(*args, **kwargs)
class Meta:
verbose_name = 'پیوست تیکت'
verbose_name_plural = 'پیوست های تیکت'
class Ticket(models.Model): class Ticket(models.Model):
# objects = jmodels.jManager() # objects = jmodels.jManager()
STATUS_CHOICES = [ STATUS_CHOICES = [
@@ -1,65 +1,92 @@
// composables/usePushNotifications.ts import { API_ENDPOINTS } from "~/constants";
import { useLocalStorage, usePermission } from "@vueuse/core"; import useSubscribeNotification from "~/composables/api/notifications/useSubscribeNotification";
import { onMounted, ref } from "vue"; import { useToast } from "~/composables/global/useToast";
import useSubscribeNotification from "../api/notifications/useSubscribeNotification";
interface VapidKeys { interface VapidKeys {
publicKey: string; publicKey: string;
} }
export const usePushNotifications = () => { export const usePushNotifications = () => {
// const isSupported = ref(false); const isSupported = ref(false);
// const permission = usePermission("notifications"); const permission = usePermission("notifications");
// const subscription = useLocalStorage<PushSubscriptionJSON | null>( const subscription = useLocalStorage<PushSubscriptionJSON | null>(
// "push-subscription", "push-subscription",
// null null
// ); );
// const vapid = ref<VapidKeys | null>(null); const vapid = ref<VapidKeys | null>(null);
// const { mutateAsync: subscribeNotification } = useSubscribeNotification(); const { mutateAsync: subscribeNotification } = useSubscribeNotification();
// const toast = useToast(); const { addToast } = useToast();
// // Only run in client-side const unsubscribe = async () => {
// onMounted(async () => { const swRegistration = await navigator.serviceWorker.ready;
// if (typeof window !== "undefined" && "serviceWorker" in navigator) { const existingSubscription =
// isSupported.value = true; await swRegistration.pushManager.getSubscription();
// vapid.value = await $fetch("/api/vapid"); if (existingSubscription) {
// } await existingSubscription.unsubscribe();
// }); }
};
// const subscribe = async () => { onMounted(async () => {
// if (!isSupported.value || !vapid.value?.publicKey) { if (typeof window !== "undefined" && "serviceWorker" in navigator) {
// throw new Error("Push notifications not supported"); isSupported.value = true;
// } vapid.value = await $fetch("/api/vapid");
}
});
// const swRegistration = await navigator.serviceWorker.ready; const subscribe = async () => {
if (!isSupported.value || !vapid.value?.publicKey) {
throw new Error("Push notifications not supported");
}
// const applicationServerKey = vapid.value.publicKey const swRegistration = await navigator.serviceWorker.ready;
// .replace(/-/g, "+")
// .replace(/_/g, "/");
// const convertedKey = Uint8Array.from(atob(applicationServerKey), (c) => await unsubscribe();
// c.charCodeAt(0)
// );
// const pushSubscription = await swRegistration.pushManager.subscribe({ const applicationServerKey = vapid.value.publicKey
// userVisibleOnly: true, .replace(/-/g, "+")
// applicationServerKey: convertedKey, .replace(/_/g, "/");
// });
// const subscriptionJson = pushSubscription.toJSON(); const convertedKey = Uint8Array.from(atob(applicationServerKey), (c) =>
c.charCodeAt(0)
);
// subscribeNotification({ const pushSubscription = await swRegistration.pushManager.subscribe({
// body: subscriptionJson, userVisibleOnly: true,
// }); applicationServerKey: convertedKey,
});
// subscription.value = subscriptionJson; const subscriptionJson = pushSubscription.toJSON();
// };
// return { subscribeNotification(
// isSupported, {
// permission, body: subscriptionJson,
// subscribe, },
// subscription, {
// }; onSuccess: () => {
addToast({
message: "اعلانات برای دستگاه شما فعال شد",
});
},
onError: () => {
addToast({
message: "خطایی در فعال شدن اعلانات رخ داد",
options: {
status: "error",
},
});
},
}
);
subscription.value = subscriptionJson;
};
return {
isSupported,
permission,
subscribe,
unsubscribe,
subscription,
};
}; };
+49 -36
View File
@@ -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,27 +52,37 @@ export default defineNuxtConfig({
customCollections: [ customCollections: [
{ {
prefix: "ci", prefix: "ci",
dir: "./public/icons" dir: "./public/icons",
} },
] ],
}, },
modules: [[ modules: [
"@nuxtjs/google-fonts", [
{ "@nuxtjs/google-fonts",
families: { {
"DM Sans": "100..900", families: {
Inter: "100..900", "DM Sans": "100..900",
download: true, Inter: "100..900",
inject: false download: true,
} inject: false,
} },
], "@nuxt/icon", "reka-ui/nuxt", "@vueuse/nuxt", "@formkit/auto-animate/nuxt", "@vite-pwa/nuxt", "@nuxt/image"], },
],
"@nuxt/icon",
"reka-ui/nuxt",
"@vueuse/nuxt",
"@formkit/auto-animate/nuxt",
"@vite-pwa/nuxt",
"@nuxt/image",
],
pwa: { pwa: {
strategies: "injectManifest", strategies: "injectManifest",
srcDir: "public", srcDir: "public",
filename: "sw.js", filename: "sw.js",
registerType:
process.env.NODE_ENV === "production" ? "autoUpdate" : "prompt",
manifest: { manifest: {
name: "Heymlz", name: "Heymlz",
short_name: "Heymlz", short_name: "Heymlz",
@@ -81,35 +91,38 @@ 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: process.env.NODE_ENV === "production",
type: "module",
}, },
devOptions: { enabled: true, type: "module" }
}, },
typescript: { typescript: {
typeCheck: false typeCheck: false,
}, },
image: { image: {
quality : 65 quality: 65,
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {
API_BASE_URL: process.env.API_BASE_URL, API_BASE_URL: process.env.API_BASE_URL,
DEBUG: process.env.DEBUG DEBUG: process.env.DEBUG,
} },
} },
}); });
+52 -8
View File
@@ -8,29 +8,73 @@ export default defineNuxtPlugin(() => {
const { isInstalledAsPWA } = usePWA(); const { isInstalledAsPWA } = usePWA();
if ("serviceWorker" in navigator && isInstalledAsPWA.value) { if ("serviceWorker" in navigator && isInstalledAsPWA.value) {
// Initialize Workbox
wb = new Workbox("/sw.js"); wb = new Workbox("/sw.js");
wb.addEventListener("waiting", () => { navigator.serviceWorker
checkForUpdate(); .register("/sw.js")
}); .then((registration) => {
// Native Service Worker API for update detection
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed") {
// Only show prompt if there's a controller (not first install)
if (navigator.serviceWorker.controller) {
checkForUpdate();
}
}
});
}
});
wb.register() // Workbox events for consistency
.then((registration: any) => { wb?.addEventListener("waiting", () => {
checkForUpdate();
});
// Check if there's already a waiting worker
if (registration.waiting) { if (registration.waiting) {
checkForUpdate(); checkForUpdate();
} }
// Periodic update checks (optional)
setInterval(() => {
registration.update().catch((err) => {
console.debug(
"Service worker update check failed:",
err
);
});
}, 60 * 60 * 1000); // Check every hour
}) })
.catch((error) => { .catch((err) => {
console.error("Service worker registration failed:", error); console.error("Service worker registration failed:", err);
}); });
// Register Workbox
wb.register().catch((error) => {
console.error("Workbox registration failed:", error);
});
} }
const checkForUpdate = () => { const checkForUpdate = () => {
updateAvailable.value = true; if (!updateAvailable.value) {
updateAvailable.value = true;
}
}; };
const handleUpdate = () => { const handleUpdate = () => {
if (wb) { if (wb) {
// Send skip waiting message
wb.messageSW({ type: "SKIP_WAITING" }).then(() => { wb.messageSW({ type: "SKIP_WAITING" }).then(() => {
// Notify all tabs to reload
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: "CLIENT_RELOAD",
});
}
window.location.reload(); window.location.reload();
}); });
} }
+17 -15
View File
@@ -3,33 +3,36 @@ import { precacheAndRoute } from "workbox-precaching";
// Precaching configuration for PWA assets // Precaching configuration for PWA assets
precacheAndRoute(self.__WB_MANIFEST); precacheAndRoute(self.__WB_MANIFEST);
// Version
const VERSION = "1.0.4"; const VERSION = "1.0.4";
// Service Worker Installation // Only enable skipWaiting and claim in production
const isProduction = process.env.NODE_ENV === "production";
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
self.skipWaiting(); if (isProduction) {
self.skipWaiting();
}
}); });
// Service Worker Activation
self.addEventListener("activate", (event) => { self.addEventListener("activate", (event) => {
event.waitUntil( event.waitUntil(
(async () => { (async () => {
const clients = await self.clients.matchAll({ type: "window" }); if (isProduction) {
const clients = await self.clients.matchAll({ type: "window" });
// Notify all open clients about the version clients.forEach((client) =>
clients.forEach((client) => client.postMessage({
client.postMessage({ type: "VERSION_CHECK", version: VERSION }) type: "VERSION_CHECK",
); version: VERSION,
})
self.clients.claim(); );
self.clients.claim();
}
console.log("Service Worker Activated (Version: " + VERSION + ")"); console.log("Service Worker Activated (Version: " + VERSION + ")");
})() })()
); );
}); });
// Push Notification Handler for Django Web Push // Rest of your existing handlers remain the same...
self.addEventListener("push", (event) => { self.addEventListener("push", (event) => {
try { try {
const payload = event.data?.json() || { const payload = event.data?.json() || {
@@ -51,7 +54,6 @@ self.addEventListener("push", (event) => {
} }
}); });
// Notification Click Handler
self.addEventListener("notificationclick", (event) => { self.addEventListener("notificationclick", (event) => {
event.notification.close(); event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data?.url || "/")); event.waitUntil(clients.openWindow(event.notification.data?.url || "/"));