first commit
Made-with: Cursor
This commit is contained in:
256
frontend/app/admin/moderation/page.tsx
Normal file
256
frontend/app/admin/moderation/page.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { EyeOff, UserX, Undo2, Plus } from "lucide-react";
|
||||
|
||||
type Tab = "hidden" | "blocked";
|
||||
|
||||
export default function ModerationPage() {
|
||||
const [tab, setTab] = useState<Tab>("hidden");
|
||||
const [hiddenContent, setHiddenContent] = useState<any[]>([]);
|
||||
const [blockedPubkeys, setBlockedPubkeys] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [hideEventId, setHideEventId] = useState("");
|
||||
const [hideReason, setHideReason] = useState("");
|
||||
const [blockPubkey, setBlockPubkey] = useState("");
|
||||
const [blockReason, setBlockReason] = useState("");
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [h, b] = await Promise.all([
|
||||
api.getHiddenContent(),
|
||||
api.getBlockedPubkeys(),
|
||||
]);
|
||||
setHiddenContent(h);
|
||||
setBlockedPubkeys(b);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleHide = async () => {
|
||||
if (!hideEventId.trim()) return;
|
||||
setError("");
|
||||
try {
|
||||
await api.hideContent(hideEventId, hideReason || undefined);
|
||||
setHideEventId("");
|
||||
setHideReason("");
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnhide = async (id: string) => {
|
||||
try {
|
||||
await api.unhideContent(id);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlock = async () => {
|
||||
if (!blockPubkey.trim()) return;
|
||||
setError("");
|
||||
try {
|
||||
await api.blockPubkey(blockPubkey, blockReason || undefined);
|
||||
setBlockPubkey("");
|
||||
setBlockReason("");
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnblock = async (id: string) => {
|
||||
try {
|
||||
await api.unblockPubkey(id);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-on-surface/50">Loading moderation data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-on-surface">Moderation</h1>
|
||||
|
||||
{error && <p className="text-error text-sm">{error}</p>}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setTab("hidden")}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors",
|
||||
tab === "hidden"
|
||||
? "bg-surface-container-high text-primary"
|
||||
: "bg-surface-container-low text-on-surface/60 hover:text-on-surface"
|
||||
)}
|
||||
>
|
||||
<EyeOff size={16} />
|
||||
Hidden Content
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("blocked")}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors",
|
||||
tab === "blocked"
|
||||
? "bg-surface-container-high text-primary"
|
||||
: "bg-surface-container-low text-on-surface/60 hover:text-on-surface"
|
||||
)}
|
||||
>
|
||||
<UserX size={16} />
|
||||
Blocked Pubkeys
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tab === "hidden" && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<h2 className="text-sm font-semibold text-on-surface/70 mb-3">Hide Content</h2>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
placeholder="Nostr event ID"
|
||||
value={hideEventId}
|
||||
onChange={(e) => setHideEventId(e.target.value)}
|
||||
className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1"
|
||||
/>
|
||||
<input
|
||||
placeholder="Reason (optional)"
|
||||
value={hideReason}
|
||||
onChange={(e) => setHideReason(e.target.value)}
|
||||
className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={handleHide}
|
||||
disabled={!hideEventId.trim()}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-primary to-primary-container text-on-primary font-semibold text-sm hover:opacity-90 transition-opacity disabled:opacity-50 whitespace-nowrap"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{hiddenContent.length === 0 ? (
|
||||
<p className="text-on-surface/50 text-sm">No hidden content.</p>
|
||||
) : (
|
||||
hiddenContent.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-surface-container-low rounded-xl p-6 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-on-surface font-mono text-sm">
|
||||
{item.nostrEventId?.slice(0, 16)}...
|
||||
</p>
|
||||
{item.reason && (
|
||||
<p className="text-on-surface/50 text-xs mt-1">{item.reason}</p>
|
||||
)}
|
||||
{item.createdAt && (
|
||||
<p className="text-on-surface/40 text-xs mt-1">
|
||||
{formatDate(item.createdAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUnhide(item.id)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface-container-highest text-on-surface/70 hover:text-on-surface text-sm transition-colors"
|
||||
>
|
||||
<Undo2 size={14} />
|
||||
Unhide
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === "blocked" && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<h2 className="text-sm font-semibold text-on-surface/70 mb-3">Block Pubkey</h2>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
placeholder="Pubkey (hex)"
|
||||
value={blockPubkey}
|
||||
onChange={(e) => setBlockPubkey(e.target.value)}
|
||||
className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1"
|
||||
/>
|
||||
<input
|
||||
placeholder="Reason (optional)"
|
||||
value={blockReason}
|
||||
onChange={(e) => setBlockReason(e.target.value)}
|
||||
className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={handleBlock}
|
||||
disabled={!blockPubkey.trim()}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-primary to-primary-container text-on-primary font-semibold text-sm hover:opacity-90 transition-opacity disabled:opacity-50 whitespace-nowrap"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{blockedPubkeys.length === 0 ? (
|
||||
<p className="text-on-surface/50 text-sm">No blocked pubkeys.</p>
|
||||
) : (
|
||||
blockedPubkeys.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-surface-container-low rounded-xl p-6 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="text-on-surface font-mono text-sm">
|
||||
{item.pubkey?.slice(0, 16)}...{item.pubkey?.slice(-8)}
|
||||
</p>
|
||||
{item.reason && (
|
||||
<p className="text-on-surface/50 text-xs mt-1">{item.reason}</p>
|
||||
)}
|
||||
{item.createdAt && (
|
||||
<p className="text-on-surface/40 text-xs mt-1">
|
||||
{formatDate(item.createdAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUnblock(item.id)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface-container-highest text-on-surface/70 hover:text-on-surface text-sm transition-colors"
|
||||
>
|
||||
<Undo2 size={14} />
|
||||
Unblock
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user