diff --git a/.gitignore b/.gitignore
index 2902a54..13cf87a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ lerna-debug.log*
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map
-
+public/worker.js
.eslintcache
*.tsbuildinfo
@@ -35,4 +35,4 @@ certificates
content/output/*
public/content/output/*
public/content/output/shifts 2024.1.json
-!public/content/uploads/*
\ No newline at end of file
+!public/content/uploads/*
diff --git a/components/PwaManager.tsx b/components/PwaManager.tsx
index 9d89667..e7fe746 100644
--- a/components/PwaManager.tsx
+++ b/components/PwaManager.tsx
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react';
import common from '../src/helpers/common'; // Ensure this path is correct
+//use session to get user role
+import { useSession } from "next-auth/react"
function PwaManager() {
const [deferredPrompt, setDeferredPrompt] = useState(null);
@@ -10,6 +12,8 @@ function PwaManager() {
const [registration, setRegistration] = useState(null);
const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
+ const { data: session } = useSession();
+
// Handle PWA installation
useEffect(() => {
@@ -54,7 +58,7 @@ function PwaManager() {
}, []);
const installPWA = async (e) => {
-
+ console.log('installing PWA');
e.preventDefault();
if (deferredPrompt) {
deferredPrompt.prompt();
@@ -107,9 +111,25 @@ function PwaManager() {
applicationServerKey: base64ToUint8Array(vapidPublicKey)
});
// Call your API to save subscription data on server
- setSubscription(sub);
- setIsSubscribed(true);
- console.log('Web push subscribed!');
+ if (session.user?.id != null) {
+ await fetch(`/api/notify`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ subscription: sub, id: session.user.id })
+ }).then(response => {
+ if (!response.ok) {
+ throw new Error('Failed to save subscription data on server.');
+ }
+ else {
+ console.log('Subscription data saved on server.');
+ setSubscription(sub);
+ setIsSubscribed(true);
+ console.log('Web push subscribed!');
+ }
+ });
+ }
console.log(sub);
} catch (error) {
console.error('Error subscribing to notifications:', error);
@@ -124,6 +144,24 @@ function PwaManager() {
// Call your API to delete or invalidate subscription data on server
setSubscription(null);
setIsSubscribed(false);
+ if (session?.user?.id != null) {
+ await fetch(`/api/notify`,
+ {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ id: session?.user.id }),
+ }
+ ).then(response => {
+ if (!response.ok) {
+ throw new Error('Failed to delete subscription data on server.');
+ }
+ else {
+ console.log('Subscription data deleted on server.');
+ }
+ });
+ }
console.log('Web push unsubscribed!');
} catch (error) {
console.error('Error unsubscribing from notifications:', error);
diff --git a/next.config.js b/next.config.js
index 7e695a0..9f43873 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,15 +1,15 @@
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
-const { InjectManifest } = require('workbox-webpack-plugin');
+const { InjectManifest, GenerateSW } = require('workbox-webpack-plugin');
const withPWA = require('next-pwa')({
dest: 'public',
register: true, // ?
publicExcludes: ["!_error*.js"], //?
-
- disable: process.env.NODE_ENV === 'development',
+ skipWaiting: true,
+ // disable: process.env.NODE_ENV === 'development',
})
-module.exports = {
+module.exports = withPWA({
typescript: {
// !! WARN !!
// Dangerously allow production builds to successfully complete even if
@@ -20,19 +20,31 @@ module.exports = {
compress: false,
pageExtensions: ['ts', 'tsx', 'md', 'mdx'], // Replace `jsx?` with `tsx?`
env: {
- env: process.env.NODE_ENV,
+ env: process.env.APP_ENV,
server: process.env.NEXT_PUBLIC_PUBLIC_URL
},
- plugins: [
- // Other plugins...
- new InjectManifest({
- // These are some common options, and not all are required.
- // Consult the docs for more info.
- //exclude: [/.../, '...'],
- maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
- swSrc: './worker/index.js',
- }),
- ],
+ // pwa: {
+ // dest: 'public',
+ // register: true,
+ // publicExcludes: ["!_error*.js"],
+ // disable: process.env.NODE_ENV === 'development',
+ // // sw: './worker/index.js', // Custom service worker file name
+ // },
+
+ // plugins: [
+ // // new InjectManifest({
+ // // // These are some common options, and not all are required.
+ // // // Consult the docs for more info.
+ // // //exclude: [/.../, '...'],
+ // // maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
+ // // swSrc: './worker.js',
+ // // }),
+ // // new GenerateSW({
+ // // //disable all files
+ // // maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
+ // // // swSrc: './worker.js',
+ // // }),
+ // ],
webpack: (config, { isServer, buildId, dev }) => {
// Configure optimization and source maps
config.optimization.minimize = !dev;
@@ -46,18 +58,18 @@ module.exports = {
// InjectManifest configuration
if (!isServer) {
- config.plugins.push(new InjectManifest({
- swSrc: './worker/index.js', // Path to source service worker file
- swDest: '/worker/index.js', // Destination filename in the build output
- maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // Adjust as needed
- exclude: [/\.map$/, /_error.js$/, /favicon.ico$/] // Customize exclusion patterns
- })
- );
+ // config.plugins.push(new InjectManifest({
+ // // swSrc: './worker.js', // Path to source service worker file
+ // // swDest: '/worker.js', // Destination filename in the build output
+ // maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // Adjust as needed
+ // exclude: [/\.map$/, /_error.js$/, /favicon.ico$/] // Customize exclusion patterns
+ // })
+ // );
}
// Bundle Analyzer Configuration
if (process.env.ANALYZE && !isServer) {
- const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
+ //const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
@@ -77,4 +89,4 @@ module.exports = {
defaultLocale: 'bg',
localeDetection: false,
},
-}
\ No newline at end of file
+})
\ No newline at end of file
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 2093f0a..1f33946 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -26,26 +26,47 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
// appleWebApp: true,
// }
+// (custom) Service worker registration and push notification logic
+// function registerServiceWorkerAndPushNotifications() {
+// useEffect(() => {
+// const registerServiceWorker = async () => {
+// if ('serviceWorker' in navigator) {
+// try {
+// const registration = await navigator.serviceWorker.register('/worker/index.js')
+// .then((registration) => console.log('reg: ', registration));
+// } catch (error) {
+// console.log('Service Worker registration failed:', error);
+// }
+// }
+// };
+
+// const askForNotificationPermission = async () => {
+// if ('serviceWorker' in navigator && 'PushManager' in window) {
+// try {
+// const permission = await Notification.requestPermission();
+// if (permission === 'granted') {
+// console.log('Notification permission granted.');
+// // TODO: Subscribe the user to push notifications here
+// } else {
+// console.log('Notification permission not granted.');
+// }
+// } catch (error) {
+// console.error('Error during service worker registration:', error);
+// }
+// } else {
+// console.log('Service Worker or Push notifications not supported in this browser.');
+// }
+// };
+
+// registerServiceWorker();
+// askForNotificationPermission();
+// }, []);
+// }
//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) {
function SmwsApp({ Component, pageProps, session, locale, messages }) {
- // dynamic locale loading using our API endpoint
- // const [locale, setLocale] = useState(_locale);
- // const [messages, setMessages] = useState(_messages);
- // useEffect(() => {
- // async function loadLocaleData() {
- // const res = await fetch(`/api/translations/${locale}`);
- // if (res.ok) {
- // const localeMessages = await res.json();
- // console.log("Loaded messages for locale:", locale, localeMessages);
- // setMessages(localeMessages);
- // } else {
- // const localeMessages = await import(`../content/i18n/${locale}.json`); setMessages(localeMessages.default);
- // }
- // console.log("locale set to'", locale, "' ",);
- // }
- // loadLocaleData();
- // }, [locale]);
+
+ //registerServiceWorkerAndPushNotifications();
useEffect(() => {
const use = async () => {
diff --git a/pages/api/notify.ts b/pages/api/notify.ts
index 94dccc1..09ba2cb 100644
--- a/pages/api/notify.ts
+++ b/pages/api/notify.ts
@@ -1,6 +1,8 @@
const webPush = require('web-push')
+import common from '../../src/helpers/common';
+
//generate and store VAPID keys in .env.local if not already done
if (!process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY || !process.env.WEB_PUSH_PRIVATE_KEY) {
const { publicKey, privateKey } = webPush.generateVAPIDKeys()
@@ -28,28 +30,66 @@ const Notification = async (req, res) => {
res.end(process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY)
res.end()
}
- // on PUT store the subscription object in the database
+ if (req.method == 'PUT') {
+ // store the subscription object in the database
+ // publisher.pushSubscription = subscription
+ const prisma = common.getPrismaClient();
+ const { subscription, id } = req.body
+ const publisher = await prisma.publisher.update({
+ where: { id },
+ data: { pushSubscription: subscription }
+ })
+ console.log('Subscription for publisher', id, 'updated:', subscription)
+ res.statusCode = 200
+ res.end()
+ }
+ if (req.method == 'DELETE') {
+ // remove the subscription object from the database
+ // publisher.pushSubscription = null
+ const prisma = common.getPrismaClient();
+ const { id } = req.body
+ const publisher = await prisma.publisher.update({
+ where: { id },
+ data: { pushSubscription: null }
+ })
+ console.log('Subscription for publisher', id, 'deleted')
+ res.statusCode = 200
+ res.end()
+ }
+
if (req.method == 'POST') {
- const { subscription } = req.body
-
- await webPush
- .sendNotification(
- subscription,
- JSON.stringify({ title: 'Hello Web Push', message: 'Your web push notification is here!' })
- )
- .then(response => {
- res.writeHead(response.statusCode, response.headers).end(response.body)
- })
- .catch(err => {
- if ('statusCode' in err) {
- res.writeHead(err.statusCode, err.headers).end(err.body)
- } else {
- console.error(err)
- res.statusCode = 500
- res.end()
- }
- })
+ const { subscription, id, broadcast, title = 'Hello Web Push', message = 'Your web push notification is here!' } = req.body
+ if (broadcast) {
+ await broadcastPush(title, message)
+ res.statusCode = 200
+ res.end()
+ return
+ }
+ else if (id) {
+ await sendPush(id, title, message)
+ res.statusCode = 200
+ res.end()
+ return
+ } else if (subscription) {
+ await webPush
+ .sendNotification(
+ subscription,
+ JSON.stringify({ title, message })
+ )
+ .then(response => {
+ res.writeHead(response.statusCode, response.headers).end(response.body)
+ })
+ .catch(err => {
+ if ('statusCode' in err) {
+ res.writeHead(err.statusCode, err.headers).end(err.body)
+ } else {
+ console.error(err)
+ res.statusCode = 500
+ res.end()
+ }
+ })
+ }
} else {
res.statusCode = 405
res.end()
@@ -57,3 +97,48 @@ const Notification = async (req, res) => {
}
export default Notification
+
+//export pushNotification(userId or email) for use in other files
+export const sendPush = async (id, title, message) => {
+ const prisma = common.getPrismaClient();
+ const publisher = await prisma.publisher.findUnique({
+ where: { id }
+ })
+ if (!publisher.pushSubscription) {
+ console.log('No push subscription found for publisher', id)
+ return
+ }
+
+ await webPush
+ .sendNotification(
+ publisher.pushSubscription,
+ JSON.stringify({ title, message })
+ )
+ .then(response => {
+ console.log('Push notification sent to publisher', id)
+ })
+ .catch(err => {
+ console.error('Error sending push notification to publisher', id, ':', err)
+ })
+}
+//export breoadcastNotification for use in other files
+export const broadcastPush = async (title, message) => {
+ const prisma = common.getPrismaClient();
+ const publishers = await prisma.publisher.findMany({
+ where: { pushSubscription: { not: null } }
+ })
+
+ for (const publisher of publishers) {
+ await webPush
+ .sendNotification(
+ publisher.pushSubscription,
+ JSON.stringify({ title, message })
+ )
+ .then(response => {
+ console.log('Push notification sent to publisher', publisher.id)
+ })
+ .catch(err => {
+ console.error('Error sending push notification to publisher', publisher.id, ':', err)
+ })
+ }
+}
diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx
index 5a4aa7d..e54d4f8 100644
--- a/pages/cart/calendar/index.tsx
+++ b/pages/cart/calendar/index.tsx
@@ -15,6 +15,8 @@ import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
+// import notify api
+import { sendPush, broadcastPush } from '../../api/notify';
const { DateTime } = require('luxon');
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
@@ -734,7 +736,19 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
{pub.currentWeekAssignments || 0}
{pub.currentMonthAssignments || 0}
{pub.previousMonthAssignments || 0}
-
+
+
+
);
diff --git a/pages/dash.tsx b/pages/dash.tsx
index fa8beb1..dc5d560 100644
--- a/pages/dash.tsx
+++ b/pages/dash.tsx
@@ -226,8 +226,8 @@ export const getServerSideProps = async (context) => {
// log first availability startTime to verify timezone and UTC conversion
- console.log("First availability startTime: " + items[0].startTime);
- console.log("First availability startTime: " + items[0].startTime.toLocaleString());
+ console.log("First availability startTime: " + items[0]?.startTime);
+ console.log("First availability startTime: " + items[0]?.startTime.toLocaleString());
const prisma = common.getPrismaClient();
let cartEvents = await prisma.cartEvent.findMany({
diff --git a/prisma/migrations/20240506162944_add_publiher_push_subsctiption/migration.sql b/prisma/migrations/20240506162944_add_publiher_push_subsctiption/migration.sql
new file mode 100644
index 0000000..def0cde
--- /dev/null
+++ b/prisma/migrations/20240506162944_add_publiher_push_subsctiption/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE `publisher` ADD COLUMN `pushSubscription` JSON NULL;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6fcc599..391079f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -123,6 +123,7 @@ model Publisher {
Message Message[]
EventLog EventLog[]
lastLogin DateTime?
+ pushSubscription Json?
}
model Availability {
diff --git a/src/axiosServer.js b/src/axiosServer.js
index 071e7e1..77faaeb 100644
--- a/src/axiosServer.js
+++ b/src/axiosServer.js
@@ -31,7 +31,7 @@ const axiosServer = async (context) => {
}
else {
//redirect to next-auth login page
- context.res.writeHead(302, { Location: '/api/auth/signin' });
+ context.res.writeHead(302, { Location: encodeURIComponent('/api/auth/signin') });
context.res.end();
return { props: {} };
}
diff --git a/workbox-config.js b/workbox-config.js
index 480eeff..a3d8487 100644
--- a/workbox-config.js
+++ b/workbox-config.js
@@ -1,34 +1,37 @@
-importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
+// importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
-// Only import the modules you need; skip precaching and routing if not needed
-workbox.core.skipWaiting();
-workbox.core.clientsClaim();
+// // ToDo: probably not used now as we use next-pwa( check config)
+// // Only import the modules you need; skip precaching and routing if not needed
-//workbox.precaching.cleanupOutdatedCaches();
-//disable precaching
-workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
+// workbox.core.skipWaiting();
+// workbox.core.clientsClaim();
-module.exports = {
- // Other webpack config...
- plugins: [
- // Other plugins...
- new InjectManifest({
- // These are some common options, and not all are required.
- // Consult the docs for more info.
- exclude: [/.../, '...'],
- maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
- swSrc: './worker/index.js',
- }),
- ],
-};
+// //workbox.precaching.cleanupOutdatedCaches();
+// //disable precaching
+// workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
-// Example: Set up push notification handling
-self.addEventListener('push', event => {
- const data = event.data.json();
- event.waitUntil(
- self.registration.showNotification(data.title, {
- body: data.message,
- icon: '/path/to/icon.png'
- })
- );
-});
+// module.exports = {
+// // Other webpack config...
+// plugins: [
+// // Other plugins...
+// new InjectManifest({
+// // These are some common options, and not all are required.
+// // Consult the docs for more info.
+// exclude: [/.../, '...'],
+// maximumFileSizeToCacheInBytes: 1 * 1024 * 1024,
+// // swSrc: './worker.js',
+// }),
+// ],
+// };
+
+// // Example: Set up push notification handling
+// self.addEventListener('push', event => {
+// console.log('Push event received at workbox.config: ', event);
+// const data = event.data.json();
+// event.waitUntil(
+// self.registration.showNotification(data.title, {
+// body: data.message,
+// icon: '/path/to/icon.png'
+// })
+// );
+// });
diff --git a/worker.js b/worker.js
new file mode 100644
index 0000000..8ef95ff
--- /dev/null
+++ b/worker.js
@@ -0,0 +1,71 @@
+// 'use strict'
+// // currently not used as we ise next-pwa and in next.config.js we have withPWA.
+// // maybe we can have withPWA({sw: './worker.js'}) ?
+// console.log('Service Worker worker/index.js Loaded...')
+// workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
+
+
+// self.addEventListener('install', () => {
+// console.log('service worker installed')
+// });
+
+// self.addEventListener('activate', () => {
+// console.log('service worker activated')
+// });
+
+// self.addEventListener('fetch', (event) => {
+// try {
+// console.log('Fetch event for ', event.request.url);
+// if (event.request.url.includes('/api/auth/callback/')) {
+// // Use network only strategy for auth routes, or bypass SW completely
+// event.respondWith(fetch(event.request));
+// return;
+// }
+// // other caching strategies...
+// } catch (error) {
+// console.error(error)
+// }
+// });
+
+// self.addEventListener('push', function (event) {
+// console.log('Push message', event)
+// if (!(self.Notification && self.Notification.permission === 'granted')) {
+// return
+// }
+// const data = JSON.parse(event.data.text())
+// event.waitUntil(
+// registration.showNotification(data.title, {
+// body: data.message,
+// icon: '/icons/android-chrome-192x192.png'
+// })
+// )
+// })
+
+// self.addEventListener('notificationclick', function (event) {
+// console.log('Notification click: tag', event.notification.tag)
+// event.notification.close()
+// event.waitUntil(
+// clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
+// if (clientList.length > 0) {
+// let client = clientList[0]
+// for (let i = 0; i < clientList.length; i++) {
+// if (clientList[i].focused) {
+// client = clientList[i]
+// }
+// }
+// return client.focus()
+// }
+// return clients.openWindow('/')
+// })
+// )
+// })
+
+// // self.addEventListener('pushsubscriptionchange', function(event) {
+// // event.waitUntil(
+// // Promise.all([
+// // Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true),
+// // Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration))
+// // .then(function(sub) { return saveSubscription(sub) })
+// // ])
+// // )
+// // })
\ No newline at end of file
diff --git a/worker/index.js b/worker/index.js
deleted file mode 100644
index c3efe3d..0000000
--- a/worker/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-'use strict'
-
-console.log('Service Worker worker/index.js Loaded...')
-workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
-
-self.addEventListener('fetch', (event) => {
- try {
- console.log('Fetch event for ', event.request.url);
- if (event.request.url.includes('/api/auth/callback/')) {
- // Use network only strategy for auth routes, or bypass SW completely
- event.respondWith(fetch(event.request));
- return;
- }
- // other caching strategies...
- } catch (error) {
- console.error(error)
- }
-});
-
-self.addEventListener('push', function (event) {
- console.log('Push message', event)
- if (!(self.Notification && self.Notification.permission === 'granted')) {
- return
- }
- const data = JSON.parse(event.data.text())
- event.waitUntil(
- registration.showNotification(data.title, {
- body: data.message,
- icon: '/icons/android-chrome-192x192.png'
- })
- )
-})
-
-self.addEventListener('notificationclick', function (event) {
- console.log('Notification click: tag', event.notification.tag)
- event.notification.close()
- event.waitUntil(
- clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
- if (clientList.length > 0) {
- let client = clientList[0]
- for (let i = 0; i < clientList.length; i++) {
- if (clientList[i].focused) {
- client = clientList[i]
- }
- }
- return client.focus()
- }
- return clients.openWindow('/')
- })
- )
-})
-
-// self.addEventListener('pushsubscriptionchange', function(event) {
-// event.waitUntil(
-// Promise.all([
-// Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true),
-// Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration))
-// .then(function(sub) { return saveSubscription(sub) })
-// ])
-// )
-// })
\ No newline at end of file