notifications menu overhaul complete

This commit is contained in:
shatfield4
2023-12-20 00:35:52 -08:00
parent d0ae466f43
commit 7088fdd1bd
5 changed files with 412 additions and 198 deletions
@@ -1,195 +0,0 @@
import { ReactNode, useEffect, useState } from 'react';
import { AlertOctagon, AlertTriangle, Bell, Info } from 'react-feather';
import { useParams } from 'react-router-dom';
import Organization from '../../../models/organization';
import { databaseTimestampFromNow } from '../../../utils/data';
import ChromaLogo from '../../../images/vectordbs/chroma.png';
import PineconeLogo from '../../../images/vectordbs/pinecone.png';
import qDrantLogo from '../../../images/vectordbs/qdrant.png';
import WeaviateLogo from '../../../images/vectordbs/weaviate.png';
const POLLING_INTERVAL = 30_000;
export type INotification = {
id: number;
organization_id: number;
seen: boolean;
textContent: string;
symbol?:
| 'info'
| 'warning'
| 'error'
| 'chroma'
| 'pinecone'
| 'weaviate'
| 'qdrant';
link?: string;
target?: '_blank' | 'self';
createdAt: string;
lastUpdatedAt: string;
};
export default function Notifications() {
const { slug } = useParams();
const [loading, setLoading] = useState(true);
const [notifications, setNotifications] = useState<INotification[]>([]);
const [hasUnseen, setHasUnseen] = useState(false);
const [showNotifs, setShowNotifs] = useState(false);
async function handleClick() {
if (!showNotifs) {
!!slug && Organization.markNotificationsSeen(slug);
setShowNotifs(true);
setHasUnseen(false);
} else {
setShowNotifs(false);
}
}
async function fetchNotifications() {
if (!slug) {
setLoading(false);
return;
}
const { notifications: _notifications } = await Organization.notifications(
slug
);
setNotifications(_notifications);
setHasUnseen(_notifications.some((notif) => notif.seen === false));
setLoading(false);
}
useEffect(() => {
if (!slug) return;
fetchNotifications();
setInterval(() => {
fetchNotifications();
}, POLLING_INTERVAL);
}, [slug]);
if (loading) return null;
return (
<div className="relative">
<button
type="button"
onClick={handleClick}
className="group rounded-lg px-4 py-2 text-slate-800 hover:bg-slate-200"
>
<div className="relative">
<p
hidden={!hasUnseen}
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
/>
<Bell
size={20}
className="group-hover:fill-blue-400 group-hover:stroke-blue-500"
/>
</div>
</button>
<div
hidden={!showNotifs}
className="absolute right-0 top-10 z-99 max-h-[50vh] w-[20rem] divide-y divide-gray-100 overflow-y-scroll rounded-lg bg-white shadow"
>
<div className="block rounded-t-lg bg-blue-100/50 bg-gray-50 px-4 py-2 text-center font-medium text-blue-700">
Recent Notifications
</div>
<div className="divide-y divide-gray-100">
{notifications.length === 0 ? (
<div className="flex px-4 py-3 hover:bg-gray-100">
<div className="w-full pl-3 text-center">
<div className="mb-1.5 text-xs text-gray-500">
no notifications
</div>
<div className="text-xs text-blue-600" />
</div>
</div>
) : (
<>
{notifications.map((notification) => (
<Notification
key={notification.id}
notification={notification}
/>
))}
</>
)}
</div>
</div>
</div>
);
}
function NotificationImage({ notification }: { notification: INotification }) {
switch (notification.symbol) {
case 'info':
return <Info className="text-blue-500" size={20} />;
case 'warning':
return <AlertTriangle className="text-orange-500" size={20} />;
case 'error':
return <AlertOctagon className="text-red-600" size={20} />;
case 'chroma':
return <img className="h-10 w-10 rounded-full" src={ChromaLogo} />;
case 'pinecone':
return <img className="h-8 w-8 rounded-full" src={PineconeLogo} />;
case 'qdrant':
return <img className="h-8 w-8 rounded-full" src={qDrantLogo} />;
case 'weaviate':
return <img className="h-8 w-8 rounded-full" src={WeaviateLogo} />;
default:
return <Info className="text-blue-500" size={20} />;
}
}
function NotificationWrapper({
notification,
children,
}: {
notification: INotification;
children: ReactNode;
}) {
if (!!notification.link) {
return (
<a
key={notification.id}
href={notification?.link || '#'}
target={notification?.target || 'self'}
className={`flex px-4 py-3 hover:bg-gray-100 ${
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
}`}
>
{children}
</a>
);
}
return (
<div
key={notification.id}
className={`flex px-4 py-3 hover:bg-gray-100 ${
!notification.seen ? 'border-l-4 !border-l-blue-600' : ''
}`}
>
{children}
</div>
);
}
function Notification({ notification }: { notification: INotification }) {
return (
<NotificationWrapper notification={notification}>
<div className="flex flex-shrink-0 items-center justify-center">
<NotificationImage notification={notification} />
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-sm text-gray-500">
{notification.textContent}
</div>
<div className="text-xs text-blue-600">
{databaseTimestampFromNow(notification.createdAt)}
</div>
</div>
</NotificationWrapper>
);
}
+6 -2
View File
@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import paths from '../../utils/paths';
import { STORE_TOKEN, STORE_USER } from '../../utils/constants';
import truncate from 'truncate';
import Notifications from './Notifications';
import Notifications from '../Notifications';
export default function Header(props: {
entity?: any | null;
@@ -134,9 +134,13 @@ export default function Header(props: {
<header
className={`${
quickActions ? 'mr-[235px]' : 'mr-[104px]'
} flex h-[76px] w-full rounded-t-xl bg-main`}
} relative flex h-[76px] w-full rounded-t-xl bg-main`}
>
<div className="flex w-full justify-between p-4">{extendedItems}</div>
{/* Container for notifications */}
{/* <div className="absolute right-0 top-0 bg-white">
<Notifications />
</div> */}
</header>
);
}
@@ -0,0 +1,394 @@
import { ReactNode, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import Organization from '../../models/organization';
import { databaseTimestampFromNow } from '../../utils/data';
import ChromaLogo from '../../images/vectordbs/chroma.png';
import PineconeLogo from '../../images/vectordbs/pinecone-inverted.png';
import qDrantLogo from '../../images/vectordbs/qdrant.png';
import WeaviateLogo from '../../images/vectordbs/weaviate.png';
import { Bell, Info, Warning, WarningOctagon } from '@phosphor-icons/react';
const POLLING_INTERVAL = 30_000;
export type INotification = {
id: number;
organization_id: number;
seen: boolean;
textContent: string;
symbol?:
| 'info'
| 'warning'
| 'error'
| 'chroma'
| 'pinecone'
| 'weaviate'
| 'qdrant';
link?: string;
target?: '_blank' | 'self';
createdAt: string;
lastUpdatedAt: string;
};
export default function Notifications() {
const { slug } = useParams();
const [loading, setLoading] = useState(true);
const [notifications, setNotifications] = useState<INotification[]>([]);
const [hasUnseen, setHasUnseen] = useState(false);
const [showNotifs, setShowNotifs] = useState(false);
const notificationRef = useRef(null);
const bellButtonRef = useRef(null);
async function handleClick() {
if (!showNotifs) {
!!slug && Organization.markNotificationsSeen(slug);
setShowNotifs(true);
setHasUnseen(false);
} else {
setShowNotifs(false);
}
}
// Close notifications when clicking outside
useEffect(() => {
function handleClickOutside(event: any) {
if (
bellButtonRef.current &&
bellButtonRef.current.contains(event.target)
) {
// Click is inside bell button, do nothing
return;
}
if (
notificationRef.current &&
!notificationRef.current.contains(event.target)
) {
// Click is outside notification menu, close it
setShowNotifs(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
async function fetchNotifications() {
if (!slug) {
setLoading(false);
return;
}
const { notifications: _notifications } = await Organization.notifications(
slug
);
setNotifications(_notifications);
setHasUnseen(_notifications.some((notif) => notif.seen === false));
setLoading(false);
}
useEffect(() => {
if (!slug) return;
fetchNotifications();
setInterval(() => {
fetchNotifications();
}, POLLING_INTERVAL);
}, [slug]);
if (loading) return null;
return (
<div className="relative">
<button
ref={bellButtonRef}
type="button"
onClick={handleClick}
className={`group rounded-lg p-2 hover:bg-main-2 ${
showNotifs && 'bg-main-2'
}`}
>
<div className="relative">
<p
hidden={!hasUnseen}
className="absolute -top-[4px] right-0 h-[12px] w-[12px] rounded-full bg-red-600"
/>
<div className="text-sky-400">
<Bell size={24} weight={showNotifs ? 'fill' : 'bold'} />
</div>
</div>
</button>
<div
hidden={!showNotifs}
ref={notificationRef}
className="absolute right-0 top-12 z-1 max-h-[50vh] w-[20rem] overflow-y-auto rounded-lg border border-neutral-600 bg-main shadow-2xl"
>
<div className="sticky left-0 top-0 z-10 block rounded-t-lg border-b border-neutral-600 bg-main px-5 py-2 text-lg text-white shadow-lg">
Notifications
</div>
<div className="divide-y divide-gray-100">
{notifications.length === 0 ? (
<div className="flex px-4 py-3 hover:bg-main-2">
<div className="w-full pl-3 text-center">
<div className="mb-1.5 text-xs text-white">
No notifications
</div>
</div>
</div>
) : (
<div>
{notifications.map((notification) => (
<Notification
key={notification.id}
notification={notification}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}
function NotificationImage({ notification }: { notification: INotification }) {
switch (notification.symbol) {
case 'info':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
<Info size={24} weight="bold" />
</div>
);
case 'warning':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-yellow-300 bg-opacity-20 p-2 text-yellow-300">
<WarningOctagon size={24} weight="bold" />
</div>
);
case 'error':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-red-700/20 p-2 text-red-600">
<Warning size={24} weight="bold" />
</div>
);
case 'chroma':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img alt="Chroma Logo" className="rounded-full" src={ChromaLogo} />
</div>
);
case 'pinecone':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img
alt="Pinecone Logo"
className="rounded-full"
src={PineconeLogo}
/>
</div>
);
case 'qdrant':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img alt="qDrant Logo" className="rounded-full" src={qDrantLogo} />
</div>
);
case 'weaviate':
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-0">
<img
alt="Weaviate Logo"
className="rounded-full"
src={WeaviateLogo}
/>
</div>
);
default:
return (
<div className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-white/10 p-2 text-white">
<Info size={24} weight="bold" />
</div>
);
}
}
function NotificationWrapper({
notification,
children,
}: {
notification: INotification;
children: ReactNode;
}) {
if (!!notification.link) {
return (
<a
key={notification.id}
href={notification?.link || '#'}
target={notification?.target || 'self'}
className={`flex px-4 py-3 transition-all duration-300 hover:bg-main-2 ${
!notification.seen
? 'border-l-2 !border-l-sky-400 bg-sky-400/10'
: 'border-l-2 !border-l-transparent'
}`}
>
{children}
</a>
);
}
return (
<div
key={notification.id}
className={`flex px-4 py-3 hover:bg-main-2 ${
!notification.seen
? 'border-l-2 !border-l-sky-400'
: 'border-l-2 !border-l-transparent'
}`}
>
{children}
</div>
);
}
function Notification({ notification }: { notification: INotification }) {
return (
<NotificationWrapper notification={notification}>
<div className="flex flex-shrink-0 items-center justify-center">
<NotificationImage notification={notification} />
</div>
<div className="w-full pl-3">
<div className="mb-1 text-sm font-medium leading-tight text-white">
{notification.textContent}
</div>
<div className="text-sm text-white text-opacity-60">
{databaseTimestampFromNow(notification.createdAt)}
</div>
</div>
</NotificationWrapper>
);
}
// <Notification
// key={'pinecone'}
// notification={{
// id: 1,
// organization_id: 1,
// seen: false,
// textContent: 'Pinecone is now available!',
// symbol: 'pinecone',
// link: 'https://pinecone.io',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'chroma'}
// notification={{
// id: 2,
// organization_id: 1,
// seen: false,
// textContent: 'Chroma is now available!',
// symbol: 'chroma',
// link: 'https://chroma.ml',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'qdrant'}
// notification={{
// id: 3,
// organization_id: 1,
// seen: false,
// textContent: 'qDrant is now available!',
// symbol: 'qdrant',
// link: 'https://qdrant.tech',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'weaviate'}
// notification={{
// id: 4,
// organization_id: 1,
// seen: false,
// textContent: 'Weaviate is now available!',
// symbol: 'weaviate',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'error'}
// notification={{
// id: 5,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'error',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'warning'}
// notification={{
// id: 6,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'warning',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: false,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />{' '}
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: true,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />{' '}
// <Notification
// key={'info'}
// notification={{
// id: 7,
// organization_id: 1,
// seen: false,
// textContent: 'Something went wrong!',
// symbol: 'info',
// link: 'https://weaviate.com',
// target: '_blank',
// createdAt: '2021-10-12T12:00:00Z',
// lastUpdatedAt: '2021-10-12T12:00:00Z',
// }}
// />
+11
View File
@@ -1,6 +1,8 @@
import { ReactNode, useState } from 'react';
import Header from '../components/Header';
import Sidebar from '../components/Sidebar';
import Notifications from '../components/Notifications';
import useUser from '../hooks/useUser';
interface DefaultLayoutProps {
headerEntity: any;
@@ -30,6 +32,7 @@ const AppLayout = ({
hasQuickActions = false,
}: DefaultLayoutProps) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { user } = useUser();
return (
<div className="bg-main-bg px-4 pt-4">
@@ -58,6 +61,14 @@ const AppLayout = ({
/>
</div>
)}
<div className="absolute right-0 top-0 mr-9 mt-7 flex items-center gap-x-2">
<Notifications />
{/* first 2 chars of user.email */}
<div className="flex h-[29px] w-[29px] items-center justify-center rounded-full bg-sky-400 bg-opacity-20 text-sm font-medium text-sky-400">
{user?.email?.slice(0, 2).toUpperCase()}
</div>
</div>
<main>
<div className="mx-auto rounded-tr-xl bg-main pr-6 pt-6">
{children}
+1 -1
View File
@@ -1,4 +1,4 @@
import { INotification } from '../components/Header/Notifications';
import { INotification } from '../components/Notifications';
import { API_BASE } from '../utils/constants';
import { baseHeaders, getAPIUrlString } from '../utils/request';