diff --git a/frontend/src/shared/components/NotifcationPopup/index.tsx b/frontend/src/shared/components/NotifcationPopup/index.tsx index 4501552..8737803 100644 --- a/frontend/src/shared/components/NotifcationPopup/index.tsx +++ b/frontend/src/shared/components/NotifcationPopup/index.tsx @@ -15,6 +15,8 @@ import dayjs from 'dayjs'; import { Popup, usePopup } from 'shared/components/PopupMenu'; import { CheckCircleOutline, Circle, CircleSolid, UserCircle } from 'shared/icons'; import produce from 'immer'; +import { useLocalStorage } from 'shared/hooks/useStateWithLocalStorage'; +import localStorage from 'shared/utils/localStorage'; const ItemWrapper = styled.div` cursor: pointer; @@ -403,7 +405,10 @@ type NotificationEntry = { }; }; const NotificationPopup: React.FC = ({ children }) => { - const [filter, setFilter] = useState(NotificationFilter.Unread); + const [filter, setFilter] = useLocalStorage( + localStorage.NOTIFICATIONS_FILTER, + NotificationFilter.Unread, + ); const [data, setData] = useState<{ nodes: Array; hasNextPage: boolean; cursor: string }>({ nodes: [], hasNextPage: false, diff --git a/frontend/src/shared/hooks/useStateWithLocalStorage.ts b/frontend/src/shared/hooks/useStateWithLocalStorage.ts index 07ac130..a71224c 100644 --- a/frontend/src/shared/hooks/useStateWithLocalStorage.ts +++ b/frontend/src/shared/hooks/useStateWithLocalStorage.ts @@ -1,4 +1,14 @@ -import React from 'react'; +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +// A wrapper for "JSON.parse()"" to support "undefined" value +function parseJSON(value: string | null): T | undefined { + try { + return value === 'undefined' ? undefined : JSON.parse(value ?? ''); + } catch (error) { + console.log('parsing error on', { value }); + return undefined; + } +} const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch>] => { const [value, setValue] = React.useState(localStorage.getItem(localStorageKey) || ''); @@ -11,3 +21,78 @@ const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispa }; export default useStateWithLocalStorage; + +type SetValue = Dispatch>; + +function useLocalStorage(key: string, initialValue: T): [T, SetValue] { + // Get from local storage then + // parse stored json or return initialValue + const readValue = (): T => { + // Prevent build error "window is undefined" but keep keep working + if (typeof window === 'undefined') { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? (parseJSON(item) as T) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key “${key}”:`, error); + return initialValue; + } + }; + + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(readValue); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue: SetValue = (value) => { + // Prevent build error "window is undefined" but keeps working + if (typeof window === 'undefined') { + console.warn(`Tried setting localStorage key “${key}” even though environment is not a client`); + } + + try { + // Allow value to be a function so we have the same API as useState + const newValue = value instanceof Function ? value(storedValue) : value; + + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(newValue)); + + // Save state + setStoredValue(newValue); + + // We dispatch a custom event so every useLocalStorage hook are notified + window.dispatchEvent(new Event('local-storage')); + } catch (error) { + console.warn(`Error setting localStorage key “${key}”:`, error); + } + }; + + useEffect(() => { + setStoredValue(readValue()); + }, []); + + useEffect(() => { + const handleStorageChange = () => { + setStoredValue(readValue()); + }; + + // this only works for other documents, not the current one + window.addEventListener('storage', handleStorageChange); + + // this is a custom event, triggered in writeValueToLocalStorage + window.addEventListener('local-storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('local-storage', handleStorageChange); + }; + }, []); + + return [storedValue, setValue]; +} + +export { useLocalStorage }; diff --git a/frontend/src/shared/utils/localStorage.ts b/frontend/src/shared/utils/localStorage.ts index 3d893e1..3e3cd1b 100644 --- a/frontend/src/shared/utils/localStorage.ts +++ b/frontend/src/shared/utils/localStorage.ts @@ -1,4 +1,5 @@ const localStorage = { + NOTIFICATIONS_FILTER: 'notifications_filter', CARD_LABEL_VARIANT_STORAGE_KEY: 'card_label_variant', };