mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-07-19 21:23:38 -04:00
notifications menu overhaul complete
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
// }}
|
||||
// />
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user