Skip to content

Commit 3556c96

Browse files
authored
Add in-site notification feature to Docusaurus (#1168)
* Create NotificationBell.tsx * Create index.tsx * Create notifications.js * Add custom notification function * Add styles for notification icon and dropdown * Fix question mark tooltip overlap issue * Add `@fortawesome/fontawesome-free` * Fix image background color on zoom * Allow each language to have its own URL * Add notification messages * Make notification message font larger * Fix broken relative links Clicking a link in the notification dropdown and then clicking another link resulted in a broken link because the relative path was taken into account when then next link was clicked. For example, if the relative link was `AAA/BBB`, then when the link `CCC/DDD` was selected, the URL would be `AAA/CCC/DDD`. This commit ensures that the base URL is called first and then the relative link in the notification message is applied to that base link. * Add `Blog post` to notification message * Add missing ending curly bracket * Remove duplicate bell icon * Fix grammar * Change object name from `languages` to `message` * Add automatic version tracking for content changes Implemented content fingerprinting to detect changes in notification messages and URLs. When content changes, the system automatically increments the version number, marking the notification as unread.
1 parent 6e6820b commit 3556c96

File tree

7 files changed

+439
-1
lines changed

7 files changed

+439
-1
lines changed

docusaurus.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// See: https://docusaurus.io/docs/api/docusaurus-config
66

77
import {themes as prismThemes} from 'prism-react-renderer';
8+
import { getNotifications } from './src/data/notifications';
89

910
/** @type {import('@docusaurus/types').Config} */
1011
const config = {
@@ -389,6 +390,12 @@ const config = {
389390
type: 'localeDropdown',
390391
position: 'right',
391392
},
393+
// Custom notification function as a React component. Update the notification messages in the /src/data/notifications.js file.
394+
{
395+
type: 'custom-NotificationBell',
396+
position: 'right',
397+
notifications: getNotifications(),
398+
},
392399
{
393400
href: 'https://github.com/scalar-labs/scalardb',
394401
position: 'right',

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@docusaurus/plugin-pwa": "^3.7.0",
2323
"@docusaurus/preset-classic": "^3.7.0",
2424
"@docusaurus/theme-mermaid": "^3.7.0",
25+
"@fortawesome/fontawesome-free": "6.6.0",
2526
"@fortawesome/fontawesome-svg-core": "6.5.2",
2627
"@fortawesome/free-brands-svg-icons": "6.5.2",
2728
"@fortawesome/free-regular-svg-icons": "6.5.2",
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { faBell } from '@fortawesome/free-regular-svg-icons';
4+
import { detectLanguage } from '../data/notifications';
5+
6+
const NotificationBell = ({ notifications }) => {
7+
const [isOpen, setIsOpen] = useState(false);
8+
const [notificationList, setNotificationList] = useState([]);
9+
const [currentLanguage, setCurrentLanguage] = useState('en');
10+
const dropdownRef = useRef(null);
11+
const wrapperRef = useRef(null);
12+
13+
// Set the current language in the component.
14+
useEffect(() => {
15+
setCurrentLanguage(detectLanguage());
16+
}, []);
17+
18+
// Toggle dropdown visibility and prevent the event from bubbling up to the outside click handler.
19+
const toggleDropdown = (event) => {
20+
event.stopPropagation(); // Prevent outside click handler from immediately reopening dropdown.
21+
setIsOpen((prev) => !prev);
22+
};
23+
24+
// Load notifications from localStorage and update it if there are new notifications.
25+
useEffect(() => {
26+
// Retrieve seen notifications from localStorage.
27+
const seenNotificationsJSON = localStorage.getItem('seenNotifications');
28+
// Update the data structure to store both the ID and the version.
29+
const seenNotifications = seenNotificationsJSON ? JSON.parse(seenNotificationsJSON) : [];
30+
31+
// Map the notifications to add read status based on seenNotifications.
32+
const updatedNotifications = notifications.map(notification => {
33+
// Find if this notification was seen
34+
const seenNotification = seenNotifications.find(item => item.id === notification.id);
35+
36+
// Mark as read only if both the ID and the version match.
37+
const isRead = seenNotification && seenNotification.version === notification.version;
38+
39+
return {
40+
...notification,
41+
read: isRead,
42+
};
43+
});
44+
45+
// Set the updated notifications in state.
46+
setNotificationList(updatedNotifications);
47+
}, [notifications]); // Dependency ensures rerun if notifications change.
48+
49+
// Save changes to localStorage when notifications are clicked.
50+
const handleNotificationClick = (notification, event) => {
51+
// If it's an internal link, prevent default behavior and use history to navigate.
52+
if (!notification.isExternal) {
53+
event.preventDefault();
54+
window.location.href = notification.url;
55+
}
56+
57+
const updatedList = notificationList.map(notif =>
58+
notif.id === notification.id ? { ...notif, read: true } : notif
59+
);
60+
61+
// Save the seen notifications in localStorage with their version.
62+
const seenNotifications = updatedList
63+
.filter(notif => notif.read)
64+
.map(notif => ({
65+
id: notif.id,
66+
version: notif.version || 1
67+
}));
68+
69+
localStorage.setItem('seenNotifications', JSON.stringify(seenNotifications));
70+
setNotificationList(updatedList); // Update the notification list with a read status.
71+
};
72+
73+
// Count unread notifications.
74+
const unreadCount = notificationList.filter(notification => !notification.read).length;
75+
76+
// Close the dropdown when clicking outside.
77+
useEffect(() => {
78+
const handleClickOutside = (event) => {
79+
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
80+
setIsOpen(false);
81+
}
82+
};
83+
84+
document.addEventListener('mousedown', handleClickOutside);
85+
return () => document.removeEventListener('mousedown', handleClickOutside);
86+
}, []);
87+
88+
return (
89+
<div className="notification-wrapper" onClick={toggleDropdown} ref={wrapperRef}>
90+
<FontAwesomeIcon icon={faBell} size="lg" />
91+
{unreadCount > 0 && <span className="notification-count">{unreadCount}</span>}
92+
{isOpen && (
93+
<div className="notification-dropdown" ref={dropdownRef}>
94+
{notificationList.map(notification => (
95+
<a
96+
key={notification.id}
97+
href={notification.url}
98+
className={`notification-item ${!notification.read ? 'unread' : ''}`}
99+
onClick={(e) => handleNotificationClick(notification, e)}
100+
target={notification.isExternal ? '_blank' : '_self'}
101+
rel={notification.isExternal ? 'noopener noreferrer' : undefined}
102+
>
103+
{notification.message}
104+
</a>
105+
))}
106+
</div>
107+
)}
108+
</div>
109+
);
110+
};
111+
112+
export default NotificationBell;

src/css/custom.css

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ html[data-theme="dark"] a[class^='fa-solid fa-circle-question'] {
199199
text-wrap: nowrap;
200200
visibility: hidden;
201201
width: auto;
202-
z-index: 1;
202+
z-index: 10;
203203
top: -2px;
204204
left: 125%;
205205
}
@@ -371,3 +371,132 @@ html[data-theme="dark"] .container img[src$=".png"] { /* Adds a white background
371371
z-index: 20;
372372
position: relative;
373373
}
374+
375+
.medium-zoom-image--opened {
376+
background-color: #ffffff;
377+
}
378+
379+
/* In-site notification feature */
380+
.notification-wrapper {
381+
position: relative;
382+
cursor: pointer;
383+
padding: 8px;
384+
}
385+
386+
.notification-count {
387+
position: absolute;
388+
top: 0;
389+
right: 0;
390+
background-color: #ff4444;
391+
color: white;
392+
border-radius: 50%;
393+
padding: 0.2rem 0.2rem;
394+
font-size: 0.8rem;
395+
min-width: 20px;
396+
height: 20px;
397+
display: flex;
398+
align-items: center;
399+
justify-content: center;
400+
transform: translate(30%, 15%);
401+
}
402+
403+
.notification-dropdown {
404+
background-color: var(--ifm-dropdown-background-color);
405+
box-shadow: var(--ifm-global-shadow-md);
406+
border-radius: 8px;
407+
font-size: 0.95rem;
408+
right: 0px;
409+
position: absolute;
410+
text-decoration: none;
411+
top: 38px;
412+
width: 330px;
413+
z-index: 1000;
414+
}
415+
416+
.notification-item {
417+
border-bottom: 3px solid none;
418+
color: inherit;
419+
display: block;
420+
padding: 10px;
421+
text-decoration: none;
422+
}
423+
424+
.notification-item.unread {
425+
border-left: 5px solid #2673BB;
426+
border-bottom: 3px solid none;
427+
margin: 8px;
428+
}
429+
430+
.notification-item {
431+
transition: background-color 0.2s ease;
432+
}
433+
434+
.notification-item:last-child {
435+
border-bottom: none;
436+
}
437+
438+
.notification-item:hover {
439+
background-color: var(--ifm-dropdown-hover-background-color);
440+
color: inherit;
441+
text-decoration: none;
442+
}
443+
444+
html[data-theme="dark"] .notification-item {
445+
color: inherit;
446+
padding: 10px;
447+
}
448+
449+
html[data-theme="dark"] .notification-item:hover {
450+
background-color: var(--ifm-dropdown-hover-background-color);
451+
text-decoration: none;
452+
}
453+
454+
html[data-theme="dark"] .notification-dropdown {
455+
background-color: var(--ifm-dropdown-background-color);
456+
box-shadow: var(--ifm-global-shadow-md);
457+
}
458+
459+
/* Hide the notification icon in the main navbar on smaller screens */
460+
@media (max-width: 997px) {
461+
.notification-wrapper {
462+
display: none; /* Hide in the main navbar on mobile */
463+
position: sticky;
464+
}
465+
}
466+
467+
/* Show notification icon in the sidebar menu on mobile */
468+
@media (max-width: 997px) {
469+
.navbar-sidebar .notification-wrapper {
470+
display: flex; /* Make it visible only in the sidebar on mobile */
471+
margin-left: 5px;
472+
position: relative;
473+
}
474+
475+
.notification-count {
476+
align-items: center;
477+
background-color: red;
478+
border-radius: 50%;
479+
color: white;
480+
display: flex;
481+
font-size: 0.8rem;
482+
height: 20px;
483+
justify-content: center;
484+
position: relative;
485+
right: 12px;
486+
top: -12px;
487+
width: 20px;
488+
}
489+
490+
.notification-dropdown {
491+
background-color: var(--ifm-dropdown-background-color);
492+
box-shadow: var(--ifm-global-shadow-md);
493+
border-radius: 8px;
494+
font-size: 0.875rem;
495+
left: 0px;
496+
position: absolute;
497+
text-decoration: none;
498+
top: 38px;
499+
width: 330px;
500+
z-index: 1000;
501+
}
502+
}

0 commit comments

Comments
 (0)