Merge branch 'main' into production

This commit is contained in:
Dobromir Popov
2024-04-08 16:30:18 +03:00
22 changed files with 4056 additions and 344 deletions

13
.env
View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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:

View File

@@ -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
View 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;

View File

@@ -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>

View File

@@ -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 --------------------------------- */}

View File

@@ -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)}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View File

@@ -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
View 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;

View File

@@ -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
View 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
View 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"
}

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -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;

View File

@@ -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
View 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) })
// ])
// )
// })