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.

use api hook over YOLO fetching

Luna 4ede6f6f 229961be

+225 -414
+54 -90
app/dashboard/[guildId]/greeting/farewell/page.tsx
··· 1 1 "use client"; 2 - import { Button } from "@nextui-org/react"; 3 2 import Image from "next/image"; 4 3 import Link from "next/link"; 5 4 import { useParams } from "next/navigation"; 6 - import { useEffect, useState } from "react"; 7 5 import { HiArrowLeft, HiChat, HiExternalLink } from "react-icons/hi"; 8 6 9 7 import { guildStore } from "@/common/guilds"; ··· 15 13 import SelectMenu from "@/components/inputs/select-menu"; 16 14 import Switch from "@/components/inputs/switch"; 17 15 import Notice from "@/components/notice"; 18 - import type { ApiError, ApiV1GuildsModulesByeGetResponse } from "@/typings"; 16 + import { Button } from "@/components/ui/button"; 17 + import { useApi } from "@/lib/api/hook"; 18 + import type { ApiV1GuildsModulesByeGetResponse } from "@/typings"; 19 + import { cn } from "@/utils/cn"; 19 20 import { createSelectableItems } from "@/utils/create-selectable-items"; 20 21 21 22 export default function Home() { 22 23 const guild = guildStore((g) => g); 23 24 const user = userStore((s) => s); 24 25 25 - const [error, setError] = useState<string>(); 26 - const [bye, setBye] = useState<ApiV1GuildsModulesByeGetResponse>(); 27 - 28 26 const params = useParams(); 29 - 30 - useEffect(() => { 31 - 32 - fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${params.guildId}/modules/bye`, { 33 - credentials: "include" 34 - }) 35 - .then(async (res) => { 36 - const response = await res.json() as ApiV1GuildsModulesByeGetResponse; 37 - if (!response) return; 38 - 39 - switch (res.status) { 40 - case 200: { 41 - setBye(response); 42 - break; 43 - } 44 - default: { 45 - setBye(undefined); 46 - setError((response as unknown as ApiError).message); 47 - break; 48 - } 49 - } 50 - 51 - }) 52 - .catch(() => { 53 - setError("Error while fetching farewell data"); 54 - }); 55 - 56 - }, []); 27 + const { data, isLoading, error, edit } = useApi<ApiV1GuildsModulesByeGetResponse>(`/guilds/${params.guildId}/modules/bye`); 57 28 58 29 const Head = () => ( 59 30 <div className="flex justify-between relative bottom-2 mb-3"> 60 31 <Button 61 - as={Link} 62 - href={`/dashboard/${guild?.id}/greeting`} 63 - startContent={<HiArrowLeft />} 32 + asChild 64 33 size="sm" 65 34 > 66 - Back 35 + <Link href={`/dashboard/${guild?.id}/greeting`}> 36 + <HiArrowLeft /> 37 + Back 38 + </Link> 67 39 </Button> 68 40 <Button 69 - as={Link} 70 - href="/docs/greetings" 71 - target="_blank" 72 - endContent={<HiExternalLink />} 41 + asChild 73 42 size="sm" 74 43 > 75 - Read docs & view placeholders 44 + <Link 45 + href="/docs/greetings" 46 + target="_blank" 47 + > 48 + <HiExternalLink /> 49 + Read docs & view placeholders 50 + </Link> 76 51 </Button> 77 52 </div> 78 53 ); 79 54 80 - if (bye === undefined) return ( 55 + if (isLoading) return <></>; 56 + 57 + if (!data || error) return ( 81 58 <div> 82 59 <Head /> 83 60 {error && <Notice message={error} />} ··· 91 68 label="Farewell module enabled" 92 69 endpoint={`/guilds/${guild?.id}/modules/bye`} 93 70 k="enabled" 94 - defaultState={bye?.enabled || false} 71 + defaultState={data.enabled || false} 95 72 disabled={false} 96 73 onSave={(s) => { 97 - setBye({ 98 - ...bye, 99 - enabled: s 100 - }); 74 + edit("enabled", s); 101 75 }} 102 76 /> 103 77 104 78 <NumberInput 105 79 name="After how many seconds the message should be deleted" 106 - description="Set to 0 to disable." 80 + description="Set to 0 to disable" 107 81 url={`/guilds/${guild?.id}/modules/bye`} 108 82 dataName="deleteAfter" 109 - defaultState={bye?.deleteAfter ?? 0} 110 - disabled={!bye.enabled} 83 + defaultState={data.deleteAfter ?? 0} 84 + disabled={!data.enabled} 111 85 /> 112 86 113 87 <div className="flex md:gap-4 gap-2"> ··· 116 90 url={`/guilds/${guild?.id}/modules/bye`} 117 91 dataName="channelId" 118 92 items={createSelectableItems(guild?.channels)} 119 - description="Select the channel where the farewell message should be send into." 120 - defaultState={bye?.channelId} 121 - disabled={!bye.enabled} 93 + description="Select the channel where the farewell message should be send into" 94 + defaultState={data.channelId} 95 + disabled={!data.enabled} 122 96 /> 123 97 124 98 <Fetch ··· 135 109 name="Message" 136 110 url={`/guilds/${guild?.id}/modules/bye`} 137 111 dataName="message" 138 - defaultMessage={bye?.message} 139 - messageAttachmentComponent={bye.card.enabled && 112 + defaultMessage={data.message} 113 + messageAttachmentComponent={data.card.enabled && ( 140 114 <Image 141 - src={`https://image-api.wamellow.com/?type=join&username=${encodeURIComponent(user?.username as string)}&members=1090&hash=${encodeURIComponent(user?.id as string)}/${encodeURIComponent(user?.avatar as string)}${bye.card.background ? `&background=${encodeURIComponent(bye.card.background)}` : ""}`} 115 + src={`https://image-api.wamellow.com/?type=leave&username=${encodeURIComponent(user?.username as string)}&members=1090&hash=${encodeURIComponent(user?.id as string)}/${encodeURIComponent(user?.avatar as string)}${data.card.background ? `&background=${encodeURIComponent(data.card.background)}` : ""}`} 142 116 width={1024 / 2} 143 117 height={(256 + 16) / 2} 144 118 loading="lazy" 145 119 alt="" 146 120 /> 147 - } 148 - showMessageAttachmentComponentInEmbed={bye.card.inEmbed} 149 - disabled={!bye.enabled} 121 + )} 122 + showMessageAttachmentComponentInEmbed={data.card.inEmbed} 123 + disabled={!data.enabled} 150 124 > 151 125 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 - 126 + <div className={cn("mt-2 mb-4 border-2 dark:border-wamellow border-wamellow-100 rounded-xl p-6", !data.card.enabled && "pb-0")}> 154 127 <Switch 155 128 label="Show image card" 156 129 endpoint={`/guilds/${guild?.id}/modules/bye`} 157 130 k="card.enabled" 158 - defaultState={bye.card.enabled} 159 - disabled={!bye.enabled} 131 + defaultState={data.card.enabled} 132 + disabled={!data.enabled} 160 133 onSave={(s) => { 161 - setBye({ 162 - ...bye, 163 - card: { 164 - ...bye.card, 165 - enabled: s 166 - } 134 + edit("card", { 135 + ...data.card, 136 + enabled: s 167 137 }); 168 138 }} 169 139 /> 170 140 171 - {bye.card.enabled && <> 141 + {data.card.enabled && <> 172 142 <Switch 173 - label="Set image inside embed." 143 + label="Set image inside embed" 174 144 endpoint={`/guilds/${guild?.id}/modules/bye`} 175 145 k="card.inEmbed" 176 - defaultState={bye.card.inEmbed || false} 177 - disabled={!bye.card.enabled || !bye.enabled} 146 + defaultState={data.card.inEmbed || false} 147 + disabled={!data.card.enabled || !data.enabled} 178 148 onSave={(s) => { 179 - setBye({ 180 - ...bye, 181 - card: { 182 - ...bye.card, 183 - inEmbed: s 184 - } 149 + edit("card", { 150 + ...data.card, 151 + inEmbed: s 185 152 }); 186 153 }} 187 154 /> ··· 192 159 ratio="aspect-[4/1]" 193 160 dataName="card.background" 194 161 description="Enter a url which should be the background for the image card. The recomended image ration is 4:1 and recommended resolution 1024x256px." 195 - defaultState={bye.card.background || ""} 196 - disabled={!bye.card.enabled || !bye.enabled} 197 - onSave={(v) => { 198 - setBye({ 199 - ...bye, 200 - card: { 201 - ...bye.card, 202 - background: v 203 - } 162 + defaultState={data.card.background || ""} 163 + disabled={!data.card.enabled || !data.enabled} 164 + onSave={(s) => { 165 + edit("card", { 166 + ...data.card, 167 + background: s 204 168 }); 205 169 }} 206 170 />
+14 -19
app/dashboard/[guildId]/greeting/passport/complete-setup.tsx
··· 1 - import React, { useEffect, useState } from "react"; 1 + import { useEffect, useState } from "react"; 2 2 3 3 import type { Guild } from "@/common/guilds"; 4 4 import SelectMenu from "@/components/inputs/select-menu"; 5 5 import Modal from "@/components/modal"; 6 + import type { ApiEdit } from "@/lib/api/hook"; 6 7 import type { ApiV1GuildsModulesPassportGetResponse } from "@/typings"; 7 8 import { createSelectableItems } from "@/utils/create-selectable-items"; 8 9 ··· 14 15 15 16 interface Props { 16 17 guild: Guild | undefined; 17 - passport: ApiV1GuildsModulesPassportGetResponse; 18 - setPassport: React.Dispatch<React.SetStateAction<ApiV1GuildsModulesPassportGetResponse | undefined>>; 18 + data: ApiV1GuildsModulesPassportGetResponse; 19 + edit: ApiEdit<ApiV1GuildsModulesPassportGetResponse>; 19 20 } 20 21 21 22 export default function CompleteSetup({ 22 23 guild, 23 - passport, 24 - setPassport 24 + data, 25 + edit 25 26 }: Props) { 26 27 const [modal, setModal] = useState<ModalType>(ModalType.None); 27 28 28 29 const [roleId, setRoleId] = useState<string>(); 29 30 30 31 useEffect(() => { 31 - if (!passport?.enabled) return; 32 + if (!data.enabled) return; 32 33 33 - if (!passport.successRoleId) { 34 + if (!data.successRoleId) { 34 35 setModal(ModalType.VerifiedRole); 35 36 return; 36 37 } 37 38 38 - if (passport.punishment === 2 && !passport.punishmentRoleId) { 39 + if (data.punishment === 2 && !data.punishmentRoleId) { 39 40 setModal(ModalType.PunishmentRole); 40 41 return; 41 42 } 42 - }, [passport]); 43 + }, [data]); 43 44 44 45 return (<> 45 46 <Modal ··· 60 61 }); 61 62 }} 62 63 onSuccess={() => { 63 - setPassport({ 64 - ...passport, 65 - successRoleId: roleId 66 - }); 64 + edit("successRoleId", roleId); 67 65 }} 68 66 > 69 67 <SelectMenu 70 68 name="Role" 71 69 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 72 70 description="Select what role members should get when completing verification." 73 - defaultState={passport.punishmentRoleId} 71 + defaultState={data.punishmentRoleId} 74 72 onSave={(o) => { 75 73 setRoleId(o.value as string); 76 74 }} ··· 95 93 }); 96 94 }} 97 95 onSuccess={() => { 98 - setPassport({ 99 - ...passport, 100 - punishmentRoleId: roleId 101 - }); 96 + edit("punishmentRoleId", roleId); 102 97 }} 103 98 > 104 99 <SelectMenu 105 100 name="Role" 106 101 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 107 102 description="Select what role members should get when failing verification." 108 - defaultState={passport.punishmentRoleId} 103 + defaultState={data.punishmentRoleId} 109 104 onSave={(o) => { 110 105 setRoleId(o.value as string); 111 106 }}
+52 -83
app/dashboard/[guildId]/greeting/passport/page.tsx
··· 1 1 "use client"; 2 - import { Button } from "@nextui-org/react"; 3 2 import Link from "next/link"; 4 3 import { useParams } from "next/navigation"; 5 - import { useEffect, useState } from "react"; 6 - import { HiArrowLeft, HiArrowNarrowLeft, HiExternalLink, HiFingerPrint } from "react-icons/hi"; 4 + import { HiArrowLeft, HiExternalLink, HiFingerPrint } from "react-icons/hi"; 7 5 8 6 import { guildStore } from "@/common/guilds"; 9 7 import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; ··· 11 9 import Switch from "@/components/inputs/switch"; 12 10 import Notice from "@/components/notice"; 13 11 import { OverviewLink } from "@/components/overview-link"; 14 - import type { ApiError, ApiV1GuildsModulesPassportGetResponse } from "@/typings"; 12 + import { Button } from "@/components/ui/button"; 13 + import { useApi } from "@/lib/api/hook"; 14 + import type { ApiV1GuildsModulesPassportGetResponse } from "@/typings"; 15 15 import { createSelectableItems } from "@/utils/create-selectable-items"; 16 16 import { getCanonicalUrl } from "@/utils/urls"; 17 17 ··· 20 20 export default function Home() { 21 21 const guild = guildStore((g) => g); 22 22 23 - const [error, setError] = useState<string>(); 24 - const [passport, setPassport] = useState<ApiV1GuildsModulesPassportGetResponse>(); 25 - 26 23 const params = useParams(); 27 - 28 - useEffect(() => { 29 - 30 - fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${params.guildId}/modules/passport`, { 31 - credentials: "include" 32 - }) 33 - .then(async (res) => { 34 - const response = await res.json() as ApiV1GuildsModulesPassportGetResponse; 35 - if (!response) return; 36 - 37 - switch (res.status) { 38 - case 200: { 39 - setPassport(response); 40 - break; 41 - } 42 - default: { 43 - setPassport(undefined); 44 - setError((response as unknown as ApiError).message); 45 - break; 46 - } 47 - } 48 - 49 - }) 50 - .catch(() => { 51 - setError("Error while fetching passport data"); 52 - }); 24 + const { data, isLoading, error, edit } = useApi<ApiV1GuildsModulesPassportGetResponse>(`/guilds/${params.guildId}/modules/passport`); 53 25 54 - }, []); 55 - 56 - if (passport === undefined) return ( 57 - <div> 58 - <Link href={`/dashboard/${guild?.id}/greeting`} className="button-underline relative bottom-3 mb-4"> 59 - <HiArrowNarrowLeft /> Greetings 60 - </Link> 61 - {error && <Notice message={error} />} 62 - </div> 63 - ); 64 - 65 - return (<> 26 + const Head = () => ( 66 27 <div className="flex justify-between relative bottom-2 mb-3"> 67 28 <Button 68 - as={Link} 69 - href={`/dashboard/${guild?.id}/greeting`} 70 - startContent={<HiArrowLeft />} 29 + asChild 71 30 size="sm" 72 31 > 73 - Back 32 + <Link href={`/dashboard/${guild?.id}/greeting`}> 33 + <HiArrowLeft /> 34 + Back 35 + </Link> 74 36 </Button> 75 37 <Button 76 - as={Link} 77 - href="/docs/passport" 78 - target="_blank" 79 - endContent={<HiExternalLink />} 38 + asChild 80 39 size="sm" 81 40 > 82 - Read docs 41 + <Link 42 + href="/docs/passport" 43 + target="_blank" 44 + > 45 + <HiExternalLink /> 46 + Read docs & view placeholders 47 + </Link> 83 48 </Button> 84 49 </div> 50 + ); 85 51 86 - {passport.enabled && passport.punishment === 2 && !passport.punishmentRoleId && 52 + if (isLoading) return <></>; 53 + 54 + if (!data || error) return ( 55 + <div> 56 + <Head /> 57 + {error && <Notice message={error} />} 58 + </div> 59 + ); 60 + 61 + return (<> 62 + <Head /> 63 + 64 + {data.enabled && data.punishment === 2 && !data.punishmentRoleId && 87 65 <Notice message="A punishment role must be set when using 'Assign role to member'." /> 88 66 } 89 67 90 - {passport.enabled && !passport.successRoleId && 68 + {data.enabled && !data.successRoleId && 91 69 <Notice message="A verified role must be set for passport to work." /> 92 70 } 93 71 94 72 <CompleteSetup 95 73 guild={guild} 96 - passport={passport} 97 - setPassport={setPassport} 74 + data={data} 75 + edit={edit} 98 76 /> 99 77 100 78 <Switch 101 79 label="Passport module enabled" 102 80 endpoint={`/guilds/${guild?.id}/modules/passport`} 103 81 k="enabled" 104 - defaultState={passport?.enabled} 82 + defaultState={data.enabled} 105 83 disabled={false} 106 84 onSave={(s) => { 107 - setPassport({ 108 - ...passport, 109 - enabled: s 110 - }); 85 + edit("enabled", s); 111 86 }} 112 87 /> 113 88 ··· 115 90 label="Send direct message to member on fail" 116 91 endpoint={`/guilds/${guild?.id}/modules/passport`} 117 92 k="sendFailedDm" 118 - defaultState={passport?.sendFailedDm} 119 - disabled={!passport.enabled} 93 + defaultState={data.sendFailedDm} 94 + disabled={!data.enabled} 120 95 /> 121 96 122 97 <SelectInput ··· 125 100 dataName="channelId" 126 101 items={createSelectableItems(guild?.channels)} 127 102 description="Select the channel where verification logs should be send into." 128 - defaultState={passport?.channelId} 129 - disabled={!passport.enabled} 103 + defaultState={data.channelId} 104 + disabled={!data.enabled} 130 105 /> 131 106 132 107 <div className="lg:flex gap-3"> ··· 137 112 dataName="unverifiedRoleId" 138 113 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 139 114 description="Select what role members should get when joining." 140 - defaultState={passport?.unverifiedRoleId} 115 + defaultState={data.unverifiedRoleId} 141 116 showClear 142 - disabled={!passport.enabled} 117 + disabled={!data.enabled} 143 118 /> 144 119 </div> 145 120 ··· 150 125 dataName="successRoleId" 151 126 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 152 127 description="Select what role members should get when completing verification." 153 - defaultState={passport?.successRoleId} 154 - disabled={!passport.enabled} 128 + defaultState={data.successRoleId} 129 + disabled={!data.enabled} 155 130 /> 156 131 </div> 157 132 </div> ··· 168 143 { name: "Assign role to member", value: 2 } 169 144 ]} 170 145 description="Choose what should happen if a member failes verification." 171 - defaultState={passport?.punishment} 172 - disabled={!passport.enabled} 146 + defaultState={data.punishment} 147 + disabled={!data.enabled} 173 148 onSave={(o) => { 174 - setPassport({ 175 - ...passport, 176 - punishment: o.value as ApiV1GuildsModulesPassportGetResponse["punishment"] 177 - }); 149 + edit("punishment", o.value as ApiV1GuildsModulesPassportGetResponse["punishment"]); 178 150 }} 179 151 /> 180 152 </div> ··· 186 158 dataName="punishmentRoleId" 187 159 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 188 160 description="Select what role members should get when failing verification." 189 - defaultState={passport?.punishmentRoleId} 190 - disabled={!passport.enabled || passport.punishment !== 2} 161 + defaultState={data.punishmentRoleId} 162 + disabled={!data.enabled || data.punishment !== 2} 191 163 onSave={(o) => { 192 - setPassport({ 193 - ...passport, 194 - punishment: o.value as ApiV1GuildsModulesPassportGetResponse["punishment"] 195 - }); 164 + edit("punishmentRoleId", o.value as string); 196 165 }} 197 166 /> 198 167 </div>
+79 -119
app/dashboard/[guildId]/greeting/welcome/page.tsx
··· 1 1 "use client"; 2 - import { Button } from "@nextui-org/react"; 3 2 import Image from "next/image"; 4 3 import Link from "next/link"; 5 4 import { useParams } from "next/navigation"; 6 - import { useEffect, useState } from "react"; 7 5 import { HiArrowLeft, HiChat, HiExternalLink } from "react-icons/hi"; 8 6 9 7 import { guildStore } from "@/common/guilds"; ··· 17 15 import Switch from "@/components/inputs/switch"; 18 16 import Notice from "@/components/notice"; 19 17 import { Section } from "@/components/section"; 20 - import type { ApiError, ApiV1GuildsModulesWelcomeGetResponse } from "@/typings"; 18 + import { Button } from "@/components/ui/button"; 19 + import { useApi } from "@/lib/api/hook"; 20 + import type { ApiV1GuildsModulesWelcomeGetResponse } from "@/typings"; 21 + import { cn } from "@/utils/cn"; 21 22 import { createSelectableEmojiItems, createSelectableItems } from "@/utils/create-selectable-items"; 22 23 23 24 export default function Home() { 24 25 const guild = guildStore((g) => g); 25 26 const user = userStore((s) => s); 26 27 27 - const [error, setError] = useState<string>(); 28 - const [welcome, setWelcome] = useState<ApiV1GuildsModulesWelcomeGetResponse>(); 29 - 30 28 const params = useParams(); 31 - 32 - useEffect(() => { 33 - 34 - fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${params.guildId}/modules/welcome`, { 35 - credentials: "include" 36 - }) 37 - .then(async (res) => { 38 - const response = await res.json() as ApiV1GuildsModulesWelcomeGetResponse; 39 - if (!response) return; 40 - 41 - switch (res.status) { 42 - case 200: { 43 - setWelcome(response); 44 - break; 45 - } 46 - default: { 47 - setWelcome(undefined); 48 - setError((response as unknown as ApiError).message); 49 - break; 50 - } 51 - } 52 - 53 - }) 54 - .catch(() => { 55 - setError("Error while fetching welcome data"); 56 - }); 57 - 58 - }, []); 29 + const { data, isLoading, error, edit } = useApi<ApiV1GuildsModulesWelcomeGetResponse>(`/guilds/${params.guildId}/modules/welcome`); 59 30 60 31 const Head = () => ( 61 32 <div className="flex justify-between relative bottom-2 mb-3"> 62 33 <Button 63 - as={Link} 64 - href={`/dashboard/${guild?.id}/greeting`} 65 - startContent={<HiArrowLeft />} 34 + asChild 66 35 size="sm" 67 36 > 68 - Back 37 + <Link href={`/dashboard/${guild?.id}/greeting`}> 38 + <HiArrowLeft /> 39 + Back 40 + </Link> 69 41 </Button> 70 42 <Button 71 - as={Link} 72 - href="/docs/greetings" 73 - target="_blank" 74 - endContent={<HiExternalLink />} 43 + asChild 75 44 size="sm" 76 45 > 77 - Read docs & view placeholders 46 + <Link 47 + href="/docs/farewell" 48 + target="_blank" 49 + > 50 + <HiExternalLink /> 51 + Read docs & view placeholders 52 + </Link> 78 53 </Button> 79 54 </div> 80 55 ); 81 56 82 - if (welcome === undefined) return ( 57 + if (isLoading) return <></>; 58 + 59 + if (!data || error) return ( 83 60 <div> 84 61 <Head /> 85 - 86 62 {error && <Notice message={error} />} 87 63 </div> 88 64 ); ··· 94 70 label="Welcome module enabled" 95 71 endpoint={`/guilds/${guild?.id}/modules/welcome`} 96 72 k="enabled" 97 - defaultState={welcome?.enabled} 73 + defaultState={data.enabled || false} 98 74 disabled={false} 99 75 onSave={(s) => { 100 - setWelcome({ 101 - ...welcome, 102 - enabled: s 103 - }); 76 + edit("enabled", s); 104 77 }} 105 78 /> 106 79 ··· 108 81 label="Restore members roles and nickname on rejoin" 109 82 endpoint={`/guilds/${guild?.id}/modules/welcome`} 110 83 k="restore" 111 - defaultState={welcome?.restore} 112 - disabled={!welcome.enabled} 84 + defaultState={data.restore} 85 + disabled={!data.enabled} 113 86 /> 114 87 115 88 <Switch ··· 117 90 description="This only takes affect if the user joined less than 24h ago." 118 91 endpoint={`/guilds/${guild?.id}/modules/welcome`} 119 92 k="deleteAfterLeave" 120 - defaultState={welcome?.deleteAfterLeave || false} 121 - disabled={!welcome.enabled} 93 + defaultState={data.deleteAfterLeave || false} 94 + disabled={!data.enabled} 122 95 /> 123 96 124 97 <NumberInput ··· 126 99 description="Set to 0 to disable." 127 100 url={`/guilds/${guild?.id}/modules/welcome`} 128 101 dataName="deleteAfter" 129 - defaultState={welcome?.deleteAfter ?? 0} 130 - disabled={!welcome.enabled} 102 + defaultState={data.deleteAfter ?? 0} 103 + disabled={!data.enabled} 131 104 /> 132 105 133 106 <div className="flex md:gap-4 gap-2"> ··· 138 111 dataName="channelId" 139 112 items={createSelectableItems(guild?.channels)} 140 113 description="Select the channel where the welcome message should be send into." 141 - defaultState={welcome?.channelId} 142 - disabled={!welcome.enabled} 114 + defaultState={data.channelId} 115 + disabled={!data.enabled} 143 116 showClear 144 117 /> 145 118 ··· 161 134 dataName="roleIds" 162 135 items={createSelectableItems(guild?.roles, ["RoleHirachy"])} 163 136 description="Select roles which members should get." 164 - defaultState={welcome?.roleIds} 137 + defaultState={data.roleIds} 165 138 max={5} 166 - disabled={!welcome.enabled} 139 + disabled={!data.enabled} 167 140 /> 168 141 </div> 169 142 ··· 174 147 dataName="pingIds" 175 148 items={createSelectableItems(guild?.channels, ["ViewChannel", "SendMessages"])} 176 149 description="Select in what channels user should get ghostpinged." 177 - defaultState={welcome?.pingIds} 150 + defaultState={data.pingIds} 178 151 max={5} 179 - disabled={!welcome.enabled} 152 + disabled={!data.enabled} 180 153 /> 181 154 </div> 182 155 </div> ··· 189 162 dataName="reactions.firstMessageEmojis" 190 163 items={createSelectableEmojiItems(guild?.emojis)} 191 164 description="Select emotes which will be reacted with on members first message." 192 - defaultState={welcome?.reactions?.firstMessageEmojis} 165 + defaultState={data.reactions?.firstMessageEmojis} 193 166 max={2} 194 - disabled={!welcome.enabled} 167 + disabled={!data.enabled} 195 168 /> 196 169 </div> 197 170 ··· 202 175 dataName="reactions.welcomeMessageEmojis" 203 176 items={createSelectableEmojiItems(guild?.emojis)} 204 177 description="Select emotes which will be reacted with on welcome messages." 205 - defaultState={welcome?.reactions?.welcomeMessageEmojis} 178 + defaultState={data.reactions?.welcomeMessageEmojis} 206 179 max={2} 207 - disabled={!welcome.enabled} 180 + disabled={!data.enabled} 208 181 /> 209 182 </div> 210 183 </div> ··· 213 186 name="Message" 214 187 url={`/guilds/${guild?.id}/modules/welcome`} 215 188 dataName="message" 216 - defaultMessage={welcome?.message} 217 - messageAttachmentComponent={welcome.card.enabled && 189 + defaultMessage={data.message} 190 + messageAttachmentComponent={data.card.enabled && ( 218 191 <Image 219 - src={`https://image-api.wamellow.com/?type=join&username=${encodeURIComponent(user?.username as string)}&members=1090&hash=${encodeURIComponent(user?.id as string)}/${encodeURIComponent(user?.avatar as string)}${welcome.card.background ? `&background=${encodeURIComponent(welcome.card.background)}` : ""}`} 192 + src={`https://image-api.wamellow.com/?type=join&username=${encodeURIComponent(user?.username as string)}&members=1090&hash=${encodeURIComponent(user?.id as string)}/${encodeURIComponent(user?.avatar as string)}${data.card.background ? `&background=${encodeURIComponent(data.card.background)}` : ""}`} 220 193 width={1024 / 2} 221 194 height={(256 + 16) / 2} 222 195 loading="lazy" 223 196 alt="" 224 197 /> 225 - } 226 - showMessageAttachmentComponentInEmbed={welcome.card.inEmbed} 227 - disabled={!welcome.enabled} 198 + )} 199 + showMessageAttachmentComponentInEmbed={data.card.inEmbed} 200 + disabled={!data.enabled} 228 201 > 229 202 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 - 203 + <div className={cn("mt-2 mb-4 border-2 dark:border-wamellow border-wamellow-100 rounded-xl p-6", !data.card.enabled && "pb-0")}> 232 204 <Switch 233 205 label="Show image card" 234 206 endpoint={`/guilds/${guild?.id}/modules/welcome`} 235 207 k="card.enabled" 236 - defaultState={welcome.card.enabled} 237 - disabled={!welcome.enabled} 208 + defaultState={data.card.enabled} 209 + disabled={!data.enabled} 238 210 onSave={(s) => { 239 - setWelcome({ 240 - ...welcome, 241 - card: { 242 - ...welcome.card, 243 - enabled: s 244 - } 211 + edit("card", { 212 + ...data.card, 213 + enabled: s 245 214 }); 246 215 }} 247 216 /> 248 217 249 - {welcome.card.enabled && <> 218 + {data.card.enabled && <> 250 219 <Switch 251 - label="Set image inside embed." 220 + label="Set image inside embed" 252 221 endpoint={`/guilds/${guild?.id}/modules/welcome`} 253 222 k="card.inEmbed" 254 - defaultState={welcome.card.inEmbed || false} 255 - disabled={!welcome.card.enabled || !welcome.enabled} 223 + defaultState={data.card.inEmbed || false} 224 + disabled={!data.card.enabled || !data.enabled} 256 225 onSave={(s) => { 257 - setWelcome({ 258 - ...welcome, 259 - card: { 260 - ...welcome.card, 261 - inEmbed: s 262 - } 226 + edit("card", { 227 + ...data.card, 228 + inEmbed: s 263 229 }); 264 230 }} 265 231 /> ··· 270 236 ratio="aspect-[4/1]" 271 237 dataName="card.background" 272 238 description="Enter a url which should be the background for the image card. The recomended image ration is 4:1 and recommended resolution 1024x256px." 273 - defaultState={welcome.card.background || ""} 274 - disabled={!welcome.card.enabled || !welcome.enabled} 275 - onSave={(v) => { 276 - setWelcome({ 277 - ...welcome, 278 - card: { 279 - ...welcome.card, 280 - background: v 281 - } 239 + defaultState={data.card.background || ""} 240 + disabled={!data.card.enabled || !data.enabled} 241 + onSave={(s) => { 242 + edit("card", { 243 + ...data.card, 244 + background: s 282 245 }); 283 246 }} 284 247 /> ··· 291 254 name="Direct Message" 292 255 url={`/guilds/${guild?.id}/modules/welcome`} 293 256 dataName="dm.message" 294 - defaultMessage={welcome.dm?.message} 257 + defaultMessage={data.dm?.message} 295 258 isCollapseable={true} 296 - disabled={!welcome.enabled} 259 + disabled={!data.enabled} 297 260 > 298 261 299 262 <div className="m-2"> ··· 301 264 label="Enabled" 302 265 endpoint={`/guilds/${guild?.id}/modules/welcome`} 303 266 k="dm.enabled" 304 - defaultState={welcome.dm?.enabled} 305 - disabled={!welcome.enabled} 267 + defaultState={data.dm?.enabled} 268 + disabled={!data.enabled} 306 269 /> 307 270 </div> 308 271 ··· 319 282 label="Enable button" 320 283 endpoint={`/guilds/${guild?.id}/modules/welcome`} 321 284 k="button.enabled" 322 - defaultState={welcome.button?.enabled} 323 - disabled={!welcome.enabled} 285 + defaultState={data.button?.enabled} 286 + disabled={!data.enabled} 324 287 onSave={(s) => { 325 - setWelcome({ 326 - ...welcome, 327 - button: { 328 - ...welcome.button, 329 - enabled: s 330 - } 288 + edit("button", { 289 + ...data.button, 290 + enabled: s 331 291 }); 332 292 }} 333 293 /> ··· 337 297 description="Whenever the mention in the greet message should ping or not." 338 298 endpoint={`/guilds/${guild?.id}/modules/welcome`} 339 299 k="button.ping" 340 - defaultState={welcome.button?.ping || false} 341 - disabled={!welcome.enabled || !welcome.button?.enabled} 300 + defaultState={data.button?.ping || false} 301 + disabled={!data.enabled || !data.button?.enabled} 342 302 /> 343 303 344 304 <div className="lg:flex gap-3 pt-3"> ··· 361 321 })) 362 322 } 363 323 description="Select the color of the button." 364 - defaultState={welcome?.button?.style} 365 - disabled={!welcome.enabled || !welcome.button?.enabled} 324 + defaultState={data.button?.style} 325 + disabled={!data.enabled || !data.button?.enabled} 366 326 /> 367 327 </div> 368 328 <div className="lg:w-1/2"> ··· 372 332 dataName="button.emoji" 373 333 items={createSelectableEmojiItems(guild?.emojis)} 374 334 description="Select an emoji which will be used in the button." 375 - defaultState={welcome?.button?.emoji} 376 - disabled={!welcome.enabled || !welcome.button?.enabled} 335 + defaultState={data.button?.emoji} 336 + disabled={!data.enabled || !data.button?.enabled} 377 337 /> 378 338 </div> 379 339 </div>
+8 -45
app/dashboard/[guildId]/moderation/page.tsx
··· 1 1 "use client"; 2 2 3 3 import { ChannelType } from "discord-api-types/v10"; 4 - import Image from "next/image"; 5 4 import { useParams } from "next/navigation"; 6 - import { useCallback } from "react"; 7 - import { HiViewGridAdd } from "react-icons/hi"; 8 - import { useQuery, useQueryClient } from "react-query"; 9 5 10 6 import { guildStore } from "@/common/guilds"; 11 7 import MultiSelectMenu from "@/components/inputs/multi-select-menu"; 12 8 import Switch from "@/components/inputs/switch"; 13 - import { ScreenMessage } from "@/components/screen-message"; 14 - import { cacheOptions, getData } from "@/lib/api"; 15 - import SadWumpusPic from "@/public/sad-wumpus.gif"; 9 + import Notice from "@/components/notice"; 10 + import { useApi } from "@/lib/api/hook"; 16 11 import { type ApiV1GuildsModulesAutomodGetResponse, AutomodType } from "@/typings"; 17 12 import { createSelectableItems } from "@/utils/create-selectable-items"; 18 13 ··· 25 20 const params = useParams(); 26 21 27 22 const url = `/guilds/${params.guildId}/modules/automod` as const; 28 - const queryClient = useQueryClient(); 29 - 30 - const { data, isLoading, error } = useQuery( 31 - url, 32 - () => getData<ApiV1GuildsModulesAutomodGetResponse>(url), 33 - { 34 - enabled: !!params.guildId, 35 - ...cacheOptions 36 - } 37 - ); 23 + const { data, isLoading, error, edit } = useApi<ApiV1GuildsModulesAutomodGetResponse>(url); 38 24 39 25 const enabled = data && !("message" in data) && Object.values(data.status).some(Boolean); 40 26 41 - const edit = useCallback( 42 - <K extends keyof ApiV1GuildsModulesAutomodGetResponse>(key: K, value: ApiV1GuildsModulesAutomodGetResponse[K]) => { 43 - if (!data || "message" in data) return; 27 + if (isLoading) return <></>; 44 28 45 - queryClient.setQueryData<ApiV1GuildsModulesAutomodGetResponse>(url, () => ({ 46 - ...data, 47 - [key]: value 48 - })); 49 - }, 50 - [data] 29 + if (!data || error) return ( 30 + <div> 31 + {error && <Notice message={error} />} 32 + </div> 51 33 ); 52 - 53 - if (error || (data && "message" in data)) { 54 - return ( 55 - <ScreenMessage 56 - top="0rem" 57 - title="Something went wrong on this page.." 58 - description={ 59 - (data && "message" in data ? data.message : `${error}`) 60 - || "An unknown error occurred."} 61 - href={`/dashboard/${guild?.id}`} 62 - button="Go back to overview" 63 - icon={<HiViewGridAdd />} 64 - > 65 - <Image src={SadWumpusPic} alt="" height={141} width={124} /> 66 - </ScreenMessage> 67 - ); 68 - } 69 - 70 - if (isLoading || !data) return <></>; 71 34 72 35 return (<> 73 36 {AUTOMOD_TYPES.map((type) => (
+9 -44
app/dashboard/[guildId]/starboard/page.tsx
··· 4 4 import Image from "next/image"; 5 5 import Link from "next/link"; 6 6 import { useParams } from "next/navigation"; 7 - import { useCallback } from "react"; 8 - import { HiExternalLink, HiViewGridAdd } from "react-icons/hi"; 9 - import { useQuery, useQueryClient } from "react-query"; 7 + import { HiExternalLink } from "react-icons/hi"; 10 8 11 9 import { guildStore } from "@/common/guilds"; 12 10 import { DiscordMarkdown } from "@/components/discord/markdown"; ··· 17 15 import SelectMenu from "@/components/inputs/select-menu"; 18 16 import Switch from "@/components/inputs/switch"; 19 17 import TextInput from "@/components/inputs/text-input"; 20 - import { ScreenMessage } from "@/components/screen-message"; 21 - import { cacheOptions, getData } from "@/lib/api"; 22 - import SadWumpusPic from "@/public/sad-wumpus.gif"; 18 + import Notice from "@/components/notice"; 19 + import { useApi } from "@/lib/api/hook"; 23 20 import { type ApiV1GuildsModulesStarboardGetResponse, StarboardStyle } from "@/typings"; 24 21 import { createSelectableItems } from "@/utils/create-selectable-items"; 25 22 ··· 30 27 const params = useParams(); 31 28 32 29 const url = `/guilds/${params.guildId}/modules/starboard` as const; 33 - const queryClient = useQueryClient(); 34 - 35 - const { data, isLoading, error } = useQuery( 36 - url, 37 - () => getData<ApiV1GuildsModulesStarboardGetResponse>(url), 38 - { 39 - enabled: !!params.guildId, 40 - ...cacheOptions 41 - } 42 - ); 30 + const { data, isLoading, error, edit } = useApi<ApiV1GuildsModulesStarboardGetResponse>(url); 43 31 44 32 const example = useExample(data && !("message" in data) 45 33 ? data.style 46 34 : StarboardStyle.Username 47 35 ); 48 36 49 - const edit = useCallback( 50 - <K extends keyof ApiV1GuildsModulesStarboardGetResponse>(key: K, value: ApiV1GuildsModulesStarboardGetResponse[K]) => { 51 - if (!data || "message" in data) return; 37 + if (isLoading) return <></>; 52 38 53 - queryClient.setQueryData<ApiV1GuildsModulesStarboardGetResponse>(url, () => ({ 54 - ...data, 55 - [key]: value 56 - })); 57 - }, 58 - [data] 39 + if (!data || error) return ( 40 + <div> 41 + {error && <Notice message={error} />} 42 + </div> 59 43 ); 60 - 61 - if (error || (data && "message" in data)) { 62 - return ( 63 - <ScreenMessage 64 - top="0rem" 65 - title="Something went wrong on this page.." 66 - description={ 67 - (data && "message" in data ? data.message : `${error}`) 68 - || "An unknown error occurred."} 69 - href={`/dashboard/${guild?.id}`} 70 - button="Go back to overview" 71 - icon={<HiViewGridAdd />} 72 - > 73 - <Image src={SadWumpusPic} alt="" height={141} width={124} /> 74 - </ScreenMessage> 75 - ); 76 - } 77 - 78 - if (isLoading || !data) return <></>; 79 44 80 45 return (<> 81 46 <div className="flex justify-between relative bottom-2 mb-3">
+6 -6
app/profile/billing/page.tsx
··· 20 20 import { Separator } from "@/components/ui/separator"; 21 21 import { Skeleton } from "@/components/ui/skeleton"; 22 22 import { type ApiEdit, useApi } from "@/lib/api/hook"; 23 - import type { ApiV1UsersMeBillingGetRequest, ApiV1UsersMeGuildsGetResponse } from "@/typings"; 23 + import type { ApiV1UsersMeBillingGetResponse, ApiV1UsersMeGuildsGetResponse } from "@/typings"; 24 24 25 25 export default function Home() { 26 26 const user = userStore((u) => u); 27 27 const [changeDonationModalOpen, setChangeDonationModalOpen] = useState(false); 28 28 29 - const { data, isLoading, error, edit } = useApi<ApiV1UsersMeBillingGetRequest>("/users/@me/billing"); 29 + const { data, isLoading, error, edit } = useApi<ApiV1UsersMeBillingGetResponse>("/users/@me/billing"); 30 30 31 31 if ((isLoading && !user?.premium) || (!isLoading && !data) || (data && data.status !== "active")) { 32 32 return (<> ··· 143 143 ); 144 144 } 145 145 146 - function PortalButton({ data }: { data: ApiV1UsersMeBillingGetRequest; }) { 146 + function PortalButton({ data }: { data: ApiV1UsersMeBillingGetResponse; }) { 147 147 const path = getPortalPath(data); 148 148 149 149 return ( ··· 158 158 ); 159 159 } 160 160 161 - function getPortalPath(data: ApiV1UsersMeBillingGetRequest) { 161 + function getPortalPath(data: ApiV1UsersMeBillingGetResponse) { 162 162 if (data.cancelAtPeriodEnd) return "subscriptions/" + data.subscriptionId + "/reactivate"; 163 163 return "subscriptions/" + data.subscriptionId + "/cancel"; 164 164 } 165 165 166 - function PaymentMethodIcon({ method }: { method: ApiV1UsersMeBillingGetRequest["paymentMethod"]; }) { 166 + function PaymentMethodIcon({ method }: { method: ApiV1UsersMeBillingGetResponse["paymentMethod"]; }) { 167 167 if (typeof method === "string") { 168 168 return <HiCreditCard className="size-6" />; 169 169 } ··· 240 240 open: boolean; 241 241 setOpen: (open: boolean) => void; 242 242 donationQuantity: number; 243 - edit: ApiEdit<ApiV1UsersMeBillingGetRequest>; 243 + edit: ApiEdit<ApiV1UsersMeBillingGetResponse>; 244 244 }) { 245 245 const [donation, setDonation] = useState(defaultDonationQuantity); 246 246 const [terms, setTerms] = useState(false);
+2 -7
lib/api/hook.ts
··· 3 3 4 4 import { cacheOptions, getData } from "@/lib/api"; 5 5 6 - interface ApiError { 7 - status: number; 8 - message: string; 9 - } 10 - 11 6 export type ApiEdit<T> = <K extends keyof T>(key: K, value: T[K]) => void; 12 7 13 8 export function useApi<T>(url: string, enabled?: boolean) { 14 9 15 10 const { data, isLoading, error } = useQuery( 16 11 url, 17 - () => getData<T | ApiError>(url), 12 + () => getData<T>(url), 18 13 { 19 14 enabled: enabled || true, 20 15 ...cacheOptions ··· 33 28 [data] 34 29 ); 35 30 36 - if (data && typeof data === "object" && "message" in data) { 31 + if (data && typeof data === "object" && "message" in data && typeof data.message === "string") { 37 32 return { data: undefined, isLoading, error: data.message || "unknown error", edit }; 38 33 } 39 34
+1 -1
typings.ts
··· 316 316 type: ConnectionType; 317 317 } 318 318 319 - export interface ApiV1UsersMeBillingGetRequest { 319 + export interface ApiV1UsersMeBillingGetResponse { 320 320 subscriptionId: string; 321 321 status: 'active' 322 322 | 'canceled'