forked from administration/panel
feat: basic report viewer and panel layouts
parent
15ae9bbf17
commit
91e58d42f2
|
@ -1,27 +1,78 @@
|
|||
@tailwind base;
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
112
app/page.tsx
112
app/page.tsx
|
@ -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
|
||||
<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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with 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">
|
||||
->
|
||||
</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">
|
||||
->
|
||||
</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>;
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Home() {
|
||||
return <main>this is the admin panel ever</main>;
|
||||
}
|
|
@ -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)} ·{" "}
|
||||
{report.content.report_reason} · {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"> </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>
|
||||
);
|
||||
}
|
|
@ -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)} ·{" "}
|
||||
{report.content.report_reason} · {report.content.type}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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 };
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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>;
|
||||
};
|
|
@ -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));
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
16
package.json
16
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
996
pnpm-lock.yaml
996
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 728 KiB |
|
@ -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")],
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
Loading…
Reference in New Issue