The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
at master 5.3 kB view raw
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}