Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
626 changes: 626 additions & 0 deletions app/admin/companies/[id]/page.tsx

Large diffs are not rendered by default.

650 changes: 650 additions & 0 deletions app/admin/companies/[id]/verify/page.tsx

Large diffs are not rendered by default.

552 changes: 552 additions & 0 deletions app/admin/companies/page.tsx

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Award,
Crown,
LifeBuoy,
Building2,
} from "lucide-react"
import { useAuth } from "@/lib/hooks/useAuth"

Expand Down Expand Up @@ -56,6 +57,16 @@ const sidebarItems: SidebarGroupType[] = [
url: "/admin/users",
icon: Users,
},
{
title: "Companies",
url: "/admin/companies",
icon: Building2,
},
{
title: "Moderation",
url: "/admin/moderation",
icon: Shield,
},
{
title: "Blog Posts",
url: "/admin/blog-posts",
Expand Down
208 changes: 208 additions & 0 deletions app/admin/moderation/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"use client"

import { useState, useEffect, useCallback } from "react"
import { useRouter, useParams } from "next/navigation"
import { Event } from "@/types/events"
import { Company, ModerationLog } from "@/types/company"
import { EventReview } from "@/components/moderation/EventReview"
import { Button } from "@/components/ui/button"
import { ArrowLeft, Loader2 } from "lucide-react"
import { toast } from "sonner"

interface AutomatedCheckResult {
passed: boolean
issues: string[]
}

interface EventWithCompany extends Event {
company?: Company
}

export default function EventReviewPage() {
const router = useRouter()
const params = useParams()
const eventId = params.id as string

const [event, setEvent] = useState<EventWithCompany | null>(null)
const [automatedChecks, setAutomatedChecks] = useState<AutomatedCheckResult | undefined>()
const [moderationHistory, setModerationHistory] = useState<ModerationLog[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

const fetchEventDetails = useCallback(async () => {
try {
setLoading(true)
setError(null)

// Fetch event details
const eventResponse = await fetch(`/api/admin/moderation/events/${eventId}`)
if (!eventResponse.ok) {
throw new Error("Failed to fetch event details")
}

const eventData = await eventResponse.json()
if (!eventData.success) {
throw new Error(eventData.error || "Failed to fetch event details")
}

setEvent(eventData.data.event)
setAutomatedChecks(eventData.data.automatedChecks)
setModerationHistory(eventData.data.moderationHistory || [])
} catch (error) {
console.error("Error fetching event details:", error)
setError(error instanceof Error ? error.message : "Failed to load event details")
toast.error("Failed to load event details")
} finally {
setLoading(false)
}
}, [eventId])

useEffect(() => {
if (eventId) {
fetchEventDetails()
}
}, [eventId, fetchEventDetails])

const handleApprove = async (notes?: string) => {
try {
const response = await fetch(`/api/admin/moderation/events/${eventId}/approve`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ notes }),
})

if (!response.ok) {
throw new Error("Failed to approve event")
}

const data = await response.json()
if (!data.success) {
throw new Error(data.error || "Failed to approve event")
}

toast.success("Event approved successfully")
router.push("/admin/moderation")
} catch (error) {
console.error("Error approving event:", error)
throw error
}
}

const handleReject = async (reason: string) => {
try {
const response = await fetch(`/api/admin/moderation/events/${eventId}/reject`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ reason }),
})

if (!response.ok) {
throw new Error("Failed to reject event")
}

const data = await response.json()
if (!data.success) {
throw new Error(data.error || "Failed to reject event")
}

toast.success("Event rejected")
router.push("/admin/moderation")
} catch (error) {
console.error("Error rejecting event:", error)
throw error
}
}

const handleRequestChanges = async (feedback: string) => {
try {
const response = await fetch(`/api/admin/moderation/events/${eventId}/request-changes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ feedback }),
})

if (!response.ok) {
throw new Error("Failed to request changes")
}

const data = await response.json()
if (!data.success) {
throw new Error(data.error || "Failed to request changes")
}

toast.success("Changes requested")
router.push("/admin/moderation")
} catch (error) {
console.error("Error requesting changes:", error)
throw error
}
}

if (loading) {
return (
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-purple-500" />
</div>
</div>
)
}

if (error || !event) {
return (
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16">
<div className="text-center py-12">
<h3 className="text-lg font-semibold text-zinc-900 dark:text-white mb-2">
{error || "Event not found"}
</h3>
<Button onClick={() => router.push("/admin/moderation")} className="mt-4">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Moderation Queue
</Button>
</div>
</div>
)
}

return (
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
<div>
<Button
variant="ghost"
onClick={() => router.push("/admin/moderation")}
className="mb-4 text-zinc-400 hover:text-white"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Queue
</Button>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
<span className="inline-block w-2 h-6 sm:h-8 bg-gradient-to-b from-purple-400 to-blue-400 rounded-full mr-2" />
Event Review
</h1>
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
Review event details and take action
</p>
</div>
</div>

{/* Event Review Component */}
<EventReview
event={event}
company={event.company}
automatedChecks={automatedChecks}
moderationHistory={moderationHistory}
onApprove={handleApprove}
onReject={handleReject}
onRequestChanges={handleRequestChanges}
/>
</div>
)
}
137 changes: 137 additions & 0 deletions app/admin/moderation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"use client"

import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ModerationQueue } from "@/components/moderation/ModerationQueue"
import { AlertCircle, CheckCircle, Clock, Filter } from "lucide-react"

export default function ModerationPage() {
const [stats, setStats] = useState({
pending: 0,
approved: 0,
rejected: 0,
})
const [loading, setLoading] = useState(true)

useEffect(() => {
// Fetch moderation stats
const fetchStats = async () => {
try {
const response = await fetch('/api/admin/moderation/events?limit=1000')
if (response.ok) {
const data = await response.json()
if (data.success) {
setStats({
pending: data.data.total,
approved: 0, // Would need separate endpoint for this
rejected: 0, // Would need separate endpoint for this
})
}
}
} catch (error) {
console.error('Error fetching stats:', error)
} finally {
setLoading(false)
}
}

fetchStats()
}, [])

return (
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
<div>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
<span className="inline-block w-2 h-6 sm:h-8 bg-gradient-to-b from-purple-400 to-blue-400 rounded-full mr-2" />
Event Moderation Queue
</h1>
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
Review and approve events submitted by companies
</p>
</div>
</div>

{/* Stats Cards */}
<div className="grid gap-4 sm:gap-6 md:gap-8 grid-cols-1 sm:grid-cols-3">
<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-yellow-100/80 to-yellow-200/60 dark:from-yellow-900/60 dark:to-yellow-800/40">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
Pending Review
</CardTitle>
<div className="p-2 rounded-xl bg-gradient-to-br from-white/80 to-zinc-100/40 dark:from-zinc-800/80 dark:to-zinc-900/40 shadow-lg">
<Clock className="h-5 w-5 text-yellow-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
{loading ? "..." : stats.pending}
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
Events awaiting approval
</p>
</CardContent>
</Card>

<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-green-100/80 to-green-200/60 dark:from-green-900/60 dark:to-green-800/40">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
Approved Today
</CardTitle>
<div className="p-2 rounded-xl bg-gradient-to-br from-white/80 to-zinc-100/40 dark:from-zinc-800/80 dark:to-zinc-900/40 shadow-lg">
<CheckCircle className="h-5 w-5 text-green-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
{loading ? "..." : stats.approved}
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
Events approved in last 24h
</p>
</CardContent>
</Card>

<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-red-100/80 to-red-200/60 dark:from-red-900/60 dark:to-red-800/40">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
Rejected Today
</CardTitle>
<div className="p-2 rounded-xl bg-gradient-to-br from-white/80 to-zinc-100/40 dark:from-zinc-800/80 dark:to-zinc-900/40 shadow-lg">
<AlertCircle className="h-5 w-5 text-red-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
{loading ? "..." : stats.rejected}
</div>
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
Events rejected in last 24h
</p>
</CardContent>
</Card>
</div>

{/* Moderation Queue */}
<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-zinc-100/80 to-zinc-200/60 dark:from-zinc-900/60 dark:to-zinc-800/40">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
<Filter className="h-5 w-5 text-purple-400" />
Pending Events
</CardTitle>
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
Review events and take action
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<ModerationQueue />
</CardContent>
</Card>
</div>
)
}
Loading
Loading