The weeb for the next gen discord boat - Wamellow
wamellow.com
bot
discord
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}