From 834138394d2f01b585026bc726937a99ca8bd979 Mon Sep 17 00:00:00 2001 From: eduardozgz Date: Wed, 16 Oct 2024 16:17:37 +0200 Subject: [PATCH] Dynamic demo data --- apps/website/src/@types/resources.d.ts | 57 +++- .../homepage/demo-servers/CreateInput.tsx | 38 +++ .../homepage/demo-servers/DemoServers.tsx | 26 ++ .../demo-servers/DisplayDemoServer.tsx | 32 +++ .../homepage/demo-servers/ListDemoServers.tsx | 31 +++ .../demo-servers/[id]/ChannelCard.tsx | 149 +++++++++++ .../demo-servers/[id]/DeleteButton.tsx | 66 +++++ .../homepage/demo-servers/[id]/LinkCard.tsx | 59 +++++ .../demo-servers/[id]/ManageDemoServer.tsx | 206 +++++++++++++++ .../admin/homepage/demo-servers/[id]/page.tsx | 48 ++++ .../website/src/app/admin/homepage/layout.tsx | 28 ++ apps/website/src/app/admin/homepage/page.tsx | 7 + .../src/app/admin/users/[id]/ManageUser.tsx | 40 +-- apps/website/src/app/components/Footer.tsx | 8 + .../LandingPage/DiscordDemo/DemoServer.tsx | 12 - .../DiscordDemo/DescriptionArea.tsx | 22 +- .../DiscordDemo/DescriptionAreaTitle.tsx | 3 +- .../LandingPage/DiscordDemo/ServerNavMenu.tsx | 23 +- .../LandingPage/DiscordDemo/index.tsx | 243 +----------------- apps/website/src/i18n/locales/en-US/main.json | 57 +++- apps/website/src/server/api/root.ts | 2 + .../src/server/api/routers/demoServers.ts | 104 ++++++++ packages/common/src/DemoServers.ts | 44 ++++ packages/common/src/Routes.ts | 3 + packages/common/src/UserPermissions.ts | 1 + packages/db/prisma/schema.prisma | 22 ++ packages/ui/package.json | 2 +- packages/ui/src/button.tsx | 44 ++-- packages/ui/src/textarea.tsx | 23 ++ 29 files changed, 1097 insertions(+), 303 deletions(-) create mode 100644 apps/website/src/app/admin/homepage/demo-servers/CreateInput.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/DemoServers.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/DisplayDemoServer.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/ListDemoServers.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/[id]/ChannelCard.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/[id]/DeleteButton.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/[id]/LinkCard.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/[id]/ManageDemoServer.tsx create mode 100644 apps/website/src/app/admin/homepage/demo-servers/[id]/page.tsx create mode 100644 apps/website/src/app/admin/homepage/layout.tsx create mode 100644 apps/website/src/app/admin/homepage/page.tsx delete mode 100644 apps/website/src/app/components/LandingPage/DiscordDemo/DemoServer.tsx create mode 100644 apps/website/src/server/api/routers/demoServers.ts create mode 100644 packages/common/src/DemoServers.ts create mode 100644 packages/ui/src/textarea.tsx diff --git a/apps/website/src/@types/resources.d.ts b/apps/website/src/@types/resources.d.ts index 5c26d48ec..140360cee 100644 --- a/apps/website/src/@types/resources.d.ts +++ b/apps/website/src/@types/resources.d.ts @@ -65,6 +65,7 @@ interface Resources { "admin": "Admin", "manageUsers": "Manage users", "manageServers": "Manage servers", + "manageHomePage": "Manage homepage", "sentry": "Sentry", "copyright": "© {{year}} Member Counter. All rights reserved. Created by .", "madePossibleThanksTo": "Made possible thanks to Alex, , , Frosty, and many more." @@ -478,7 +479,8 @@ interface Resources { "seeUsers": "See users", "manageUsers": "Manage users", "seeGuilds": "See servers", - "manageGuilds": "Manage servers" + "manageGuilds": "Manage servers", + "manageHomePage": "Manage landing page" }, "badges": { "title": "Badges", @@ -500,6 +502,59 @@ interface Resources { "loadUser": "Load user", "recentUsers": "Recent users", "userNotRegistered": "This user isn't registered." + }, + "homePage": { + "demoServers": { + "title": "Demo servers", + "createInputPlaceholder": "New demo server name", + "createBtn": "Create", + "list": { + "display": { + "priority": "Priority: {{priority}}" + } + }, + "manage": { + "title": "Edit demo server", + "name": "Server name", + "description": "Description", + "priority": "Priority", + "icon": "Icon URL", + "channels": { + "title": "Channels", + "add": "Add channel", + "channel": { + "name": "Name", + "type": "Type", + "types": { + "announcementChannel": "Announcement channel", + "voiceChannel": "Voice channel", + "categoryChannel": "Category channel", + "textChannel": "Text channel" + }, + "topic": "Topic", + "showAsSkeleton": "Show as skeleton", + "remove": "Remove" + } + }, + "links": { + "title": "Links", + "add": "Add link", + "link": { + "label": "Label", + "url": "URL", + "remove": "Remove" + } + }, + "saved": "Saved", + "save": "Save" + }, + "delete": { + "button": "Delete demo server", + "dialogTitle": "Are you absolutely sure?", + "dialogDescription": "This action cannot be undone.", + "closeButton": "Close" + } + } } } }, diff --git a/apps/website/src/app/admin/homepage/demo-servers/CreateInput.tsx b/apps/website/src/app/admin/homepage/demo-servers/CreateInput.tsx new file mode 100644 index 000000000..14290d0f5 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/CreateInput.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { Input } from "@mc/ui/input"; + +import { Routes } from "~/other/routes"; +import { api } from "~/trpc/react"; + +export function CreateInput() { + const { t } = useTranslation(); + const [name, setName] = useState(""); + const createDemoServer = api.demoServers.create.useMutation(); + const router = useRouter(); + + const create = async () => { + if (!name) return; + const demoServer = await createDemoServer.mutateAsync({ name: name }); + router.push(Routes.ManageHomeDemoServer(demoServer.id)); + }; + + return ( +
+ setName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && create()} + placeholder={t( + "pages.admin.homePage.demoServers.createInputPlaceholder", + )} + /> + +
+ ); +} diff --git a/apps/website/src/app/admin/homepage/demo-servers/DemoServers.tsx b/apps/website/src/app/admin/homepage/demo-servers/DemoServers.tsx new file mode 100644 index 000000000..f785ceaa0 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/DemoServers.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { Card, CardContent, CardHeader } from "@mc/ui/card"; +import { Input } from "@mc/ui/input"; +import { TypographyH4 } from "@mc/ui/TypographyH4"; + +import { CreateInput } from "./CreateInput"; +import { ListDemoServers } from "./ListDemoServers"; + +export function DemoServers() { + const { t } = useTranslation(); + return ( + + + + {t("pages.admin.homePage.demoServers.title")} + + + + + + + + ); +} diff --git a/apps/website/src/app/admin/homepage/demo-servers/DisplayDemoServer.tsx b/apps/website/src/app/admin/homepage/demo-servers/DisplayDemoServer.tsx new file mode 100644 index 000000000..872f0ca42 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/DisplayDemoServer.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useTranslation } from "react-i18next"; + +/* eslint-disable @next/next/no-img-element */ +export const DisplayDemoServer = ({ + name, + icon, + priority, +}: { + name: string; + priority: number; + icon?: string | null; +}) => { + const { t } = useTranslation(); + + return ( +
+ {icon && ( + {``} + )} +
+
{name}
+
+ {t("pages.admin.homePage.demoServers.list.display.priority", { + priority: priority.toString(), + })} +
+
+
+ ); +}; diff --git a/apps/website/src/app/admin/homepage/demo-servers/ListDemoServers.tsx b/apps/website/src/app/admin/homepage/demo-servers/ListDemoServers.tsx new file mode 100644 index 000000000..2524e8528 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/ListDemoServers.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { Button } from "@mc/ui/button"; + +import { Routes } from "~/other/routes"; +import { api } from "~/trpc/react"; +import { DisplayDemoServer } from "./DisplayDemoServer"; + +export const ListDemoServers = () => { + const demoServers = api.demoServers.geAll.useQuery(); + const router = useRouter(); + + return ( +
+ {demoServers.data?.map((demoServer) => ( + + ))} +
+ ); +}; diff --git a/apps/website/src/app/admin/homepage/demo-servers/[id]/ChannelCard.tsx b/apps/website/src/app/admin/homepage/demo-servers/[id]/ChannelCard.tsx new file mode 100644 index 000000000..110ae465d --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/[id]/ChannelCard.tsx @@ -0,0 +1,149 @@ +import type { DemoServerData } from "@mc/common/DemoServers"; +import { useId } from "react"; +import { ChannelType } from "discord-api-types/v10"; +import { TrashIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { Card } from "@mc/ui/card"; +import { Checkbox } from "@mc/ui/checkbox"; +import { Input } from "@mc/ui/input"; +import { Label } from "@mc/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectTrigger, + SelectValue, +} from "@mc/ui/select"; +import { SelectItemWithIcon } from "@mc/ui/selectItemWithIcon"; + +import { ChannelIconMap } from "~/app/dashboard/servers/[guildId]/ChannelMaps"; + +interface ChannelCardProps { + channel: DemoServerData["channels"][number]; + index: number; + updateChannel: ( + index: number, + channel: DemoServerData["channels"][number], + ) => void; + removeChannel: (index: number) => void; +} + +export function ChannelCard({ + channel, + index, + updateChannel, + removeChannel, +}: ChannelCardProps) { + const { t } = useTranslation(); + const skeletonCheckboxId = useId(); + return ( + +
+ + +
+
+ + + updateChannel(index, { ...channel, name: e.target.value }) + } + /> +
+
+ + {[ChannelType.GuildText, ChannelType.GuildAnnouncement].includes( + channel.type, + ) && ( + + updateChannel(index, { ...channel, topic: e.target.value }) + } + /> + )} +
+
+
+ { + updateChannel(index, { + ...channel, + showAsSkeleton: Boolean(state), + }); + }} + /> + +
+
+
+ +
+
+ ); +} diff --git a/apps/website/src/app/admin/homepage/demo-servers/[id]/DeleteButton.tsx b/apps/website/src/app/admin/homepage/demo-servers/[id]/DeleteButton.tsx new file mode 100644 index 000000000..420fe25e0 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/[id]/DeleteButton.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { TrashIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@mc/ui/dialog"; + +import { api } from "~/trpc/react"; + +export function DeleteButton({ + id, + disabled, +}: { + id: string; + disabled?: boolean; +}) { + const { t } = useTranslation(); + const router = useRouter(); + const deleteMutation = api.demoServers.delete.useMutation(); + + const deleteCB = async () => { + await deleteMutation.mutateAsync({ id }); + router.back(); + }; + + return ( + + + + + + + + {t("pages.admin.homePage.demoServers.delete.dialogTitle")} + + + {t("pages.admin.homePage.demoServers.delete.dialogDescription")} + + + + + + + + + + + ); +} diff --git a/apps/website/src/app/admin/homepage/demo-servers/[id]/LinkCard.tsx b/apps/website/src/app/admin/homepage/demo-servers/[id]/LinkCard.tsx new file mode 100644 index 000000000..b968be217 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/[id]/LinkCard.tsx @@ -0,0 +1,59 @@ +import type { DemoServerData } from "@mc/common/DemoServers"; +import { TrashIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { Card } from "@mc/ui/card"; +import { Input } from "@mc/ui/input"; +import { Label } from "@mc/ui/label"; + +interface LinkCardProps { + link: DemoServerData["links"][number]; + index: number; + updateLink: (index: number, link: DemoServerData["links"][number]) => void; + removeLink: (index: number) => void; +} + +export function LinkCard({ + link, + index, + updateLink, + removeLink, +}: LinkCardProps) { + const { t } = useTranslation(); + + return ( + +
+ + + updateLink(index, { ...link, label: e.target.value }) + } + /> +
+
+ + updateLink(index, { ...link, href: e.target.value })} + /> +
+
+ +
+
+ ); +} diff --git a/apps/website/src/app/admin/homepage/demo-servers/[id]/ManageDemoServer.tsx b/apps/website/src/app/admin/homepage/demo-servers/[id]/ManageDemoServer.tsx new file mode 100644 index 000000000..3b81bebf5 --- /dev/null +++ b/apps/website/src/app/admin/homepage/demo-servers/[id]/ManageDemoServer.tsx @@ -0,0 +1,206 @@ +import { useEffect, useState } from "react"; +import { PlusIcon, SaveIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@mc/ui/button"; +import { CardContent, CardFooter } from "@mc/ui/card"; +import { Input } from "@mc/ui/input"; +import { Label } from "@mc/ui/label"; +import { Textarea } from "@mc/ui/textarea"; + +import useConfirmOnLeave from "~/hooks/useConfirmOnLeave"; +import { addTo, removeFrom, updateIn } from "~/other/array"; +import { api } from "~/trpc/react"; +import { ChannelCard } from "./ChannelCard"; +import { DeleteButton } from "./DeleteButton"; +import { LinkCard } from "./LinkCard"; + +export default function ManageDemoServer({ id }: { id: string }) { + const { t } = useTranslation(); + const [isDirty, setIsDirty] = useState(false); + + const demoServer = api.demoServers.get.useQuery({ id: id }); + const demoServerMutation = api.demoServers.update.useMutation(); + const [mutableDemoServer, _setMutableDemoServer] = useState< + typeof demoServer.data | null + >(null); + + useConfirmOnLeave(isDirty); + + useEffect(() => { + if (!demoServer.data) return; + _setMutableDemoServer(demoServer.data); + }, [demoServer.data]); + + if (!mutableDemoServer) return; + + const setMutableDemoServer = (value: typeof demoServer.data) => { + _setMutableDemoServer(value); + setIsDirty(true); + }; + + const saveDemoServer = async () => { + await demoServerMutation.mutateAsync({ + ...mutableDemoServer, + }); + setIsDirty(false); + }; + + return ( +
{ + e.preventDefault(); + await saveDemoServer(); + }} + > + +
+ + + setMutableDemoServer({ + ...mutableDemoServer, + name: e.target.value, + }) + } + /> +
+
+ +