kahuna/public/js/components/gr-notifications-banner/gr-notifications-banner.tsx (222 lines of code) (raw):
import * as React from "react";
import * as angular from "angular";
import {react2angular} from "react2angular";
import {useEffect, useState} from "react";
import "./gr-notifications-banner.css";
const NOTIFICATION_LABEL = "Acknowledge and Close Notification";
const DEFAULT_URL_TEXT = "Click here for further information";
const PERSISTENT = "persistent";
const TRANSIENT = "transient";
const NOTIFICATION_COOKIE = "notification_cookie";
const cookie_age = 31536000;
const checkNotificationsUri = window._clientConfig.rootUri + "/notifications";
const checkNotificationsInterval = 60000; // in ms
const tickIcon = () =>
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<polyline fill="none" stroke="inherit" points="3.7 14.3 9.6 19 20.3 5" strokeLinecap="round" strokeLinejoin="round"
strokeWidth="2"/>
</svg>;
const emptyIcon = () =>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="100%" height="100%" fill="none" stroke="none"/>
</svg>;
const triangleIcon = () =>
<svg width="24px" height="24px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="add" fill="#000000" transform="translate(32.000000, 42.666667)">
<path
d="M246.312928,5.62892705 C252.927596,9.40873724 258.409564,14.8907053 262.189374,21.5053731 L444.667042,340.84129 C456.358134,361.300701 449.250007,387.363834 428.790595,399.054926 C422.34376,402.738832 415.04715,404.676552 407.622001,404.676552 L42.6666667,404.676552 C19.1025173,404.676552 7.10542736e-15,385.574034 7.10542736e-15,362.009885 C7.10542736e-15,354.584736 1.93772021,347.288125 5.62162594,340.84129 L188.099293,21.5053731 C199.790385,1.04596203 225.853517,-6.06216498 246.312928,5.62892705 Z M224,272 C208.761905,272 197.333333,283.264 197.333333,298.282667 C197.333333,313.984 208.415584,325.248 224,325.248 C239.238095,325.248 250.666667,313.984 250.666667,298.624 C250.666667,283.264 239.238095,272 224,272 Z M245.333333,106.666667 L202.666667,106.666667 L202.666667,234.666667 L245.333333,234.666667 L245.333333,106.666667 Z"
id="Combined-Shape">
</path>
</g>
</g>
</svg>;
const crossIcon = () =>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="none" stroke="none"/>
<path d="M7 17L16.8995 7.10051" stroke="#000" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"/>
<path d="M7 7.00001L16.8995 16.8995" stroke="#000" strokeLinecap="round" strokeLinejoin="round"
strokeWidth="2"/>
</svg>;
const circleIcon = () =>
<svg width="24px" height="24px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="add" fill="#000000" transform="translate(42.666667, 42.666667)">
<path
d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z"
id="Shape">
</path>
</g>
</g>
</svg>;
export interface Notification {
announceId: string,
description: string,
endDate: string,
url: string,
urlText: string,
category: string,
lifespan: string
}
const todayStr = (): string => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const day = today.getDate();
const mthStr = (month < 10) ? `0${month}` : `${month}`;
const dayStr = (day < 10) ? `0${day}` : `${day}`;
return (`${year}-${mthStr}-${dayStr}`);
};
const getCookie = (cookieName: string): string => {
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(';');
const temp = cookieArray.find((cookie) => cookie.trim().startsWith(cookieName));
if (temp) {
return temp.trim().replace((cookieName + "="), "");
}
return null;
};
const mergeArraysByKey = (array1: Notification[], array2: Notification[], key: keyof Notification): Notification[] => {
const merged = new Map<string, Notification>();
const addOrUpdate = (item: Notification) => {
merged.set(item[key], item);
};
array1.forEach(addOrUpdate);
array2.forEach(addOrUpdate);
return Array.from(merged.values());
};
const getIcon = (notification: Notification): JSX.Element => {
switch (notification.category) {
case "success":
return tickIcon();
case "error":
case "warning":
case "information":
return triangleIcon();
case "announcement":
return circleIcon();
default:
return emptyIcon();
}
};
const NotificationsBanner: React.FC = () => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const autoHideListener = (event: any) => {
if (event.type === "keydown" && event.key === "Escape") {
setNotifications(prevNotifs => prevNotifs.filter(n => n.lifespan !== TRANSIENT));
} else if (event.type !== "keydown") {
if (event.target.className !== "notification-url") {
setNotifications(prevNotifs => prevNotifs.filter(n => n.lifespan !== TRANSIENT));
}
}
};
const checkNotifications = () => {
fetch(checkNotificationsUri)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
})
.then(data => {
const announce: Notification[] = data;
const tdy = todayStr();
let notif_cookie = getCookie(NOTIFICATION_COOKIE);
if (!notif_cookie) {
notif_cookie = "";
}
const current_notifs = announce.filter(ann => ann.endDate > tdy)
.filter(ann => !notif_cookie.includes(ann.announceId));
setNotifications(prev_notifs => mergeArraysByKey(prev_notifs, current_notifs, 'announceId'));
})
.catch(error => {
console.error('There was a problem checking for Notifications:', error);
});
};
const newNotification = (event:any) => {
const notification = event.detail;
setNotifications(prev_notifs => mergeArraysByKey(prev_notifs, [notification], 'announceId'));
};
useEffect(() => {
const announce = window._clientConfig.announcements;
const tdy = todayStr();
let notif_cookie = getCookie(NOTIFICATION_COOKIE);
if (!notif_cookie) {
notif_cookie = "";
}
const current_notifs = announce.filter(ann => ann.endDate > tdy)
.filter(ann => !notif_cookie.includes(ann.announceId));
setNotifications(current_notifs);
// trigger server call to check notifications
const checkNotificationsRef:NodeJS.Timeout = setInterval(checkNotifications, checkNotificationsInterval);
document.addEventListener("mouseup", autoHideListener);
document.addEventListener("scroll", autoHideListener);
document.addEventListener("keydown", autoHideListener);
window.addEventListener("newNotification", newNotification);
// clean up cookie
if (notif_cookie) {
const current_notif_ids = announce.map(ann => ann.announceId).join(",");
const notif_ids = notif_cookie.split(',');
const new_notif_ids = notif_ids.filter(n_id => current_notif_ids.includes(n_id)).join(",");
document.cookie = `${NOTIFICATION_COOKIE}=${new_notif_ids}; max-age=${cookie_age}`;
}
// Clean up the event listener when the component unmounts
return () => {
document.removeEventListener("mouseup", autoHideListener);
document.removeEventListener("scroll", autoHideListener);
document.removeEventListener("keydown", autoHideListener);
window.removeEventListener("newNotification", newNotification);
clearInterval(checkNotificationsRef);
};
}, []);
const handleNotificationClick = (notif: Notification) => {
const ns = notifications.filter(n => n.announceId !== notif.announceId);
// persistent management
if (notif.lifespan == PERSISTENT) {
const current_cookie = getCookie(NOTIFICATION_COOKIE);
let new_cookie = notif.announceId;
if (current_cookie) {
new_cookie = current_cookie + "," + notif.announceId;
}
document.cookie = `${NOTIFICATION_COOKIE}=${new_cookie}; max-age=${cookie_age}`;
}
setNotifications(ns);
};
return (
<div className="outer-notifications" key="notification-banner">
{notifications.map((notification, index, array) => (
<div className={'notification-container' + ((index === array.length - 1) ? '-last' : '') + ' notification-' + notification.category}
key={"notification-" + notification.announceId}>
<div className="notification-start" key={"notification-start-" + notification.announceId}>
<div className="notification-start-icon" key={"notification-start-icon-" + notification.announceId}>
{getIcon(notification)}
</div>
</div>
<div className={'notification'} key={"notification-descrip-" + notification.announceId}>
<div className={'notification-inner'} key={"notification-inner-" + notification.announceId}>
<span tabIndex={0} role="alert" aria-label={'Notification ' + notification.description}
key={"notification-inner-" + notification.announceId}>
{notification.description}
</span>
{(notification.url && notification.url != "") &&
<span tabIndex={0}
key={"notification-url-" + notification.announceId}
role="link"
aria-label={DEFAULT_URL_TEXT}>
<a className="notification-url" target="_blank" rel="noreferrer" href={notification.url}>
{notification.urlText ? notification.urlText : DEFAULT_URL_TEXT}
</a>
</span>
}
</div>
</div>
<div className={'notification-end'} key={"notification-close-" + notification.announceId}>
<div tabIndex={0}
role="button"
aria-label={NOTIFICATION_LABEL}
className={'notification-button notification-' + notification.category}
key={"notification-end-icon-" + notification.announceId}
onClick={() => handleNotificationClick(notification)}>
{crossIcon()}
</div>
</div>
</div>
))}
</div>
);
};
export const notificationsBanner = angular.module('gr.notificationsBanner', [])
.component('notificationsBanner', react2angular(NotificationsBanner));