The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 143 lines 4.2 kB view raw
1import type { ApiError } from "@/typings"; 2import type { HTMLProps } from "react"; 3import { useCallback, useEffect, useRef, useState } from "react"; 4 5export enum InputState { 6 Idle = 0, 7 Loading = 1, 8 Success = 2 9} 10 11interface InputOptions<T> { 12 endpoint?: string; 13 k?: string; 14 15 defaultState: T; 16 transform?: (value: T) => unknown; 17 18 onSave?: (value: T) => void; 19 20 manual?: boolean; 21 debounceMs?: number; 22 23 isEqual?: (a: T, b: T) => boolean; 24} 25 26export type InputProps<T> = InputOptions<T> & HTMLProps<HTMLDivElement> & { 27 label: string; 28 description?: string; 29 disabled?: boolean; 30}; 31 32export function useInput<T>(options: InputOptions<T>) { 33 const [value, setValue] = useState<T>(options.defaultState); 34 const [savedValue, setSavedValue] = useState<T>(options.defaultState); 35 const [state, setState] = useState<InputState>(InputState.Idle); 36 const [error, setError] = useState<string | null>(null); 37 const timeout = useRef<NodeJS.Timeout | null>(null); 38 const debounceRef = useRef<NodeJS.Timeout | null>(null); 39 40 const { endpoint, k, onSave, transform, manual, debounceMs, defaultState, isEqual } = options; 41 42 const defaultStateKey = JSON.stringify(defaultState); 43 const [prevDefaultStateKey, setPrevDefaultStateKey] = useState(defaultStateKey); 44 if (defaultStateKey !== prevDefaultStateKey) { 45 setPrevDefaultStateKey(defaultStateKey); 46 setValue(defaultState); 47 setSavedValue(defaultState); 48 } 49 50 useEffect(() => { 51 return () => { 52 if (timeout.current) { 53 clearTimeout(timeout.current); 54 timeout.current = null; 55 } 56 57 if (debounceRef.current) { 58 clearTimeout(debounceRef.current); 59 debounceRef.current = null; 60 } 61 }; 62 }, []); 63 64 const save = useCallback( 65 async (val?: T) => { 66 const valueToSave = val === undefined ? value : val; 67 onSave?.(valueToSave); 68 setSavedValue(valueToSave); 69 70 if (!endpoint || !k) return; 71 72 if (timeout.current) { 73 clearTimeout(timeout.current); 74 timeout.current = null; 75 } 76 77 setState(InputState.Loading); 78 setError(null); 79 80 const res = await fetch(process.env.NEXT_PUBLIC_API + endpoint, { 81 method: "PATCH", 82 credentials: "include", 83 headers: { 84 "Content-Type": "application/json" 85 }, 86 body: JSON.stringify(k.includes(".") 87 ? { [k.split(".")[0]]: { [k.split(".")[1]]: transform?.(valueToSave) ?? valueToSave } } 88 : { [k]: transform?.(valueToSave) ?? valueToSave } 89 ) 90 }) 91 .catch((error) => String(error)); 92 93 if (typeof res === "string" || !res.ok) { 94 setState(InputState.Idle); 95 96 if (typeof res === "string") { 97 setError(res); 98 } else { 99 const data = await res 100 .json() 101 .catch(() => null) as ApiError | null; 102 103 setError(data?.message || "Unknown error"); 104 } 105 106 return; 107 } 108 109 setState(InputState.Success); 110 timeout.current = setTimeout(() => setState(InputState.Idle), 1_000 * 8); 111 }, 112 [onSave, endpoint, k, transform, value] 113 ); 114 115 const update = useCallback( 116 (val: T) => { 117 setValue(val); 118 119 if (manual) return; 120 121 if (debounceRef.current) { 122 clearTimeout(debounceRef.current); 123 } 124 125 if (debounceMs) { 126 debounceRef.current = setTimeout(() => save(val), debounceMs); 127 } else { 128 save(val); 129 } 130 }, 131 [manual, debounceMs, save] 132 ); 133 134 return { 135 value, 136 state, 137 error, 138 isDirty: isEqual ? !isEqual(value, savedValue) : value !== savedValue, 139 update, 140 save, 141 reset: () => setValue(savedValue) 142 }; 143}