The weeb for the next gen discord boat - Wamellow
wamellow.com
bot
discord
1"use client";
2
3import type { ApiError } from "@/typings";
4import { cn } from "@/utils/cn";
5import { useEffect, useState } from "react";
6import { TailSpin } from "react-loading-icons";
7
8import DumbTextInput from "./dumb-text-input";
9import { useStateDebounced } from "../../utils/useDebounce";
10
11type Type<T extends "text" | "color"> = T extends "text" ? string : number;
12
13enum State {
14 Idle = 0,
15 Loading = 1,
16 Success = 2
17}
18
19interface Props<T extends "text" | "color"> {
20 className?: string;
21
22 name?: string;
23 url?: string;
24 dataName?: string;
25 disabled?: boolean;
26 description?: string;
27 defaultState: Type<T>;
28 resetState?: Type<T>;
29
30 type?: T;
31 max?: number;
32 placeholder?: string;
33
34 onSave?: (value: Type<T> | null) => void;
35}
36
37export default function TextInput<T extends "text" | "color" = "text">({
38 className,
39 name,
40 url,
41 dataName,
42 disabled,
43 description,
44 defaultState,
45 resetState,
46 type = "text" as T,
47 max,
48 placeholder,
49 onSave
50}: Props<T>) {
51 const [state, setState] = useState<State>(State.Idle);
52 const [error, setError] = useState<string | null>(null);
53
54 const [valuedebounced, setValueDebounced] = useStateDebounced<T extends "text" ? string : number>((type === "text" ? "" : 0) as Type<T>, 1_000);
55 const [value, setValue] = useState<T extends "text" ? string : number>((type === "text" ? "" : 0) as Type<T>);
56 const [defaultStateValue, setdefaultStateValue] = useState<T extends "text" ? string : number>((type === "text" ? "" : 0) as Type<T>);
57
58 useEffect(() => {
59 if (!defaultStateValue) setdefaultStateValue(defaultState);
60 setValue(defaultState);
61 }, [defaultState]);
62
63 useEffect(() => {
64 if (defaultStateValue === value) return;
65 setError(null);
66
67 if (!url) {
68 if (!onSave) throw new Error("Warning: <TextInput.onSave> must be defined when not using <TextInput.url>.");
69
70 onSave(value);
71 setState(State.Idle);
72 return;
73 }
74
75 if (!dataName) throw new Error("Warning: <TextInput.dataName> must be defined when using <TextInput.url>.");
76
77 setState(State.Loading);
78
79 const def = type === "color"
80 ? 0
81 : null;
82
83 fetch(`${process.env.NEXT_PUBLIC_API}${url}`, {
84 method: "PATCH",
85 credentials: "include",
86 headers: {
87 "Content-Type": "application/json"
88 },
89 body: JSON.stringify({ [dataName]: value || def })
90 })
91 .then(async (res) => {
92 const response = await res.json();
93 if (!response) return;
94
95 switch (res.status) {
96 case 200: {
97 setValue(value || def || (type === "text" ? "" : 0) as Type<T>);
98 onSave?.(value || def || (type === "text" ? "" : 0) as Type<T>);
99 setdefaultStateValue(value || def || (type === "text" ? "" : 0) as Type<T>);
100
101 setState(State.Success);
102 setTimeout(() => setState(State.Idle), 1_000 * 8);
103 break;
104 }
105 default: {
106 setState(State.Idle);
107 setError((response as unknown as ApiError).message);
108 break;
109 }
110 }
111
112 })
113 .catch(() => {
114 setState(State.Idle);
115 setError("Error while updatung");
116 });
117
118 }, [valuedebounced]);
119
120 return (
121 <div className={cn("relative w-full", className)}>
122
123 <div className="flex items-center gap-2 mb-1">
124 <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">{name}</span>
125 {state === State.Loading && <TailSpin stroke="#d4d4d4" strokeWidth={8} className="relative h-3 w-3 overflow-visible" />}
126
127 {(resetState && resetState !== value) &&
128 <button
129 className="text-sm ml-auto text-violet-400/60 hover:text-violet-400/90 duration-200"
130 onClick={() => {
131 setValue(resetState);
132 setValueDebounced(resetState);
133 setState(State.Idle);
134 }}
135 disabled={disabled}
136 >
137 reset
138 </button>
139 }
140 </div>
141
142 <DumbTextInput
143 value={value}
144 setValue={(v) => {
145 setValue(v);
146 setValueDebounced(v);
147 setState(State.Idle);
148 }}
149 disabled={disabled}
150 placeholder={placeholder}
151 max={max}
152 type={type}
153 description={description}
154 />
155
156 <div className="flex absolute right-0 bottom-0">
157 {error &&
158 <div className="ml-auto text-red-500 text-sm">
159 {error}
160 </div>
161 }
162 </div>
163
164 </div>
165 );
166}