feat(frontend): UI / UX Notifications (#2826)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):
This commit is contained in:
Antoine Dewez 2024-07-10 17:13:47 +02:00 committed by GitHub
parent f496e013d3
commit 6056450bd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 500 additions and 334 deletions

View File

@ -17,6 +17,7 @@ import {
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { ChatsProvider } from "@/lib/context/ChatsProvider";
import { MenuProvider } from "@/lib/context/MenuProvider/Menu-provider";
import { NotificationsProvider } from "@/lib/context/NotificationsProvider/notifications-provider";
import { OnboardingProvider } from "@/lib/context/OnboardingProvider/Onboarding-provider";
import { SearchModalProvider } from "@/lib/context/SearchModalProvider/search-modal-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider";
@ -90,17 +91,19 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
<BrainProvider>
<KnowledgeToFeedProvider>
<BrainCreationProvider>
<MenuProvider>
<OnboardingProvider>
<FromConnectionsProvider>
<ChatsProvider>
<ChatProvider>
<App>{children}</App>
</ChatProvider>
</ChatsProvider>
</FromConnectionsProvider>
</OnboardingProvider>
</MenuProvider>
<NotificationsProvider>
<MenuProvider>
<OnboardingProvider>
<FromConnectionsProvider>
<ChatsProvider>
<ChatProvider>
<App>{children}</App>
</ChatProvider>
</ChatsProvider>
</FromConnectionsProvider>
</OnboardingProvider>
</MenuProvider>
</NotificationsProvider>
</BrainCreationProvider>
</KnowledgeToFeedProvider>
</BrainProvider>

View File

@ -1,9 +1,18 @@
@use "styles/BoxShadow.module.scss";
@use "styles/ScreenSizes.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Variables.module.scss";
@use "styles/ZIndexes.module.scss";
.menu_container {
background-color: var(--background-1);
border-right: 1px solid var(--border-1);
width: Variables.$menuWidth;
transition: width 0.2s ease-in-out;
&.hidden {
width: 0;
}
.menu_wrapper {
padding-top: Spacings.$spacing05;
@ -52,4 +61,26 @@
&.shifted {
margin-left: 180px;
}
}
}
.notifications_panel {
width: 400px;
position: absolute;
top: Variables.$pageHeaderHeight;
min-height: calc(100% - Variables.$pageHeaderHeight);
max-height: calc(100% - Variables.$pageHeaderHeight);
overflow: scroll;
left: calc(Variables.$menuWidth + 1px);
z-index: ZIndexes.$overlay;
border-right: 1px solid var(--border-1);
box-shadow: BoxShadow.$small;
background-color: var(--background-0);
@media (max-width: ScreenSizes.$small) {
width: 100%;
left: 0;
min-height: 100vh;
max-height: 100vh;
top: 0;
}
}

View File

@ -7,12 +7,15 @@ import { useChatsList } from "@/app/chat/[chatId]/hooks/useChatsList";
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { nonProtectedPaths } from "@/lib/config/routesConfig";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import { useNotificationsContext } from "@/lib/context/NotificationsProvider/hooks/useNotificationsContext";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import styles from "./Menu.module.scss";
import { AnimatedDiv } from "./components/AnimationDiv";
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
import { HomeButton } from "./components/HomeButton/HomeButton";
import { Notifications } from "./components/Notifications/Notifications";
import { NotificationsButton } from "./components/NotificationsButton/NotificationsButton";
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
import { SocialsButtons } from "./components/SocialsButtons/SocialsButtons";
import { StudioButton } from "./components/StudioButton/StudioButton";
@ -21,6 +24,7 @@ import { UpgradeToPlusButton } from "./components/UpgradeToPlusButton/UpgradeToP
export const Menu = (): JSX.Element => {
const { isOpened } = useMenuContext();
const { isVisible } = useNotificationsContext();
const router = useRouter();
const pathname = usePathname() ?? "";
const [isLogoHovered, setIsLogoHovered] = useState<boolean>(false);
@ -50,50 +54,62 @@ export const Menu = (): JSX.Element => {
}
return (
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.1 }}>
<div className={styles.menu_container}>
<AnimatedDiv>
<div className={styles.menu_wrapper}>
<div
className={styles.quivr_logo_wrapper}
onClick={() => router.push("/search")}
onMouseEnter={() => setIsLogoHovered(true)}
onMouseLeave={() => setIsLogoHovered(false)}
>
<QuivrLogo
size={50}
color={
isLogoHovered ? "primary" : isDarkMode ? "white" : "black"
}
/>
</div>
<div>
<MotionConfig transition={{ mass: 1, damping: 10, duration: 0.1 }}>
<div
className={`${styles.menu_container} ${
!isOpened ? styles.hidden : ""
}`}
>
<AnimatedDiv>
<div className={styles.menu_wrapper}>
<div
className={styles.quivr_logo_wrapper}
onClick={() => router.push("/search")}
onMouseEnter={() => setIsLogoHovered(true)}
onMouseLeave={() => setIsLogoHovered(false)}
>
<QuivrLogo
size={50}
color={
isLogoHovered ? "primary" : isDarkMode ? "white" : "black"
}
/>
</div>
<div className={styles.buttons_wrapper}>
<div className={styles.block}>
<DiscussionButton />
<HomeButton />
<StudioButton />
<ThreadsButton />
<div className={styles.buttons_wrapper}>
<div className={styles.block}>
<DiscussionButton />
<HomeButton />
<StudioButton />
<NotificationsButton />
<ThreadsButton />
</div>
<div className={styles.block}>
<UpgradeToPlusButton />
<ProfileButton />
</div>
</div>
<div className={styles.block}>
<UpgradeToPlusButton />
<ProfileButton />
<div className={styles.social_buttons_wrapper}>
<SocialsButtons />
</div>
</div>
<div className={styles.social_buttons_wrapper}>
<SocialsButtons />
</div>
</div>
</AnimatedDiv>
</div>
<div
className={`
</AnimatedDiv>
</div>
<div
className={`
${styles.menu_control_button_wrapper}
${isOpened ? styles.shifted : ""}
`}
>
<MenuControlButton />
</div>
</MotionConfig>
>
<MenuControlButton />
</div>
</MotionConfig>
{isVisible && (
<div className={styles.notifications_panel}>
<Notifications />
</div>
)}
</div>
);
};

View File

@ -3,7 +3,8 @@
@use "styles/Typography.module.scss";
.notification_wrapper {
padding: Spacings.$spacing03;
padding-block: Spacings.$spacing04;
padding-inline: Spacings.$spacing06;
display: flex;
flex-direction: column;
gap: Spacings.$spacing02;
@ -18,25 +19,27 @@
display: flex;
justify-content: space-between;
align-items: center;
font-size: Typography.$small;
overflow: hidden;
gap: Spacings.$spacing06;
.left {
display: flex;
align-items: center;
gap: Spacings.$spacing02;
gap: Spacings.$spacing03;
overflow: hidden;
.badge {
min-width: 6px;
max-width: 6px;
min-height: 6px;
max-height: 6px;
border-radius: Radius.$circle;
background-color: var(--primary-0);
}
.title {
@include Typography.EllipsisOverflow;
font-size: Typography.$tiny;
}
}
@ -69,7 +72,7 @@
}
.date {
font-size: Typography.$tiny;
font-size: Typography.$very-tiny;
color: var(--text-2);
}
}

View File

@ -5,7 +5,7 @@ import { useSupabase } from "@/lib/context/SupabaseProvider";
import styles from "./Notification.module.scss";
import { NotificationType } from "../types/types";
import { NotificationType } from "../../../types/types";
interface NotificationProps {
notification: NotificationType;
@ -50,14 +50,14 @@ export const Notification = ({
name={notification.read ? "unread" : "read"}
color="black"
handleHover={true}
size="normal"
size="small"
onClick={() => readNotif()}
/>
<Icon
name="delete"
color="black"
handleHover={true}
size="normal"
size="small"
onClick={() => deleteNotif()}
/>
</div>

View File

@ -0,0 +1,43 @@
@use "styles/BoxShadow.module.scss";
@use "styles/Radius.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Typography.module.scss";
@use "styles/ZIndexes.module.scss";
.notifications_wrapper {
position: relative;
z-index: ZIndexes.$overlay;
.notifications_panel_header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: Typography.$small;
padding-inline: Spacings.$spacing05;
padding-top: Spacings.$spacing03;
padding-bottom: Spacings.$spacing05;
.left {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
.title {
color: var(--text-2);
font-size: Typography.$small;
font-weight: 500;
}
}
.buttons {
display: flex;
gap: Spacings.$spacing02;
}
}
.no_notifications {
padding: Spacings.$spacing05;
font-size: Typography.$tiny;
color: var(--text-2);
}
}

View File

@ -0,0 +1,109 @@
import { useEffect } from "react";
import Icon from "@/lib/components/ui/Icon/Icon";
import TextButton from "@/lib/components/ui/TextButton/TextButton";
import { useNotificationsContext } from "@/lib/context/NotificationsProvider/hooks/useNotificationsContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useDevice } from "@/lib/hooks/useDevice";
import { Notification } from "./Notification/Notification";
import styles from "./Notifications.module.scss";
export const Notifications = (): JSX.Element => {
const {
notifications,
updateNotifications,
unreadNotifications,
setIsVisible,
} = useNotificationsContext();
const { supabase } = useSupabase();
const { isMobile } = useDevice();
const deleteAllNotifications = async () => {
for (const notification of notifications) {
await supabase.from("notifications").delete().eq("id", notification.id);
}
await updateNotifications();
};
const markAllAsRead = async () => {
for (const notification of notifications) {
await supabase
.from("notifications")
.update({ read: true })
.eq("id", notification.id);
}
await updateNotifications();
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const panel = document.getElementById("notifications-panel");
const button = document.getElementById("notifications-button");
if (!panel?.contains(target) && !button?.contains(target)) {
setIsVisible(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div id="notifications-panel" className={styles.notifications_wrapper}>
<div className={styles.notifications_panel}>
<div className={styles.notifications_panel_header}>
<div className={styles.left}>
{isMobile && (
<Icon
name="hide"
size="small"
handleHover={true}
color="black"
onClick={() => setIsVisible(false)}
/>
)}
<span className={styles.title}>Notifications</span>
</div>
<div className={styles.buttons}>
<TextButton
label="Mark all as read"
color="black"
onClick={() => void markAllAsRead()}
disabled={unreadNotifications === 0}
small={true}
/>
<span>|</span>
<TextButton
label="Delete all"
color="black"
onClick={() => void deleteAllNotifications()}
disabled={notifications.length === 0}
small={true}
/>
</div>
</div>
{notifications.length === 0 && (
<div className={styles.no_notifications}>
You have no notifications
</div>
)}
{notifications.map((notification, i) => (
<Notification
key={i}
notification={notification}
lastNotification={i === notifications.length - 1}
updateNotifications={updateNotifications}
/>
))}
</div>
</div>
);
};
export default Notifications;

View File

@ -0,0 +1,31 @@
@use "styles/Radius.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Typography.module.scss";
.button_wrapper {
display: flex;
gap: Spacings.$spacing02;
align-items: center;
border-radius: Radius.$normal;
justify-content: space-between;
padding-right: Spacings.$spacing03;
cursor: pointer;
.badge {
color: var(--white-0);
background-color: var(--dangerous-dark);
border-radius: Radius.$normal;
width: 18px;
height: 18px;
display: flex;
justify-content: center;
align-items: center;
font-size: Typography.$very_tiny;
top: -(Spacings.$spacing03);
right: -(Spacings.$spacing02);
}
&:hover {
background-color: var(--background-3);
}
}

View File

@ -0,0 +1,58 @@
import { useEffect } from "react";
import { MenuButton } from "@/lib/components/Menu/components/MenuButton/MenuButton";
import { useNotificationsContext } from "@/lib/context/NotificationsProvider/hooks/useNotificationsContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import styles from "./NotificationsButton.module.scss";
export const NotificationsButton = (): JSX.Element => {
const { isVisible, setIsVisible, unreadNotifications, updateNotifications } =
useNotificationsContext();
const { supabase } = useSupabase();
useEffect(() => {
const channel = supabase
.channel("notifications")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "notifications" },
() => {
void updateNotifications();
}
)
.subscribe();
return () => {
void supabase.removeChannel(channel);
};
}, []);
useEffect(() => {
void updateNotifications();
}, []);
return (
<div
className={styles.button_wrapper}
onClick={(event) => {
setIsVisible(!isVisible);
event.preventDefault();
event.nativeEvent.stopImmediatePropagation();
}}
id="notifications-button"
>
<MenuButton
label="Notifications"
iconName="notifications"
type="open"
color="primary"
/>
{!!unreadNotifications && (
<span className={styles.badge}>
{unreadNotifications > 9 ? "9+" : unreadNotifications}
</span>
)}
</div>
);
};

View File

@ -3,5 +3,12 @@
.socials_buttons_wrapper {
display: flex;
gap: Spacings.$spacing05;
justify-content: center;
}
justify-content: space-between;
padding-inline: Spacings.$spacing06;
.left {
display: flex;
gap: Spacings.$spacing05;
justify-content: center;
}
}

View File

@ -1,41 +1,66 @@
import { useEffect, useState } from "react";
import { Icon } from "@/lib/components/ui/Icon/Icon";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import styles from "./SocialsButtons.module.scss";
export const SocialsButtons = (): JSX.Element => {
const { isDarkMode, setIsDarkMode } = useUserSettingsContext();
const [lightModeIconName, setLightModeIconName] = useState("sun");
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
useEffect(() => {
setLightModeIconName(isDarkMode ? "sun" : "moon");
}, [isDarkMode]);
const handleClick = (url: string) => {
window.open(url, "_blank");
};
return (
<div className={styles.socials_buttons_wrapper}>
<div className={styles.left}>
<Icon
name="github"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://github.com/QuivrHQ/quivr")}
/>
<Icon
name="linkedin"
color="black"
size="small"
handleHover={true}
onClick={() =>
handleClick("https://www.linkedin.com/company/getquivr")
}
/>
<Icon
name="twitter"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://twitter.com/quivr_brain")}
/>
<Icon
name="discord"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://discord.gg/HUpRgp2HG8")}
/>
</div>
<Icon
name="github"
name={lightModeIconName}
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://github.com/QuivrHQ/quivr")}
/>
<Icon
name="linkedin"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://www.linkedin.com/company/getquivr")}
/>
<Icon
name="twitter"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://twitter.com/quivr_brain")}
/>
<Icon
name="discord"
color="black"
size="small"
handleHover={true}
onClick={() => handleClick("https://discord.gg/HUpRgp2HG8")}
size="normal"
onClick={toggleTheme}
/>
</div>
);

View File

@ -1,62 +0,0 @@
@use "styles/BoxShadow.module.scss";
@use "styles/Radius.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Typography.module.scss";
@use "styles/ZIndexes.module.scss";
.notifications_wrapper {
position: relative;
z-index: ZIndexes.$overlay;
.badge {
position: absolute;
color: var(--white-0);
background-color: var(--dangerous-dark);
border-radius: Radius.$normal;
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
font-size: Typography.$very_tiny;
top: -(Spacings.$spacing03);
right: -(Spacings.$spacing02);
}
.notifications_panel {
position: absolute;
background-color: var(--background-0);
width: 400px;
max-height: 60vh;
overflow: scroll;
right: 0;
top: Spacings.$spacing07;
border-radius: Radius.$normal;
border: 1px solid var(--border-2);
box-shadow: BoxShadow.$large;
.notifications_panel_header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-2);
font-size: Typography.$small;
padding: Spacings.$spacing03;
.title {
color: var(--text-2);
}
.buttons {
display: flex;
gap: Spacings.$spacing02;
}
}
.no_notifications {
padding: Spacings.$spacing05;
font-size: Typography.$tiny;
color: var(--text-2);
}
}
}

View File

@ -1,152 +0,0 @@
import { useEffect, useState } from "react";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { Notification } from "./Notification/Notification";
import styles from "./Notifications.module.scss";
import { NotificationType } from "./types/types";
import { Icon } from "../../ui/Icon/Icon";
import { TextButton } from "../../ui/TextButton/TextButton";
export const Notifications = (): JSX.Element => {
const [notifications, setNotifications] = useState<NotificationType[]>([]);
const [unreadNotifications, setUnreadNotifications] = useState<number>(0);
const [panelOpened, setPanelOpened] = useState<boolean>(false);
const { supabase } = useSupabase();
const updateNotifications = async () => {
try {
let notifs = (await supabase.from("notifications").select()).data;
if (notifs) {
notifs = notifs.sort(
(a: NotificationType, b: NotificationType) =>
new Date(b.datetime).getTime() - new Date(a.datetime).getTime()
);
}
setNotifications(notifs ?? []);
setUnreadNotifications(
notifs?.filter((n: NotificationType) => !n.read).length ?? 0
);
} catch (error) {
console.error(error);
}
};
const deleteAllNotifications = async () => {
for (const notification of notifications) {
await supabase.from("notifications").delete().eq("id", notification.id);
}
await updateNotifications();
};
const markAllAsRead = async () => {
for (const notification of notifications) {
await supabase
.from("notifications")
.update({ read: true })
.eq("id", notification.id);
}
await updateNotifications();
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const panel = document.getElementById("notifications-panel");
if (!panel?.contains(target)) {
setPanelOpened(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
useEffect(() => {
const channel = supabase
.channel("notifications")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "notifications" },
() => {
void updateNotifications();
}
)
.subscribe();
return () => {
void supabase.removeChannel(channel);
};
}, []);
useEffect(() => {
void (async () => {
await updateNotifications();
})();
}, []);
return (
<div id="notifications-panel" className={styles.notifications_wrapper}>
<div
onClick={(event) => {
setPanelOpened(!panelOpened);
event.nativeEvent.stopImmediatePropagation();
}}
>
<Icon
name="notifications"
size="large"
color="black"
handleHover={true}
/>
{!!unreadNotifications && (
<span className={styles.badge}>
{unreadNotifications > 9 ? "9+" : unreadNotifications}
</span>
)}
</div>
{panelOpened && (
<div className={styles.notifications_panel}>
<div className={styles.notifications_panel_header}>
<span className={styles.title}>Notifications</span>
<div className={styles.buttons}>
<TextButton
label="Mark all as read"
color="black"
onClick={() => void markAllAsRead()}
disabled={unreadNotifications === 0}
/>
<span>|</span>
<TextButton
label="Delete all"
color="black"
onClick={() => void deleteAllNotifications()}
disabled={notifications.length === 0}
/>
</div>
</div>
{notifications.length === 0 && (
<div className={styles.no_notifications}>
You have no notifications
</div>
)}
{notifications.map((notification, i) => (
<Notification
key={i}
notification={notification}
lastNotification={i === notifications.length - 1}
updateNotifications={updateNotifications}
/>
))}
</div>
)}
</div>
);
};
export default Notifications;

View File

@ -1,6 +1,7 @@
@use "styles/ScreenSizes.module.scss";
@use "styles/Spacings.module.scss";
@use "styles/Typography.module.scss";
@use "styles/Variables.module.scss";
.page_header_wrapper {
display: flex;
@ -9,7 +10,7 @@
padding: Spacings.$spacing04;
padding-left: Spacings.$spacing09;
border-bottom: 1px solid var(--border-1);
height: 3rem;
height: Variables.$pageHeaderHeight;
width: 100%;
.left {

View File

@ -1,12 +1,6 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { useMenuContext } from "@/lib/context/MenuProvider/hooks/useMenuContext";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import { useDevice } from "@/lib/hooks/useDevice";
import { ButtonType } from "@/lib/types/QuivrButton";
import { Notifications } from "./Notifications/Notifications";
import styles from "./PageHeader.module.scss";
import { Icon } from "../ui/Icon/Icon";
@ -24,17 +18,6 @@ export const PageHeader = ({
buttons,
}: Props): JSX.Element => {
const { isOpened } = useMenuContext();
const { isDarkMode, setIsDarkMode } = useUserSettingsContext();
const [lightModeIconName, setLightModeIconName] = useState("sun");
const { isMobile } = useDevice();
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
useEffect(() => {
setLightModeIconName(isDarkMode ? "sun" : "moon");
}, [isDarkMode]);
return (
<div className={styles.page_header_wrapper}>
@ -53,22 +36,6 @@ export const PageHeader = ({
hidden={button.hidden}
/>
))}
{!isMobile && <Notifications />}
<Link href="/user">
<Icon
name="settings"
color="black"
handleHover={true}
size="normal"
/>
</Link>
<Icon
name={lightModeIconName}
color="black"
handleHover={true}
size="normal"
onClick={toggleTheme}
/>
</div>
</div>
);

View File

@ -0,0 +1,15 @@
import { useContext } from "react";
import { NotificationsContext } from "../notifications-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useNotificationsContext = () => {
const context = useContext(NotificationsContext);
if (context === undefined) {
throw new Error(
"useNotificationsContext must be used within a MenuProvider"
);
}
return context;
};

View File

@ -0,0 +1,64 @@
import { createContext, useState } from "react";
import { NotificationType } from "@/lib/components/Menu/types/types";
import { useSupabase } from "../SupabaseProvider";
type NotificationsContextType = {
isVisible: boolean;
setIsVisible: React.Dispatch<React.SetStateAction<boolean>>;
notifications: NotificationType[];
setNotifications: React.Dispatch<React.SetStateAction<NotificationType[]>>;
unreadNotifications: number;
setUnreadNotifications: React.Dispatch<React.SetStateAction<number>>;
updateNotifications: () => Promise<void>;
};
export const NotificationsContext = createContext<
NotificationsContextType | undefined
>(undefined);
export const NotificationsProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const [isVisible, setIsVisible] = useState(false);
const [notifications, setNotifications] = useState<NotificationType[]>([]);
const [unreadNotifications, setUnreadNotifications] = useState<number>(0);
const { supabase } = useSupabase();
const updateNotifications = async () => {
try {
let notifs = (await supabase.from("notifications").select()).data;
if (notifs) {
notifs = notifs.sort(
(a: NotificationType, b: NotificationType) =>
new Date(b.datetime).getTime() - new Date(a.datetime).getTime()
);
}
setNotifications(notifs ?? []);
setUnreadNotifications(
notifs?.filter((n: NotificationType) => !n.read).length ?? 0
);
} catch (error) {
console.error(error);
}
};
return (
<NotificationsContext.Provider
value={{
isVisible,
setIsVisible,
notifications,
setNotifications,
unreadNotifications,
setUnreadNotifications,
updateNotifications,
}}
>
{children}
</NotificationsContext.Provider>
);
};

View File

@ -47,7 +47,6 @@ import { HiBuildingOffice } from "react-icons/hi2";
import {
IoIosAdd,
IoIosHelpCircleOutline,
IoIosNotifications,
IoIosRadio,
IoMdClose,
IoMdLogOut,
@ -66,6 +65,7 @@ import {
import { LiaFileVideo, LiaRobotSolid } from "react-icons/lia";
import { IconType } from "react-icons/lib";
import {
LuArrowLeftFromLine,
LuBrain,
LuBrainCircuit,
LuChevronDown,
@ -95,7 +95,11 @@ import {
MdUploadFile,
} from "react-icons/md";
import { PiOfficeChairFill } from "react-icons/pi";
import { RiDeleteBackLine, RiHashtag } from "react-icons/ri";
import {
RiDeleteBackLine,
RiHashtag,
RiNotification2Line,
} from "react-icons/ri";
import { SlOptions } from "react-icons/sl";
import { TbNetwork, TbRobot } from "react-icons/tb";
import { VscGraph } from "react-icons/vsc";
@ -141,6 +145,7 @@ export const iconList: { [name: string]: IconType } = {
graph: VscGraph,
hashtag: RiHashtag,
help: IoIosHelpCircleOutline,
hide: LuArrowLeftFromLine,
history: MdHistory,
home: IoHomeOutline,
html: BsFiletypeHtml,
@ -159,7 +164,7 @@ export const iconList: { [name: string]: IconType } = {
mp4: BsFiletypeMp4,
mpga: FaRegFileAudio,
mpeg: LiaFileVideo,
notifications: IoIosNotifications,
notifications: RiNotification2Line,
office: HiBuildingOffice,
odt: BsFiletypeDocx,
options: SlOptions,

View File

@ -1 +1,3 @@
$searchBarHeight: 62px;
$pageHeaderHeight: 48px;
$menuWidth: 230px;