Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -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": 'فروشگاه هی ملز',
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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 || "/"));
|
||||||
|
|||||||
Reference in New Issue
Block a user