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

Configure Feed

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

improve inputs and modal

Luna eacf6501 41c923a2

+175 -80
+44 -8
components/Modal.tsx
··· 1 1 "use client"; 2 + import { Progress } from "@nextui-org/react"; 2 3 import { AnimatePresence, motion, MotionConfig } from "framer-motion"; 3 4 import { FunctionComponent, useState } from "react"; 4 5 import { HiX } from "react-icons/hi"; 6 + import TailSpin from "react-loading-icons/dist/esm/components/tail-spin"; 5 7 6 8 import { RouteErrorResponse } from "@/typings"; 9 + import cn from "@/utils/cn"; 7 10 8 11 import ErrorBanner from "./Error"; 9 12 10 13 interface Props { 14 + className?: string; 15 + variant?: "default" | "danger"; 16 + 11 17 title: string; 12 18 children: React.ReactNode; 19 + subChildren?: React.ReactNode; 20 + 13 21 onSubmit?: () => Promise<Response>; 14 22 onSuccess?: () => void; 15 23 onClose: () => void; 24 + 16 25 show: boolean; 17 26 buttonName?: string 18 27 } 19 - const Modal: FunctionComponent<Props> = ({ title, children, onSubmit, onClose, onSuccess, show, buttonName = "Submit" }) => { 28 + const Modal: FunctionComponent<Props> = ({ className, variant, title, children, subChildren, onSubmit, onClose, onSuccess, show, buttonName = "Submit" }) => { 20 29 21 - const [error, setError] = useState<string>(); 30 + const [state, setState] = useState<"LOADING" | undefined>(undefined); 31 + const [error, setError] = useState<string | undefined>(undefined); 22 32 23 33 return ( 24 34 <MotionConfig ··· 33 43 exit="closed" 34 44 variants={{ closed: { opacity: 0 }, open: { opacity: 1 } }} 35 45 className="fixed top-0 left-0 h-screen w-full inset-0 bg-black/70 flex items-center justify-center z-50" 46 + style={{ backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" }} 36 47 > 37 48 <motion.div 38 49 initial="closed" ··· 58 69 " 59 70 > 60 71 61 - <div className="p-4"> 72 + <div className="p-4 pb-0 mb-16 md:mb-0"> 62 73 63 74 <div className="flex items-center"> 64 75 <span className="text-2xl font-semibold dark:text-neutral-200 text-neutral-800">{title}</span> ··· 70 81 </button> 71 82 </div> 72 83 73 - <hr className="mt-2 mb-3 dark:border-wamellow-light border-wamellow-100-light" /> 84 + <Progress 85 + size="sm" 86 + isIndeterminate={state === "LOADING"} 87 + aria-label="Loading..." 88 + className="mt-2 mb-3 h-0.5" 89 + classNames={{ 90 + track: "dark:bg-wamellow-light bg-wamellow-100-light", 91 + indicator: "bg-violet-500" 92 + }} 93 + value={0} 94 + /> 74 95 75 - {error && <ErrorBanner message={error} removeButton={true} />} 96 + <div className={"scrollbar-none p-0.5 pb-4 sm:max-h-[512px] max-h-[384px] overflow-y-scroll " + className}> {/* sm:max-h-[512px] max-h-[384px] overflow-y-scroll */} 97 + {error && <ErrorBanner message={error} removeButton={true} />} 76 98 77 - {children} 99 + {children} 100 + </div> 78 101 79 102 </div> 80 103 81 104 <div className="md:relative absolute bottom-0 left-0 w-full dark:bg-wamellow/40 bg-wamellow-100/40 rounded-bl-md rounded-br-md"> 82 - <div className="flex w-full items-baseline gap-4 p-4"> 105 + <div className="flex items-center w-full gap-4 p-4"> 106 + 107 + {state === "LOADING" ? 108 + <div className="flex items-center gap-2"> 109 + <TailSpin stroke="#a3a3a3" strokeWidth={6} className="relative h-3 w-3 overflow-visible" /> 110 + Submitting... 111 + </div> 112 + : 113 + subChildren 114 + } 83 115 84 116 <button 85 117 onClick={() => onClose()} ··· 90 122 91 123 <button 92 124 onClick={() => { 125 + if (state === "LOADING") return; 93 126 if (!onSubmit) return onClose(); 94 127 128 + setState("LOADING"); 95 129 onSubmit?.() 96 130 .then(async (res) => { 131 + setState(undefined); 97 132 if (res.ok) { 98 133 onClose(); 99 134 onSuccess?.(); ··· 102 137 }) 103 138 .catch((e) => setError(e || "Unknown server error")); 104 139 }} 105 - className="flex bg-violet-600 hover:bg-violet-700 text-neutral-200 font-medium py-2 px-5 duration-200 rounded-md" 140 + className={cn(`flex bg-violet-600 hover:bg-violet-700 text-neutral-200 font-medium py-2 px-5 duration-200 rounded-md ${state === "LOADING" && "opacity-50 cursor-wait"}`, variant === "danger" && "bg-red-500/80 hover:bg-red-600")} 141 + disabled={state === "LOADING"} 106 142 > 107 143 <span>{buttonName}</span> 108 144 </button>
+1 -1
components/OverviewLinkComponent.tsx
··· 14 14 return ( 15 15 <div className={className}> 16 16 <Link href={url}> 17 - <div className="w-full dark:text-neutral-100 text-neutral-900 dark:bg-wamellow bg-wamellow-100 py-4 px-5 mb-6 rounded-md outline-violet-500 hover:outline flex gap-2 group/item duration-100"> 17 + <div className="w-full dark:text-neutral-100 text-neutral-900 dark:bg-wamellow bg-wamellow-100 py-4 px-5 mb-6 rounded-xl outline-violet-500 hover:outline flex gap-2 group/item duration-100"> 18 18 19 19 <div> 20 20
+52 -31
components/inputs/Dumb_TextInput.tsx
··· 1 1 import React, { FunctionComponent, useEffect, useState } from "react"; 2 2 3 + import cn from "@/utils/cn"; 4 + 3 5 type Props = { 4 6 name?: string; 5 7 placeholder?: string; ··· 42 44 </div> 43 45 } 44 46 45 - {max > 300 ? 46 - <textarea 47 - className={className} 48 - placeholder={placeholder} 49 - onChange={(e) => { 50 - if (dataName) { 51 - setValue(JSON.stringify(Object.assign(JSON.parse(value), { [dataName]: type === "color" ? parseInt(e.target.value.slice(1), 16) : e.target.value || null }))); 52 - } else { 53 - setValue(type === "color" ? parseInt(e.target.value.slice(1), 16) : e.target.value || null); 54 - } 55 - }} 56 - value={type === "color" ? `#${(dataName ? JSON.parse(value)[dataName] : value)?.toString(16)}` : dataName ? JSON.parse(value)[dataName] : value} 57 - disabled={disabled} 58 - rows={2} 59 - maxLength={max || Infinity} 60 - /> 47 + {type === "color" ? 48 + <div className={cn(className, "mt-1 h-12 w-full rounded-md border dark:border-wamellow border-wamellow-100 bg-none")} style={{ backgroundColor: `#${(dataName ? JSON.parse(value)[dataName] : value)?.toString(16)}` }}> 49 + <input 50 + className="opacity-0 w-full h-full cursor-pointer" 51 + type="color" 52 + value={`#${(dataName ? JSON.parse(value)[dataName] : value)?.toString(16)}`} 53 + onChange={(e) => { 54 + const color = parseInt(e.target.value.replace(/#/, ""), 16); 55 + 56 + if (dataName) { 57 + setValue(JSON.stringify(Object.assign(JSON.parse(value), { [dataName]: color || null }))); 58 + } else { 59 + setValue(color || null); 60 + } 61 + }} 62 + disabled={disabled} 63 + /> 64 + </div> 61 65 : 62 - <input 63 - className={className} 64 - placeholder={placeholder} 65 - onChange={(e) => { 66 - if (dataName) { 67 - setValue(JSON.stringify(Object.assign(JSON.parse(value), { [dataName]: type === "color" ? parseInt(e.target.value.slice(1), 16) : e.target.value || null }))); 68 - } else { 69 - setValue(type === "color" ? parseInt(e.target.value.slice(1), 16) : e.target.value || null); 70 - } 71 - }} 72 - value={type === "color" ? `#${(dataName ? JSON.parse(value)[dataName] : value)?.toString(16)}` : dataName ? JSON.parse(value)[dataName] : value} 73 - disabled={disabled} 74 - type={type} 75 - maxLength={max || Infinity} 76 - /> 66 + max > 300 ? 67 + <textarea 68 + className={className} 69 + placeholder={placeholder} 70 + onChange={(e) => { 71 + if (dataName) { 72 + setValue(JSON.stringify(Object.assign(JSON.parse(value), { [dataName]: e.target.value || null }))); 73 + } else { 74 + setValue(e.target.value || null); 75 + } 76 + }} 77 + value={dataName ? JSON.parse(value)[dataName] : value} 78 + disabled={disabled} 79 + rows={2} 80 + maxLength={max || Infinity} 81 + /> 82 + : 83 + <input 84 + className={className} 85 + placeholder={placeholder} 86 + onChange={(e) => { 87 + if (dataName) { 88 + setValue(JSON.stringify(Object.assign(JSON.parse(value), { [dataName]: e.target.value || null }))); 89 + } else { 90 + setValue(e.target.value || null); 91 + } 92 + }} 93 + value={dataName ? JSON.parse(value)[dataName] : value} 94 + disabled={disabled} 95 + type={type} 96 + maxLength={max || Infinity} 97 + /> 77 98 } 78 99 79 100 {description && <div className="dark:text-neutral-500 text-neutral-400 text-sm mt-1">{description}</div>}
+19 -14
components/inputs/SelectMenu.tsx
··· 4 4 5 5 import { webStore } from "@/common/webstore"; 6 6 import { RouteErrorResponse } from "@/typings"; 7 + import cn from "@/utils/cn"; 7 8 import { truncate } from "@/utils/truncate"; 8 9 9 10 type Props = { 11 + className?: string; 12 + 10 13 name: string; 11 14 url?: string; 12 - dataName: string; 13 - items: { icon?: React.ReactNode; name: string; value: string | number; error?: string }[] | undefined; 15 + dataName?: string; 16 + items: { icon?: React.ReactNode; name: string; value: string | number | null; error?: string }[] | undefined; 14 17 disabled?: boolean; 15 18 description?: string; 16 - __defaultState?: string | number; 19 + defaultState?: string | number | null; 17 20 18 - onSave?: (options: { name: string; value: string | number; error?: string }) => void; 21 + onSave?: (options: { name: string; value: string | number | null; error?: string }) => void; 19 22 }; 20 23 21 24 22 - const SelectInput: FunctionComponent<Props> = ({ name, url, dataName, items = [], disabled, description, __defaultState, onSave }) => { 25 + const SelectInput: FunctionComponent<Props> = ({ className, name, url, dataName, items = [], disabled, description, defaultState, onSave }) => { 23 26 const web = webStore((w) => w); 24 27 25 28 const [state, setState] = useState<"LOADING" | "ERRORED" | "SUCCESS" | undefined>(); 26 29 const [error, setError] = useState<string>(); 27 30 28 31 const [open, setOpen] = useState<boolean>(false); 29 - const [defaultvalue, setDefaultalue] = useState<string | number | undefined>(); 30 - const [value, setValue] = useState<{ icon?: React.ReactNode; name: string; value: string | number; error?: string } | undefined>(); 32 + const [_defaultvalue, _setDefaultalue] = useState<string | number | null | undefined>(); 33 + const [value, setValue] = useState<{ icon?: React.ReactNode; name: string; value: string | number | null; error?: string } | undefined>(); 31 34 32 35 useEffect(() => { 33 - setValue(items.find((i) => i.value === __defaultState)); 34 - setDefaultalue(__defaultState); 35 - }, [__defaultState]); 36 + setValue(items.find((i) => i.value === defaultState)); 37 + _setDefaultalue(defaultState); 38 + }, [defaultState]); 36 39 37 40 useEffect(() => { 38 41 setError(undefined); 39 - if (!value || value.error || value.value === defaultvalue) { 42 + 43 + if (!value || value.error || value.value === _defaultvalue) { 40 44 setState(undefined); 41 45 return; 42 46 } ··· 48 52 return; 49 53 } 50 54 55 + if (!dataName) throw new Error("Warning: <SelectInput.dataName> must be defined when using <SelectInput.url>."); 56 + 51 57 setState("LOADING"); 52 - 53 58 fetch(`${process.env.NEXT_PUBLIC_API}${url}`, { 54 59 method: "PATCH", 55 60 headers: { ··· 89 94 }, [value]); 90 95 91 96 return ( 92 - <div className="select-none w-full max-w-full mb-3 relative"> 97 + <div className={cn("select-none w-full max-w-full mb-2 relative", className)}> 93 98 <div className="flex items-center gap-2"> 94 99 <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">{name}</span> 95 100 {state === "LOADING" && <TailSpin stroke="#d4d4d4" strokeWidth={8} className="relative h-3 w-3 overflow-visible" />} ··· 124 129 onClick={() => { 125 130 setOpen(false); 126 131 setState(undefined); 127 - if (value?.value) setDefaultalue(value.value); 132 + if (value?.value) _setDefaultalue(value.value); 128 133 setValue(item); 129 134 }} 130 135 >
+32 -11
components/inputs/Switch.tsx
··· 1 1 import { FunctionComponent, useEffect, useState } from "react"; 2 + import { HiCheck } from "react-icons/hi"; 2 3 import { TailSpin } from "react-loading-icons"; 3 4 4 5 import { RouteErrorResponse } from "@/typings"; 5 6 6 7 type Props = { 8 + className?: string; 9 + 7 10 name: string; 8 - url: string; 9 - dataName: string; 11 + url?: string; 12 + dataName?: string; 10 13 disabled?: boolean; 11 14 description?: string; 12 15 defaultState: boolean; 16 + tickbox?: boolean; 13 17 14 18 onSave?: (state: boolean) => void; 15 19 }; 16 20 17 21 18 - const Switch: FunctionComponent<Props> = ({ name, url, dataName, disabled, description, defaultState, onSave }) => { 22 + const Switch: FunctionComponent<Props> = ({ className, name, url, dataName, disabled, description, defaultState, onSave, tickbox }) => { 19 23 const [state, setState] = useState<"LOADING" | "ERRORED" | "SUCCESS" | undefined>(); 20 24 const [error, setError] = useState<string>(); 21 25 ··· 24 28 25 29 useEffect(() => { 26 30 setValue(defaultState); 31 + setChanged(false); 27 32 }, [defaultState]); 28 33 29 34 useEffect(() => { 35 + 36 + if (!url) { 37 + if (!onSave) throw new Error("Warning: <Switch.onSave> must be defined when not using <Switch.url>."); 38 + onSave(value); 39 + setState(undefined); 40 + return; 41 + } 42 + 43 + if (!dataName) throw new Error("Warning: <Switch.dataName> must be defined when using <Switch.url>."); 44 + 30 45 if (!changed) return; 31 46 setError(undefined); 47 + 32 48 setState("LOADING"); 33 - 34 49 fetch(`${process.env.NEXT_PUBLIC_API}${url}`, { 35 50 method: "PATCH", 36 51 headers: { ··· 70 85 }, [value]); 71 86 72 87 return ( 73 - <div className={`relative ${description && "mb-8"}`}> 88 + <div className={`relative ${description && "mb-8"} ` + className}> 74 89 75 - <div className="flex items-center mb-6"> 90 + <div className={`flex items-center ${!tickbox && "mb-6"}`}> 76 91 <div className="flex items-center gap-2"> 77 92 <span className={`sm:text-lg ${value ? "dark:text-neutral-300 text-neutral-700" : "dark:text-neutral-400 text-neutral-600"} font-medium`}>{name}</span> 78 93 {state === "LOADING" && <TailSpin stroke="#d4d4d4" strokeWidth={8} className="relative h-3 w-3 overflow-visible" />} 79 94 </div> 80 95 81 - <label className={`ml-auto relative inline-flex items-center cursor-pointer ${(state === "LOADING" || disabled) && "cursor-not-allowed opacity-50"}`}> 96 + <label className={`ml-auto relative inline-flex items-center cursor-pointer ${(state === "LOADING" || disabled) && "cursor-not-allowed opacity-50"} duration-700`}> 82 97 <input 83 98 type="checkbox" 84 - className="sr-only peer" 99 + className={"sr-only peer"} 85 100 checked={value} 86 101 onChange={() => { 87 102 setState(undefined); ··· 90 105 }} 91 106 disabled={(state === "LOADING" || disabled)} 92 107 /> 93 - <div 94 - className={`w-11 h-6 bg-neutral-300 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-600 ${(state === "LOADING" || disabled) && "cursor-not-allowed"}`} 95 - /> 108 + {tickbox ? 109 + <div className={`w-6 h-6 border ${value ? "bg-violet-500/80 border-violet-500/80" : "dark:bg-wamellow bg-wamellow-100 dark:border-wamellow-light border-wamellow-100-light"} rounded-md flex items-center justify-center`}> 110 + {value && <HiCheck className="dark:text-violet-200 text-violet-800" />} 111 + </div> 112 + : 113 + <div 114 + className={`w-11 h-6 bg-neutral-300 rounded-full peer dark:bg-neutral-700 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all ease-in-out peer-checked:bg-violet-600 ${(state === "LOADING" || disabled) && "cursor-not-allowed"}`} 115 + /> 116 + } 96 117 </label> 97 118 98 119 </div>
+27 -15
components/inputs/TextInput.tsx
··· 7 7 import DumbTextInput from "./Dumb_TextInput"; 8 8 9 9 type Props = { 10 + className?: string; 11 + 10 12 name: string; 11 - url: string; 12 - dataName: string; 13 + url?: string; 14 + dataName?: string; 13 15 disabled?: boolean; 14 16 description?: string; 15 - __defaultState: string | number; 17 + defaultState: string | number; 16 18 resetState?: string | number; 17 19 18 20 type?: string; ··· 23 25 }; 24 26 25 27 26 - const TextInput: FunctionComponent<Props> = ({ name, url, dataName, disabled, description, __defaultState, resetState, type, max, placeholder, onSave }) => { 28 + const TextInput: FunctionComponent<Props> = ({ className, name, url, dataName, disabled, description, defaultState, resetState, type, max, placeholder, onSave }) => { 27 29 const [state, setState] = useState<"LOADING" | "ERRORED" | "SUCCESS" | undefined>(); 28 30 const [error, setError] = useState<string>(); 29 31 30 - const [valuedebounced, setValuedebounced] = useStateDebounced<string | number>("", 1000); 32 + const [valuedebounced, setValueDebounced] = useStateDebounced<string | number>("", 1000); 31 33 const [value, setValue] = useState<string | number>(""); 32 - const [__defaultStatealue, set__defaultStatealue] = useState<string | number>(""); 34 + const [defaultStateValue, setdefaultStateValue] = useState<string | number>(""); 33 35 34 36 useEffect(() => { 35 - if (!__defaultStatealue) set__defaultStatealue(__defaultState); 36 - setValue(__defaultState); 37 - }, [__defaultState]); 37 + if (!defaultStateValue) setdefaultStateValue(defaultState); 38 + setValue(defaultState); 39 + }, [defaultState]); 38 40 39 41 useEffect(() => { 40 - if (__defaultStatealue === value) return; 42 + if (defaultStateValue === value) return; 41 43 setError(undefined); 44 + 45 + if (!url) { 46 + if (!onSave) throw new Error("Warning: <TextInput.onSave> must be defined when not using <TextInput.url>."); 47 + onSave(value); 48 + setState(undefined); 49 + return; 50 + } 51 + 52 + if (!dataName) throw new Error("Warning: <TextInput.dataName> must be defined when using <TextInput.url>."); 53 + 42 54 setState("LOADING"); 43 - 44 55 fetch(`${process.env.NEXT_PUBLIC_API}${url}`, { 45 56 method: "PATCH", 46 57 headers: { ··· 57 68 case 200: { 58 69 setValue(value || 0x000000); 59 70 onSave?.(value || 0x000000); 60 - set__defaultStatealue(value || 0x000000); 71 + setdefaultStateValue(value || 0x000000); 61 72 62 73 setState("SUCCESS"); 63 74 setTimeout(() => setState(undefined), 1_000 * 8); ··· 79 90 }, [valuedebounced]); 80 91 81 92 return ( 82 - <div className="relative w-full"> 93 + <div className={"relative w-full " + className}> 83 94 84 95 <div className="flex items-center gap-2"> 85 96 <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">{name}</span> ··· 90 101 className="text-sm ml-auto text-violet-400/60 hover:text-violet-400/90 duration-200" 91 102 onClick={() => { 92 103 setValue(resetState); 93 - setValuedebounced(resetState); 104 + setValueDebounced(resetState); 94 105 setState(undefined); 95 106 }} 96 107 disabled={disabled} ··· 104 115 value={value} 105 116 setValue={(v) => { 106 117 setValue(v); 107 - setValuedebounced(v); 118 + setValueDebounced(v); 108 119 setState(undefined); 109 120 }} 110 121 disabled={disabled} ··· 113 124 type={type} 114 125 description={description} 115 126 /> 127 + 116 128 117 129 <div className="flex absolute right-0 bottom-0"> 118 130 {(error || state === "ERRORED") && <div className="ml-auto text-red-500 text-sm">{error || "Unknown error while saving"}</div>}