Merge branch 'main' into production
This commit is contained in:
13
.env
13
.env
@@ -42,11 +42,20 @@ GITHUB_SECRET=
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||
EMAIL_FROM=noreply@example.com
|
||||
|
||||
# EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||
EMAIL_FROM=noreply@mwitnessing.com
|
||||
|
||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
MAILTRAP_HOST=sandbox.smtp.mailtrap.io
|
||||
MAILTRAP_USER=8ec69527ff2104
|
||||
MAILTRAP_PASS=c7bc05f171c96c
|
||||
|
||||
GMAIL_EMAIL_USERNAME=
|
||||
GMAIL_EMAIL_APP_PASS=
|
||||
|
||||
TELEGRAM_BOT=false
|
||||
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
||||
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BGxXJ0jdsQ4ihE7zp8mxrBO-QPSjeEtO9aCtPoMTuxc1VLW0OfRIt-DYinK9ekjTl2w-j0eQbeprIyBCpmmfciI
|
||||
VAPID_PRIVATE_KEY=VXHu2NgcyM4J4w3O4grkS_0yLwWHCvVKDJexyBjqgx0
|
||||
|
@@ -6,4 +6,10 @@ NEXT_PUBLIC_PUBLIC_URL= https://sofia.mwitnessing.com
|
||||
# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
|
||||
NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
||||
# ? do we need to duplicate this? already defined in the deoployment yml file
|
||||
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||
|
||||
|
||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||
MAILTRAP_USER=api
|
||||
MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
@@ -30,5 +30,7 @@ GITHUB_SECRET=
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||
EMAIL_FROM=noreply@example.com
|
||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
||||
MAILTRAP_USER=api
|
||||
MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -10,8 +10,16 @@ lerna-debug.log*
|
||||
.yarn-integrity
|
||||
.npm
|
||||
|
||||
.eslintcache
|
||||
# PWA files
|
||||
**/public/sw.js
|
||||
**/public/workbox-*.js
|
||||
**/public/worker-*.js
|
||||
**/public/sw.js.map
|
||||
**/public/workbox-*.js.map
|
||||
**/public/worker-*.js.map
|
||||
|
||||
|
||||
.eslintcache
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
version: "3"
|
||||
services:
|
||||
nextjs-app: # https://sofia.mwhitnessing.com/
|
||||
nextjs-app: # https://sofia.mwitnessing.com/
|
||||
hostname: jwpw-app-staging # jwpw-nextjs-app-1
|
||||
image: docker.d-popov.com/jwpw:latest
|
||||
volumes:
|
||||
|
@@ -198,7 +198,13 @@ add assignment in calendar planner
|
||||
fix database
|
||||
|
||||
--
|
||||
emails
|
||||
emails: new shifts, replacements, announcements
|
||||
mobile apps
|
||||
apple login
|
||||
разрешителни - upload
|
||||
wpa: android? apple? pc?
|
||||
push notifications
|
||||
store replacement
|
||||
test email
|
||||
|
||||
|
||||
|
227
components/PwaManager.tsx
Normal file
227
components/PwaManager.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import common from '../src/helpers/common'; // Ensure this path is correct
|
||||
|
||||
function PwaManager() {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
||||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||||
const [isStandAlone, setIsStandAlone] = useState(false);
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
const [subscription, setSubscription] = useState(null);
|
||||
const [registration, setRegistration] = useState(null);
|
||||
const [notificationPermission, setNotificationPermission] = useState(Notification.permission);
|
||||
|
||||
// Handle PWA installation
|
||||
useEffect(() => {
|
||||
|
||||
setNotificationPermission(Notification.permission);
|
||||
|
||||
// Handle Push Notification Subscription
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
navigator.serviceWorker.ready.then(reg => {
|
||||
reg.pushManager.getSubscription().then(sub => {
|
||||
if (sub) {
|
||||
setSubscription(sub);
|
||||
setIsSubscribed(true);
|
||||
}
|
||||
});
|
||||
setRegistration(reg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if the app is running in standalone mode
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
setIsStandAlone(true);
|
||||
}
|
||||
|
||||
const handleBeforeInstallPrompt = (e) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e);
|
||||
};
|
||||
|
||||
const handleAppInstalled = () => {
|
||||
setIsPWAInstalled(true);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.addEventListener('appinstalled', handleAppInstalled);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const installPWA = async (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
setIsPWAInstalled(true);
|
||||
}
|
||||
setDeferredPrompt(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function for converting base64 string to Uint8Array
|
||||
const base64ToUint8Array = base64 => {
|
||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const rawData = window.atob(b64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
const subscribeToNotifications = async (e) => {
|
||||
|
||||
try {
|
||||
e.preventDefault();
|
||||
if (!registration) {
|
||||
console.error('Service worker registration not found.');
|
||||
registration
|
||||
return;
|
||||
}
|
||||
const sub = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: base64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY)
|
||||
});
|
||||
// Call your API to save subscription data on server
|
||||
setSubscription(sub);
|
||||
setIsSubscribed(true);
|
||||
console.log('Web push subscribed!');
|
||||
console.log(sub);
|
||||
} catch (error) {
|
||||
console.error('Error subscribing to notifications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribeFromNotifications = async (e) => {
|
||||
|
||||
try {
|
||||
e.preventDefault();
|
||||
await subscription.unsubscribe();
|
||||
// Call your API to delete or invalidate subscription data on server
|
||||
setSubscription(null);
|
||||
setIsSubscribed(false);
|
||||
console.log('Web push unsubscribed!');
|
||||
} catch (error) {
|
||||
console.error('Error unsubscribing from notifications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to request push notification permission
|
||||
const requestNotificationPermission = async (e) => {
|
||||
e.preventDefault();
|
||||
const permission = await Notification.requestPermission();
|
||||
setNotificationPermission(permission);
|
||||
if (permission === "granted") {
|
||||
// User granted permission
|
||||
subscribeToNotifications(null); // Pass the required argument here
|
||||
} else {
|
||||
// User denied or dismissed permission
|
||||
console.log("Push notifications permission denied.");
|
||||
}
|
||||
};
|
||||
|
||||
// Function to toggle push notifications
|
||||
const togglePushNotifications = async (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
if (notificationPermission === "granted") {
|
||||
// If already subscribed, unsubscribe
|
||||
unsubscribeFromNotifications(null); // Pass null as the argument
|
||||
} else if (notificationPermission === "default" || notificationPermission === "denied") {
|
||||
// Request permission if not already granted
|
||||
await requestNotificationPermission(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const sendTestNotification = async (e) => {
|
||||
|
||||
e.preventDefault();
|
||||
if (!subscription) {
|
||||
console.error('Web push not subscribed');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('/api/notification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ subscription })
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1>PWA Manager</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>}
|
||||
<button
|
||||
onClick={subscribeToNotifications}
|
||||
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-green-500 hover:bg-green-700 text-white'
|
||||
}`}
|
||||
>
|
||||
Subscribe to Notifications
|
||||
</button>
|
||||
<button
|
||||
onClick={unsubscribeFromNotifications}
|
||||
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-red-500 hover:bg-red-700 text-white'
|
||||
}`}
|
||||
>
|
||||
Unsubscribe from Notifications
|
||||
</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>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default PwaManager;
|
@@ -64,9 +64,14 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
|
||||
// Define the minimum and maximum times
|
||||
const minTime = new Date();
|
||||
minTime.setHours(8, 0, 0, 0); // 8:00 AM
|
||||
minTime.setHours(9, 0, 0, 0); // 8:00 AM
|
||||
const maxTime = new Date();
|
||||
maxTime.setHours(20, 0, 0, 0); // 8:00 PM
|
||||
maxTime.setHours(19, 30, 0, 0); // 8:00 PM
|
||||
|
||||
useEffect(() => {
|
||||
setTimeSlots(generateTimeSlots(minTime, maxTime, 90, availabilities));
|
||||
}, []);
|
||||
|
||||
|
||||
const fetchItemFromDB = async () => {
|
||||
const id = parseInt(router.query.id);
|
||||
@@ -330,12 +335,9 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
|
||||
const generateTimeSlots = (start, end, increment, items) => {
|
||||
const slots = [];
|
||||
// Initialize baseDate at the start of the day
|
||||
const baseDate = new Date(items?.startTime || day);
|
||||
baseDate.setHours(start, 0, 0, 0);
|
||||
let currentTime = baseDate.getTime();
|
||||
let currentTime = start.getTime();
|
||||
|
||||
const endTime = new Date(baseDate).setHours(end, 0, 0, 0);
|
||||
const endTime = end.getTime();
|
||||
|
||||
while (currentTime < endTime) {
|
||||
let slotStart = new Date(currentTime);
|
||||
@@ -343,11 +345,10 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
|
||||
const isChecked = items.some(item =>
|
||||
item.startTime && item.endTime &&
|
||||
(slotStart.getHours() * 60 + slotStart.getMinutes()) < (item.endTime.getHours() * 60 + item.endTime.getMinutes()) &&
|
||||
(slotEnd.getHours() * 60 + slotEnd.getMinutes()) > (item.startTime.getHours() * 60 + item.startTime.getMinutes())
|
||||
(slotStart.getTime() < item.endTime.getTime()) &&
|
||||
(slotEnd.getTime() > item.startTime.getTime())
|
||||
);
|
||||
|
||||
|
||||
slots.push({
|
||||
startTime: slotStart,
|
||||
endTime: slotEnd,
|
||||
@@ -358,17 +359,16 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
}
|
||||
|
||||
// Optional: Add isFirst, isLast, and isWithTransport properties
|
||||
if (slots.length > 0) {
|
||||
if (slots.length > 0 && items?.length > 0) {
|
||||
slots[0].isFirst = true;
|
||||
slots[slots.length - 1].isLast = true;
|
||||
slots[0].isWithTransport = items[0].isWithTransportIn;
|
||||
slots[slots.length - 1].isWithTransport = items[items.length - 1].isWithTransportOut;
|
||||
slots[0].isWithTransport = items[0]?.isWithTransportIn;
|
||||
slots[slots.length - 1].isWithTransport = items[items.length - 1]?.isWithTransportOut;
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
|
||||
|
||||
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
||||
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||||
const handleAllDayChange = (e) => {
|
||||
@@ -462,10 +462,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeSlots(generateTimeSlots(9, 18, 90, availabilities));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ToastContainer></ToastContainer>
|
||||
|
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
//import { getDate } from "date-fns";
|
||||
|
||||
import PwaManager from "../PwaManager";
|
||||
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../src/helpers/const"
|
||||
import PublisherSearchBox from './PublisherSearchBox';
|
||||
import AvailabilityList from "../availability/AvailabilityList";
|
||||
@@ -179,11 +180,11 @@ export default function PublisherForm({ item, me }) {
|
||||
onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="firstName">Име</label>
|
||||
<input type="text" name="firstName" value={publisher.firstName} onChange={handleChange} className="textbox" placeholder="First Name" autoFocus />
|
||||
<input type="text" id="firstName" name="firstName" value={publisher.firstName} onChange={handleChange} className="textbox" placeholder="First Name" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="lastName">Фамилия</label>
|
||||
<input type="text" name="lastName" value={publisher.lastName} onChange={handleChange} className="textbox" placeholder="Last Name" autoFocus />
|
||||
<input type="text" id="lastName" name="lastName" value={publisher.lastName} onChange={handleChange} className="textbox" placeholder="Last Name" autoFocus />
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
||||
|
||||
<div className="border border-blue-500 border-solid p-2">
|
||||
@@ -198,21 +199,21 @@ export default function PublisherForm({ item, me }) {
|
||||
{/* //desiredShiftsPerMonth */}
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="desiredShiftsPerMonth">Желани смeни на месец</label>
|
||||
<input type="number" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
|
||||
<input type="number" id="desiredShiftsPerMonth" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="email">Имейл</label>
|
||||
<input type="text" name="email" value={publisher.email} onChange={handleChange} className="textbox" placeholder="Email" autoFocus />
|
||||
<input type="text" id="email" name="email" value={publisher.email} onChange={handleChange} className="textbox" placeholder="Email" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="phone">Телефон</label>
|
||||
<input type="text" name="phone" value={publisher.phone} onChange={handleChange} className="textbox" placeholder="Phone" autoFocus />
|
||||
<input type="text" id="phone" name="phone" value={publisher.phone} onChange={handleChange} className="textbox" placeholder="Phone" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="parentPublisher">
|
||||
<label className="label" htmlFor="familyHeadId">
|
||||
Семейство (избери главата на семейството)
|
||||
</label>
|
||||
<PublisherSearchBox selectedId={publisher.familyHeadId} onChange={handleParentSelection} />
|
||||
<PublisherSearchBox id="familyHeadId" selectedId={publisher.familyHeadId} onChange={handleParentSelection} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
@@ -238,23 +239,35 @@ export default function PublisherForm({ item, me }) {
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="town">Град</label>
|
||||
<input type="text" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||
<input type="text" id="town" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
{/* notifications */}
|
||||
<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>
|
||||
<input className="checkbox" type="checkbox" id="isSubscribedToReminders" name="isSubscribedToReminders" onChange={handleChange} checked={publisher.isSubscribedToReminders} autoComplete="off" />
|
||||
<label className="label" htmlFor="isSubscribedToReminders">Абониран за напомняния (имейл)</label>
|
||||
{/* prompt to install PWA */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* button to install PWA */}
|
||||
{/* <div className="mb-4">
|
||||
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="button" onClick={() => window.installPWA()}>
|
||||
Инсталирай приложението
|
||||
</div> */}
|
||||
|
||||
|
||||
{/* ADMINISTRATORS ONLY */}
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" " className="">
|
||||
|
||||
<PwaManager />
|
||||
|
||||
<div className="border border-blue-500 border-solid p-2">
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="type">Тип</label>
|
||||
<select name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||
<select id="type" name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||
<option value="Publisher">Вестител</option>
|
||||
<option value="Bethelite">Бетелит</option>
|
||||
<option value="RegularPioneer">Редовен Пионер</option>
|
||||
@@ -265,11 +278,11 @@ export default function PublisherForm({ item, me }) {
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="comments">Коментари</label>
|
||||
<input type="text" name="comments" value={publisher.comments} onChange={handleChange} className="textbox" placeholder="Коментари" autoFocus />
|
||||
<input type="text" id="comments" name="comments" value={publisher.comments} onChange={handleChange} className="textbox" placeholder="Коментари" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="age">Възраст</label>
|
||||
<input type="number" name="age" value={publisher.age} onChange={handleChange} className="textbox" placeholder="Age" autoFocus />
|
||||
<input type="number" id="age" name="age" value={publisher.age} onChange={handleChange} className="textbox" placeholder="Age" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="form-check">
|
||||
@@ -292,10 +305,7 @@ export default function PublisherForm({ item, me }) {
|
||||
{/* Add other roles as needed */}
|
||||
</select>
|
||||
</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>
|
||||
</ProtectedRoute>
|
||||
{/* ---------------------------- Actions --------------------------------- */}
|
||||
|
@@ -3,7 +3,7 @@ import axiosInstance from '../../src/axiosSecure';
|
||||
import common from '../../src/helpers/common';
|
||||
//import { is } from 'date-fns/locale';
|
||||
|
||||
function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showSearch = true, showList = false, showAllAuto = false, infoText = " Семеен глава" }) {
|
||||
function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, showSearch = true, showList = false, showAllAuto = false, infoText = " Семеен глава" }) {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -95,6 +95,7 @@ function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showS
|
||||
{showSearch ? (
|
||||
<>
|
||||
<input ref={inputRef}
|
||||
{...(id ? { id } : {})}
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
|
||||
module.exports = {
|
||||
const withPWA = require('next-pwa')({
|
||||
dest: 'public'
|
||||
})
|
||||
|
||||
module.exports = withPWA({
|
||||
typescript: {
|
||||
// !! WARN !!
|
||||
// Dangerously allow production builds to successfully complete even if
|
||||
@@ -42,4 +46,4 @@ module.exports = {
|
||||
|
||||
return config;
|
||||
},
|
||||
}
|
||||
})
|
3712
package-lock.json
generated
3712
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.6",
|
||||
"next-connect": "^1.0.0",
|
||||
"next-pwa": "^5.6.0",
|
||||
"node-excel-export": "^1.4.4",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
@@ -98,6 +99,7 @@
|
||||
"tw-elements": "^1.1.0",
|
||||
"typescript": "^5",
|
||||
"uuid": "^9.0.1",
|
||||
"web-push": "^3.6.7",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"winston": "^3.11.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz",
|
||||
@@ -110,4 +112,4 @@
|
||||
"depcheck": "^1.4.7",
|
||||
"prisma": "^5.11.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import type { Metadata } from "next"
|
||||
import "../styles/styles.css"
|
||||
import "../styles/global.css"
|
||||
import "tailwindcss/tailwind.css"
|
||||
@@ -13,6 +14,14 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
||||
// Use of the <SessionProvider> is mandatory to allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
|
||||
// export const metadata: Metadata = {
|
||||
// title: "Специално Свидетелстване София",
|
||||
// description: "Специално Свидетелстване София",
|
||||
// viewport: "width=device-width, initial-scale=1",
|
||||
// appleWebApp: true,
|
||||
// }
|
||||
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
@@ -26,15 +35,38 @@ export default function App({
|
||||
}, []);
|
||||
|
||||
|
||||
// PUSH NOTIFICATIONS
|
||||
useEffect(() => {
|
||||
// Function to ask for Notification permission
|
||||
const askForNotificationPermission = async () => {
|
||||
// Check if the browser supports service workers and push notifications
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
try {
|
||||
// Wait for service worker registration
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
// Ask for notification permission
|
||||
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.');
|
||||
}
|
||||
};
|
||||
|
||||
// Call the function to ask for permission on first load
|
||||
askForNotificationPermission();
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* Other tags */}
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<SessionProvider session={session} >
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<Component {...pageProps} />
|
||||
|
29
pages/_document.tsx
Normal file
29
pages/_document.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="theme-color" content="#fff" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Your PWA Name" />
|
||||
|
||||
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
@@ -86,7 +86,7 @@ export default async function handler(req, res) {
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
const messagePageUrl = `/message?message=${encodeURIComponent('Някой друг вече е отговорил на рази заявка за заместване')}&type=info&caption=${encodeURIComponent('Заявката е вече обработена')}`;
|
||||
const messagePageUrl = `/message?message=${encodeURIComponent('Благодаря за желанието, но някой е отговорил на тази заявка за заместване и тя вече е неактивна')}&type=info&caption=${encodeURIComponent('Някой вече те изпревари. Заявката е вече обработена')}`;
|
||||
res.redirect(messagePageUrl);
|
||||
return;
|
||||
}
|
||||
|
46
pages/api/system.ts
Normal file
46
pages/api/system.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
let findGitRoot = (path = __dirname): string | false => {
|
||||
if (existsSync(join(path, ".git/HEAD"))) {
|
||||
return path;
|
||||
} else {
|
||||
let parent = resolve(path, "..");
|
||||
if (path === parent) {
|
||||
return false;
|
||||
} else {
|
||||
return findGitRoot(parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let getGitVersion = async () => {
|
||||
let root = findGitRoot();
|
||||
|
||||
if (!root) {
|
||||
throw new Error("Cannot call getGitVersion from non git project.");
|
||||
}
|
||||
|
||||
let rev = readFileSync(join(root, ".git/HEAD")).toString().trim();
|
||||
|
||||
if (rev.indexOf(":") === -1) {
|
||||
return rev;
|
||||
} else {
|
||||
return readFileSync(join(root, ".git", rev.substring(5)))
|
||||
.toString()
|
||||
.trim();
|
||||
}
|
||||
};
|
||||
export default async function handler(req, res) {
|
||||
const token = await getToken({ req: req });
|
||||
if (!token) {
|
||||
// If no token or invalid token, return unauthorized status
|
||||
return res.status(401).json({ message: "Unauthorized to call this API endpoint" });
|
||||
}
|
||||
else {
|
||||
// If token is valid, log the user
|
||||
//console.log("JWT | User: " + token.email);
|
||||
}
|
||||
res.status(200).json({ v: getGitVersion() })
|
||||
}
|
26
public/manifest.json
Normal file
26
public/manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#e36600",
|
||||
"icons": [
|
||||
{
|
||||
"purpose": "maskable",
|
||||
"sizes": "512x512",
|
||||
"src": "favicon.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"purpose": "any",
|
||||
"sizes": "512x512",
|
||||
"src": "favicon.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"orientation": "any",
|
||||
"display": "standalone",
|
||||
"dir": "auto",
|
||||
"lang": "en-US",
|
||||
"name": "Специално Свидетелстване София",
|
||||
"short_name": "ССС",
|
||||
"start_url": "/",
|
||||
"scope": "/cart"
|
||||
}
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
20
server.js
20
server.js
@@ -18,7 +18,7 @@ import('get-port').then(module => {
|
||||
|
||||
process.env.TZ = 'Europe/Sofia';
|
||||
// Global variable to store the base URL
|
||||
let baseUrlGlobal;
|
||||
// let baseUrlGlobal;
|
||||
|
||||
console.log("initial process.env.APP_ENV = ", process.env.APP_ENV);
|
||||
console.log("initial process.env.NODE_ENV = ", process.env.NODE_ENV); //NODE_ENV can be passed as docker param
|
||||
@@ -93,14 +93,14 @@ nextApp
|
||||
server.use((req, res, next) => {
|
||||
req.headers['x-forwarded-host'] = req.headers['x-forwarded-host'] || req.headers.host;
|
||||
// ---------------
|
||||
if (!baseUrlGlobal) {
|
||||
const protocol = req.headers['x-forwarded-proto'] || 'http';
|
||||
const host = req.headers.host;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
baseUrlGlobal = baseUrl;
|
||||
fs.writeFileSync(path.join(__dirname, 'baseUrl.txt'), baseUrlGlobal, 'utf8');
|
||||
console.log("baseUrlGlobal set to: " + baseUrlGlobal);
|
||||
}
|
||||
// if (!baseUrlGlobal) {
|
||||
// const protocol = req.headers['x-forwarded-proto'] || 'http';
|
||||
// const host = req.headers.host;
|
||||
// const baseUrl = `${protocol}://${host}`;
|
||||
// baseUrlGlobal = baseUrl;
|
||||
// fs.writeFileSync(path.join(__dirname, 'baseUrl.txt'), baseUrlGlobal, 'utf8');
|
||||
// console.log("baseUrlGlobal set to: " + baseUrlGlobal);
|
||||
// }
|
||||
next();
|
||||
});
|
||||
server.use("/favicon.ico", express.static("styles/favicon_io/favicon.ico"));
|
||||
@@ -656,8 +656,6 @@ async function Stat() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
Stat();
|
||||
|
||||
exports.baseUrlGlobal = baseUrlGlobal;
|
||||
exports.default = nextApp;
|
||||
|
@@ -12,11 +12,13 @@ const Handlebars = require('handlebars');
|
||||
// const OAuth2 = google.auth.OAuth2;
|
||||
|
||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||
const { env } = require("../../next.config");
|
||||
|
||||
// const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
||||
// const SENDER_EMAIL = "sofia@mwitnessing.com";
|
||||
// const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL };
|
||||
// const client = new MailtrapClient({ token: TOKEN });
|
||||
|
||||
const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
||||
const SENDER_EMAIL = "sofia@mwitnessing.com";
|
||||
const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL };
|
||||
const client = new MailtrapClient({ token: TOKEN });
|
||||
let mailtrapTestClient = null;
|
||||
// const mailtrapTestClient = new MailtrapClient({
|
||||
// username: '8ec69527ff2104',//not working now
|
||||
@@ -25,11 +27,11 @@ let mailtrapTestClient = null;
|
||||
|
||||
//test
|
||||
var transporter = nodemailer.createTransport({
|
||||
host: "sandbox.smtp.mailtrap.io",
|
||||
host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io",
|
||||
port: 2525,
|
||||
auth: {
|
||||
user: "8ec69527ff2104",
|
||||
pass: "c7bc05f171c96c"
|
||||
user: process.env.MAILTRAP_USER,
|
||||
pass: process.env.MAILTRAP_PASS
|
||||
}
|
||||
});
|
||||
// production
|
||||
@@ -196,7 +198,7 @@ exports.SendEmail_NewShifts = async function (publisher, shifts) {
|
||||
|
||||
|
||||
|
||||
//----------------------- OLD -----------------------------
|
||||
//----------------------- OLD -----------------------------
|
||||
|
||||
// exports.SendEmail_NewShifts = async function (publisher, shifts) {
|
||||
// if (shifts.length == 0) return;
|
||||
@@ -245,51 +247,51 @@ exports.SendEmail_NewShifts = async function (publisher, shifts) {
|
||||
// };
|
||||
|
||||
|
||||
// https://mailtrap.io/blog/sending-emails-with-nodemailer/
|
||||
exports.SendEmail_Example = async function (to) {
|
||||
const welcomeImage = fs.readFileSync(
|
||||
path.join(CON.contentPath, "welcome.png")
|
||||
);
|
||||
// // https://mailtrap.io/blog/sending-emails-with-nodemailer/
|
||||
// exports.SendEmail_Example = async function (to) {
|
||||
// const welcomeImage = fs.readFileSync(
|
||||
// path.join(CON.contentPath, "welcome.png")
|
||||
// );
|
||||
|
||||
await client
|
||||
.send({
|
||||
category: "test",
|
||||
custom_variables: {
|
||||
hello: "world",
|
||||
year: 2022,
|
||||
anticipated: true,
|
||||
},
|
||||
from: sender,
|
||||
to: [{ email: to }],
|
||||
subject: "Hello from Mailtrap!",
|
||||
html: `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body style="font-family: sans-serif;">
|
||||
<div style="display: block; margin: auto; max-width: 600px;" class="main">
|
||||
<h1 style="font-size: 18px; font-weight: bold; margin-top: 20px">Congrats for sending test email with Mailtrap!</h1>
|
||||
<p>Inspect it using the tabs you see above and learn how this email can be improved.</p>
|
||||
<img alt="Inspect with Tabs" src="cid:welcome.png" style="width: 100%;">
|
||||
<p>Now send your email using our fake SMTP server and integration of your choice!</p>
|
||||
<p>Good luck! Hope it works.</p>
|
||||
</div>
|
||||
<!-- Example of invalid for email html/css, will be detected by Mailtrap: -->
|
||||
<style>
|
||||
.main { background-color: white; }
|
||||
a:hover { border-left-width: 1em; min-height: 2em; }
|
||||
</style>
|
||||
</body>
|
||||
</html>`,
|
||||
attachments: [
|
||||
{
|
||||
filename: "welcome.png",
|
||||
content_id: "welcome.png",
|
||||
disposition: "inline",
|
||||
content: welcomeImage,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(console.log, console.error, setResult);
|
||||
};
|
||||
// await client
|
||||
// .send({
|
||||
// category: "test",
|
||||
// custom_variables: {
|
||||
// hello: "world",
|
||||
// year: 2022,
|
||||
// anticipated: true,
|
||||
// },
|
||||
// from: sender,
|
||||
// to: [{ email: to }],
|
||||
// subject: "Hello from Mailtrap!",
|
||||
// html: `<!doctype html>
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
// </head>
|
||||
// <body style="font-family: sans-serif;">
|
||||
// <div style="display: block; margin: auto; max-width: 600px;" class="main">
|
||||
// <h1 style="font-size: 18px; font-weight: bold; margin-top: 20px">Congrats for sending test email with Mailtrap!</h1>
|
||||
// <p>Inspect it using the tabs you see above and learn how this email can be improved.</p>
|
||||
// <img alt="Inspect with Tabs" src="cid:welcome.png" style="width: 100%;">
|
||||
// <p>Now send your email using our fake SMTP server and integration of your choice!</p>
|
||||
// <p>Good luck! Hope it works.</p>
|
||||
// </div>
|
||||
// <!-- Example of invalid for email html/css, will be detected by Mailtrap: -->
|
||||
// <style>
|
||||
// .main { background-color: white; }
|
||||
// a:hover { border-left-width: 1em; min-height: 2em; }
|
||||
// </style>
|
||||
// </body>
|
||||
// </html>`,
|
||||
// attachments: [
|
||||
// {
|
||||
// filename: "welcome.png",
|
||||
// content_id: "welcome.png",
|
||||
// disposition: "inline",
|
||||
// content: welcomeImage,
|
||||
// },
|
||||
// ],
|
||||
// })
|
||||
// .then(console.log, console.error, setResult);
|
||||
// };
|
||||
|
46
worker/index.js
Normal file
46
worker/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict'
|
||||
|
||||
console.log('Service Worker Loaded...')
|
||||
|
||||
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) })
|
||||
// ])
|
||||
// )
|
||||
// })
|
Reference in New Issue
Block a user