-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
198 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { type RefObject, useEffect, useRef } from "react"; | ||
|
||
/** | ||
* It returns a function that adds an event listener to a target element, and removes it when the | ||
* component unmounts | ||
* @param {keyof WindowEventMap | string} eventName - The name of the event to listen for. | ||
* @param handler - The function that will be called when the event is fired. | ||
* @param [element] - The element to listen to the event on. If not provided, it will default to | ||
* window. | ||
*/ | ||
const useEventListener = <T extends HTMLElement = HTMLDivElement>( | ||
eventName: keyof WindowEventMap | string, // string to allow custom event | ||
handler: (event: Event) => void, | ||
element?: RefObject<T>, | ||
) => { | ||
// Create a ref that stores handler | ||
const savedHandler = useRef<(event: Event) => void>(); | ||
|
||
useEffect(() => { | ||
// Define the listening target | ||
const targetElement: T | Window = element?.current ?? window; | ||
|
||
if (!targetElement?.addEventListener) { | ||
return; | ||
} | ||
|
||
// Update saved handler if necessary | ||
if (savedHandler.current !== handler) { | ||
savedHandler.current = handler; | ||
} | ||
|
||
// Create event listener that calls handler function stored in ref | ||
const eventListener = (event: Event) => { | ||
if (savedHandler?.current) { | ||
savedHandler.current(event); | ||
} | ||
}; | ||
|
||
targetElement.addEventListener(eventName, eventListener); | ||
|
||
// Remove event listener on cleanup | ||
return () => { | ||
targetElement.removeEventListener(eventName, eventListener); | ||
}; | ||
}, [eventName, element, handler]); | ||
}; | ||
|
||
export default useEventListener; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { isEqual, isFunction, isNil } from "lodash-es"; | ||
import { type Dispatch, type SetStateAction, useEffect, useRef, useState } from "react"; | ||
import parseJSON from "../utils/parseJSON"; | ||
import useEventListener from "./useEventListener"; | ||
|
||
type SetValue<T> = Dispatch<SetStateAction<T>>; | ||
|
||
/** | ||
* Creating and read here and using window.setItem for writes. This avoids | ||
* tapping into the react rendering pipeline unnecessarily when it isnt needed | ||
* | ||
* @param key the key to use to store | ||
* @returns the value requested | ||
*/ | ||
const readValueFromStorage = <T>(key: string) => { | ||
try { | ||
const item = window.localStorage.getItem(key); | ||
|
||
const value = item && (parseJSON(item) as T); | ||
|
||
return value ?? undefined; | ||
} catch (error) { | ||
console.warn(`Error reading localStorage key "${key}":`, error); | ||
|
||
return { error: "unable to read value" }; | ||
} | ||
}; | ||
|
||
type StorageError = { | ||
error: string; | ||
}; | ||
|
||
const isError = (value: StorageError | any): value is StorageError => !!value?.error; | ||
|
||
const useLocalStorage = <T>(key: string, initialValue: T): [T, SetValue<T>] => { | ||
const previousValueRef = useRef(initialValue); | ||
|
||
// 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; | ||
} | ||
|
||
const value = readValueFromStorage<T>(key); | ||
|
||
return isError(value) || isNil(value) || value === "" ? initialValue : value; | ||
}; | ||
|
||
// State to store our value | ||
// Pass initial state function to useState so logic is only executed once | ||
const [storedValue, setStoredValue] = useState<T>(readValue); | ||
|
||
// Return a wrapped version of useState's setter function that ... | ||
// ... persists the new value to localStorage. | ||
const setValue: SetValue<T> = (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 = isFunction(value) ? 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); | ||
} | ||
}; | ||
|
||
// biome-ignore lint/correctness/useExhaustiveDependencies: run only once | ||
useEffect(() => { | ||
const newValue = readValue(); | ||
setStoredValue(newValue); | ||
previousValueRef.current = newValue; | ||
// run only once | ||
}, []); | ||
|
||
const handleStorageChange = () => { | ||
const newValue = readValue(); | ||
|
||
if (isEqual(newValue, previousValueRef.current)) { | ||
return; | ||
} | ||
setStoredValue(newValue); | ||
previousValueRef.current = newValue; | ||
// run only once | ||
}; | ||
|
||
// this only works for other documents, not the current one | ||
useEventListener("storage", handleStorageChange); | ||
|
||
// this is a custom event, triggered in writeValueToLocalStorage | ||
// See: useLocalStorage() | ||
useEventListener("local-storage", handleStorageChange); | ||
|
||
return [storedValue, setValue]; | ||
}; | ||
|
||
export default useLocalStorage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,5 @@ | ||
{ | ||
"extends": "./tsconfig.base.json", | ||
"compilerOptions": { | ||
/* Path Aliases */ | ||
"baseUrl": ".", | ||
"paths": { | ||
"~/*": ["./*"] | ||
} | ||
}, | ||
"include": ["**/*.ts", "**/*.tsx"], | ||
"exclude": ["node_modules"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// A wrapper for "JSON.parse()"" to support "undefined" value | ||
const parseJSON = <T>(value?: string | null): T | undefined => { | ||
try { | ||
return value === "undefined" ? undefined : (JSON.parse(value ?? "") as T); | ||
} catch { | ||
console.error("parsing error on", { value }); | ||
|
||
return undefined; | ||
} | ||
}; | ||
|
||
export default parseJSON; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { toast } from "sonner"; | ||
|
||
type SetClipboardParams = { | ||
onFailure?: () => void; | ||
onSuccess?: () => void; | ||
value: string; | ||
}; | ||
|
||
const setClipboard = ({ | ||
value, | ||
onSuccess = () => toast.success("Copied to clipboard"), | ||
onFailure = () => toast.error("Failed to copy to clipboard"), | ||
}: SetClipboardParams) => { | ||
navigator.clipboard | ||
.writeText(value) | ||
.then(() => { | ||
onSuccess?.(); | ||
}) | ||
.catch(() => { | ||
onFailure?.(); | ||
}); | ||
}; | ||
|
||
export default setClipboard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Helpful function to sleep for a given amount of time | ||
export default function sleep(ms: number) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} |