From 216340708083b8c5426a53796d46c8eec785c72e Mon Sep 17 00:00:00 2001
From: Izan Gil <66965250+SrIzan10@users.noreply.github.com>
Date: Sun, 19 May 2024 16:32:59 +0200
Subject: [PATCH] feat: admin panel
---
.prettierrc | 8 ++
package.json | 1 +
.../20240518221152_is_admin/migration.sql | 2 +
.../migration.sql | 2 +
prisma/schema.prisma | 3 +-
src/app/admin/page.tsx | 24 ++++++
src/app/globals.css | 4 +
src/app/page.tsx | 2 +-
src/components/app/BotForm/BotForm.tsx | 46 ++++++++++-
.../app/CheckedLoading/CheckedLoading.tsx | 11 +++
src/components/app/NavBar/NavBar.tsx | 7 ++
src/components/app/UserCard/UserCard.tsx | 79 ++++++++++--------
.../app/VerifiedSwitch/VerifiedSwitch.tsx | 43 ++++++++++
src/components/ui/alert.tsx | 61 ++++++++++++++
src/components/ui/switch.tsx | 29 +++++++
src/lib/actions.ts | 80 ++++++++++++++++++-
src/lib/auth/index.ts | 4 +-
src/lib/zod.ts | 7 +-
tailwind.config.ts | 4 +
yarn.lock | 21 +++++
20 files changed, 396 insertions(+), 42 deletions(-)
create mode 100644 .prettierrc
create mode 100644 prisma/migrations/20240518221152_is_admin/migration.sql
create mode 100644 prisma/migrations/20240519122204_optional_invite_link/migration.sql
create mode 100644 src/app/admin/page.tsx
create mode 100644 src/components/app/CheckedLoading/CheckedLoading.tsx
create mode 100644 src/components/app/VerifiedSwitch/VerifiedSwitch.tsx
create mode 100644 src/components/ui/alert.tsx
create mode 100644 src/components/ui/switch.tsx
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..e536681
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "useTabs": false,
+ "printWidth": 800,
+ "tabWidth": 2,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "semi": false
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index cd42874..36be3df 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
"@sern/poster": "^1.2.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
diff --git a/prisma/migrations/20240518221152_is_admin/migration.sql b/prisma/migrations/20240518221152_is_admin/migration.sql
new file mode 100644
index 0000000..7db3f39
--- /dev/null
+++ b/prisma/migrations/20240518221152_is_admin/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/20240519122204_optional_invite_link/migration.sql b/prisma/migrations/20240519122204_optional_invite_link/migration.sql
new file mode 100644
index 0000000..cd9795c
--- /dev/null
+++ b/prisma/migrations/20240519122204_optional_invite_link/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Bot" ALTER COLUMN "inviteLink" DROP NOT NULL;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 291befe..7cafb8d 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -17,6 +17,7 @@ model User {
id String @id @default(cuid())
username String @unique
hashed_password String
+ isAdmin Boolean @default(false)
sessions Session[]
bots Bot[]
}
@@ -36,8 +37,8 @@ model Bot {
name String
description String
verified Boolean @default(false)
- inviteLink String
pfpLink String
+ inviteLink String?
srcLink String?
botId String
}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
new file mode 100644
index 0000000..be046ad
--- /dev/null
+++ b/src/app/admin/page.tsx
@@ -0,0 +1,24 @@
+import UserCard from "@/components/app/UserCard/UserCard";
+import { validateRequest } from "@/lib/auth";
+import prisma from "@/lib/db";
+import { redirect } from "next/navigation";
+
+export default async function Page() {
+ const { user } = await validateRequest()
+ if (!user) return redirect('/auth/signIn')
+ if (!user.isAdmin) return (
+
Sorry
+ )
+
+ const getBots = await prisma.bot.findMany()
+ return (
+ <>
+ wow an admin panel
+
+ {getBots.map((bot) => (
+
+ ))}
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/app/globals.css b/src/app/globals.css
index 5aef2b5..4c43fbf 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -22,6 +22,8 @@
--accent-foreground: 0 0% 100%;
--destructive: 360 80.95% 45.49%;
--destructive-foreground: 0 0% 100%;
+ --warning: 41deg, 86%, 83%;
+ --warning-foreground: 41deg, 86%, 83%;
--ring: 342 59% 54%;
--radius: 0.5rem;
}
@@ -45,6 +47,8 @@
--accent-foreground: 0 0% 100%;
--destructive: 360 69.52% 39.02%;
--destructive-foreground: 0 0% 100%;
+ --warning: 41deg, 86%, 83%;
+ --warning-foreground: 41deg, 86%, 83%;
--ring: 342 59% 54%;
}
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index eafbc38..b4ded77 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -19,7 +19,7 @@ export default function Home() {
- Start right NOW!
+ Let's go!
diff --git a/src/components/app/BotForm/BotForm.tsx b/src/components/app/BotForm/BotForm.tsx
index 7c07046..4cb7aed 100644
--- a/src/components/app/BotForm/BotForm.tsx
+++ b/src/components/app/BotForm/BotForm.tsx
@@ -3,13 +3,19 @@
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import SubmitButton from "../SubmitButton/SubmitButton";
-import { submitBotData } from "@/lib/actions";
+import { revalidatePathServer, submitBotData, updateBotProfilePicture } from "@/lib/actions";
import { useFormState } from "react-dom";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { toast } from "sonner";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { AlertCircle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
export default function BotForm(props: Props) {
+ const router = useRouter()
const [submitData, submitDataAction] = useFormState(submitBotData, null);
+ const [regenLoading, setRegenLoading] = useState(false)
useEffect(() => {
if (submitData?.error) {
toast.error(submitData.error)
@@ -34,15 +40,47 @@ export default function BotForm(props: Props) {
Invite link
-
+
Repo link (optional)
-
+
+ {props.botId && (
+ {
+ setRegenLoading(true)
+ updateBotProfilePicture({ botId: props.botId! }).then((res) => {
+ setRegenLoading(false)
+ if (res.error) {
+ toast.error(res.error)
+ }
+ if (res.success) {
+ toast.success(res.message)
+ revalidatePathServer('/dashboard')
+ router.push('/dashboard')
+ }
+ })
+ }}>
+ Regenerate profile picture
+
+ )}
+
+
+ {props.id && (
+
+
+
+ Warning
+
+ When resubmitting, the bot will be unverified until the devteam verifies it again.{'\n'}
+ This doesn't apply when it's just regenerating the profile picture
+
+
+
+ )}
)
}
diff --git a/src/components/app/CheckedLoading/CheckedLoading.tsx b/src/components/app/CheckedLoading/CheckedLoading.tsx
new file mode 100644
index 0000000..3fdc2e3
--- /dev/null
+++ b/src/components/app/CheckedLoading/CheckedLoading.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import { LoaderIcon } from "lucide-react";
+
+export default function CheckedLoading(props: Props) {
+ return props.loading &&
+}
+
+interface Props {
+ loading: boolean;
+}
\ No newline at end of file
diff --git a/src/components/app/NavBar/NavBar.tsx b/src/components/app/NavBar/NavBar.tsx
index 89912c3..6f2a05b 100644
--- a/src/components/app/NavBar/NavBar.tsx
+++ b/src/components/app/NavBar/NavBar.tsx
@@ -63,6 +63,13 @@ export default function Navbar() {
My Account
+ {user.isAdmin && (
+
+
+ Admin panel
+
+
+ )}
{
logoutAction()
}}>
diff --git a/src/components/app/UserCard/UserCard.tsx b/src/components/app/UserCard/UserCard.tsx
index 85c3b05..28297f5 100644
--- a/src/components/app/UserCard/UserCard.tsx
+++ b/src/components/app/UserCard/UserCard.tsx
@@ -1,38 +1,51 @@
import { Avatar, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
+import { Label } from "@/components/ui/label"
+import { Switch } from "@/components/ui/switch"
import type { Bot } from "@prisma/client"
import Link from "next/link"
+import VerifiedSwitch from "../VerifiedSwitch/VerifiedSwitch"
-export default function UserCard(props: Bot) {
- const yes = Yes
- const no = No
- return (
- <>
-
-
-
-
{props.description}
-
Verified: {props.verified ? yes : no}
-
-
- {props.srcLink && (
-
- Source
-
- )}
-
- Invite
-
-
- Settings
-
-
-
- >
- )
-}
\ No newline at end of file
+export default function UserCard(props: Bot & { isAdminPanel?: boolean }) {
+ const yes = Yes
+ const no = No
+ return (
+ <>
+
+
+
+
+
+
+ {props.name}
+
+
+
+
{props.description}
+ {!props.isAdminPanel &&
Verified: {props.verified ? yes : no}
}
+ {props.isAdminPanel &&
Bot ID: {props.botId}
}
+
+
+ {props.srcLink && (
+
+ Source
+
+ )}
+ {props.inviteLink && (
+
+ Invite
+
+ )}
+ {props.isAdminPanel && }
+ {!props.isAdminPanel && (
+ <>
+
+ Settings
+
+ >
+ )}
+
+
+ >
+ );
+}
diff --git a/src/components/app/VerifiedSwitch/VerifiedSwitch.tsx b/src/components/app/VerifiedSwitch/VerifiedSwitch.tsx
new file mode 100644
index 0000000..30e67ae
--- /dev/null
+++ b/src/components/app/VerifiedSwitch/VerifiedSwitch.tsx
@@ -0,0 +1,43 @@
+'use client'
+
+import { Label } from '@/components/ui/label'
+import { Switch } from '@/components/ui/switch'
+import CheckedLoading from '../CheckedLoading/CheckedLoading'
+import { handleBotVerificationSwitch } from '@/lib/actions'
+import { useState } from 'react'
+import { toast } from 'sonner'
+
+export default function VerifiedSwitch(props: Props) {
+ const [loading, setLoading] = useState(false)
+ const [checked, setChecked] = useState(props.verified)
+ return (
+
+ Verified
+ {
+ setLoading(true)
+ handleBotVerificationSwitch({ id: props.id, verified: c }).then((ver) => {
+ setLoading(false)
+ // sets to the opposite of the current value, like a normal switch
+ setChecked(!checked)
+ if (ver.error) {
+ toast.error(ver.error)
+ }
+ if (ver.success) {
+ toast.success(ver.message)
+ }
+ })
+ }}
+ />
+
+
+ )
+}
+
+interface Props {
+ id: string
+ verified: boolean
+}
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..f02dd6f
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,61 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ warning:
+ "border-warning/50 text-warning dark:border-warning dark:text-warning",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..bc69cf2
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/src/lib/actions.ts b/src/lib/actions.ts
index 11d1c1d..8f1bc47 100644
--- a/src/lib/actions.ts
+++ b/src/lib/actions.ts
@@ -3,8 +3,9 @@
import { redirect } from "next/navigation";
import { validateRequest } from "./auth";
import prisma from "./db";
-import { botSubmitSchema } from "./zod";
+import { botSubmitSchema, botVerificationSwitchSchema } from "./zod";
import poster from '@sern/poster';
+import { revalidatePath } from "next/cache";
export async function submitBotData(prev: any, formData: FormData): Promise {
const parsedData = await botSubmitSchema.safeParseAsync(Object.fromEntries(formData.entries()));
@@ -58,6 +59,83 @@ export async function submitBotData(prev: any, formData: FormData): Promise {
+ const parsedData = await botVerificationSwitchSchema.safeParseAsync(data);
+ const { user } = await validateRequest();
+ if (!user) {
+ return {
+ success: false,
+ error: "You must be logged in to perform this action",
+ };
+ }
+ if (!parsedData.success) {
+ return {
+ success: false,
+ error: `From ${parsedData.error.errors[0].path[0]}: ${parsedData.error.errors[0].message}`,
+ };
+ }
+ console.log(parsedData.data.id, parsedData.data.verified)
+
+ const botUpdate = await prisma.bot.update({
+ where: {
+ id: parsedData.data.id,
+ },
+ data: {
+ verified: parsedData.data.verified,
+ },
+ });
+ return {
+ success: true,
+ message: `Bot verification status for ${botUpdate.name} updated`,
+ }
+}
+
+export async function updateBotProfilePicture(data: { botId: string }): Promise {
+ const { user } = await validateRequest()
+ if (!user) {
+ return {
+ success: false,
+ error: 'You must be logged in to perform this action',
+ }
+ }
+ const bot = await prisma.bot.findFirst({
+ where: {
+ botId: data.botId,
+ },
+ })
+ if (!bot) {
+ return {
+ success: false,
+ error: 'Bot not found',
+ }
+ }
+ const botClient = await poster.client(process.env.DSC_TOKEN!)
+ const userAvatarHash = (await (await botClient('user/get', { user_id: bot.botId })).json()).avatar
+ if (!userAvatarHash) {
+ return {
+ success: false,
+ error: 'Bot not found on Discord',
+ }
+ }
+ const userAvatar = `https://cdn.discordapp.com/avatars/${bot.botId}/${userAvatarHash}.webp`
+ await prisma.bot.update({
+ where: {
+ id: bot.id,
+ },
+ data: {
+ pfpLink: userAvatar,
+ },
+ })
+ return {
+ success: true,
+ message: 'Profile picture updated',
+ }
+}
+
+export async function revalidatePathServer(path: string) {
+ return revalidatePath(path)
+}
+
interface DefaultActionResponse {
success: boolean;
error?: string;
diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts
index 1dcdac4..c0556c7 100644
--- a/src/lib/auth/index.ts
+++ b/src/lib/auth/index.ts
@@ -18,7 +18,8 @@ export const lucia = new Lucia(adapter, {
},
getUserAttributes: (attributes) => {
return {
- username: attributes.username
+ username: attributes.username,
+ isAdmin: attributes.isAdmin
};
}
});
@@ -68,4 +69,5 @@ declare module "lucia" {
interface DatabaseUserAttributes {
username: string;
+ isAdmin: boolean;
}
\ No newline at end of file
diff --git a/src/lib/zod.ts b/src/lib/zod.ts
index a88b802..731cb00 100644
--- a/src/lib/zod.ts
+++ b/src/lib/zod.ts
@@ -6,6 +6,11 @@ export const botSubmitSchema = z.object({
name: z.string(),
description: z.string(),
botId: z.string(),
- inviteLink: z.string(),
+ inviteLink: z.string().nullable().optional(),
srcLink: z.string().nullable().optional()
+})
+
+export const botVerificationSwitchSchema = z.object({
+ id: z.string().min(1),
+ verified: z.boolean()
})
\ No newline at end of file
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 84287e8..bcef2af 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -36,6 +36,10 @@ const config = {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
+ warning: {
+ DEFAULT: "hsl(var(--warning))",
+ foreground: "hsl(var(--warning-foreground))",
+ },
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
diff --git a/yarn.lock b/yarn.lock
index ec8dddc..2335e8c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -682,6 +682,20 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
+"@radix-ui/react-switch@^1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
+ integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.0.1"
+ "@radix-ui/react-compose-refs" "1.0.1"
+ "@radix-ui/react-context" "1.0.1"
+ "@radix-ui/react-primitive" "1.0.3"
+ "@radix-ui/react-use-controllable-state" "1.0.1"
+ "@radix-ui/react-use-previous" "1.0.1"
+ "@radix-ui/react-use-size" "1.0.1"
+
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
@@ -712,6 +726,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
+"@radix-ui/react-use-previous@1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
+ integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+
"@radix-ui/react-use-rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"