mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-02 08:40:53 +03:00
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:
parent
f496e013d3
commit
6056450bd6
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
|
@ -1 +1,3 @@
|
||||
$searchBarHeight: 62px;
|
||||
$pageHeaderHeight: 48px;
|
||||
$menuWidth: 230px;
|
||||
|
Loading…
Reference in New Issue
Block a user