Notifications working

This commit is contained in:
Maxime Lalo 2023-09-26 11:55:24 +02:00
parent 4b49d91fcf
commit 7f6cd05ebf
11 changed files with 5157 additions and 55 deletions

4993
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.73", "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.82",
"next": "13.2.4", "next": "13.2.4",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"react": "18.2.0", "react": "18.2.0",
@ -36,5 +36,3 @@
}, },
"devDependencies": {} "devDependencies": {}
} }

View File

@ -0,0 +1,54 @@
import BaseApiService from "@Front/Api/BaseApiService";
import { UserNotification } from "le-coffre-resources/dist/Notary";
// TODO Type get query params -> Where + inclue + orderby
export interface IGetNotificationsParams {
where?: {};
include?: {};
select?: {};
}
export type IPutNotificationsParams = {
read?: boolean;
};
export default class Notifications extends BaseApiService {
private static instance: Notifications;
private baseUrl = this.getBaseUrl().concat("/notifications");
private constructor() {
super();
}
public static getInstance() {
if (!this.instance) {
return new this();
} else {
return this.instance;
}
}
public async get(q?: IGetNotificationsParams): Promise<UserNotification[]> {
const url = new URL(this.baseUrl);
if (q) {
const query = { q };
Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, JSON.stringify(value)));
}
try {
return await this.getRequest<UserNotification[]>(url);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
public async put(uid: string, body: IPutNotificationsParams): Promise<UserNotification> {
const url = new URL(this.baseUrl.concat(`/${uid}`));
try {
return await this.putRequest<UserNotification>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
}

View File

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2251 3.36088C13.4893 3.59571 13.5131 4.00024 13.2783 4.26442L6.4516 11.9444C6.33015 12.0811 6.15607 12.1592 5.97326 12.1592C5.79045 12.1592 5.61637 12.0811 5.49492 11.9444L2.08159 8.10442C1.84676 7.84024 1.87055 7.43571 2.13474 7.20088C2.39892 6.96606 2.80344 6.98985 3.03827 7.25403L5.97326 10.5559L12.3216 3.41403C12.5564 3.14985 12.9609 3.12606 13.2251 3.36088Z" fill="white"/> <path d="M18 6L8.375 16L4 11.4545" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 219 B

View File

@ -1,46 +1,65 @@
import Module from "@Front/Config/Module"; import Module from "@Front/Config/Module";
import React from "react"; import React, { useCallback, useEffect } from "react";
import HeaderLink from "../HeaderLink"; import HeaderLink from "../HeaderLink";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules"; import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule"; import { AppRuleActions, AppRuleNames } from "@Front/Api/Entities/rule";
import { usePathname } from "next/navigation";
import Notifications from "@Front/Api/LeCoffreApi/Notifications/Notifications";
import Toasts from "@Front/Stores/Toasts";
export default function Navigation() {
const pathname = usePathname();
type IProps = {}; const getNotifications = useCallback(async () => {
type IState = {}; const notifications = await Notifications.getInstance().get({
where: {
read: false,
},
});
notifications.forEach((notification) => {
Toasts.getInstance().open({
title: notification.notification.message,
uid: notification.uid,
redirectUrl: notification.notification.redirection_url,
});
});
console.log(notifications);
}, []);
export default class Navigation extends React.Component<IProps, IState> { useEffect(() => {
public override render(): JSX.Element { getNotifications();
return ( }, [pathname, getNotifications]);
<div className={classes["root"]}>
return (
<div className={classes["root"]}>
<HeaderLink
text={"Dossiers en cours"}
path={Module.getInstance().get().modules.pages.Folder.props.path}
routesActive={[
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]}
/>
<HeaderLink
text={"Dossiers archivés"}
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path}
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
]}>
<HeaderLink <HeaderLink
text={"Dossiers en cours"} text={"Collaborateurs"}
path={Module.getInstance().get().modules.pages.Folder.props.path} path={Module.getInstance().get().modules.pages.Collaborators.props.path}
routesActive={[ routesActive={[Module.getInstance().get().modules.pages.Collaborators.props.path]}
Module.getInstance().get().modules.pages.Folder.pages.FolderInformation.props.path,
Module.getInstance().get().modules.pages.Folder.pages.CreateFolder.props.path,
]}
/> />
<HeaderLink </Rules>
text={"Dossiers archivés"} </div>
path={Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path} );
routesActive={[Module.getInstance().get().modules.pages.Folder.pages.FolderArchived.props.path]}
/>
<Rules
mode={RulesMode.NECESSARY}
rules={[
{
action: AppRuleActions.update,
name: AppRuleNames.officeRoles,
},
]}>
<HeaderLink
text={"Collaborateurs"}
path={Module.getInstance().get().modules.pages.Collaborators.props.path}
routesActive={[Module.getInstance().get().modules.pages.Collaborators.props.path]}
/>
</Rules>
</div>
);
}
} }

View File

@ -26,7 +26,7 @@
pointer-events: all; pointer-events: all;
position: relative; position: relative;
padding: 24px; padding: 24px;
background: $orange-soft; background: var(--orange-soft);
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.11); box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.11);
border-radius: 5px; border-radius: 5px;
@ -42,6 +42,13 @@
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
&[data-clickable="true"] {
cursor: pointer;
&:hover {
background: var(--orange-soft-hover);
}
}
.loadbar { .loadbar {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -8,18 +8,25 @@ import React from "react";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import Toasts, { IToast } from "@Front/Stores/Toasts"; import Toasts, { IToast } from "@Front/Stores/Toasts";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import CheckIcon from "@Assets/Icons/check.svg";
import Image from "next/image";
import { NextRouter, useRouter } from "next/router";
type IProps = { type IProps = {
toast: IToast; toast: IToast;
}; };
type IPropsClass = IProps & {
router: NextRouter;
};
type IState = { type IState = {
willClose: boolean; willClose: boolean;
}; };
export default class ToastElement extends React.Component<IProps, IState> { class ToastElementClass extends React.Component<IPropsClass, IState> {
private closeTimeout = 0; private closeTimeout = 0;
constructor(props: IProps) { constructor(props: IPropsClass) {
super(props); super(props);
this.state = { this.state = {
@ -27,6 +34,7 @@ export default class ToastElement extends React.Component<IProps, IState> {
}; };
this.onClose = this.onClose.bind(this); this.onClose = this.onClose.bind(this);
this.handleClick = this.handleClick.bind(this);
} }
public override render(): JSX.Element { public override render(): JSX.Element {
@ -35,7 +43,11 @@ export default class ToastElement extends React.Component<IProps, IState> {
"--data-duration": `${toast.time}ms`, "--data-duration": `${toast.time}ms`,
} as React.CSSProperties; } as React.CSSProperties;
return ( return (
<div className={classes["root"]} data-will-close={this.state.willClose}> <div
className={classes["root"]}
data-will-close={this.state.willClose}
data-clickable={toast.redirectUrl ? true : false}
onClick={this.handleClick}>
{toast.time !== 0 && <div className={classes["loadbar"]} style={style} />} {toast.time !== 0 && <div className={classes["loadbar"]} style={style} />}
<div className={classes["header"]}> <div className={classes["header"]}>
<div className={classes["text-icon_row"]}> <div className={classes["text-icon_row"]}>
@ -45,7 +57,7 @@ export default class ToastElement extends React.Component<IProps, IState> {
{this.getToastText(toast.text)} {this.getToastText(toast.text)}
</div> </div>
</div> </div>
{/* {toast.closable && <Cross className={classes["cross"]} onClick={this.onClose} />} */} {toast.closable && <Image src={CheckIcon} alt="Document check" className={classes["cross"]} onClick={this.onClose} />}
</div> </div>
{toast.button} {toast.button}
</div> </div>
@ -95,4 +107,16 @@ export default class ToastElement extends React.Component<IProps, IState> {
Toasts.getInstance().close(this.props.toast); Toasts.getInstance().close(this.props.toast);
}, 200); }, 200);
} }
private handleClick(e: React.MouseEvent) {
if (this.props.toast.redirectUrl) {
this.props.router.push(this.props.toast.redirectUrl);
this.onClose(e);
}
}
}
export default function ToastElement(props: IProps) {
const router = useRouter();
return <ToastElementClass {...props} router={router} />;
} }

View File

@ -1,3 +1,4 @@
import Notifications from "@Front/Api/LeCoffreApi/Notifications/Notifications";
import EventEmitter from "@Front/Services/EventEmitter"; import EventEmitter from "@Front/Services/EventEmitter";
// import I18n from "Components/Elements/I18n"; // import I18n from "Components/Elements/I18n";
@ -7,6 +8,8 @@ export enum EToastPriority {
} }
export interface IToast { export interface IToast {
uid?: string;
redirectUrl?: string;
id?: number; id?: number;
title: string | React.ReactNode; title: string | React.ReactNode;
icon?: React.ReactNode; icon?: React.ReactNode;
@ -23,7 +26,7 @@ export default class Toasts {
private toastList: IToast[] = []; private toastList: IToast[] = [];
private uid: number = 0; private uid: number = 0;
private defaultTime: IToast["time"] = 10000; private defaultTime: IToast["time"] = 0;
private defaultClosable: IToast["closable"] = true; private defaultClosable: IToast["closable"] = true;
private defaultPriority: IToast["priority"] = EToastPriority.LOW; private defaultPriority: IToast["priority"] = EToastPriority.LOW;
@ -49,6 +52,8 @@ export default class Toasts {
} }
public open(toast: IToast): () => void { public open(toast: IToast): () => void {
const toastExists = this.toastList.find((t) => t.uid === toast.uid);
if (toastExists) return () => {};
const index = this.toastList.indexOf(toast); const index = this.toastList.indexOf(toast);
if (index !== -1) return () => this.close(toast); if (index !== -1) return () => this.close(toast);
@ -84,6 +89,11 @@ export default class Toasts {
const index = this.toastList.indexOf(toast); const index = this.toastList.indexOf(toast);
if (index === -1) return; if (index === -1) return;
this.toastList.splice(index, 1); this.toastList.splice(index, 1);
if (toast.uid)
Notifications.getInstance().put(toast.uid, {
read: true,
});
this.event.emit("change", this.toastList); this.event.emit("change", this.toastList);
} }

View File

@ -33,6 +33,7 @@ $orange-soft: #ffdc99;
$red-soft: #f08771; $red-soft: #f08771;
$pink-soft: #f8b9df; $pink-soft: #f8b9df;
$orange-soft-hover: #ffd078;
$grey: #939393; $grey: #939393;
$grey-medium: #e7e7e7; $grey-medium: #e7e7e7;
$grey-soft: #f9f9f9; $grey-soft: #f9f9f9;

View File

@ -23,6 +23,7 @@
--turquoise-soft: #{$turquoise-soft}; --turquoise-soft: #{$turquoise-soft};
--purple-soft: #{$purple-soft}; --purple-soft: #{$purple-soft};
--orange-soft: #{$orange-soft}; --orange-soft: #{$orange-soft};
--orange-soft-hover: #{$orange-soft-hover};
--red-soft: #{$red-soft}; --red-soft: #{$red-soft};
--pink-soft: #{$pink-soft}; --pink-soft: #{$pink-soft};

View File

@ -1,3 +1,4 @@
import Notifications from "@Front/Api/LeCoffreApi/Notifications/Notifications";
import { ICustomerJwtPayload, IUserJwtPayload } from "@Front/Services/JwtService/JwtService"; import { ICustomerJwtPayload, IUserJwtPayload } from "@Front/Services/JwtService/JwtService";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
@ -6,7 +7,6 @@ import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
// Get the JWT from the cookies // Get the JWT from the cookies
const cookies = request.cookies.get("leCoffreAccessToken"); const cookies = request.cookies.get("leCoffreAccessToken");
console.log("cookies", cookies)
if (!cookies) return NextResponse.redirect(new URL("/login", request.url)); if (!cookies) return NextResponse.redirect(new URL("/login", request.url));
// Decode it // Decode it
@ -14,21 +14,16 @@ export async function middleware(request: NextRequest) {
const customerDecodedToken = jwt_decode(cookies.value) as ICustomerJwtPayload; const customerDecodedToken = jwt_decode(cookies.value) as ICustomerJwtPayload;
// If no JWT provided, redirect to login page // If no JWT provided, redirect to login page
console.log("decoded tokens", userDecodedToken, customerDecodedToken )
if (!userDecodedToken && !customerDecodedToken) return NextResponse.redirect(new URL("/login", request.url)); if (!userDecodedToken && !customerDecodedToken) return NextResponse.redirect(new URL("/login", request.url));
// If JWT expired, redirect to login page // If JWT expired, redirect to login page
const token = userDecodedToken ?? customerDecodedToken; const token = userDecodedToken ?? customerDecodedToken;
const currentDate = new Date(); const currentDate = new Date();
const time = currentDate.getTime(); const time = currentDate.getTime();
const now = Math.floor(time / 1000); const now = Math.floor(time / 1000);
console.log("date now", Date.now());
console.log("date local", new Date().toLocaleString())
console.log("date iso",new Date().toISOString());
if (token.exp < now) { if (token.exp < now) {
console.log('token expired') return NextResponse.redirect(new URL("/login", request.url));
//return NextResponse.redirect(new URL("/login", request.url));
} }
return NextResponse.next(); return NextResponse.next();