From 2826994d4241dad85f93bdad5eaf042cd8fe9005 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Fri, 19 Apr 2024 18:44:58 +0200 Subject: [PATCH 01/10] initial commit --- src/NotificationPlaceholder/hooks/index.ts | 108 +++++++++++++++++++++ src/NotificationPlaceholder/index.ts | 18 ++++ src/NotificationPlaceholder/readme.md | 29 ++++++ src/index.ts | 1 + 4 files changed, 156 insertions(+) create mode 100644 src/NotificationPlaceholder/hooks/index.ts create mode 100644 src/NotificationPlaceholder/index.ts create mode 100644 src/NotificationPlaceholder/readme.md diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts new file mode 100644 index 0000000..0a0ca85 --- /dev/null +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -0,0 +1,108 @@ +import {useEffect, useReducer} from 'react'; + +declare const window: { + GiveNotifications: { + apiNonce: string; + }; +} & Window; + +interface NotificationState { + isLoading: boolean; + notifications: string[]; +} + +interface NotificationAction { + type: string; + notifications: string[]; +} + +/** + * Fetch notifications from session storage or server + */ +const fetchNotifications = async () => { + //Session storage + const notifications = JSON.parse(sessionStorage.getItem('give_notifications')) || []; + + if (notifications.length > 0) { + return notifications; + } + + // Server + const response = await fetch('/wp-json/give-api/v2/get-notifications', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': window.GiveNotifications.apiNonce + } + }) + + const data = response.ok ? await response.json() : []; + + if (data.length > 0) { + sessionStorage.setItem('give_notifications', JSON.stringify(data)); + } + + return data; +} + +/** + * Dismiss a notification + */ +const dismissNotification = async (id: string) => { + const response = await fetch('/wp-json/give-api/v2/dismiss-notification', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': window.GiveNotifications.apiNonce + }, + body: JSON.stringify({notification: id}) + }) + + const data = response.ok ? await response.json() : []; + + sessionStorage.setItem('give_notifications', JSON.stringify(data)); + + return data; +}; + +/** + * Hook + */ +export const useNotifications = (): [NotificationState, Function] => { + useEffect(() => { + fetchNotifications().then((notifications) => { + dispatch({ + type: 'SET_NOTIFICATIONS', + notifications + }); + }); + }, []); + + const [state, dispatch] = useReducer((state: NotificationState, action: NotificationAction) => { + switch (action.type) { + case 'SET_NOTIFICATIONS': + return { + ...state, + isLoading: false, + notifications: action.notifications, + }; + } + + return state; + }, { + isLoading: true, + notifications: [] + }); + + return [ + state as NotificationState, + (id: string) => { + dismissNotification(id).then((notifications) => { + dispatch({ + type: 'SET_NOTIFICATIONS', + notifications + }); + }) + } + ]; +} diff --git a/src/NotificationPlaceholder/index.ts b/src/NotificationPlaceholder/index.ts new file mode 100644 index 0000000..034f17f --- /dev/null +++ b/src/NotificationPlaceholder/index.ts @@ -0,0 +1,18 @@ +import {useNotifications} from './hooks'; + +interface NotificationProps { + id: string; + render: (dismiss: Function) => JSX.Element +} + +export default ({id, render}: NotificationProps) => { + const [{isLoading, notifications}, dismissNotification] = useNotifications(); + + if (isLoading || notifications.includes(id)) { + return null; + } + + return render(() => { + dismissNotification(id) + }); +} diff --git a/src/NotificationPlaceholder/readme.md b/src/NotificationPlaceholder/readme.md new file mode 100644 index 0000000..f839b5f --- /dev/null +++ b/src/NotificationPlaceholder/readme.md @@ -0,0 +1,29 @@ +## Usage + +```jsx +import {NotificationPlaceholder} from '@givewp/form-builder-library'; + +function App() { + return ( +
+

+ My app title +

+ ( +
+

Notification content

+ +
+ )} + /> + +
+ My app content +
+
+ ); + +} +``` diff --git a/src/index.ts b/src/index.ts index 8cf72ef..f55d6e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export {default as ClassicEditor} from "./ClassicEditor"; export {default as OptionsPanel} from "./OptionsPanel"; export {default as SettingsSection} from "./SettingsSection"; export {default as BlockNotice} from "./BlockNotice"; +export {default as NotificationPlaceholder} from "./NotificationPlaceholder"; From 54e1618c5e8b139172b4d039f0fa752cadef1680 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Fri, 19 Apr 2024 18:59:26 +0200 Subject: [PATCH 02/10] reorganize --- src/NotificationPlaceholder/hooks/index.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 0a0ca85..5d955ea 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -69,20 +69,10 @@ const dismissNotification = async (id: string) => { * Hook */ export const useNotifications = (): [NotificationState, Function] => { - useEffect(() => { - fetchNotifications().then((notifications) => { - dispatch({ - type: 'SET_NOTIFICATIONS', - notifications - }); - }); - }, []); - const [state, dispatch] = useReducer((state: NotificationState, action: NotificationAction) => { switch (action.type) { case 'SET_NOTIFICATIONS': return { - ...state, isLoading: false, notifications: action.notifications, }; @@ -94,6 +84,15 @@ export const useNotifications = (): [NotificationState, Function] => { notifications: [] }); + useEffect(() => { + fetchNotifications().then((notifications) => { + dispatch({ + type: 'SET_NOTIFICATIONS', + notifications + }); + }); + }, []); + return [ state as NotificationState, (id: string) => { From e7cfa4e3536cb752a85ab9e0d846d25e35c9c7e4 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Fri, 19 Apr 2024 19:03:10 +0200 Subject: [PATCH 03/10] change function name to match type --- src/NotificationPlaceholder/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NotificationPlaceholder/readme.md b/src/NotificationPlaceholder/readme.md index f839b5f..e699851 100644 --- a/src/NotificationPlaceholder/readme.md +++ b/src/NotificationPlaceholder/readme.md @@ -11,10 +11,10 @@ function App() { ( + render={dismiss => (

Notification content

- +
)} /> From b40464ceb2685d8de98549efc1e7062565ad4d82 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Fri, 19 Apr 2024 19:11:14 +0200 Subject: [PATCH 04/10] change request method to post --- src/NotificationPlaceholder/hooks/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 5d955ea..49b803d 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -50,7 +50,7 @@ const fetchNotifications = async () => { */ const dismissNotification = async (id: string) => { const response = await fetch('/wp-json/give-api/v2/dismiss-notification', { - method: 'DELETE', + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': window.GiveNotifications.apiNonce From fe0a1301f163f2f01799d438984e1f157071d959 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Mon, 22 Apr 2024 08:30:53 +0200 Subject: [PATCH 05/10] refactor: use useState instead of useReducer --- src/NotificationPlaceholder/hooks/index.ts | 29 ++++++---------------- src/NotificationPlaceholder/index.ts | 3 ++- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 49b803d..33fb66c 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -1,4 +1,4 @@ -import {useEffect, useReducer} from 'react'; +import {useEffect, useState} from 'react'; declare const window: { GiveNotifications: { @@ -11,11 +11,6 @@ interface NotificationState { notifications: string[]; } -interface NotificationAction { - type: string; - notifications: string[]; -} - /** * Fetch notifications from session storage or server */ @@ -69,36 +64,26 @@ const dismissNotification = async (id: string) => { * Hook */ export const useNotifications = (): [NotificationState, Function] => { - const [state, dispatch] = useReducer((state: NotificationState, action: NotificationAction) => { - switch (action.type) { - case 'SET_NOTIFICATIONS': - return { - isLoading: false, - notifications: action.notifications, - }; - } - - return state; - }, { + const [state, setState] = useState({ isLoading: true, notifications: [] }); useEffect(() => { fetchNotifications().then((notifications) => { - dispatch({ - type: 'SET_NOTIFICATIONS', + setState({ + isLoading: false, notifications }); }); }, []); return [ - state as NotificationState, + state, (id: string) => { dismissNotification(id).then((notifications) => { - dispatch({ - type: 'SET_NOTIFICATIONS', + setState({ + isLoading: false, notifications }); }) diff --git a/src/NotificationPlaceholder/index.ts b/src/NotificationPlaceholder/index.ts index 034f17f..3c3279b 100644 --- a/src/NotificationPlaceholder/index.ts +++ b/src/NotificationPlaceholder/index.ts @@ -1,8 +1,9 @@ +import {MouseEventHandler} from 'react'; import {useNotifications} from './hooks'; interface NotificationProps { id: string; - render: (dismiss: Function) => JSX.Element + render: (dismiss: MouseEventHandler) => JSX.Element } export default ({id, render}: NotificationProps) => { From f5f39e4b534c222cf9fa16239bd7e2ce761f1a85 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Mon, 22 Apr 2024 08:37:46 +0200 Subject: [PATCH 06/10] refactor: remove data check --- src/NotificationPlaceholder/hooks/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 33fb66c..9436aa8 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -33,9 +33,7 @@ const fetchNotifications = async () => { const data = response.ok ? await response.json() : []; - if (data.length > 0) { - sessionStorage.setItem('give_notifications', JSON.stringify(data)); - } + sessionStorage.setItem('give_notifications', JSON.stringify(data)); return data; } From c8b363f8b1f7faeae93730e48150619893a6fa18 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Wed, 24 Apr 2024 10:36:48 +0200 Subject: [PATCH 07/10] refactor: use localStorage instead of sessionStorage; add support for storage type --- src/NotificationPlaceholder/hooks/index.ts | 57 ++++++++++++---------- src/NotificationPlaceholder/index.ts | 9 ++-- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 9436aa8..74c7e6f 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -1,7 +1,7 @@ import {useEffect, useState} from 'react'; declare const window: { - GiveNotifications: { + givewpNotifications: { apiNonce: string; }; } & Window; @@ -11,49 +11,54 @@ interface NotificationState { notifications: string[]; } +export type StorageType = 'user' | 'system'; + /** - * Fetch notifications from session storage or server + * Fetch notifications */ -const fetchNotifications = async () => { - //Session storage - const notifications = JSON.parse(sessionStorage.getItem('give_notifications')) || []; +const fetchNotifications = async (type: StorageType) => { + //local storage + const notifications = JSON.parse(localStorage.getItem(`give_notifications_${type}`)) || []; if (notifications.length > 0) { return notifications; } // Server - const response = await fetch('/wp-json/give-api/v2/get-notifications', { + const response = await fetch(`/wp-json/give-api/v2/get-notifications?type=${type}`, { method: 'GET', headers: { 'Content-Type': 'application/json', - 'X-WP-Nonce': window.GiveNotifications.apiNonce - } - }) + 'X-WP-Nonce': window.givewpNotifications.apiNonce, + }, + }); const data = response.ok ? await response.json() : []; - sessionStorage.setItem('give_notifications', JSON.stringify(data)); + localStorage.setItem(`give_notifications_${type}`, JSON.stringify(data)); return data; -} +}; /** * Dismiss a notification */ -const dismissNotification = async (id: string) => { +const dismissNotification = async (id: string, type: StorageType) => { const response = await fetch('/wp-json/give-api/v2/dismiss-notification', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-WP-Nonce': window.GiveNotifications.apiNonce + 'X-WP-Nonce': window.givewpNotifications.apiNonce, }, - body: JSON.stringify({notification: id}) - }) + body: JSON.stringify({ + notification: id, + type + }), + }); const data = response.ok ? await response.json() : []; - sessionStorage.setItem('give_notifications', JSON.stringify(data)); + localStorage.setItem(`give_notifications_${type}`, JSON.stringify(data)); return data; }; @@ -61,17 +66,17 @@ const dismissNotification = async (id: string) => { /** * Hook */ -export const useNotifications = (): [NotificationState, Function] => { +export const useNotifications = (type: StorageType): [NotificationState, Function] => { const [state, setState] = useState({ isLoading: true, - notifications: [] + notifications: [], }); useEffect(() => { - fetchNotifications().then((notifications) => { + fetchNotifications(type).then((notifications) => { setState({ isLoading: false, - notifications + notifications, }); }); }, []); @@ -79,12 +84,12 @@ export const useNotifications = (): [NotificationState, Function] => { return [ state, (id: string) => { - dismissNotification(id).then((notifications) => { + dismissNotification(id, type).then((notifications) => { setState({ - isLoading: false, - notifications + ...state, + notifications, }); - }) - } + }); + }, ]; -} +}; diff --git a/src/NotificationPlaceholder/index.ts b/src/NotificationPlaceholder/index.ts index 3c3279b..66ea81e 100644 --- a/src/NotificationPlaceholder/index.ts +++ b/src/NotificationPlaceholder/index.ts @@ -1,13 +1,14 @@ import {MouseEventHandler} from 'react'; -import {useNotifications} from './hooks'; +import {useNotifications, StorageType} from './hooks'; interface NotificationProps { id: string; - render: (dismiss: MouseEventHandler) => JSX.Element + render: (dismiss: MouseEventHandler) => JSX.Element, + type: StorageType; } -export default ({id, render}: NotificationProps) => { - const [{isLoading, notifications}, dismissNotification] = useNotifications(); +export default ({id, render, type = 'user'}: NotificationProps) => { + const [{isLoading, notifications}, dismissNotification] = useNotifications(type); if (isLoading || notifications.includes(id)) { return null; From 61ab05fae20dc9130a74cccf424cd8b0b56e6fac Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Wed, 24 Apr 2024 12:09:08 +0200 Subject: [PATCH 08/10] refactor: make type optional --- src/NotificationPlaceholder/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NotificationPlaceholder/index.ts b/src/NotificationPlaceholder/index.ts index 66ea81e..5cffd38 100644 --- a/src/NotificationPlaceholder/index.ts +++ b/src/NotificationPlaceholder/index.ts @@ -4,7 +4,7 @@ import {useNotifications, StorageType} from './hooks'; interface NotificationProps { id: string; render: (dismiss: MouseEventHandler) => JSX.Element, - type: StorageType; + type?: StorageType; } export default ({id, render, type = 'user'}: NotificationProps) => { From 2a9bf651265929b277c5b3916d9dce0e911ce931 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Wed, 24 Apr 2024 12:29:28 +0200 Subject: [PATCH 09/10] feature: update readme --- src/NotificationPlaceholder/readme.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/NotificationPlaceholder/readme.md b/src/NotificationPlaceholder/readme.md index e699851..b0653bd 100644 --- a/src/NotificationPlaceholder/readme.md +++ b/src/NotificationPlaceholder/readme.md @@ -1,3 +1,8 @@ +## Notification types +There are two notification types, `user` and `system`. +User type notifications when dismissed, won't be shown again only to the user that dismissed the notification, while system notifications when dismissed, won't be shown again to any user. +You will probably use `user` type notifications 99% of the time, but sometimes in some special cases like upgrade action that can be done only once for all users, you might want to use `system` type notifications. + ## Usage ```jsx @@ -13,7 +18,18 @@ function App() { id="notificationUniqueId" render={dismiss => (
-

Notification content

+

User notification - if dismissed it won't be shown again to this user only.

+ +
+ )} + /> + + ( +
+

System notification - if dismissed it won't be shown again to any user.

)} From df07f65e60b5738b9a723baf904857ba34d3d820 Mon Sep 17 00:00:00 2001 From: Ante Laca Date: Wed, 24 Apr 2024 13:54:49 +0200 Subject: [PATCH 10/10] refactor: remove notification fetch --- src/NotificationPlaceholder/hooks/index.ts | 73 +++++----------------- src/NotificationPlaceholder/index.ts | 8 +-- 2 files changed, 18 insertions(+), 63 deletions(-) diff --git a/src/NotificationPlaceholder/hooks/index.ts b/src/NotificationPlaceholder/hooks/index.ts index 74c7e6f..fbf8ce6 100644 --- a/src/NotificationPlaceholder/hooks/index.ts +++ b/src/NotificationPlaceholder/hooks/index.ts @@ -1,49 +1,21 @@ -import {useEffect, useState} from 'react'; +import {useState} from 'react'; declare const window: { givewpNotifications: { apiNonce: string; + notifications: { + user: string[]; + system: string[]; + }; }; } & Window; -interface NotificationState { - isLoading: boolean; - notifications: string[]; -} - -export type StorageType = 'user' | 'system'; - -/** - * Fetch notifications - */ -const fetchNotifications = async (type: StorageType) => { - //local storage - const notifications = JSON.parse(localStorage.getItem(`give_notifications_${type}`)) || []; - - if (notifications.length > 0) { - return notifications; - } - - // Server - const response = await fetch(`/wp-json/give-api/v2/get-notifications?type=${type}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-WP-Nonce': window.givewpNotifications.apiNonce, - }, - }); - - const data = response.ok ? await response.json() : []; - - localStorage.setItem(`give_notifications_${type}`, JSON.stringify(data)); - - return data; -}; +export type NotificationType = 'user' | 'system'; /** * Dismiss a notification */ -const dismissNotification = async (id: string, type: StorageType) => { +const dismissNotification = async (id: string, type: NotificationType) => { const response = await fetch('/wp-json/give-api/v2/dismiss-notification', { method: 'POST', headers: { @@ -56,39 +28,22 @@ const dismissNotification = async (id: string, type: StorageType) => { }), }); - const data = response.ok ? await response.json() : []; - - localStorage.setItem(`give_notifications_${type}`, JSON.stringify(data)); - - return data; + return response.ok ? await response.json() : []; }; /** * Hook */ -export const useNotifications = (type: StorageType): [NotificationState, Function] => { - const [state, setState] = useState({ - isLoading: true, - notifications: [], - }); - - useEffect(() => { - fetchNotifications(type).then((notifications) => { - setState({ - isLoading: false, - notifications, - }); - }); - }, []); +export const useNotifications = (type: NotificationType): [Array, Function] => { + const [notifications, setNotifications] = useState( + window.givewpNotifications.notifications[type] + ); return [ - state, + notifications, (id: string) => { dismissNotification(id, type).then((notifications) => { - setState({ - ...state, - notifications, - }); + setNotifications(notifications); }); }, ]; diff --git a/src/NotificationPlaceholder/index.ts b/src/NotificationPlaceholder/index.ts index 5cffd38..98afd2a 100644 --- a/src/NotificationPlaceholder/index.ts +++ b/src/NotificationPlaceholder/index.ts @@ -1,16 +1,16 @@ import {MouseEventHandler} from 'react'; -import {useNotifications, StorageType} from './hooks'; +import {useNotifications, NotificationType} from './hooks'; interface NotificationProps { id: string; render: (dismiss: MouseEventHandler) => JSX.Element, - type?: StorageType; + type?: NotificationType; } export default ({id, render, type = 'user'}: NotificationProps) => { - const [{isLoading, notifications}, dismissNotification] = useNotifications(type); + const [notifications, dismissNotification] = useNotifications(type); - if (isLoading || notifications.includes(id)) { + if (notifications.includes(id)) { return null; }