feat(don): add moderator page
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEllipsisVisible } from "react-hook-text-overflow";
|
||||
import { DataTable } from "@/components/tantable";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
@@ -165,15 +165,6 @@ export function DonateAdminPage({
|
||||
}: {
|
||||
data: (typeof donations.$inferSelect)[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!router) return;
|
||||
return sse.donate.subMany({
|
||||
ping: () => router.refresh(),
|
||||
update: () => router.refresh(),
|
||||
}).clean;
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[max(1280px,90%)] flex-col">
|
||||
<span className="flex items-center gap-1 pt-1 pb-2 text-3xl font-semibold">
|
||||
@@ -186,6 +177,29 @@ export function DonateAdminPage({
|
||||
data={data}
|
||||
className="max-h-[calc(100vh-93px)] w-full overflow-y-auto bg-black/25 backdrop-blur-sm"
|
||||
></DataTable>
|
||||
<DonateWatcher />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// component cuz simplicity sake
|
||||
export function DonateWatcher({ sfx = false }: { sfx?: boolean }) {
|
||||
const player = useRef<HTMLAudioElement | null>(null);
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!router) return;
|
||||
return sse.donate.subMany({
|
||||
ping: () => {
|
||||
router.refresh();
|
||||
player.current?.play();
|
||||
},
|
||||
update: () => router.refresh(),
|
||||
}).clean;
|
||||
}, [router]);
|
||||
|
||||
if (sfx)
|
||||
return (
|
||||
<audio src="/assets/donate-mod-sfx.wav" preload="auto" ref={player} />
|
||||
);
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { ExternalLink, RadioTower } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useRef, useState, type ComponentProps } from "react";
|
||||
|
||||
const ReactPIP = dynamic(() => import("react-document-picture-in-picture"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function PIP(props: ComponentProps<typeof ReactPIP>) {
|
||||
return (
|
||||
<ReactPIP
|
||||
{...props}
|
||||
width={400}
|
||||
height={600}
|
||||
shareStyles
|
||||
buttonRenderer={({ toggle, isOpen }) => (
|
||||
<div>
|
||||
{isOpen ? <span>Content popped out.</span> : props.children}
|
||||
<Button onClick={toggle} variant="outline" size="icon-sm">
|
||||
<ExternalLink />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
featureUnavailableRenderer={(r) => r}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientTracker() {
|
||||
const [connected, setConnected] = useState(0);
|
||||
const tracking = useRef(0);
|
||||
|
||||
useEffect(
|
||||
() => sse.donate.sub("heartbeat", () => tracking.current++).clean,
|
||||
[],
|
||||
);
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setConnected(tracking.current);
|
||||
if (tracking.current > 0) tracking.current--;
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return (
|
||||
<Button variant="outline" disabled size="sm">
|
||||
<RadioTower /> Connected: {connected}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DonateWatcher } from "../client";
|
||||
import { db } from "@/lib/db";
|
||||
import { donations } from "@/lib/db/schema";
|
||||
import { desc } from "drizzle-orm";
|
||||
import { Gavel, ImageOff, RefreshCw } from "lucide-react";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import Image from "@/components/image";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import { reloadWidget } from "../api";
|
||||
import { markDone } from "@/app/widget/donate/api";
|
||||
import { ClientTracker, PIP } from "./client";
|
||||
|
||||
export default async function DonateModeratorPage() {
|
||||
if (!(await adminCheck()))
|
||||
redirect(`/login?next=${encodeURIComponent("/donate/admin/moderator")}`);
|
||||
|
||||
const [sub] = await db
|
||||
.select()
|
||||
.from(donations)
|
||||
.orderBy(desc(donations.created))
|
||||
// .where(isNotNull(donations.image))
|
||||
.limit(1);
|
||||
|
||||
async function reject() {
|
||||
"use server";
|
||||
markDone(sub.id);
|
||||
reloadWidget();
|
||||
}
|
||||
|
||||
return (
|
||||
<PIP>
|
||||
<div className="dark flex w-full max-w-100 flex-col gap-1 bg-background p-1 text-foreground scheme-dark">
|
||||
<span>Donate Moderator</span>
|
||||
{sub.image ? (
|
||||
<div className="relative flex aspect-square w-full max-w-100 items-center justify-center overflow-hidden rounded-lg border bg-black/50">
|
||||
<Image
|
||||
src={await fileToDataUrl(
|
||||
new File([Buffer.from(sub.image)], "abc.jpeg"),
|
||||
)}
|
||||
alt="donation image"
|
||||
fill
|
||||
className="z-10"
|
||||
/>
|
||||
<Spinner className="size-16" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex aspect-square items-center justify-center rounded-lg border bg-black/50">
|
||||
<ImageOff className="size-16 opacity-20" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{sub.name}</span>
|
||||
<span>{sub.message}</span>
|
||||
</div>
|
||||
<span className="font-semibold">{sub.amount}฿</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-evenly gap-1">
|
||||
<ActionButton action={reject} variant="destructive" size="sm">
|
||||
<Gavel /> Reject
|
||||
</ActionButton>
|
||||
<ActionButton action={reloadWidget} variant="outline" size="sm">
|
||||
<RefreshCw /> Reload Widgets
|
||||
</ActionButton>
|
||||
<ClientTracker />
|
||||
</div>
|
||||
<DonateWatcher sfx />
|
||||
</div>
|
||||
</PIP>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user