First working version

This commit is contained in:
Sebastian Seedorf
2022-08-08 23:12:11 +02:00
parent 78dcee42ca
commit 940149cec8
22 changed files with 12436 additions and 1586 deletions

4
src/hooks/useDetails.ts Normal file
View File

@@ -0,0 +1,4 @@
import {Entity} from "../types";
import details from "../../res/details.json";
export const useDetails = () => details as Entity[]

View File

@@ -0,0 +1,69 @@
import { RefObject, useEffect, useRef } from 'react'
// See: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
// Window Event based useEventListener interface
export function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions,
): void
// Element Event based useEventListener interface
export function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement,
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: RefObject<T>,
options?: boolean | AddEventListenerOptions,
): void
// Document Event based useEventListener interface
export function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: RefObject<Document>,
options?: boolean | AddEventListenerOptions,
): void
export function useEventListener<
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
T extends HTMLElement | void = void,
>(
eventName: KW | KH,
handler: (
event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event,
) => void,
element?: RefObject<T>,
options?: boolean | AddEventListenerOptions,
) {
// Create a ref that stores handler
const savedHandler = useRef(handler)
useIsomorphicLayoutEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current || window
if (!(targetElement && targetElement.addEventListener)) {
return
}
// Create event listener that calls handler function stored in ref
const eventListener: typeof handler = event => savedHandler.current(event)
targetElement.addEventListener(eventName, eventListener, options)
// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener)
}
}, [eventName, element, options])
}

View File

@@ -0,0 +1,4 @@
import { useEffect, useLayoutEffect } from 'react'
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect

View File

@@ -0,0 +1,102 @@
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react'
// See: https://usehooks-ts.com/react-hook/use-event-listener
import { useEventListener } from './useEventListener'
declare global {
interface WindowEventMap {
'local-storage': CustomEvent
}
}
type SetValue<T> = Dispatch<SetStateAction<T>>
export function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Get from local storage then
// parse stored json or return initialValue
const readValue = useCallback((): 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
}
}, [initialValue, key])
// 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> = useCallback(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)
}
}, [key, storedValue])
useEffect(() => {
setStoredValue(readValue())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleStorageChange = useCallback(
(event: StorageEvent | CustomEvent) => {
if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
return
}
setStoredValue(readValue())
},
[key, readValue],
)
// 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]
}
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch {
console.log('parsing error on', { value })
return undefined
}
}

15
src/types.ts Normal file
View File

@@ -0,0 +1,15 @@
export interface Recipe {
prerequisites: Record<string, number>
time: number
output: Record<string, number>
}
export interface UnfetchedEntity {
name: string
image: string
href: string
}
export interface Entity extends UnfetchedEntity {
recipe?: Recipe
}

12
src/utils.ts Normal file
View File

@@ -0,0 +1,12 @@
export function isNonNullable<T>(any: T): any is NonNullable<T> {
return any !== undefined && any !== null
}
export function sortByProperty<T>(transform: (val: T) => number | string): (a: T, b: T) => number {
return (a, b) => {
const a2 = transform(a)
const b2 = transform(b)
if (a2 > b2) return 1
return a2 === b2 ? 0 : -1
}
}