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.

new switch component

Luna 3d33cd2b 8ebf3d35

+264 -351
+9 -9
app/dashboard/[guildId]/greeting/farewell/page.tsx
··· 88 88 <Head /> 89 89 90 90 <Switch 91 - name="Farewell module enabled" 92 - url={`/guilds/${guild?.id}/modules/bye`} 93 - dataName="enabled" 91 + label="Farewell module enabled" 92 + endpoint={`/guilds/${guild?.id}/modules/bye`} 93 + k="enabled" 94 94 defaultState={bye?.enabled || false} 95 95 disabled={false} 96 96 onSave={(s) => { ··· 152 152 <div className={`mt-2 mb-4 border-2 dark:border-wamellow border-wamellow-100 rounded-xl p-6 ${!bye.card.enabled && "pb-[0px]"}`}> 153 153 154 154 <Switch 155 - name="Show image card" 156 - url={`/guilds/${guild?.id}/modules/bye`} 157 - dataName="card.enabled" 155 + label="Show image card" 156 + endpoint={`/guilds/${guild?.id}/modules/bye`} 157 + k="card.enabled" 158 158 defaultState={bye.card.enabled} 159 159 disabled={!bye.enabled} 160 160 onSave={(s) => { ··· 170 170 171 171 {bye.card.enabled && <> 172 172 <Switch 173 - name="Set image inside embed." 174 - url={`/guilds/${guild?.id}/modules/bye`} 175 - dataName="card.inEmbed" 173 + label="Set image inside embed." 174 + endpoint={`/guilds/${guild?.id}/modules/bye`} 175 + k="card.inEmbed" 176 176 defaultState={bye.card.inEmbed || false} 177 177 disabled={!bye.card.enabled || !bye.enabled} 178 178 onSave={(s) => {
+6 -6
app/dashboard/[guildId]/greeting/passport/page.tsx
··· 98 98 /> 99 99 100 100 <Switch 101 - name="Passport module enabled" 102 - url={`/guilds/${guild?.id}/modules/passport`} 103 - dataName="enabled" 101 + label="Passport module enabled" 102 + endpoint={`/guilds/${guild?.id}/modules/passport`} 103 + k="enabled" 104 104 defaultState={passport?.enabled} 105 105 disabled={false} 106 106 onSave={(s) => { ··· 112 112 /> 113 113 114 114 <Switch 115 - name="Send direct message to member on fail" 116 - url={`/guilds/${guild?.id}/modules/passport`} 117 - dataName="sendFailedDm" 115 + label="Send direct message to member on fail" 116 + endpoint={`/guilds/${guild?.id}/modules/passport`} 117 + k="sendFailedDm" 118 118 defaultState={passport?.sendFailedDm} 119 119 disabled={!passport.enabled} 120 120 />
+21 -21
app/dashboard/[guildId]/greeting/welcome/page.tsx
··· 91 91 <Head /> 92 92 93 93 <Switch 94 - name="Welcome module enabled" 95 - url={`/guilds/${guild?.id}/modules/welcome`} 96 - dataName="enabled" 94 + label="Welcome module enabled" 95 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 96 + k="enabled" 97 97 defaultState={welcome?.enabled} 98 98 disabled={false} 99 99 onSave={(s) => { ··· 105 105 /> 106 106 107 107 <Switch 108 - name="Restore members roles and nickname on rejoin" 109 - url={`/guilds/${guild?.id}/modules/welcome`} 110 - dataName="restore" 108 + label="Restore members roles and nickname on rejoin" 109 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 110 + k="restore" 111 111 defaultState={welcome?.restore} 112 112 disabled={!welcome.enabled} 113 113 /> ··· 230 230 <div className={`mt-2 mb-4 border-2 dark:border-wamellow border-wamellow-100 rounded-xl p-6 ${!welcome.card.enabled && "pb-[0px]"}`}> 231 231 232 232 <Switch 233 - name="Show image card" 234 - url={`/guilds/${guild?.id}/modules/welcome`} 235 - dataName="card.enabled" 233 + label="Show image card" 234 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 235 + k="card.enabled" 236 236 defaultState={welcome.card.enabled} 237 237 disabled={!welcome.enabled} 238 238 onSave={(s) => { ··· 248 248 249 249 {welcome.card.enabled && <> 250 250 <Switch 251 - name="Set image inside embed." 252 - url={`/guilds/${guild?.id}/modules/welcome`} 253 - dataName="card.inEmbed" 251 + label="Set image inside embed." 252 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 253 + k="card.inEmbed" 254 254 defaultState={welcome.card.inEmbed || false} 255 255 disabled={!welcome.card.enabled || !welcome.enabled} 256 256 onSave={(s) => { ··· 298 298 299 299 <div className="m-2"> 300 300 <Switch 301 - name="Enabled" 302 - url={`/guilds/${guild?.id}/modules/welcome`} 303 - dataName="dm.enabled" 301 + label="Enabled" 302 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 303 + k="dm.enabled" 304 304 defaultState={welcome.dm?.enabled} 305 305 disabled={!welcome.enabled} 306 306 /> ··· 316 316 </Section> 317 317 318 318 <Switch 319 - name="Enable button" 320 - url={`/guilds/${guild?.id}/modules/welcome`} 321 - dataName="button.enabled" 319 + label="Enable button" 320 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 321 + k="button.enabled" 322 322 defaultState={welcome.button?.enabled} 323 323 disabled={!welcome.enabled} 324 324 onSave={(s) => { ··· 333 333 /> 334 334 335 335 <Switch 336 - name="Ping new member" 336 + label="Ping new member" 337 337 description="Whenever the mention in the greet message should ping or not." 338 - url={`/guilds/${guild?.id}/modules/welcome`} 339 - dataName="button.ping" 338 + endpoint={`/guilds/${guild?.id}/modules/welcome`} 339 + k="button.ping" 340 340 defaultState={welcome.button?.ping || false} 341 341 disabled={!welcome.enabled || !welcome.button?.enabled} 342 342 />
+1 -1
app/dashboard/[guildId]/leaderboards/updating.component.tsx
··· 284 284 /> 285 285 286 286 <Switch 287 - name="Use quotes for text" 287 + label="Use quotes for text" 288 288 isTickbox 289 289 defaultState={leaderboard?.styles?.useQuotes || false} 290 290 onSave={(s) => {
+3 -3
app/dashboard/[guildId]/moderation/page.tsx
··· 73 73 {AUTOMOD_TYPES.map((type) => ( 74 74 <Switch 75 75 key={type} 76 - name={`Block ${type.replace(/^\w/, (c) => c.toUpperCase())}`} 76 + label={`Block ${type.replace(/^\w/, (c) => c.toUpperCase())}`} 77 77 description={`Prevent ${type.replace(/s$/, "")} links from being sent.`} 78 - url={`${url}/${type}`} 79 - dataName="enabled" 78 + endpoint={`${url}/${type}`} 79 + k="enabled" 80 80 defaultState={data.status[type] || false} 81 81 onSave={(value) => { 82 82 data.status[type] = value;
+3 -3
app/dashboard/[guildId]/page.tsx
··· 40 40 </Section> 41 41 42 42 <Switch 43 - name="Embed message links" 43 + label="Embed message links" 44 44 badge="Experimental" 45 - url={`/guilds/${params.guildId}`} 46 - dataName="embedLinks" 45 + endpoint={`/guilds/${params.guildId}`} 46 + k="embedLinks" 47 47 description="Reply with the original content of a message if a message link is sent." 48 48 defaultState={guild?.embedLinks || false} 49 49 />
+21 -21
app/dashboard/[guildId]/starboard/page.tsx
··· 92 92 </div> 93 93 94 94 <Switch 95 - name="Starboard module enabled" 96 - url={url} 97 - dataName="enabled" 95 + label="Starboard module enabled" 96 + endpoint={url} 97 + k="enabled" 98 98 defaultState={data.enabled} 99 99 disabled={false} 100 100 onSave={(v) => edit("enabled", v)} 101 101 /> 102 102 103 103 <Switch 104 - name="Allow bots, apps and webhooks" 105 - url={url} 106 - dataName="allowBots" 104 + label="Allow bots, apps and webhooks" 105 + endpoint={url} 106 + k="allowBots" 107 107 defaultState={data.allowBots} 108 108 disabled={!data.enabled} 109 109 onSave={(v) => edit("allowBots", v)} 110 110 /> 111 111 112 112 <Switch 113 - name="Allow NSFW channels" 114 - url={url} 115 - dataName="allowNSFW" 113 + label="Allow NSFW channels" 114 + endpoint={url} 115 + k="allowNSFW" 116 116 defaultState={data.allowNSFW} 117 117 disabled={!data.enabled} 118 118 onSave={(v) => edit("allowNSFW", v)} 119 119 /> 120 120 121 121 <Switch 122 - name="Allow message edits" 122 + label="Allow message edits" 123 123 description="If a message is being edited, update it in the data." 124 - url={url} 125 - dataName="allowEdits" 124 + endpoint={url} 125 + k="allowEdits" 126 126 defaultState={data.allowEdits} 127 127 disabled={!data.enabled} 128 128 onSave={(v) => edit("allowEdits", v)} 129 129 /> 130 130 131 131 <Switch 132 - name="Allow author reaction" 132 + label="Allow author reaction" 133 133 description="Lets the message author star their own messages." 134 - url={url} 135 - dataName="allowSelfReact" 134 + endpoint={url} 135 + k="allowSelfReact" 136 136 defaultState={data.allowSelfReact} 137 137 disabled={!data.enabled} 138 138 onSave={(v) => edit("allowSelfReact", v)} 139 139 /> 140 140 141 141 <Switch 142 - name="Display stared message reference" 142 + label="Display stared message reference" 143 143 description="Repost the message reply in the data." 144 - url={url} 145 - dataName="displayReference" 144 + endpoint={url} 145 + k="displayReference" 146 146 defaultState={data.displayReference} 147 147 disabled={!data.enabled} 148 148 onSave={(v) => edit("displayReference", v)} 149 149 /> 150 150 151 151 <Switch 152 - name="Delete message from starboard upon losing reactions" 152 + label="Delete message from starboard upon losing reactions" 153 153 description="If a message in the starboard looses the required reactions, it gets deleted." 154 - url={url} 155 - dataName="delete" 154 + endpoint={url} 155 + k="delete" 156 156 defaultState={data.delete} 157 157 disabled={!data.enabled} 158 158 onSave={(v) => edit("delete", v)}
+6 -6
app/dashboard/[guildId]/tts.component.tsx
··· 43 43 showClear 44 44 /> 45 45 <Switch 46 - name="Announce user" 46 + label="Announce user" 47 47 badge="Experimental" 48 - url={`/guilds/${params.guildId}`} 49 - dataName="tts.announceUser" 48 + endpoint={`/guilds/${params.guildId}`} 49 + k="tts.announceUser" 50 50 description="If I should say who is currently speaking via tts." 51 51 defaultState={guild?.tts.announceUser || false} 52 52 /> 53 53 <Switch 54 - name="Queue messages" 55 - url={`/guilds/${params.guildId}`} 56 - dataName="tts.queue" 54 + label="Queue messages" 55 + endpoint={`/guilds/${params.guildId}`} 56 + k="tts.queue" 57 57 description="Queue sent messages instead of refusing to speak." 58 58 defaultState={guild?.tts.queue || false} 59 59 />
+2
bun.lock
··· 9 9 "@nextui-org/react": "^2.6.11", 10 10 "@odiffey/discord-markdown": "^3.3.0", 11 11 "@radix-ui/react-avatar": "^1.1.10", 12 + "@radix-ui/react-checkbox": "^1.3.2", 12 13 "@radix-ui/react-popover": "^1.1.14", 13 14 "@radix-ui/react-separator": "^1.1.7", 14 15 "@radix-ui/react-slot": "^1.2.3", 16 + "@radix-ui/react-switch": "^1.2.5", 15 17 "@radix-ui/react-tooltip": "^1.2.7", 16 18 "autoprefixer": "^10.4.21", 17 19 "class-variance-authority": "^0.7.1",
-41
components/inputs/request.ts
··· 1 - import type { ApiError } from "@/typings"; 2 - 3 - interface Props { 4 - key: string; 5 - body: unknown; 6 - onSuccess: () => void; 7 - onError: (err: string) => void; 8 - } 9 - 10 - export async function request(url: string, { 11 - key, 12 - body, 13 - 14 - onSuccess, 15 - onError 16 - }: Props) { 17 - const res = await fetch([process.env.NEXT_PUBLIC_API, url].join(""), { 18 - method: "PATCH", 19 - credentials: "include", 20 - headers: { 21 - "Content-Type": "application/json" 22 - }, 23 - body: JSON.stringify(key.includes(".") ? 24 - { [key.split(".")[0]]: { [key.split(".")[1]]: body } } 25 - : 26 - { [key]: body } 27 - ) 28 - }) 29 - .catch(() => null); 30 - 31 - if (res?.ok) { 32 - onSuccess(); 33 - return; 34 - } 35 - 36 - 37 - const response = await res?.json() 38 - .catch(() => null); 39 - 40 - onError((response as unknown as ApiError | null)?.message || "unknown server error"); 41 - }
-124
components/inputs/slider-input.tsx
··· 1 - import { Chip, Slider as UiSlider } from "@nextui-org/react"; 2 - import { useState } from "react"; 3 - import { TailSpin } from "react-loading-icons"; 4 - 5 - import { cn } from "@/utils/cn"; 6 - 7 - import { request } from "./request"; 8 - 9 - enum State { 10 - Idle = 0, 11 - Loading = 1, 12 - Success = 2 13 - } 14 - 15 - interface Props { 16 - className?: string; 17 - 18 - name: string; 19 - badge?: string; 20 - disabled?: boolean; 21 - description?: string; 22 - 23 - minValue?: number; 24 - maxValue?: number; 25 - steps?: number; 26 - 27 - url: string; 28 - dataName: string; 29 - defaultState: number; 30 - } 31 - 32 - export default function Slider({ 33 - className, 34 - 35 - name, 36 - badge, 37 - description, 38 - disabled, 39 - 40 - minValue = 0.1, 41 - maxValue = 1, 42 - steps = 0.1, 43 - 44 - url, 45 - dataName, 46 - defaultState 47 - }: Props) { 48 - const [state, setState] = useState<State>(State.Idle); 49 - const [error, setError] = useState<string | null>(null); 50 - 51 - function update(now: number | number[]) { 52 - setState(State.Loading); 53 - setError(null); 54 - 55 - request(url, { 56 - key: dataName, 57 - body: now, 58 - 59 - onSuccess: () => { 60 - setState(State.Success); 61 - setTimeout(() => setState(State.Idle), 1_000 * 8); 62 - }, 63 - onError: (err) => { 64 - setState(State.Idle); 65 - setError(err); 66 - } 67 - }); 68 - } 69 - 70 - return ( 71 - <div className={cn("relative mb-4", className)}> 72 - 73 - <div> 74 - <div className="flex items-center gap-2"> 75 - <span className="sm:text-lg font-medium dark:text-neutral-400 text-neutral-600"> 76 - {name} 77 - </span> 78 - 79 - {badge && 80 - <Chip 81 - variant="flat" 82 - color="secondary" 83 - size="sm" 84 - > 85 - {badge} 86 - </Chip> 87 - } 88 - 89 - {state === State.Loading && 90 - <TailSpin stroke="#d4d4d4" strokeWidth={8} className="relative h-3 w-3 overflow-visible" /> 91 - } 92 - </div> 93 - 94 - <UiSlider 95 - size="md" 96 - step={steps} 97 - color="secondary" 98 - showSteps={true} 99 - maxValue={maxValue} 100 - minValue={minValue} 101 - defaultValue={defaultState} 102 - onChangeEnd={update} 103 - isDisabled={disabled} 104 - /> 105 - </div> 106 - 107 - 108 - <div className="flex gap-2"> 109 - {description && 110 - <div className="text-neutral-500 text-sm"> 111 - {description} 112 - </div> 113 - } 114 - 115 - {error && 116 - <div className="ml-auto text-red-500 text-sm shrink-0"> 117 - {error} 118 - </div> 119 - } 120 - </div> 121 - 122 - </div> 123 - ); 124 - }
+38 -116
components/inputs/switch.tsx
··· 1 - import { Checkbox, Chip, Switch as UiSwitch } from "@nextui-org/react"; 2 - import { useEffect, useState } from "react"; 3 1 import { TailSpin } from "react-loading-icons"; 4 2 5 - import type { ApiError } from "@/typings"; 6 3 import { cn } from "@/utils/cn"; 4 + import { type InputProps, InputState, useInput } from "@/utils/input"; 7 5 8 - enum State { 9 - Idle = 0, 10 - Loading = 1, 11 - Success = 2 12 - } 6 + import { Badge } from "../ui/badge"; 7 + import { Checkbox } from "../ui/checkbox"; 8 + import { Switch } from "../ui/switch"; 13 9 14 10 interface Props { 15 - className?: string; 16 - 17 - name: string; 18 - badge?: string; 19 - disabled?: boolean; 20 - description?: string; 21 - isTickbox?: boolean; 22 - 23 - url?: string; 24 - dataName?: string; 25 - defaultState: boolean; 26 - 27 - onSave?: (state: boolean) => void; 11 + badge: string; 12 + isTickbox: boolean; 28 13 } 29 14 30 - export default function Switch({ 15 + export default function InputSwitch({ 31 16 className, 32 17 33 - name, 18 + label, 34 19 badge, 35 20 description, 21 + isTickbox, 36 22 disabled, 37 - isTickbox = false, 23 + 24 + endpoint, 25 + k, 38 26 39 - url, 40 - dataName, 41 27 defaultState, 28 + transform, 42 29 43 30 onSave 44 - }: Props) { 45 - const [state, setState] = useState<State>(State.Idle); 46 - const [error, setError] = useState<string | null>(null); 47 - 48 - const [value, setValue] = useState<boolean>(defaultState); 49 - 50 - useEffect(() => { 51 - setValue(defaultState); 52 - }, [defaultState]); 53 - 54 - function update(now: boolean) { 55 - setState(State.Loading); 56 - setError(null); 57 - 58 - setValue(now); 59 - 60 - if (!url) { 61 - if (!onSave) { 62 - setValue(!now); 63 - throw new Error("Warning: <Switch.onSave> must be defined when not using <Switch.url>."); 64 - } 65 - 66 - setState(State.Idle); 67 - onSave(value); 68 - return; 69 - } 70 - 71 - if (!dataName) { 72 - setValue(!now); 73 - throw new Error("Warning: <Switch.dataName> must be defined when using <Switch.url>."); 74 - } 75 - 76 - fetch(`${process.env.NEXT_PUBLIC_API}${url}`, { 77 - method: "PATCH", 78 - credentials: "include", 79 - headers: { 80 - "Content-Type": "application/json" 81 - }, 82 - body: JSON.stringify(dataName.includes(".") ? 83 - { [dataName.split(".")[0]]: { [dataName.split(".")[1]]: now } } 84 - : 85 - { [dataName]: now } 86 - ) 87 - }) 88 - .then(async (res) => { 89 - const response = await res.json(); 90 - if (!response) return; 31 + }: InputProps<boolean> & Partial<Props>) { 32 + const { 33 + value, 34 + state, 35 + error, 36 + update 37 + } = useInput({ 38 + endpoint, 39 + k, 91 40 92 - switch (res.status) { 93 - case 200: { 94 - onSave?.(now); 41 + defaultState, 42 + transform, 95 43 96 - setState(State.Success); 97 - setTimeout(() => setState(State.Idle), 1_000 * 8); 98 - break; 99 - } 100 - default: { 101 - setValue(!now); 102 - 103 - setState(State.Idle); 104 - setError((response as unknown as ApiError).message); 105 - break; 106 - } 107 - } 108 - 109 - }) 110 - .catch(() => { 111 - setValue(!now); 112 - 113 - setState(State.Idle); 114 - setError("Error while saving"); 115 - }); 116 - 117 - } 44 + onSave 45 + }); 118 46 119 47 return ( 120 48 <div className={cn("relative", description && "mb-2", className)}> 121 - 122 49 <div className={cn("flex items-center gap-2", !isTickbox && "mb-6")}> 123 50 <div className="flex items-center gap-2"> 124 51 <span ··· 127 54 value && "dark:text-neutral-300 text-neutral-700" 128 55 )} 129 56 > 130 - {name} 57 + {label} 131 58 </span> 132 59 133 60 {badge && 134 - <Chip 61 + <Badge 135 62 variant="flat" 136 - color="secondary" 137 63 size="sm" 138 64 > 139 65 {badge} 140 - </Chip> 66 + </Badge> 141 67 } 142 68 143 - {state === State.Loading && 69 + {state === InputState.Loading && 144 70 <TailSpin stroke="#d4d4d4" strokeWidth={8} className="relative h-3 w-3 overflow-visible" /> 145 71 } 146 72 </div> ··· 148 74 {isTickbox ? 149 75 <Checkbox 150 76 className="ml-auto" 151 - isSelected={value} 152 - onValueChange={update} 153 - aria-label={name} 154 - color="secondary" 155 - isDisabled={disabled} 77 + checked={value} 78 + onCheckedChange={update} 79 + disabled={disabled} 156 80 /> 157 81 : 158 - <UiSwitch 82 + <Switch 159 83 className="ml-auto" 160 - isSelected={value} 161 - onValueChange={update} 162 - aria-label={name} 163 - color="secondary" 164 - isDisabled={disabled} 84 + checked={value} 85 + onCheckedChange={update} 86 + aria-label={label} 87 + disabled={disabled} 165 88 /> 166 89 } 167 90 </div> ··· 180 103 </div> 181 104 } 182 105 </div> 183 - 184 106 </div> 185 107 ); 186 108 }
+30
components/ui/checkbox.tsx
··· 1 + "use client"; 2 + 3 + import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 + import { Check } from "lucide-react"; 5 + import * as React from "react"; 6 + 7 + import { cn } from "@/utils/cn"; 8 + 9 + const Checkbox = React.forwardRef< 10 + React.ElementRef<typeof CheckboxPrimitive.Root>, 11 + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> 12 + >(({ className, ...props }, ref) => ( 13 + <CheckboxPrimitive.Root 14 + ref={ref} 15 + className={cn( 16 + "peer size-6 shrink-0 rounded-md border border-muted ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-wamellow data-[state=checked]:bg-secondary data-[state=checked]:text-secondary-foreground data-[state=checked]:border-secondary duration-100", 17 + className 18 + )} 19 + {...props} 20 + > 21 + <CheckboxPrimitive.Indicator 22 + className={cn("flex items-center justify-center text-current")} 23 + > 24 + <Check className="size-6 p-1" /> 25 + </CheckboxPrimitive.Indicator> 26 + </CheckboxPrimitive.Root> 27 + )); 28 + Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 + 30 + export { Checkbox };
+29
components/ui/switch.tsx
··· 1 + "use client"; 2 + 3 + import * as SwitchPrimitives from "@radix-ui/react-switch"; 4 + import * as React from "react"; 5 + 6 + import { cn } from "@/utils/cn"; 7 + 8 + const Switch = React.forwardRef< 9 + React.ElementRef<typeof SwitchPrimitives.Root>, 10 + React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> 11 + >(({ className, ...props }, ref) => ( 12 + <SwitchPrimitives.Root 13 + className={cn( 14 + "peer inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-secondary data-[state=unchecked]:bg-muted", 15 + className 16 + )} 17 + {...props} 18 + ref={ref} 19 + > 20 + <SwitchPrimitives.Thumb 21 + className={cn( 22 + "pointer-events-none block size-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-[22px] data-[state=unchecked]:translate-x-0.5" 23 + )} 24 + /> 25 + </SwitchPrimitives.Root> 26 + )); 27 + Switch.displayName = SwitchPrimitives.Root.displayName; 28 + 29 + export { Switch };
+2
package.json
··· 18 18 "@nextui-org/react": "^2.6.11", 19 19 "@odiffey/discord-markdown": "^3.3.0", 20 20 "@radix-ui/react-avatar": "^1.1.10", 21 + "@radix-ui/react-checkbox": "^1.3.2", 21 22 "@radix-ui/react-popover": "^1.1.14", 22 23 "@radix-ui/react-separator": "^1.1.7", 23 24 "@radix-ui/react-slot": "^1.2.3", 25 + "@radix-ui/react-switch": "^1.2.5", 24 26 "@radix-ui/react-tooltip": "^1.2.7", 25 27 "autoprefixer": "^10.4.21", 26 28 "class-variance-authority": "^0.7.1",
+93
utils/input.ts
··· 1 + import type { HTMLProps } from "react"; 2 + import { useCallback, useRef, useState } from "react"; 3 + 4 + import type { ApiError } from "@/typings"; 5 + 6 + 7 + export enum InputState { 8 + Idle = 0, 9 + Loading = 1, 10 + Success = 2 11 + } 12 + 13 + interface InputOptions<T> { 14 + endpoint?: string; 15 + k?: string; 16 + 17 + defaultState: T; 18 + transform?: (value: T) => unknown; 19 + 20 + onSave?: (value: T) => void; 21 + } 22 + 23 + export type InputProps<T> = InputOptions<T> & HTMLProps<HTMLDivElement> & { 24 + label: string; 25 + description?: string; 26 + disabled?: boolean; 27 + }; 28 + 29 + export function useInput<T>(options: InputOptions<T>) { 30 + const [value, setValue] = useState<T>(options.defaultState); 31 + const [state, setState] = useState<InputState>(InputState.Idle); 32 + const [error, setError] = useState<string | null>(null); 33 + const timeout = useRef<NodeJS.Timeout | null>(null); 34 + 35 + const onSave = useCallback( 36 + async (val: T) => { 37 + options.onSave?.(val); 38 + 39 + if (!options.endpoint || !options.k) return; 40 + 41 + if (timeout.current) { 42 + clearTimeout(timeout.current); 43 + timeout.current = null; 44 + } 45 + 46 + setState(InputState.Loading); 47 + setError(null); 48 + 49 + const res = await fetch(process.env.NEXT_PUBLIC_API + options.endpoint, { 50 + method: "PATCH", 51 + credentials: "include", 52 + headers: { 53 + "Content-Type": "application/json" 54 + }, 55 + body: JSON.stringify(options.k.includes(".") 56 + ? { [options.k.split(".")[0]]: { [options.k.split(".")[1]]: options.transform?.(val) || val } } 57 + : { [options.k]: options.transform?.(val) || val } 58 + ) 59 + }) 60 + .catch((error) => String(error)); 61 + 62 + if (typeof res === "string" || !res.ok) { 63 + setState(InputState.Idle); 64 + 65 + if (typeof res === "string") { 66 + setError(res); 67 + } else { 68 + const data = await res 69 + .json() 70 + .catch(() => null) as ApiError | null; 71 + 72 + setError(data?.message || "Unknown error"); 73 + } 74 + 75 + return; 76 + } 77 + 78 + setState(InputState.Success); 79 + timeout.current = setTimeout(() => setState(InputState.Idle), 1_000 * 8); 80 + }, 81 + [options.onSave, options.endpoint, options.k, options.transform] 82 + ); 83 + 84 + return { 85 + value, 86 + state, 87 + error, 88 + update: (val: T) => { 89 + setValue(val); 90 + onSave(val); 91 + } 92 + }; 93 + }