diff --git a/apps/api/package.json b/apps/api/package.json index 3021c3e..d5b733e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -34,8 +34,10 @@ "fastify": "^4.27.0", "fastify-plugin": "^4.5.1", "langchain": "^0.2.5", + "node-schedule": "^2.1.1", "orchid-orm": "^1.31.7", "orchid-orm-schema-to-zod": "^0.8.8", + "web-push": "^3.6.7", "zod-to-json-schema": "^3.23.0" }, "devDependencies": { @@ -49,6 +51,8 @@ "tap": "^18.8.0", "ts-node": "^10.9.2", "tsx": "^4.10.5", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "@types/node-schedule": "^2.1.7", + "@types/web-push": "^3.6.3" } } diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index fe8b163..9f4df5f 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -10,6 +10,15 @@ import { logsConfig } from "./configs/logger.config"; import { trpcContext } from "./context.trpc"; import { ApiRouter, trpcRouter } from "./router.trpc"; import { googleAuth } from "./auth/google-auth"; +import webpush from 'web-push'; +import { PushSubscription } from 'web-push'; +import { addSubscription, removeSubscription, scheduleFrequentNotification } from './controllers/pushNotifications'; + +function isSubscriptionActive(subscription: PushSubscription): boolean { + return Array.from(pushSubscriptions).some( + sub => sub.endpoint === subscription.endpoint + ); +} export const app = fastify({ logger: logsConfig[env.ENVIRONMENT], @@ -19,7 +28,7 @@ export const logger = app.log; // Declare a route app.get("/", function (_, reply) { - reply.send("Hello World!"); + reply.send("Fastify is running"); }); // Fastify level centralized error handling @@ -37,6 +46,55 @@ app.setErrorHandler(function (error, _request, reply) { app.register(cookiePlugin).register(fastifyJwt, { secret: env.JWT_SECRET as string }).register(googleAuth) +// Configure web-push +webpush.setVapidDetails( + 'mailto:mohit@teziapp.com', + env.VAPID_PUBLIC_KEY, + env.VAPID_PRIVATE_KEY +); + +// In-memory storage for subscriptions (replace with database in production) +const pushSubscriptions = new Set(); + +// Add push subscription +app.post('/subscribe', async (request, reply) => { + const subscription = request.body as PushSubscription; + + if (isSubscriptionActive(subscription)) { + reply.send({ success: false, message: 'Subscription already active' }); + } else { + addSubscription(subscription); + scheduleFrequentNotification(); + reply.send({ success: true }); + } +}); + +// Unsubscribe from push notifications +app.post<{ + Body: PushSubscription +}>('/unsubscribe', async (request, reply) => { + const subscription = request.body; + removeSubscription(subscription); + reply.send({ success: true }); +}); + +// Trigger push notification + +app.post<{ + Body: { title: string; body: string } +}>('/send-notification', async (request, reply) => { + const { title, body } = request.body; + for (const subscription of pushSubscriptions) { + try { + await webpush.sendNotification(subscription, JSON.stringify({ title, body })); + } catch (error) { + console.error('Error sending notification:', error); + pushSubscriptions.delete(subscription); + } + } + + reply.send({ success: true }); +}); app.register(fastifyTRPCPlugin, { prefix: "/v1", @@ -49,4 +107,4 @@ app.register(fastifyTRPCPlugin, { console.error(`Error in tRPC handler on path '${path}':`, error); }, } satisfies FastifyTRPCPluginOptions["trpcOptions"], -}); +}); \ No newline at end of file diff --git a/apps/api/src/configs/env.config.ts b/apps/api/src/configs/env.config.ts index 20b57f1..88894c3 100644 --- a/apps/api/src/configs/env.config.ts +++ b/apps/api/src/configs/env.config.ts @@ -15,6 +15,8 @@ const envDefaultFields = z // for storing the logs which are sent to axiom AXIOM_DATASET: z.string().optional(), AXIOM_TOKEN: z.string().optional(), + VAPID_PUBLIC_KEY: z.string().min(1), + VAPID_PRIVATE_KEY: z.string().min(1) }) .refine((env) => { if (env.ENVIRONMENT === "prod" && env.DB_TEST_URL) { diff --git a/apps/api/src/controllers/pushNotifications.ts b/apps/api/src/controllers/pushNotifications.ts new file mode 100644 index 0000000..be0ad8a --- /dev/null +++ b/apps/api/src/controllers/pushNotifications.ts @@ -0,0 +1,40 @@ +import webpush, { PushSubscription } from 'web-push'; +import { env } from "../configs/env.config"; +import { scheduleJob } from 'node-schedule'; + +// Configure web-push +webpush.setVapidDetails( + 'mailto:mohit@teziapp.com', + env.VAPID_PUBLIC_KEY, + env.VAPID_PRIVATE_KEY +); + +// In-memory storage for subscriptions (replace with database in production) +const pushSubscriptions = new Set(); + +export function addSubscription(subscription: PushSubscription) { + pushSubscriptions.add(subscription); +} + +export function removeSubscription(subscription: PushSubscription) { + pushSubscriptions.delete(subscription); +} + +export async function sendNotificationToAll(title: string, body: string) { + for (const subscription of pushSubscriptions) { + try { + await webpush.sendNotification(subscription, JSON.stringify({ title, body })); + } catch (error) { + console.error('Error sending notification:', error); + pushSubscriptions.delete(subscription); + } + } +} + +// Schedule a notification every 5 seconds +export function scheduleFrequentNotification() { + scheduleJob('*/5 * * * * *', async () => { + console.log('Sending notification'); + await sendNotificationToAll('Frequent Update', 'Here is your notification every 5 seconds!'); + }); +} \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 2b813a7..7b58542 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -17,7 +17,8 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "ESNext" + "target": "ESNext", + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": [ "src/**/*.ts", diff --git a/apps/pwa/package.json b/apps/pwa/package.json index d334d72..95f31a6 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -40,12 +40,16 @@ "react-lazy-load-image-component": "^1.6.2", "react-microsoft-clarity": "^1.2.0", "react-router-dom": "^6.26.2", + "react-toastify": "^10.0.5", "simplebar": "^6.2.7", "simplebar-react": "^3.2.6", "stylis": "^4.3.4", "stylis-plugin-rtl": "^2.1.1", "vite-plugin-svg-icons": "^2.0.1", "vite-tsconfig-paths": "^4.3.2", + "web-push": "^3.6.7", + "workbox-core": "^7.1.0", + "workbox-precaching": "^7.1.0", "zustand": "^4.5.4" }, "devDependencies": { diff --git a/apps/pwa/src/App.tsx b/apps/pwa/src/App.tsx index 6cef26d..fadb047 100644 --- a/apps/pwa/src/App.tsx +++ b/apps/pwa/src/App.tsx @@ -20,6 +20,7 @@ import ThemeProvider from '@/theme'; import SnackbarProvider from '@/component/snackbar/SnackbarProvider'; import Router from '@/routes'; import ThemeSettings from '@/component/settings/ThemeSettings'; +import ServiceWorkerUpdateDialog from './component/ServiceWorkerUpdate'; // Add these constants for PostHog configuration const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY; @@ -45,6 +46,7 @@ export const App = () => { {/* */} + diff --git a/apps/pwa/src/component/ServiceWorkerUpdate.tsx b/apps/pwa/src/component/ServiceWorkerUpdate.tsx new file mode 100644 index 0000000..9a868e6 --- /dev/null +++ b/apps/pwa/src/component/ServiceWorkerUpdate.tsx @@ -0,0 +1,73 @@ +/// +import React, { useState, useCallback } from "react"; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box} from "@mui/material"; +import { registerSW } from "virtual:pwa-register"; +import { toast } from 'react-toastify'; +import { NotificationToggle } from "./settings/drawer/NotificationToggle"; + +const ServiceWorkerUpdateDialog: React.FC = () => { + const [updateAvailable, setUpdateAvailable] = useState(false); + + // // Static update function + const updateSWFunction = registerSW({ + onNeedRefresh() { + setUpdateAvailable(true); + }, + onOfflineReady() { + toast.success('App is ready for offline use'); + }, + }); + + const handleUpdate = () => { + updateSWFunction(true); // This triggers the installation of the new SW + + navigator.serviceWorker.getRegistration().then((registration) => { + if (registration?.waiting) { + // Tell the service worker to skip the waiting phase and activate + registration.waiting.postMessage({ type: 'SKIP_WAITING' }); + + // Reload the page when the new service worker takes control + registration.waiting.addEventListener('statechange', (event) => { + if ((event.target as ServiceWorker).state === 'activated') { + window.location.reload(); + } + }); + } + }); + + setUpdateAvailable(false); // Close the modal + }; + + const handleClose = useCallback(() => { + setUpdateAvailable(false); + }, []); + + return ( + + An update is available! + + + Performance Improvement Update + New Features: +
    +
  • Customizations Added
  • +
  • Bug Fixes
  • +
  • Feature Improvements
  • +
+
+ +
+ + + +
+ ); +}; + +export default ServiceWorkerUpdateDialog; diff --git a/apps/pwa/src/component/settings/drawer/NotificationToggle.tsx b/apps/pwa/src/component/settings/drawer/NotificationToggle.tsx new file mode 100644 index 0000000..5434bd4 --- /dev/null +++ b/apps/pwa/src/component/settings/drawer/NotificationToggle.tsx @@ -0,0 +1,14 @@ +import CustomToggleButton, { CustomToggleButtonProps } from '@/theme/CustomToggleButton'; +import { handleNotificationSubscription } from '@/utils/handleNotification'; + +export const NotificationToggle = () => { + + const property : CustomToggleButtonProps= { + initialState: false, + label: 'Enable Notifications', + direction: 'row' as const, + onToggleFunction: handleNotificationSubscription + }; + + return ; +}; diff --git a/apps/pwa/src/component/settings/drawer/SettingsDrawer.tsx b/apps/pwa/src/component/settings/drawer/SettingsDrawer.tsx index 5d8698e..f56aeb7 100644 --- a/apps/pwa/src/component/settings/drawer/SettingsDrawer.tsx +++ b/apps/pwa/src/component/settings/drawer/SettingsDrawer.tsx @@ -21,6 +21,7 @@ import DirectionOptions from './DirectionOptions'; import FullScreenOptions from './FullScreenOptions'; import ColorPresetsOptions from './ColorPresetsOptions'; import { useSettingsContext } from '@/component/settings/settingContext'; +import { NotificationToggle } from './NotificationToggle'; // ---------------------------------------------------------------------- @@ -121,6 +122,10 @@ export default function SettingsDrawer() { + + + + diff --git a/apps/pwa/src/layouts/dashboard/DashboardLayout.tsx b/apps/pwa/src/layouts/dashboard/DashboardLayout.tsx index 7139c9a..c100377 100644 --- a/apps/pwa/src/layouts/dashboard/DashboardLayout.tsx +++ b/apps/pwa/src/layouts/dashboard/DashboardLayout.tsx @@ -5,7 +5,7 @@ import { Box } from '@mui/material'; // hooks import useResponsive from '../../hooks/useResponsive'; // -import Main from './Main'; +import Main from './main'; import Header from './header'; import NavMini from './nav/NavMini'; import NavVertical from './nav/NavVertical'; diff --git a/apps/pwa/src/sw.ts b/apps/pwa/src/sw.ts new file mode 100644 index 0000000..af49862 --- /dev/null +++ b/apps/pwa/src/sw.ts @@ -0,0 +1,98 @@ +/// +import { precacheAndRoute } from 'workbox-precaching' + +declare let self: ServiceWorkerGlobalScope & typeof globalThis & { __WB_MANIFEST: any }; +console.log("New service worker file"); +precacheAndRoute(self.__WB_MANIFEST); + +interface NotificationData { + title: string; + body: string; + icon: string; + badge: string; + data: string; +} + +// Activate event to take control of all clients as soon as the service worker is activated +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +// Listen for manual update trigger +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +// Function to check if notifications are already subscribed +const checkNotificationSubscription = async (): Promise => { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + return subscription ? true : false; + } catch (error) { + console.error('Error checking subscription:', error); + return undefined; + } +}; + +// Handle push notifications +self.addEventListener('push', (event: PushEvent) => { + const data = event.data?.json() || {}; + const notification: NotificationData = { + title: data.title || 'New Notification', + body: data.body || 'You have a new notification', + icon: '/favicon/android-chrome-192x192.png', + badge: '/favicon/android-chrome-192x192.png', + data: data.url || '/', + }; + + event.waitUntil( + self.registration.showNotification(data.title, notification) + .then(() => console.log('Notification shown')) + .catch(error => console.error('Error showing notification:', error)) + ); +}); + +// Handle notification click event +self.addEventListener('notificationclick', (event: NotificationEvent) => { + event.notification.close(); + event.waitUntil( + self.clients.openWindow(event.notification.data) + ); +}); + +export async function subscribeToPushNotifications(): Promise { + try { + // Check if there is already a subscription + const isSubscribed = await checkNotificationSubscription(); + if (isSubscribed) { + console.log('Already subscribed to push notifications'); + return; + } + + // Proceed with subscription if not already subscribed + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: 'BLA70jg5Wgi6XD6BAElOfW7YXcQ3l3iFRzyPj5AV5ZuSr_uTugv-9hbgXwfPhuw_JfbDAqn-Fl5nKSvnQpjFV8g', + }); + + const response = await fetch(import.meta.env.VITE_BE_URL + '/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(subscription), + }); + + if (!response.ok) { + throw new Error('Failed to send subscription to server'); + } + + console.log('Successfully subscribed to push notifications'); + } catch (error) { + console.error('Failed to subscribe to push notifications:', error); + } +} \ No newline at end of file diff --git a/apps/pwa/src/theme/CustomToggleButton.tsx b/apps/pwa/src/theme/CustomToggleButton.tsx new file mode 100644 index 0000000..48e1f67 --- /dev/null +++ b/apps/pwa/src/theme/CustomToggleButton.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { Switch, Typography, Stack } from '@mui/material'; + +export interface CustomToggleButtonProps { + initialState?: boolean; + label: string; + direction?: 'row' | 'column'; + onToggleFunction?: (checked: boolean) => void; +} + +export const CustomToggleButton: React.FC = ({ + initialState = false, + label, + direction = "row", + onToggleFunction, +}) => { + const [enabled, setEnabled] = useState(initialState); + + return ( + + {label} + { + const checked = event.target.checked; + if (onToggleFunction) { + onToggleFunction(checked); + } + setEnabled(checked); + }} + name="notifications" + color="primary" + /> + + ); +}; + +export default CustomToggleButton; diff --git a/apps/pwa/src/utils/handleNotification.ts b/apps/pwa/src/utils/handleNotification.ts new file mode 100644 index 0000000..197d63e --- /dev/null +++ b/apps/pwa/src/utils/handleNotification.ts @@ -0,0 +1,56 @@ +import { subscribeToPushNotifications } from "@/sw"; + +export const handleNotificationSubscription = async (subscribe: boolean): Promise => { + if (subscribe) { + if ('Notification' in window) { + const permission = await Notification.requestPermission(); + if (permission === 'granted') { + await subscribeToPushNotifications(); + const subscription = await navigator.serviceWorker.ready.then(async registration => { + return await registration.pushManager.getSubscription(); + }); + if (subscription) { + // Add event listener for push notifications + navigator.serviceWorker.addEventListener('push', handlePushEvent as EventListener); + return true; + } + } + console.log('Notification permission denied'); + return false; + } + console.log('Notifications not supported in this browser'); + return false; + } else { + // Unsubscribe from notifications + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + if (subscription) { + await subscription.unsubscribe(); + + // Send unsubscribe request to backend + await fetch(import.meta.env.VITE_BE_URL + '/unsubscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(subscription), + }); + + // Remove the event listener for push notifications + navigator.serviceWorker.removeEventListener('push', handlePushEvent as EventListener); + + registration.active?.postMessage({ type: 'UNSUBSCRIBE_NOTIFICATIONS' }); + console.log('Notification turned off'); + return false; + } + } + return false; + } +}; + +// Define the handlePushEvent function +function handlePushEvent(event: PushEvent) { + // Handle the push event here + console.log('Received a push event', event); +} \ No newline at end of file diff --git a/apps/pwa/vite.config.ts b/apps/pwa/vite.config.ts index 9e7fa56..1ff2943 100644 --- a/apps/pwa/vite.config.ts +++ b/apps/pwa/vite.config.ts @@ -1,11 +1,12 @@ import path from "path"; import { defineConfig } from "vite"; import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; +// use without react-swc use react only import reactSWC from "@vitejs/plugin-react-swc"; import tsconfigPaths from "vite-tsconfig-paths"; import { VitePWA, VitePWAOptions } from "vite-plugin-pwa"; -const manifestForPlugIn:Partial = { +const manifestForPlugIn: Partial = { devOptions: { enabled: true, type: "module", @@ -14,10 +15,22 @@ const manifestForPlugIn:Partial = { registerType: 'prompt', workbox: { globPatterns: ["**/*.{js,jsx,css,html,ico,png,svg,ts,tsx}"], + maximumFileSizeToCacheInBytes: 10000000, + // Enable sourcemaps for the service worker. This allows for easier debugging of the service worker code. by mapping the compiled code back to the original source + sourcemap: true, runtimeCaching: [ { - urlPattern: ({ request }: { request: Request }) => request.destination === "image", - handler: "StaleWhileRevalidate", + urlPattern: ({ request }: { request: Request }) => + request.destination === "image" || + /\.(?:png|gif|jpg|jpeg|svg)$/.test(request.url), + handler: "CacheFirst", + options: { + cacheName: "images", + expiration: { + maxEntries: 60, + maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days + } + } }, { urlPattern: /\.(?:woff|woff2)$/, @@ -32,20 +45,28 @@ const manifestForPlugIn:Partial = { statuses: [0, 200] } } - }, - { - urlPattern: /\.(?:png|gif|jpg|jpeg|svg)$/, - handler: 'CacheFirst', - options: { - cacheName: 'images', - expiration: { - maxEntries: 60, - maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days - } - } - } + } ], }, + includeAssets: [ + "/favicon.ico", + "/favicon/apple-touch-icon.png", + "/favicon/favicon-32x32.png", + "/favicon/favicon-16x16.png", + "/favicon/android-chrome-192x192.png", + "/favicon/android-chrome-512x512.png", + "/screenshots/screenshot-1.png", + "/screenshots/screenshot-2.png", + "/screenshots/screenshot-3.png", + "/screenshots/screenshot-4.png", + "/screenshots/screenshot-5.png", + "/screenshots/screenshot-6.png", + "/screenshots/screenshot-7.jpeg", + "/screenshots/screenshot-8.jpeg", + "/screenshots/screenshot-9.jpeg", + "/screenshots/screenshot-10.jpeg", + "masked-icon.svg" + ], manifest: { short_name: "Boilerplate", name: "Boilerplate", @@ -77,54 +98,138 @@ const manifestForPlugIn:Partial = { sizes: "512x512", }, ], - start_url: "/", + start_url: '/', + lang: 'en', + dir: 'ltr', + scope: '/', display: "standalone", theme_color: "#5296d9", orientation: "any", - categories: ["business", "productivity", "content", "curation"], + screenshots: [ + { + src: "/screenshots/screenshot-1.png", + type: "image/png", + sizes: "1280x720", + form_factor: "wide", + platform: "windows", + }, + { + src: "/screenshots/screenshot-2.png", + type: "image/png", + sizes: "1280x720", + form_factor: "wide", + platform: "windows", + }, + { + src: "/screenshots/screenshot-3.png", + type: "image/png", + sizes: "1280x720", + platform: "windows", + }, + { + src: "/screenshots/screenshot-4.png", + type: "image/png", + sizes: "1280x720", + platform: "windows", + }, + { + src: "/screenshots/screenshot-4.png", + type: "image/png", + sizes: "1280x720", + platform: "windows", + }, + { + src: "/screenshots/screenshot-5.png", + type: "image/png", + sizes: "1280x720", + platform: "windows", + }, + { + src: "/screenshots/screenshot-6.png", + type: "image/png", + sizes: "1280x720", + platform: "windows", + }, + { + src: "/screenshots/screenshot-7.jpeg", + type: "image/jpeg", + sizes: "1080x2400", + platform: "android", + }, + { + src: "/screenshots/screenshot-8.jpeg", + type: "image/jpeg", + sizes: "1080x2400", + platform: "android", + }, + { + src: "/screenshots/screenshot-9.jpeg", + type: "image/jpeg", + sizes: "1080x2400", + platform: "android", + }, + { + src: "/screenshots/screenshot-10.jpeg", + type: "image/jpeg", + sizes: "1080x2400", + platform: "android", + }, + ], + categories: ["business", "productivity", "boilerplate"], background_color: "#ffffff", - // This is useless code, When user share some doc, it'll show our app there. It will bring users attention to our existence. Cheers! - "share_target": { - "action": "/share-target/", - "method": "POST", - "enctype": "multipart/form-data", - "params": { - "title": "title", - "text": "text", - "url": "url", - "files": [ + share_target: { + action: "/share-target/", + method: "POST", + enctype: "multipart/form-data", + params: { + title: "title", + text: "text", + url: "url", + files: [ { - "name": "images", - "accept": ["image/*"] + name: "images", + accept: ["image/*"] } ] } - } - } -}; - -const replaceOptions = { - __DATE__: new Date().toISOString(), - preventAssignment: true, + }, + shortcuts: [ + { + name: "New voucher", + description: "Create a new voucher", + url: "/voucher/add-new", + icons: [ + { + src: "/icons/ic_cart.svg", + } + ], + }, + { + name: "Outstanding List", + description: "Create a new order", + url: "/order/add-new", + icons: [ + { + src: "/icons/ic_cart.svg", + } + ], + } + ], + }, + strategies: 'injectManifest', // Use injectManifest strategy + srcDir: 'src', // Point to the source directory + filename: 'sw.ts', // Custom service worker filename }; -const claims = process.env.CLAIMS === "true"; -const reload = process.env.RELOAD_SW === "true"; -const selfDestroying = process.env.SW_DESTROY === "true"; - export default defineConfig({ - base: "/", - esbuild: { - // drop: ['console', 'debugger'], - }, + esbuild: {}, css: { devSourcemap: true, }, plugins: [ reactSWC(), VitePWA(manifestForPlugIn), - // replace(replaceOptions), tsconfigPaths(), createSvgIconsPlugin({ iconDirs: [path.resolve(process.cwd(), "src/assets/icons")], @@ -135,7 +240,6 @@ export default defineConfig({ plugins: () => [reactSWC()], format: "es", }, - build: { target: "esnext", minify: "esbuild", @@ -145,5 +249,5 @@ export default defineConfig({ drop_debugger: true, }, }, - }, -}); \ No newline at end of file + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2be3d7d..9bd9739 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,12 +101,18 @@ importers: langchain: specifier: ^0.2.5 version: 0.2.19(@langchain/community@0.0.44)(encoding@0.1.13)(ignore@5.3.2)(openai@4.60.1(encoding@0.1.13)(zod@3.23.8))(ws@8.18.0) + node-schedule: + specifier: ^2.1.1 + version: 2.1.1 orchid-orm: specifier: ^1.31.7 version: 1.35.11(typescript@5.6.2) orchid-orm-schema-to-zod: specifier: ^0.8.8 version: 0.8.47 + web-push: + specifier: ^3.6.7 + version: 3.6.7 zod-to-json-schema: specifier: ^3.23.0 version: 3.23.3(zod@3.23.8) @@ -120,6 +126,12 @@ importers: '@types/node': specifier: ^20.12.12 version: 20.16.5 + '@types/node-schedule': + specifier: ^2.1.7 + version: 2.1.7 + '@types/web-push': + specifier: ^3.6.3 + version: 3.6.3 esbuild: specifier: ^0.21.3 version: 0.21.5 @@ -237,6 +249,9 @@ importers: react-router-dom: specifier: ^6.26.2 version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-toastify: + specifier: ^10.0.5 + version: 10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) simplebar: specifier: ^6.2.7 version: 6.2.7 @@ -255,6 +270,15 @@ importers: vite-tsconfig-paths: specifier: ^4.3.2 version: 4.3.2(typescript@5.6.2)(vite@5.4.5(@types/node@20.16.5)(terser@5.32.0)) + web-push: + specifier: ^3.6.7 + version: 3.6.7 + workbox-core: + specifier: ^7.1.0 + version: 7.1.0 + workbox-precaching: + specifier: ^7.1.0 + version: 7.1.0 zustand: specifier: ^4.5.4 version: 4.5.5(@types/react@18.3.5)(react@18.3.1) @@ -348,7 +372,7 @@ importers: version: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) antd: specifier: ^5.17.3 - version: 5.20.6(date-fns@3.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.20.6(date-fns@3.6.0)(luxon@3.5.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -2744,6 +2768,9 @@ packages: '@types/node-fetch@2.6.11': resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + '@types/node-schedule@2.1.7': + resolution: {integrity: sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==} + '@types/node@18.19.50': resolution: {integrity: sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==} @@ -2804,6 +2831,9 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/web-push@3.6.3': + resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==} + '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -3298,6 +3328,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3532,6 +3565,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -4537,6 +4574,10 @@ packages: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + https-proxy-agent@7.0.5: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} @@ -4989,6 +5030,12 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5260,6 +5307,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5280,6 +5330,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -5515,6 +5569,10 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + nodemon@3.1.4: resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} engines: {node: '>=10'} @@ -6389,6 +6447,12 @@ packages: peerDependencies: react: '>=16.8' + react-toastify@10.0.5: + resolution: {integrity: sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -6777,6 +6841,9 @@ packages: resolution: {integrity: sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w==} hasBin: true + sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -7475,6 +7542,11 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -10025,6 +10097,10 @@ snapshots: '@types/node': 20.16.5 form-data: 4.0.0 + '@types/node-schedule@2.1.7': + dependencies: + '@types/node': 20.16.5 + '@types/node@18.19.50': dependencies: undici-types: 5.26.5 @@ -10087,6 +10163,10 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/web-push@3.6.3': + dependencies: + '@types/node': 20.16.5 + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)': dependencies: '@eslint-community/regexpp': 4.11.0 @@ -10426,7 +10506,7 @@ snapshots: ansi-styles@6.2.1: {} - antd@5.20.6(date-fns@3.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + antd@5.20.6(date-fns@3.6.0)(luxon@3.5.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@ant-design/colors': 7.1.0 '@ant-design/cssinjs': 1.21.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10458,7 +10538,7 @@ snapshots: rc-motion: 2.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-notification: 5.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-pagination: 4.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rc-picker: 4.6.14(date-fns@3.6.0)(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rc-picker: 4.6.14(date-fns@3.6.0)(dayjs@1.11.13)(luxon@3.5.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-progress: 4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-rate: 2.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-resize-observer: 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10745,6 +10825,8 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@6.0.3: @@ -10996,6 +11078,10 @@ snapshots: create-require@1.1.1: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.5.0 + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -12254,6 +12340,8 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + http_ece@1.2.0: {} + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 @@ -12675,6 +12763,17 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -12790,6 +12889,8 @@ snapshots: lodash@4.17.21: {} + long-timeout@0.1.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -12811,6 +12912,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.5.0: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -13068,6 +13171,12 @@ snapshots: node-releases@2.0.18: {} + node-schedule@2.1.1: + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + nodemon@3.1.4: dependencies: chokidar: 3.6.0 @@ -13833,7 +13942,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - rc-picker@4.6.14(date-fns@3.6.0)(dayjs@1.11.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + rc-picker@4.6.14(date-fns@3.6.0)(dayjs@1.11.13)(luxon@3.5.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 '@rc-component/trigger': 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13846,6 +13955,7 @@ snapshots: optionalDependencies: date-fns: 3.6.0 dayjs: 1.11.13 + luxon: 3.5.0 rc-progress@4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -14074,6 +14184,12 @@ snapshots: '@remix-run/router': 1.19.2 react: 18.3.1 + react-toastify@10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 @@ -14525,6 +14641,8 @@ snapshots: semver: 7.6.3 sort-object-keys: 1.1.3 + sorted-array-functions@1.3.0: {} + source-map-js@1.2.1: {} source-map-resolve@0.5.3: @@ -15280,6 +15398,16 @@ snapshots: walk-up-path@3.0.1: {} + web-push@3.6.7: + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.5 + jws: 4.0.0 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color + web-streams-polyfill@4.0.0-beta.3: {} web-vitals@4.2.3: {}