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",
"form-data": "^4.0.0",
"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",
"prettier": "^2.8.7",
"react": "18.2.0",
@ -36,5 +36,3 @@
},
"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">
<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"/>
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L8.375 16L4 11.4545" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 219 B

View File

@ -1,16 +1,36 @@
import Module from "@Front/Config/Module";
import React from "react";
import React, { useCallback, useEffect } from "react";
import HeaderLink from "../HeaderLink";
import classes from "./classes.module.scss";
import Rules, { RulesMode } from "@Front/Components/Elements/Rules";
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 = {};
type IState = {};
const getNotifications = useCallback(async () => {
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);
}, []);
useEffect(() => {
getNotifications();
}, [pathname, getNotifications]);
export default class Navigation extends React.Component<IProps, IState> {
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<HeaderLink
@ -43,4 +63,3 @@ export default class Navigation extends React.Component<IProps, IState> {
</div>
);
}
}

View File

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

View File

@ -8,18 +8,25 @@ import React from "react";
import classes from "./classes.module.scss";
import Toasts, { IToast } from "@Front/Stores/Toasts";
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 = {
toast: IToast;
};
type IPropsClass = IProps & {
router: NextRouter;
};
type IState = {
willClose: boolean;
};
export default class ToastElement extends React.Component<IProps, IState> {
class ToastElementClass extends React.Component<IPropsClass, IState> {
private closeTimeout = 0;
constructor(props: IProps) {
constructor(props: IPropsClass) {
super(props);
this.state = {
@ -27,6 +34,7 @@ export default class ToastElement extends React.Component<IProps, IState> {
};
this.onClose = this.onClose.bind(this);
this.handleClick = this.handleClick.bind(this);
}
public override render(): JSX.Element {
@ -35,7 +43,11 @@ export default class ToastElement extends React.Component<IProps, IState> {
"--data-duration": `${toast.time}ms`,
} as React.CSSProperties;
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} />}
<div className={classes["header"]}>
<div className={classes["text-icon_row"]}>
@ -45,7 +57,7 @@ export default class ToastElement extends React.Component<IProps, IState> {
{this.getToastText(toast.text)}
</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>
{toast.button}
</div>
@ -95,4 +107,16 @@ export default class ToastElement extends React.Component<IProps, IState> {
Toasts.getInstance().close(this.props.toast);
}, 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 I18n from "Components/Elements/I18n";
@ -7,6 +8,8 @@ export enum EToastPriority {
}
export interface IToast {
uid?: string;
redirectUrl?: string;
id?: number;
title: string | React.ReactNode;
icon?: React.ReactNode;
@ -23,7 +26,7 @@ export default class Toasts {
private toastList: IToast[] = [];
private uid: number = 0;
private defaultTime: IToast["time"] = 10000;
private defaultTime: IToast["time"] = 0;
private defaultClosable: IToast["closable"] = true;
private defaultPriority: IToast["priority"] = EToastPriority.LOW;
@ -49,6 +52,8 @@ export default class Toasts {
}
public open(toast: IToast): () => void {
const toastExists = this.toastList.find((t) => t.uid === toast.uid);
if (toastExists) return () => {};
const index = this.toastList.indexOf(toast);
if (index !== -1) return () => this.close(toast);
@ -84,6 +89,11 @@ export default class Toasts {
const index = this.toastList.indexOf(toast);
if (index === -1) return;
this.toastList.splice(index, 1);
if (toast.uid)
Notifications.getInstance().put(toast.uid, {
read: true,
});
this.event.emit("change", this.toastList);
}

View File

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

View File

@ -23,6 +23,7 @@
--turquoise-soft: #{$turquoise-soft};
--purple-soft: #{$purple-soft};
--orange-soft: #{$orange-soft};
--orange-soft-hover: #{$orange-soft-hover};
--red-soft: #{$red-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 jwt_decode from "jwt-decode";
import { NextResponse } from "next/server";
@ -6,7 +7,6 @@ import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
// Get the JWT from the cookies
const cookies = request.cookies.get("leCoffreAccessToken");
console.log("cookies", cookies)
if (!cookies) return NextResponse.redirect(new URL("/login", request.url));
// Decode it
@ -14,7 +14,6 @@ export async function middleware(request: NextRequest) {
const customerDecodedToken = jwt_decode(cookies.value) as ICustomerJwtPayload;
// 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 JWT expired, redirect to login page
@ -22,13 +21,9 @@ export async function middleware(request: NextRequest) {
const currentDate = new Date();
const time = currentDate.getTime();
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) {
console.log('token expired')
//return NextResponse.redirect(new URL("/login", request.url));
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();