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

Configure Feed

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

add dailyposts

Luna 26c656bf 521ed370

+430 -2
+124
app/dashboard/[guildId]/dailyposts/create.component.tsx
··· 1 + "use client"; 2 + 3 + import { Button, Chip } from "@nextui-org/react"; 4 + import { useMemo, useState } from "react"; 5 + import { HiPencil } from "react-icons/hi"; 6 + 7 + import { guildStore } from "@/common/guilds"; 8 + import MultiSelectMenu from "@/components/inputs/multi-select-menu"; 9 + import SelectMenu from "@/components/inputs/select-menu"; 10 + import Modal from "@/components/modal"; 11 + import { ApiV1GuildsModulesDailypostsGetResponse, DailypostType } from "@/typings"; 12 + 13 + import { generateHourArray, typeToName } from "./util"; 14 + 15 + export const Style = { 16 + Compact: 1, 17 + Big: 2 18 + } as const; 19 + 20 + interface Props { 21 + style: typeof Style[keyof typeof Style]; 22 + add: (dailypost: ApiV1GuildsModulesDailypostsGetResponse) => void; 23 + set: (id: string) => void; 24 + } 25 + 26 + export default function CreateDailypost({ 27 + style, 28 + add, 29 + set 30 + }: Props) { 31 + const guildId = guildStore((g) => g?.id); 32 + const channels = guildStore((g) => g?.channels); 33 + 34 + const [open, setOpen] = useState(false); 35 + const [type, setType] = useState<DailypostType>(DailypostType.Anime); 36 + const [hours, setHours] = useState<number[]>([]); 37 + const [channelId, setChannelId] = useState<string | null>(null); 38 + 39 + const hoursArray = useMemo(() => generateHourArray(), []); 40 + 41 + return (<> 42 + {style === Style.Compact 43 + ? 44 + <Chip 45 + as={Button} 46 + className="default" 47 + onClick={() => setOpen(true)} 48 + startContent={<HiPencil className="relative left-1 ml-1" />} 49 + > 50 + Add Dailypost 51 + </Chip> 52 + : 53 + <Button 54 + color="secondary" 55 + onClick={() => setOpen(true)} 56 + startContent={<HiPencil />} 57 + > 58 + Create a new Dailypost 59 + </Button> 60 + } 61 + 62 + <Modal<ApiV1GuildsModulesDailypostsGetResponse> 63 + className="!overflow-visible" 64 + title="Create new dailypost" 65 + isOpen={open} 66 + onClose={() => setOpen(false)} 67 + onSubmit={() => { 68 + const date = new Date(); 69 + 70 + return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/dailyposts`, { 71 + method: "POST", 72 + credentials: "include", 73 + headers: { 74 + "Content-Type": "application/json" 75 + }, 76 + body: JSON.stringify({ 77 + channelId, 78 + runtimeHours: hours.map((hour) => hour + (date.getTimezoneOffset() / 60)), 79 + type 80 + }) 81 + }); 82 + }} 83 + onSuccess={(tag) => { 84 + add(tag); 85 + set(tag.id); 86 + 87 + setChannelId(null); 88 + }} 89 + > 90 + <SelectMenu 91 + name="Channel" 92 + dataName="channelId" 93 + items={channels?.sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({ name: `#${c.name}`, value: c.id, error: c.missingPermissions.join(", ") }))} 94 + description="Select a channel where dailyposts should be send into." 95 + onSave={(o) => { 96 + setChannelId(o.value as string); 97 + }} 98 + /> 99 + 100 + <MultiSelectMenu 101 + name="Run at" 102 + items={hoursArray} 103 + description="Select one or multiple hours when posts should be made." 104 + onSave={(o) => { 105 + setHours(o.map((i) => i.value as number)); 106 + }} 107 + /> 108 + 109 + <SelectMenu 110 + name="Type" 111 + dataName="type" 112 + items={ 113 + Object.entries(DailypostType) 114 + .filter(([key]) => key.length > 2) 115 + .map(([, value]) => ({ name: typeToName(parseInt(value as string)), value })) 116 + } 117 + description="Select what type of content should be posted daily." 118 + onSave={(o) => { 119 + setType(o.value as number); 120 + }} 121 + /> 122 + </Modal > 123 + </>); 124 + }
+64
app/dashboard/[guildId]/dailyposts/delete.component.tsx
··· 1 + "use client"; 2 + 3 + import { Button, Tooltip } from "@nextui-org/react"; 4 + import { useState } from "react"; 5 + import { HiTrash } from "react-icons/hi"; 6 + 7 + import { guildStore } from "@/common/guilds"; 8 + import Modal from "@/components/modal"; 9 + 10 + interface Props { 11 + id: string | null; 12 + name: string; 13 + 14 + remove: (id: string) => void; 15 + } 16 + 17 + export default function DeleteDailypost({ 18 + id, 19 + name, 20 + 21 + remove 22 + }: Props) { 23 + const guildId = guildStore((g) => g?.id); 24 + const [open, setOpen] = useState(false); 25 + 26 + return (<> 27 + <Tooltip 28 + content="Delete Dailypost" 29 + closeDelay={0} 30 + > 31 + <Button 32 + isIconOnly 33 + color="danger" 34 + variant="flat" 35 + onClick={() => setOpen(true)} 36 + isDisabled={!id} 37 + > 38 + <span> 39 + <HiTrash /> 40 + </span> 41 + <span className="sr-only">Delete selected dailypost</span> 42 + </Button> 43 + </Tooltip> 44 + 45 + <Modal 46 + buttonName="Delete" 47 + variant="danger" 48 + title={"Delete Dailypost: " + name} 49 + isOpen={open} 50 + onClose={() => setOpen(false)} 51 + onSubmit={() => { 52 + return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/dailyposts/${id}`, { 53 + method: "DELETE", 54 + credentials: "include" 55 + }); 56 + }} 57 + onSuccess={() => { 58 + if (id) remove(id); 59 + }} 60 + > 61 + Are you sure you want to delete the {'"'}{name}{'"'} dailypost? It will be gone forever, probably, who knows. 62 + </Modal> 63 + </>); 64 + }
+193
app/dashboard/[guildId]/dailyposts/page.tsx
··· 1 + "use client"; 2 + 3 + import Image from "next/image"; 4 + import { useParams } from "next/navigation"; 5 + import { useMemo } from "react"; 6 + import { HiViewGridAdd } from "react-icons/hi"; 7 + 8 + import { guildStore } from "@/common/guilds"; 9 + import { CreateSplash } from "@/components/dashboard/lists/create-splash"; 10 + import { useList } from "@/components/dashboard/lists/hook"; 11 + import { Navigation } from "@/components/dashboard/lists/navigation"; 12 + import { ItemSelector } from "@/components/dashboard/lists/selector"; 13 + import MultiSelectMenu from "@/components/inputs/multi-select-menu"; 14 + import SelectMenu from "@/components/inputs/select-menu"; 15 + import { ScreenMessage } from "@/components/screen-message"; 16 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 17 + import { ApiV1GuildsModulesDailypostsGetResponse } from "@/typings"; 18 + 19 + import CreateNotification, { Style } from "./create.component"; 20 + import DeleteDailypost from "./delete.component"; 21 + import { generateHourArray, typeToIcon, typeToName } from "./util"; 22 + 23 + export default function Home() { 24 + const guild = guildStore((g) => g); 25 + const params = useParams(); 26 + 27 + const hoursArray = useMemo(() => generateHourArray(), []); 28 + const date = useMemo(() => new Date(), []); 29 + 30 + const url = `/guilds/${params.guildId}/modules/dailyposts` as const; 31 + const { 32 + item, 33 + items, 34 + setItemId, 35 + editItem, 36 + addItem, 37 + removeItem, 38 + isLoading, 39 + error 40 + } = useList<ApiV1GuildsModulesDailypostsGetResponse>({ url }); 41 + 42 + if (error) { 43 + return ( 44 + <ScreenMessage 45 + top="20vh" 46 + title="Something went wrong on this page.." 47 + description={error} 48 + href={`/dashboard/${guild?.id}`} 49 + button="Go back to overview" 50 + icon={<HiViewGridAdd />} 51 + > 52 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 53 + </ScreenMessage> 54 + ); 55 + } 56 + 57 + if (isLoading || !items) return <></>; 58 + 59 + if (!item) { 60 + return ( 61 + <ItemSelector<ApiV1GuildsModulesDailypostsGetResponse> 62 + items={items} 63 + 64 + set={setItemId} 65 + sort={(a, b) => a.type - b.type} 66 + name={(item) => typeToName(item.type)} 67 + 68 + docs="/dailyposts" 69 + 70 + createButton={(options) => ( 71 + <CreateNotification 72 + style={options.style} 73 + add={addItem} 74 + set={setItemId} 75 + /> 76 + )} 77 + 78 + deleteButton={(options) => ( 79 + <DeleteDailypost 80 + id={options.id} 81 + name={options.name} 82 + remove={removeItem} 83 + /> 84 + )} 85 + 86 + item={(item) => { 87 + const channel = guild?.channels?.find((channel) => channel.id === item.channelId); 88 + 89 + return (<> 90 + <Image 91 + alt={`${typeToName} icon`} 92 + className="rounded-full" 93 + src={typeToIcon(item.type)} 94 + width={46} 95 + height={46} 96 + /> 97 + 98 + <div className="flex flex-col items-start"> 99 + <span className="text-neutral-100 text-lg font-medium -mb-[0.5]"> 100 + {typeToName(item.type)} 101 + </span> 102 + 103 + <div className="bg-blurple/50 text-neutral-100 px-1 rounded-md"> 104 + #{channel?.name || "unknown"} 105 + </div> 106 + </div> 107 + </>); 108 + }} 109 + > 110 + <CreateSplash 111 + name="notifications" 112 + description="Notify your community when new videos are released." 113 + > 114 + <CreateNotification 115 + style={Style.Big} 116 + add={addItem} 117 + set={setItemId} 118 + /> 119 + </CreateSplash> 120 + </ItemSelector> 121 + ); 122 + } 123 + 124 + return (<> 125 + <Navigation 126 + href="/dailyposts" 127 + docs="/dailyposts" 128 + 129 + icon={ 130 + <Image 131 + alt={`${typeToName} icon`} 132 + className="rounded-full size-5.5" 133 + src={typeToIcon(item.type)} 134 + width={24} 135 + height={24} 136 + /> 137 + } 138 + name={typeToName(item.type)} 139 + /> 140 + 141 + <div className="flex md:gap-4 gap-2"> 142 + <SelectMenu 143 + name="Channel" 144 + url={url + "/" + item.id} 145 + dataName="channelId" 146 + items={guild?.channels?.sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({ name: `#${c.name}`, value: c.id, error: c.missingPermissions.join(", ") }))} 147 + description="Select a channel where dailyposts should be send into." 148 + defaultState={item.channelId} 149 + onSave={(o) => editItem("channelId", o.value as string)} 150 + /> 151 + 152 + {/* <Fetch 153 + className="w-1/3 md:w-1/6 relative top-8" 154 + url={url + "/" + item.id + "/test"} 155 + icon={<HiChat className="min-h-4 min-w-4" />} 156 + label="Test Message" 157 + method="POST" 158 + size="lg" 159 + /> */} 160 + </div> 161 + 162 + <div className="lg:flex gap-3"> 163 + <SelectMenu 164 + className="lg:w-1/2 w-full" 165 + name="Ping role" 166 + url={url + "/" + item.id} 167 + dataName="roleId" 168 + items={[ 169 + { name: "@everyone (everyone in server)", value: "everyone" }, 170 + { name: "@here (everyone online)", value: "here" }, 171 + ...guild?.roles?.sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({ name: `@${c.name}`, value: c.id, color: c.color })) || [] 172 + ]} 173 + description="Select a role which should get pinged on posts." 174 + defaultState={item.roleId} 175 + onSave={(o) => editItem("roleId", o.value as string)} 176 + showClear 177 + /> 178 + <MultiSelectMenu 179 + className="lg:w-1/2 w-full" 180 + name="Run at" 181 + url={url + "/" + item.id} 182 + dataName="runetimeHours" 183 + items={hoursArray} 184 + defaultState={ 185 + item.runtimeHours 186 + .map(parseInt) 187 + .map((hour) => hour - (date.getTimezoneOffset() / 60)) 188 + } 189 + description="Select one or multiple hours when posts should be made." 190 + /> 191 + </div> 192 + </>); 193 + }
+28
app/dashboard/[guildId]/dailyposts/util.ts
··· 1 + import { DailypostType } from "@/typings"; 2 + 3 + export function typeToName(type: DailypostType) { 4 + switch (type) { 5 + case DailypostType.Anime: return "Anime"; 6 + case DailypostType.Blahaj: return "Blåhaj"; 7 + case DailypostType.NekosBest: return "Nekos.best"; 8 + } 9 + } 10 + 11 + export function typeToIcon(type: DailypostType) { 12 + switch (type) { 13 + case DailypostType.Anime: return "/icons/anime.webp"; 14 + case DailypostType.Blahaj: return "/icons/blahaj.webp"; 15 + case DailypostType.NekosBest: return "/icons/neko.webp"; 16 + } 17 + } 18 + 19 + export function generateHourArray() { 20 + const hoursArray = []; 21 + 22 + for (let hour = 0; hour < 24; hour++) { 23 + const name = `${((hour + 11) % 12 + 1)}${hour < 12 ? "am" : "pm"}`; 24 + hoursArray.push({ name, value: hour }); 25 + } 26 + 27 + return hoursArray; 28 + }
+2 -2
components/list.tsx
··· 108 108 109 109 {tabs.length > 4 && position > 0 && 110 110 <button 111 - className="absolute md:hidden top-1 left-0 bg-wamellow backdrop-blur-lg rounded-xl p-2" 111 + className="absolute xl:hidden top-1 left-0 bg-wamellow backdrop-blur-lg rounded-xl p-2" 112 112 onClick={() => scroll("left")} 113 113 > 114 114 <HiChevronLeft className="size-5" /> ··· 117 117 118 118 {tabs.length > 4 && position < (ref.current?.clientWidth || 0) && 119 119 <button 120 - className="absolute md:hidden top-1 right-0 bg-wamellow backdrop-blur-lg rounded-xl p-2" 120 + className="absolute xl:hidden top-1 right-0 bg-wamellow backdrop-blur-lg rounded-xl p-2" 121 121 onClick={() => scroll("right")} 122 122 > 123 123 <HiChevronRight className="size-5" />
public/icons/blahaj.webp

This is a binary file and will not be displayed.

public/icons/neko.webp

This is a binary file and will not be displayed.

public/icons/waifu.webp

This is a binary file and will not be displayed.

+19
typings.ts
··· 424 424 } 425 425 426 426 } 427 + 428 + export enum DailypostType { 429 + Anime = 1, 430 + Blahaj = 2, 431 + NekosBest = 3 432 + } 433 + 434 + export interface ApiV1GuildsModulesDailypostsGetResponse { 435 + id: string; 436 + guildId: string; 437 + channelId: string; 438 + roleId: string | null; 439 + 440 + runtimeHours: `${number}`[]; 441 + 442 + type: DailypostType; 443 + query: string | null; 444 + } 445 + 427 446 export interface ApiV1UsersMeRankEmojiPutResponse { 428 447 id: string; 429 448 url: string;