PWA manager is now visible to all users
This commit is contained in:
1
.env
1
.env
@ -10,6 +10,7 @@ NODE_ENV=development
|
||||
# mysql. ONLY THIS ENV is respected when generating/applying migrations in prisma
|
||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||
# DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev
|
||||
NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003
|
||||
|
||||
# // owner: dobromir.popov@gmail.com | Специално Свидетелстване София
|
||||
# // https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
|
||||
|
@ -35,9 +35,11 @@ services:
|
||||
MYSQL_DATABASE: jwpwsofia_demo
|
||||
MYSQL_USER: jwpwsofia_demo
|
||||
MYSQL_PASSWORD: dwxhns9p9vp248
|
||||
# networks:
|
||||
# - infrastructure_default
|
||||
# - default
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 5002:8080
|
||||
networks:
|
||||
infrastructure_default:
|
||||
external: true
|
||||
|
@ -3,6 +3,8 @@ import common from '../src/helpers/common'; // Ensure this path is correct
|
||||
//use session to get user role
|
||||
import { useSession } from "next-auth/react"
|
||||
import e from 'express';
|
||||
import ProtectedRoute from './protectedRoute';
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
function PwaManager() {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
||||
@ -12,8 +14,14 @@ function PwaManager() {
|
||||
const [subscription, setSubscription] = useState(null);
|
||||
const [registration, setRegistration] = useState(null);
|
||||
const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
|
||||
const [subs, setSubs] = useState("")
|
||||
|
||||
const { data: session } = useSession();
|
||||
// let isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||||
let isAdmin = false;
|
||||
if (session) {
|
||||
isAdmin = session.user.role === UserRole.ADMIN;
|
||||
}
|
||||
|
||||
// Handle PWA installation
|
||||
useEffect(() => {
|
||||
@ -33,9 +41,13 @@ function PwaManager() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if the app is running in standalone mode
|
||||
// const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||
// if (isStandalone) {
|
||||
// console.log('Running in standalone mode');
|
||||
// setIsPWAInstalled(true);
|
||||
// }
|
||||
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
setIsStandAlone(true);
|
||||
}
|
||||
@ -58,19 +70,28 @@ function PwaManager() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const installPWA = async (e) => {
|
||||
console.log('installing PWA');
|
||||
e.preventDefault();
|
||||
console.log('Attempting to install PWA');
|
||||
e.preventDefault(); // Prevent default button action
|
||||
if (deferredPrompt) {
|
||||
console.log('Prompting install');
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log('Installation outcome:', outcome);
|
||||
if (outcome === 'accepted') {
|
||||
console.log('User accepted the A2HS prompt');
|
||||
setIsPWAInstalled(true);
|
||||
} else {
|
||||
console.log('User dismissed the A2HS prompt');
|
||||
}
|
||||
setDeferredPrompt(null);
|
||||
setDeferredPrompt(null); // Clear the deferred prompt to manage its lifecycle
|
||||
} else {
|
||||
console.log('No deferred prompt available');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const subscribeToNotifications = async (e) => {
|
||||
|
||||
try {
|
||||
@ -90,7 +111,9 @@ function PwaManager() {
|
||||
if (!vapidPublicKey) {
|
||||
// Fetch the public key from the server if not present in env variables
|
||||
const response = await fetch('/api/notify', { method: 'GET' });
|
||||
vapidPublicKey = await response.text();
|
||||
const responseData = await response.json();
|
||||
vapidPublicKey = responseData.pk;
|
||||
setSubs(responseData.subs);
|
||||
if (!vapidPublicKey) {
|
||||
throw new Error("Failed to fetch VAPID public key from server.");
|
||||
}
|
||||
@ -107,12 +130,14 @@ function PwaManager() {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ subscription: sub, id: session.user.id })
|
||||
}).then(response => {
|
||||
}).then(async response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save subscription data on server.');
|
||||
}
|
||||
else {
|
||||
console.log('Subscription data saved on server.');
|
||||
const s = await response.json();
|
||||
setSubs(s.subs);
|
||||
setSubscription(sub);
|
||||
setIsSubscribed(true);
|
||||
console.log('Web push subscribed!');
|
||||
@ -144,12 +169,14 @@ function PwaManager() {
|
||||
//send the current subscription to be removed
|
||||
body: JSON.stringify({ id: session.user.id, subscriptionId: subscription.endpoint })
|
||||
}
|
||||
).then(response => {
|
||||
).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete subscription data on server.');
|
||||
}
|
||||
else {
|
||||
console.log('Subscription data deleted on server.');
|
||||
const s = await response.json();
|
||||
setSubs(s.subs);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -261,6 +288,7 @@ function PwaManager() {
|
||||
if (subscription) {
|
||||
await subscription.unsubscribe();
|
||||
}
|
||||
setSubs("");
|
||||
setSubscription(null);
|
||||
setIsSubscribed(false);
|
||||
}
|
||||
@ -270,77 +298,81 @@ function PwaManager() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1>PWA Manager</h1>
|
||||
<h1>{isAdmin && " PWA (admin)"}</h1>
|
||||
{!isStandAlone && !isPWAInstalled && (
|
||||
<button
|
||||
onClick={installPWA}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white text-xs py-1 px-2 rounded-full focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
|
||||
>
|
||||
Install PWA
|
||||
Инсталирай приложението
|
||||
</button>
|
||||
)}
|
||||
{isPWAInstalled && <p>App is installed!</p>}
|
||||
{isStandAlone && <p>PWA App</p>}
|
||||
{isPWAInstalled && <p>Инсталирано!</p>}
|
||||
{/* {isStandAlone && <p>PWA App</p>} */}
|
||||
<button
|
||||
onClick={isSubscribed ? unsubscribeFromNotifications : subscribeToNotifications}
|
||||
disabled={false} // Since the button itself acts as a toggle, the disabled attribute might not be needed
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${isSubscribed ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'}`} >
|
||||
{isSubscribed ? 'Unsubscribe from Notifications' : 'Subscribe to Notifications'}
|
||||
{isSubscribed ? 'Спри известията' : 'Показвай известия'}
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteAllSubscriptions}
|
||||
className="text-xs py-1 px-2 rounded-full focus:outline-none bg-red-500 hover:bg-red-700 text-white"
|
||||
>
|
||||
Delete All Subscriptions
|
||||
Спри известията на всички мои устройства {subs != "" ? `(${subs})` : ""}
|
||||
</button>
|
||||
</div >
|
||||
<div>
|
||||
<button
|
||||
onClick={sendTestNotification}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
Send Test Notification
|
||||
</button>
|
||||
<button
|
||||
onClick={sendTestReminder}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
Broadcast Reminder
|
||||
</button>
|
||||
<button
|
||||
onClick={sendTestCoverMe}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
Broadcast CoverMe
|
||||
</button>
|
||||
|
||||
{notificationPermission !== "granted" && (
|
||||
</div>
|
||||
{isAdmin &&
|
||||
<div>
|
||||
<button
|
||||
onClick={togglePushNotifications}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${notificationPermission === "denied" ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'
|
||||
onClick={sendTestNotification}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
|
||||
Send Test Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
||||
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
||||
<span className="align-middle">Телеграм</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4" target="_blank">
|
||||
<span className="align-middle">Apple sign-in</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={sendTestReminder}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
Broadcast Reminder
|
||||
</button>
|
||||
<button
|
||||
onClick={sendTestCoverMe}
|
||||
disabled={!isSubscribed}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||
}`}
|
||||
>
|
||||
Broadcast CoverMe
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{notificationPermission !== "granted" && (
|
||||
<button
|
||||
onClick={togglePushNotifications}
|
||||
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${notificationPermission === "denied" ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAdmin && <div>
|
||||
<div>
|
||||
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
|
||||
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
|
||||
<span className="align-middle">Телеграм</span>
|
||||
</a>
|
||||
|
||||
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4 bg-gray-100 button" target="_blank">
|
||||
<span className="align-middle">Apple sign-in</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -250,49 +250,60 @@ export default function PublisherForm({ item, me }) {
|
||||
{/* notifications */}
|
||||
<div className="mb-6 p-4 border border-gray-300 rounded-lg">
|
||||
<fieldset>
|
||||
<legend className="text-lg font-medium mb-2">Известия по имейл</legend>
|
||||
<div className="space-y-4">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox cursor-not-allowed opacity-50"
|
||||
type="checkbox"
|
||||
id="isSubscribedToCoverMeMandatory"
|
||||
name="isSubscribedToCoverMeMandatory"
|
||||
onChange={handleChange} // This will not fire due to being disabled, but kept for consistency
|
||||
checked={true} // Always checked
|
||||
disabled={true} // User cannot change this field
|
||||
autoComplete="off" />
|
||||
<label className="label cursor-not-allowed opacity-50" htmlFor="isSubscribedToCoverMeMandatory">
|
||||
Имейли за заместване които отговарят на моите предпочитания
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox"
|
||||
type="checkbox"
|
||||
id="isSubscribedToCoverMe"
|
||||
name="isSubscribedToCoverMe"
|
||||
onChange={handleChange}
|
||||
checked={publisher.isSubscribedToCoverMe}
|
||||
autoComplete="off" />
|
||||
<label className="label" htmlFor="isSubscribedToCoverMe">
|
||||
Всички заявки за заместване
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox"
|
||||
type="checkbox"
|
||||
id="isSubscribedToReminders"
|
||||
name="isSubscribedToReminders"
|
||||
onChange={handleChange}
|
||||
checked={publisher.isSubscribedToReminders}
|
||||
autoComplete="off" />
|
||||
<label className="label" htmlFor="isSubscribedToReminders">
|
||||
Други напомняния
|
||||
</label>
|
||||
<legend className="text-lg font-medium mb-2">Известия</legend>
|
||||
|
||||
{/* Email notifications group */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold mb-2">Известия по имейл</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox cursor-not-allowed opacity-50"
|
||||
type="checkbox"
|
||||
id="isSubscribedToCoverMeMandatory"
|
||||
name="isSubscribedToCoverMeMandatory"
|
||||
onChange={handleChange} // This will not fire due to being disabled, but kept for consistency
|
||||
checked={true} // Always checked
|
||||
disabled={true} // User cannot change this field
|
||||
autoComplete="off" />
|
||||
<label className="label cursor-not-allowed opacity-50" htmlFor="isSubscribedToCoverMeMandatory">
|
||||
Имейли за заместване които отговарят на моите предпочитания
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox"
|
||||
type="checkbox"
|
||||
id="isSubscribedToCoverMe"
|
||||
name="isSubscribedToCoverMe"
|
||||
onChange={handleChange}
|
||||
checked={publisher.isSubscribedToCoverMe}
|
||||
autoComplete="off" />
|
||||
<label className="label" htmlFor="isSubscribedToCoverMe">
|
||||
Всички заявки за заместване
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="checkbox"
|
||||
type="checkbox"
|
||||
id="isSubscribedToReminders"
|
||||
name="isSubscribedToReminders"
|
||||
onChange={handleChange}
|
||||
checked={publisher.isSubscribedToReminders}
|
||||
autoComplete="off" />
|
||||
<label className="label" htmlFor="isSubscribedToReminders">
|
||||
Други напомняния
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* In-App notifications group */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-semibold mb-2">Известия в приложението</h3>
|
||||
<PwaManager />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@ -307,7 +318,6 @@ export default function PublisherForm({ item, me }) {
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
||||
<div className="border border-blue-500 border-solid p-2">
|
||||
{/* prompt to install PWA */}
|
||||
<PwaManager />
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="type">Тип</label>
|
||||
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||
@ -366,7 +376,7 @@ export default function PublisherForm({ item, me }) {
|
||||
|
||||
{/* save */}
|
||||
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit">
|
||||
{router.query?.id ? "Update" : "Create"}
|
||||
{router.query?.id ? "Запази" : "Създай"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -25,9 +25,20 @@ const Notification = async (req, res) => {
|
||||
if (req.method == 'GET') {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Allow', 'POST')
|
||||
let subs = 0
|
||||
if (req.query && req.query.id) {
|
||||
const prisma = common.getPrismaClient();
|
||||
const publisher = await prisma.publisher.findUnique({
|
||||
where: { id: req.query.id },
|
||||
select: { pushSubscription: true }
|
||||
});
|
||||
subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0);
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
// send the public key in the response headers
|
||||
//res.setHeader('Content-Type', 'text/plain')
|
||||
res.end(process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY)
|
||||
res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
|
||||
res.end()
|
||||
}
|
||||
if (req.method == 'PUT') {
|
||||
|
@ -64,13 +64,23 @@ export const getServerSideProps = async (context) => {
|
||||
}
|
||||
});
|
||||
if (!item) {
|
||||
const user = context.req.session.user;
|
||||
const user = context.req.session?.user;
|
||||
if (!user) {
|
||||
return {
|
||||
// redirect to '/auth/signin'. assure it is not relative path
|
||||
redirect: {
|
||||
destination: process.env.NEXT_PUBLIC_PUBLIC_URL + "/auth/signin",
|
||||
permanent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
const message = encodeURIComponent(`Този имейл (${user?.email}) не е регистриран. Моля свържете се с администратора.`);
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.',
|
||||
destination: `/message?message=${message}`,
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
|
||||
|
||||
|
Reference in New Issue
Block a user