mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
refactor: modularize moderator panel components
This commit is contained in:
@@ -1,22 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
|
||||
import { Badge } from '@/components/ui'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui'
|
||||
import { Stack, Typography } from '@/components/ui'
|
||||
import { toast } from 'sonner'
|
||||
import { AppHeader } from '@/components/shared/AppHeader'
|
||||
import { Database } from '@/lib/database'
|
||||
import type { Comment, User } from '@/lib/level-types'
|
||||
import { AppHeader } from '@/components/shared/AppHeader'
|
||||
import { ModeratorActions } from './ModeratorPanel/Actions'
|
||||
import { ModeratorHeader } from './ModeratorPanel/Header'
|
||||
import { ModeratorLogList } from './ModeratorPanel/LogList'
|
||||
|
||||
const FLAGGED_TERMS = ['spam', 'error', 'abuse', 'illegal', 'urgent', 'offensive']
|
||||
|
||||
@@ -70,8 +61,6 @@ export function ModeratorPanel({ user, onLogout, onNavigate }: ModeratorPanelPro
|
||||
toast.success('Flag resolved and archived from the queue')
|
||||
}
|
||||
|
||||
const highlightLabel = (term: string) => term.charAt(0).toUpperCase() + term.slice(1)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<AppHeader
|
||||
@@ -85,116 +74,19 @@ export function ModeratorPanel({ user, onLogout, onNavigate }: ModeratorPanelPro
|
||||
/>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Typography variant="h4">Moderation queue</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Keep the community healthy by resolving flags, reviewing reports, and guiding the tone.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flagged content</CardTitle>
|
||||
<CardDescription>Automated signal based on keywords</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Typography variant="h3">{flaggedComments.length}</Typography>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Pending items in the moderation queue
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Resolved this session</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Typography variant="h3">{resolvedIds.length}</Typography>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Items you flagged as handled
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Community signals</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{FLAGGED_TERMS.map((term) => (
|
||||
<Badge key={term}>{highlightLabel(term)}</Badge>
|
||||
))}
|
||||
</Stack>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Track the keywords that pulled items into the queue
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Flagged comments</CardTitle>
|
||||
<CardDescription>A curated view of the comments that triggered a signal</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onNavigate(2)}>
|
||||
Go to user dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Typography color="text.secondary">Loading flagged comments…</Typography>
|
||||
) : flaggedComments.length === 0 ? (
|
||||
<Typography color="text.secondary">
|
||||
No flagged comments at the moment. Enjoy the calm.
|
||||
</Typography>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Comment</TableHead>
|
||||
<TableHead>Matched terms</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{flaggedComments.map((comment) => {
|
||||
const matches = FLAGGED_TERMS.filter((term) =>
|
||||
comment.content.toLowerCase().includes(term)
|
||||
)
|
||||
return (
|
||||
<TableRow key={comment.id}>
|
||||
<TableCell className="font-mono text-sm">{comment.userId}</TableCell>
|
||||
<TableCell>{comment.content}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{matches.map((match) => (
|
||||
<Badge key={`${comment.id}-${match}`} variant="outline">
|
||||
{match}
|
||||
</Badge>
|
||||
))}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button size="sm" variant="ghost" onClick={() => handleResolve(comment.id)}>
|
||||
Mark safe
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ModeratorHeader />
|
||||
<ModeratorActions
|
||||
flaggedCount={flaggedComments.length}
|
||||
resolvedCount={resolvedIds.length}
|
||||
flaggedTerms={FLAGGED_TERMS}
|
||||
/>
|
||||
<ModeratorLogList
|
||||
flaggedComments={flaggedComments}
|
||||
flaggedTerms={FLAGGED_TERMS}
|
||||
isLoading={isLoading}
|
||||
onNavigate={onNavigate}
|
||||
onResolve={handleResolve}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Stack, Typography } from '@/components/ui'
|
||||
|
||||
interface ModeratorActionsProps {
|
||||
flaggedCount: number
|
||||
resolvedCount: number
|
||||
flaggedTerms: string[]
|
||||
}
|
||||
|
||||
export function ModeratorActions({ flaggedCount, resolvedCount, flaggedTerms }: ModeratorActionsProps) {
|
||||
const highlightLabel = (term: string) => term.charAt(0).toUpperCase() + term.slice(1)
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flagged content</CardTitle>
|
||||
<CardDescription>Automated signal based on keywords</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Typography variant="h3">{flaggedCount}</Typography>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Pending items in the moderation queue
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Resolved this session</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Typography variant="h3">{resolvedCount}</Typography>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Items you flagged as handled
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Community signals</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{flaggedTerms.map((term) => (
|
||||
<Badge key={term}>{highlightLabel(term)}</Badge>
|
||||
))}
|
||||
</Stack>
|
||||
<Typography color="text.secondary" className="mt-2">
|
||||
Track the keywords that pulled items into the queue
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Typography } from '@/components/ui'
|
||||
|
||||
export function ModeratorHeader() {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Typography variant="h4">Moderation queue</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Keep the community healthy by resolving flags, reviewing reports, and guiding the tone.
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Stack } from '@/components/ui'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@/components/ui'
|
||||
import type { Comment } from '@/lib/level-types'
|
||||
|
||||
interface ModeratorLogListProps {
|
||||
flaggedComments: Comment[]
|
||||
flaggedTerms: string[]
|
||||
isLoading: boolean
|
||||
onNavigate: (level: number) => void
|
||||
onResolve: (commentId: string) => void
|
||||
}
|
||||
|
||||
export function ModeratorLogList({
|
||||
flaggedComments,
|
||||
flaggedTerms,
|
||||
isLoading,
|
||||
onNavigate,
|
||||
onResolve,
|
||||
}: ModeratorLogListProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Flagged comments</CardTitle>
|
||||
<CardDescription>A curated view of the comments that triggered a signal</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onNavigate(2)}>
|
||||
Go to user dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Typography color="text.secondary">Loading flagged comments…</Typography>
|
||||
) : flaggedComments.length === 0 ? (
|
||||
<Typography color="text.secondary">
|
||||
No flagged comments at the moment. Enjoy the calm.
|
||||
</Typography>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Comment</TableHead>
|
||||
<TableHead>Matched terms</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{flaggedComments.map((comment) => {
|
||||
const matches = flaggedTerms.filter((term) =>
|
||||
comment.content.toLowerCase().includes(term)
|
||||
)
|
||||
|
||||
return (
|
||||
<TableRow key={comment.id}>
|
||||
<TableCell className="font-mono text-sm">{comment.userId}</TableCell>
|
||||
<TableCell>{comment.content}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{matches.map((match) => (
|
||||
<Badge key={`${comment.id}-${match}`} variant="outline">
|
||||
{match}
|
||||
</Badge>
|
||||
))}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button size="sm" variant="ghost" onClick={() => onResolve(comment.id)}>
|
||||
Mark safe
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user