1
0
Fork 0

feat: basic report viewer and panel layouts

fix-1
Paul Makles 2023-07-24 20:31:17 +01:00
parent 15ae9bbf17
commit 91e58d42f2
No known key found for this signature in database
GPG Key ID: 5059F398521BB0F6
28 changed files with 2101 additions and 155 deletions

View File

@ -2,26 +2,77 @@
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 217.2 32.6% 17.5%;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,22 +1,22 @@
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ['latin'] })
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
title: "Revolt Admin",
description: "Revolt administration panel",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
);
}

View File

@ -1,113 +1,3 @@
import Image from 'next/image'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore the Next.js 13 playground.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
return <main>helo</main>;
}

View File

@ -0,0 +1,5 @@
export default function Grafana() {
return (
<iframe src="https://monitor.revolt.chat/d/fb0383b2-86c8-4c9c-b401-0af8ce9869af/global-stats?orgId=1&kiosk" />
);
}

86
app/panel/layout.tsx Normal file
View File

@ -0,0 +1,86 @@
import Image from "next/image";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { buttonVariants } from "@/components/ui/button";
import {
Globe2,
Home,
ScrollText,
Search,
Siren,
Sparkles,
} from "lucide-react";
import Link from "next/link";
export default function PanelLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<main className="flex flex-col h-[100vh]">
<div className="p-4 flex justify-between items-center">
<span className="flex gap-4 items-center">
<Image
height={48}
width={160}
alt="Revolt"
className="invert"
src="https://app.revolt.chat/assets/wide.svg"
/>
<h1 className="text-3xl font-semibold">Admin Panel</h1>
</span>
<Avatar>
<AvatarImage src="/honse.png" />
<AvatarFallback>i</AvatarFallback>
</Avatar>
</div>
<div className="flex">
<div className="pt-2 pl-4 pr-0 gap-2 flex flex-col">
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel"
>
<Home className="h-4 w-4" />
</Link>
{/*<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/grafana"
>
<TrendingUp className="h-4 w-4" />
</Link>*/}
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/search"
>
<Search className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/reports"
>
<Siren className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/discover"
>
<Globe2 className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/audit"
>
<ScrollText className="h-4 w-4" />
</Link>
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/sparkle"
>
<Sparkles className="h-4 w-4" />
</Link>
</div>
<div className="flex-grow overflow-hidden p-2 pr-4">{children}</div>
</div>
</main>
);
}

3
app/panel/page.tsx Normal file
View File

@ -0,0 +1,3 @@
export default function Home() {
return <main>this is the admin panel ever</main>;
}

View File

@ -0,0 +1,78 @@
import { MessageContextCard } from "@/components/cards/MessageContextCard";
import { UserCard } from "@/components/cards/UserCard";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import {
fetchReportById,
fetchSnapshotsByReport,
fetchUserById,
} from "@/lib/db";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import { Fragment } from "react";
export default async function Reports({ params }: { params: { id: string } }) {
const report = await fetchReportById(params.id);
if (!report) return <span>404</span>;
const author = await fetchUserById(report.author_id);
const snapshots = await fetchSnapshotsByReport(report._id);
return (
<div className="flex flex-col gap-2">
<Link
className={buttonVariants({ variant: "outline", size: "icon" })}
href="/panel/reports"
>
<ArrowLeft className="h-4 w-4" />
</Link>
<Card className="shadow-none">
<CardHeader>
<CardTitle className="overflow-ellipsis whitespace-nowrap overflow-x-clip flex gap-2 items-center">
{report.content.report_reason.includes("Illegal") && (
<Badge variant="destructive">Urgent</Badge>
)}{" "}
{report.additional_context || "No reason specified"}
</CardTitle>
<CardDescription>
{report._id.toString().substring(20, 26)} &middot;{" "}
{report.content.report_reason} &middot; {report.content.type}
</CardDescription>
</CardHeader>
</Card>
<UserCard user={author!} subtitle="Report Author" />
{snapshots.map((snapshot) => (
<Fragment key={snapshot._id}>
{/*<Separator />*/}
<div className="flex gap-2">
<div className="flex-[2]">
{/*<h2 className="text-md">&nbsp;</h2>*/}
{snapshot._type === "Message" && (
<MessageContextCard snapshot={snapshot} />
)}
</div>
<div className="flex-1">
{/*<h2 className="text-md text-center">Related Open Reports</h2>*/}
</div>
</div>
</Fragment>
))}
</div>
);
}

View File

@ -0,0 +1,40 @@
import { Badge } from "@/components/ui/badge";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { fetchReports } from "@/lib/db";
import Link from "next/link";
export default async function Reports() {
const reports = (await fetchReports()).sort((b, _) =>
b.content.report_reason.includes("Illegal") ? -1 : 0
);
return (
<div className="flex flex-col gap-2">
<Input placeholder="Search for reports..." disabled />
{reports.map((report) => (
<Link href={`/panel/reports/${report._id}`} key={report._id as never}>
<Card className="transition-all hover:-translate-y-1 hover:shadow-md">
<CardHeader>
<CardTitle className="overflow-ellipsis whitespace-nowrap overflow-x-clip flex gap-2 items-center">
{report.content.report_reason.includes("Illegal") && (
<Badge variant="destructive">Urgent</Badge>
)}{" "}
{report.additional_context || "No reason specified"}
</CardTitle>
<CardDescription>
{report._id.toString().substring(20, 26)} &middot;{" "}
{report.content.report_reason} &middot; {report.content.type}
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
);
}

View File

@ -0,0 +1,10 @@
export default function Sparkle() {
return (
<div className="grid place-items-center">
<img
className="absolute right-0 bottom-0"
src="https://api.gifbox.me/file/posts/MF3oORlDjfHAVJ-DgPyRQSjMdy9WNIxk.webp"
/>
</div>
);
}

16
components.json Normal file
View File

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -0,0 +1,145 @@
import { Message, SnapshotContent, User } from "revolt-api";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { fetchUsersById } from "@/lib/db";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Image as ImageIcon } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../ui/alert-dialog";
function Message({
message,
users,
}: {
message: Message;
users?: Record<string, User>;
}) {
const user = users?.[message.author];
return (
<AlertDialog>
<AlertDialogTrigger className="w-full">
<div className="flex gap-2">
<div className="flex-1 min-w-0 overflow-ellipsis overflow-hidden text-right">
{user?.avatar && (
<Avatar className="w-4 h-4 inline-block align-middle mr-1">
<AvatarImage
src={`https://autumn.revolt.chat/avatars/${user.avatar._id}`}
/>
</Avatar>
)}
{user?.username}
</div>
<div className="flex-[2] min-w-0 overflow-ellipsis overflow-hidden text-left">
{(message.attachments || message.embeds) && (
<ImageIcon size={16} className="inline align-middle" />
)}{" "}
{message.content ?? <span className="text-gray-500">No text.</span>}
</div>
</div>
</AlertDialogTrigger>
<AlertDialogContent className="max-h-[100vh] overflow-y-auto">
<AlertDialogHeader className="min-w-0">
<AlertDialogTitle>
{user?.avatar && (
<Avatar className="inline-block align-middle mr-1">
<AvatarImage
src={`https://autumn.revolt.chat/avatars/${user.avatar._id}`}
/>
</Avatar>
)}
{user?.username}
</AlertDialogTitle>
<AlertDialogDescription className="flex flex-col gap-2 max-w-full">
{message.content && <span>{message.content}</span>}
{message.attachments?.map((attachment) =>
attachment.metadata.type === "Image" ? (
<a
target="_blank"
className="w-fit"
key={attachment._id}
href={`https://autumn.revolt.chat/attachments/${attachment._id}`}
>
<img
className="max-h-[240px] object-contain"
src={`https://autumn.revolt.chat/attachments/${attachment._id}`}
/>
</a>
) : (
"unsupported"
)
)}
{message.embeds?.map((embed, index) => (
<pre key={index} className="overflow-auto max-w-full">
<code>{JSON.stringify(embed, null, 2)}</code>
</pre>
))}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Delete</AlertDialogCancel>
<AlertDialogAction>Close</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
export async function MessageContextCard({
snapshot,
}: {
snapshot: SnapshotContent & { _type: "Message" };
}) {
const userIds = [
...new Set(
[
...(snapshot._leading_context ?? []),
...(snapshot._prior_context ?? []),
snapshot,
].map((x) => x.author)
),
];
const users = await fetchUsersById(userIds).then((users) =>
users.reduce((prev, next) => {
prev[next._id] = next;
return prev;
}, {} as Record<string, any>)
);
return (
<Card>
<CardHeader>
<CardTitle>Message(s)</CardTitle>
<CardDescription>Reported Content</CardDescription>
</CardHeader>
<CardContent>
<div className="opacity-40">
{[...(snapshot._prior_context ?? [])].reverse()?.map((message) => (
<Message key={message._id} message={message} users={users} />
))}
</div>
<Message message={snapshot} users={users} />
<div className="opacity-40">
{snapshot._leading_context?.map((message) => (
<Message key={message._id} message={message} users={users} />
))}
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,38 @@
import { User } from "revolt-api";
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import Link from "next/link";
export function UserCard({ user, subtitle }: { user: User; subtitle: string }) {
return (
<Link href={`/panel/inspect/${user._id}`}>
<Card
className="shadow-none bg-no-repeat bg-right text-left"
style={{
backgroundImage: user.profile?.background
? `linear-gradient(to right, white, rgba(255,0,0,0)), url('https://autumn.revolt.chat/backgrounds/${user.profile.background._id}')`
: "",
backgroundSize: "75%",
}}
>
<CardHeader>
<CardTitle>
<Avatar>
<AvatarImage
src={`https://autumn.revolt.chat/avatars/${user.avatar?._id}`}
/>
<AvatarFallback>
{(user.display_name ?? user.username)
.split(" ")
.slice(0, 2)
.join("")}
</AvatarFallback>
</Avatar>
{user.username}#{user.discriminator} {user.display_name}
</CardTitle>
<CardDescription>{subtitle}</CardDescription>
</CardHeader>
</Card>
</Link>
);
}

View File

@ -0,0 +1,145 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props} />
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

50
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full object-cover", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

56
components/ui/button.tsx Normal file
View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

79
components/ui/card.tsx Normal file
View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(" flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

25
components/ui/input.tsx Normal file
View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

31
components/ui/popover.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

11
lib/Provider.tsx Normal file
View File

@ -0,0 +1,11 @@
"use client";
import { SessionProvider } from "next-auth/react";
type Props = {
children?: React.ReactNode;
};
export const Provider = ({ children }: Props) => {
return <SessionProvider>{children}</SessionProvider>;
};

50
lib/db.ts Normal file
View File

@ -0,0 +1,50 @@
import { Filter, MongoClient } from "mongodb";
import type { Report, SnapshotContent, User } from "revolt-api";
let client: MongoClient;
function mongo() {
if (!client) {
client = new MongoClient(process.env.MONGODB!);
}
return client;
}
export default mongo;
export function fetchUserById(id: string) {
return mongo().db("revolt").collection<User>("users").findOne({ _id: id });
}
export function fetchUsersById(ids: string[]) {
return mongo()
.db("revolt")
.collection<User>("users")
.find({ _id: { $in: ids } })
.toArray();
}
export function fetchReports(query: Filter<Report> = { status: "Created" }) {
return mongo()
.db("revolt")
.collection<Report>("safety_reports")
.find(query as never)
.toArray();
}
export function fetchReportById(id: string) {
return mongo()
.db("revolt")
.collection<Report>("safety_reports")
.findOne({ _id: id });
}
export function fetchSnapshotsByReport(reportId: string) {
return mongo()
.db("revolt")
.collection<{ content: SnapshotContent }>("safety_snapshots")
.find({ report_id: reportId })
.toArray()
.then((snapshots) => snapshots.map((snapshot) => snapshot!.content));
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -1,5 +1,5 @@
{
"name": "revolt-moderation",
"name": "revolt-admin",
"version": "0.1.0",
"private": true,
"scripts": {
@ -9,17 +9,31 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@types/node": "20.4.4",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"eslint": "8.45.0",
"eslint-config-next": "13.4.12",
"lucide-react": "^0.263.0",
"mongodb": "^5.7.0",
"next": "13.4.12",
"next-auth": "^4.22.3",
"postcss": "8.4.27",
"react": "18.2.0",
"react-dom": "18.2.0",
"revolt-api": "^0.6.5",
"sass": "^1.64.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.6",
"typescript": "5.1.6"
}
}

File diff suppressed because it is too large Load Diff

BIN
public/honse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

View File

@ -1,18 +1,76 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,