notification system

This commit is contained in:
MathieuG-P
2022-07-03 22:57:54 +02:00
parent 79ef95f33c
commit b6627d322b
8 changed files with 156 additions and 3 deletions
+2 -1
View File
@@ -8,7 +8,7 @@ import { SettingsPage } from "./pages/settings-page.component";
import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.component";
import { useEffect } from "react";
import { ThemeService } from "./services/theme.service";
import { ThemeConfig } from "./config/default-configuration.config";
import { NotificationOverlay } from "./components/notification/notification-overlay.component";
export default function App() {
@@ -26,6 +26,7 @@ export default function App() {
<div className="relative w-screen h-screen overflow-hidden flex bg-light-main-color-1 dark:bg-main-color-1 z-0 max-w-full">
<Modal/>
<NavBar/>
<NotificationOverlay/>
<div className="relative flex flex-col grow max-w-full min-w-0">
<TitleBar/>
<div className="bg-light-main-color-2 dark:bg-main-color-2 relative rounded-tl-lg grow overflow-hidden max-w-full">
@@ -0,0 +1,46 @@
import { NotificationResult, NotificationType, Notification } from "renderer/services/notification.service"
import { motion } from "framer-motion"
import { BsmImage } from "../shared/bsm-image.component";
import BeatRunningImg from "../../../../assets/images/apngs/beat-running.png";
import BeatConflictImg from "../../../../assets/images/apngs/beat-conflict.png";
import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png";
import BeatImpatientImg from "../../../../assets/images/apngs/beat-impatient.png";
import { BsmButton } from "../shared/bsm-button.component";
export function NotificationItem({resolver, index, notification}: {resolver?: (value: NotificationResult|string) => void, index: number, notification: Notification}) {
const renderImage = () => {
if(notification.type === NotificationType.SUCCESS){ return <BsmImage className="h-14" image={BeatRunningImg}/>; }
if(notification.type === NotificationType.WARNING){ return <BsmImage className="h-14" image={BeatWaitingImg}/>; }
if(notification.type === NotificationType.ERROR){ return <BsmImage className="h-14" image={BeatConflictImg}/>; }
return <BsmImage className="h-14" image={BeatImpatientImg}/>
}
const renderNeonColors = () => {
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
}
return (
<motion.div layout initial={{x: "110%"}} animate={{x: "0%"}} exit={{x:"110%"}} className="relative w-60 rounded-md overflow-hidden -left-64 mb-4 shadow-md shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2 text-gray-800 dark:text-white">
<div className="w-60 flex flex-col">
<div className="flex items-center justify-start pl-1">
{renderImage()}
<div className="flex flex-col py-2">
<h1 className="font-bold leading-5 tracking-wide pr-5">{notification.title}</h1>
{notification.desc && <p className="text-sm leading-4">{notification.desc}</p>}
</div>
</div>
{notification.actions && (
<div className="flex pb-1 w-full flex-wrap px-[2px]">
{notification.actions.map(a => <span className={`px-1 flex-1 grow rounded-md text-center bg-blue-500 hover:brightness-110 transition-all text-gray-200 whitespace-nowrap m-[3px] ${a.cancel && "!bg-gray-500"}`}>{a.title}</span>)}
</div>
)}
</div>
<span className={`absolute w-full h-1 shadow-center left-0 top-0 ${renderNeonColors()}`}/>
<BsmButton icon="cross" className="absolute top-2 right-2" withBar={false}/>
</motion.div>
)
}
@@ -0,0 +1,20 @@
import { useObservable } from "renderer/hooks/use-observable.hook"
import { NotificationService } from "renderer/services/notification.service"
import { NotificationItem } from "./notification-item.component";
import { AnimatePresence, AnimateSharedLayout, motion } from "framer-motion";
export function NotificationOverlay() {
const notificationService = NotificationService.getInstance();
const notifications = useObservable(notificationService.notifications$);
return (
<motion.ul className="absolute h-full w-0 top-0 right-0 z-40 bg-red-500 pt-10">
<AnimatePresence>
{notifications?.map((n, index) => <NotificationItem key={n.id} index={index} resolver={n.resolver} notification={n.notification}/>)}
</AnimatePresence>
</motion.ul>
)
}
@@ -7,8 +7,9 @@ import { TerminalIcon } from "./terminal-icon.component";
import { DesktopIcon } from "./desktop-icon.component";
import { OculusIcon } from "./oculus-icon.component";
import { AddIcon } from "./add-icon.component";
import CrossIcon from "./cross-icon.component";
export type BsmIconType = "settings"|"trash"|"favorite"|"folder"|"bsNote"|"terminal"|"desktop"|"oculus"|"add";
export type BsmIconType = "settings"|"trash"|"favorite"|"folder"|"bsNote"|"terminal"|"desktop"|"oculus"|"add"|"cross";
export function BsmIcon({className, icon}: {className?: string, icon: BsmIconType}) {
@@ -22,6 +23,7 @@ export function BsmIcon({className, icon}: {className?: string, icon: BsmIconTyp
if(icon === "desktop"){ return <DesktopIcon className={className}/> }
if(icon === "oculus"){ return <OculusIcon className={className}/> }
if(icon === "add"){ return <AddIcon className={className}/> }
if(icon === "cross"){ return <CrossIcon className={className}/> }
}
return (
@@ -0,0 +1,7 @@
export default function CrossIcon(props: {className: string}){
return (
<svg className={props.className} aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"/>
</svg>
)
}
@@ -0,0 +1,70 @@
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from 'uuid';
export class NotificationService{
private static instance: NotificationService;
public readonly notifications$: BehaviorSubject<ResolvableNotification[]>;
public static getInstance(): NotificationService{
if(!NotificationService.instance){ NotificationService.instance = new NotificationService(); }
return NotificationService.instance;
}
private constructor(){
this.notifications$ = new BehaviorSubject<ResolvableNotification[]>([]);
setInterval(() => {
this.notify({title: "coucou le testaze aze azeazeaze", desc: "ceci est un test de notification ne rien faire", actions:[{id: "1",title: "Continuer"}, {id: "1",title: "Faire autre"}, {id: "1",title: "Annuler", cancel: true}]});
}, 5000)
}
public notify(notification: Notification): Promise<NotificationResult|string>{
let resovableNotification: ResolvableNotification;
const promise = new Promise<NotificationResult|string>(resolve => {
resovableNotification = {id: uuidv4(), notification: notification, resolver: resolve }
setTimeout(() => resolve(NotificationResult.NO_CHOICE), notification.duration || 10000);
});
this.notifications$.next([...this.notifications$.value, resovableNotification]);
promise.then(() => {
this.notifications$.next(this.notifications$.value.filter(n => n.id !== resovableNotification.id));
})
return promise;
}
}
export interface Notification {
title: string,
type?: NotificationType
desc?: string,
actions?: NotificationAction[],
duration?: number,
}
export enum NotificationType {
SUCCESS = 0,
WARNING = 1,
ERROR = 2,
}
export interface NotificationAction {
id: string,
title: string,
cancel?: boolean
}
export enum NotificationResult {
NO_CHOICE = "no_choice",
CLOSE = "close",
}
interface ResolvableNotification{
id: string,
resolver: (value: NotificationResult|string) => void,
notification: Notification
}