Skip to content

Commit 2544805

Browse files
authored
Merge pull request #312 from codeunia-dev/feat/multicompanylevel
feat(admin): Add comprehensive company details page for admin review
2 parents 2ae7f4d + c5d1589 commit 2544805

File tree

149 files changed

+31083
-294
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

149 files changed

+31083
-294
lines changed

app/admin/companies/[id]/page.tsx

Lines changed: 626 additions & 0 deletions
Large diffs are not rendered by default.

app/admin/companies/[id]/verify/page.tsx

Lines changed: 650 additions & 0 deletions
Large diffs are not rendered by default.

app/admin/companies/page.tsx

Lines changed: 552 additions & 0 deletions
Large diffs are not rendered by default.

app/admin/layout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Award,
2525
Crown,
2626
LifeBuoy,
27+
Building2,
2728
} from "lucide-react"
2829
import { useAuth } from "@/lib/hooks/useAuth"
2930

@@ -56,6 +57,16 @@ const sidebarItems: SidebarGroupType[] = [
5657
url: "/admin/users",
5758
icon: Users,
5859
},
60+
{
61+
title: "Companies",
62+
url: "/admin/companies",
63+
icon: Building2,
64+
},
65+
{
66+
title: "Moderation",
67+
url: "/admin/moderation",
68+
icon: Shield,
69+
},
5970
{
6071
title: "Blog Posts",
6172
url: "/admin/blog-posts",

app/admin/moderation/[id]/page.tsx

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"use client"
2+
3+
import { useState, useEffect, useCallback } from "react"
4+
import { useRouter, useParams } from "next/navigation"
5+
import { Event } from "@/types/events"
6+
import { Company, ModerationLog } from "@/types/company"
7+
import { EventReview } from "@/components/moderation/EventReview"
8+
import { Button } from "@/components/ui/button"
9+
import { ArrowLeft, Loader2 } from "lucide-react"
10+
import { toast } from "sonner"
11+
12+
interface AutomatedCheckResult {
13+
passed: boolean
14+
issues: string[]
15+
}
16+
17+
interface EventWithCompany extends Event {
18+
company?: Company
19+
}
20+
21+
export default function EventReviewPage() {
22+
const router = useRouter()
23+
const params = useParams()
24+
const eventId = params.id as string
25+
26+
const [event, setEvent] = useState<EventWithCompany | null>(null)
27+
const [automatedChecks, setAutomatedChecks] = useState<AutomatedCheckResult | undefined>()
28+
const [moderationHistory, setModerationHistory] = useState<ModerationLog[]>([])
29+
const [loading, setLoading] = useState(true)
30+
const [error, setError] = useState<string | null>(null)
31+
32+
const fetchEventDetails = useCallback(async () => {
33+
try {
34+
setLoading(true)
35+
setError(null)
36+
37+
// Fetch event details
38+
const eventResponse = await fetch(`/api/admin/moderation/events/${eventId}`)
39+
if (!eventResponse.ok) {
40+
throw new Error("Failed to fetch event details")
41+
}
42+
43+
const eventData = await eventResponse.json()
44+
if (!eventData.success) {
45+
throw new Error(eventData.error || "Failed to fetch event details")
46+
}
47+
48+
setEvent(eventData.data.event)
49+
setAutomatedChecks(eventData.data.automatedChecks)
50+
setModerationHistory(eventData.data.moderationHistory || [])
51+
} catch (error) {
52+
console.error("Error fetching event details:", error)
53+
setError(error instanceof Error ? error.message : "Failed to load event details")
54+
toast.error("Failed to load event details")
55+
} finally {
56+
setLoading(false)
57+
}
58+
}, [eventId])
59+
60+
useEffect(() => {
61+
if (eventId) {
62+
fetchEventDetails()
63+
}
64+
}, [eventId, fetchEventDetails])
65+
66+
const handleApprove = async (notes?: string) => {
67+
try {
68+
const response = await fetch(`/api/admin/moderation/events/${eventId}/approve`, {
69+
method: "POST",
70+
headers: {
71+
"Content-Type": "application/json",
72+
},
73+
body: JSON.stringify({ notes }),
74+
})
75+
76+
if (!response.ok) {
77+
throw new Error("Failed to approve event")
78+
}
79+
80+
const data = await response.json()
81+
if (!data.success) {
82+
throw new Error(data.error || "Failed to approve event")
83+
}
84+
85+
toast.success("Event approved successfully")
86+
router.push("/admin/moderation")
87+
} catch (error) {
88+
console.error("Error approving event:", error)
89+
throw error
90+
}
91+
}
92+
93+
const handleReject = async (reason: string) => {
94+
try {
95+
const response = await fetch(`/api/admin/moderation/events/${eventId}/reject`, {
96+
method: "POST",
97+
headers: {
98+
"Content-Type": "application/json",
99+
},
100+
body: JSON.stringify({ reason }),
101+
})
102+
103+
if (!response.ok) {
104+
throw new Error("Failed to reject event")
105+
}
106+
107+
const data = await response.json()
108+
if (!data.success) {
109+
throw new Error(data.error || "Failed to reject event")
110+
}
111+
112+
toast.success("Event rejected")
113+
router.push("/admin/moderation")
114+
} catch (error) {
115+
console.error("Error rejecting event:", error)
116+
throw error
117+
}
118+
}
119+
120+
const handleRequestChanges = async (feedback: string) => {
121+
try {
122+
const response = await fetch(`/api/admin/moderation/events/${eventId}/request-changes`, {
123+
method: "POST",
124+
headers: {
125+
"Content-Type": "application/json",
126+
},
127+
body: JSON.stringify({ feedback }),
128+
})
129+
130+
if (!response.ok) {
131+
throw new Error("Failed to request changes")
132+
}
133+
134+
const data = await response.json()
135+
if (!data.success) {
136+
throw new Error(data.error || "Failed to request changes")
137+
}
138+
139+
toast.success("Changes requested")
140+
router.push("/admin/moderation")
141+
} catch (error) {
142+
console.error("Error requesting changes:", error)
143+
throw error
144+
}
145+
}
146+
147+
if (loading) {
148+
return (
149+
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16">
150+
<div className="flex items-center justify-center py-12">
151+
<Loader2 className="h-8 w-8 animate-spin text-purple-500" />
152+
</div>
153+
</div>
154+
)
155+
}
156+
157+
if (error || !event) {
158+
return (
159+
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16">
160+
<div className="text-center py-12">
161+
<h3 className="text-lg font-semibold text-zinc-900 dark:text-white mb-2">
162+
{error || "Event not found"}
163+
</h3>
164+
<Button onClick={() => router.push("/admin/moderation")} className="mt-4">
165+
<ArrowLeft className="h-4 w-4 mr-2" />
166+
Back to Moderation Queue
167+
</Button>
168+
</div>
169+
</div>
170+
)
171+
}
172+
173+
return (
174+
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
175+
{/* Header */}
176+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
177+
<div>
178+
<Button
179+
variant="ghost"
180+
onClick={() => router.push("/admin/moderation")}
181+
className="mb-4 text-zinc-400 hover:text-white"
182+
>
183+
<ArrowLeft className="h-4 w-4 mr-2" />
184+
Back to Queue
185+
</Button>
186+
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
187+
<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" />
188+
Event Review
189+
</h1>
190+
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
191+
Review event details and take action
192+
</p>
193+
</div>
194+
</div>
195+
196+
{/* Event Review Component */}
197+
<EventReview
198+
event={event}
199+
company={event.company}
200+
automatedChecks={automatedChecks}
201+
moderationHistory={moderationHistory}
202+
onApprove={handleApprove}
203+
onReject={handleReject}
204+
onRequestChanges={handleRequestChanges}
205+
/>
206+
</div>
207+
)
208+
}

app/admin/moderation/page.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"use client"
2+
3+
import { useState, useEffect } from "react"
4+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5+
import { ModerationQueue } from "@/components/moderation/ModerationQueue"
6+
import { AlertCircle, CheckCircle, Clock, Filter } from "lucide-react"
7+
8+
export default function ModerationPage() {
9+
const [stats, setStats] = useState({
10+
pending: 0,
11+
approved: 0,
12+
rejected: 0,
13+
})
14+
const [loading, setLoading] = useState(true)
15+
16+
useEffect(() => {
17+
// Fetch moderation stats
18+
const fetchStats = async () => {
19+
try {
20+
const response = await fetch('/api/admin/moderation/events?limit=1000')
21+
if (response.ok) {
22+
const data = await response.json()
23+
if (data.success) {
24+
setStats({
25+
pending: data.data.total,
26+
approved: 0, // Would need separate endpoint for this
27+
rejected: 0, // Would need separate endpoint for this
28+
})
29+
}
30+
}
31+
} catch (error) {
32+
console.error('Error fetching stats:', error)
33+
} finally {
34+
setLoading(false)
35+
}
36+
}
37+
38+
fetchStats()
39+
}, [])
40+
41+
return (
42+
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
43+
{/* Header */}
44+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
45+
<div>
46+
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
47+
<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" />
48+
Event Moderation Queue
49+
</h1>
50+
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
51+
Review and approve events submitted by companies
52+
</p>
53+
</div>
54+
</div>
55+
56+
{/* Stats Cards */}
57+
<div className="grid gap-4 sm:gap-6 md:gap-8 grid-cols-1 sm:grid-cols-3">
58+
<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">
59+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
60+
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
61+
Pending Review
62+
</CardTitle>
63+
<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">
64+
<Clock className="h-5 w-5 text-yellow-600" />
65+
</div>
66+
</CardHeader>
67+
<CardContent>
68+
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
69+
{loading ? "..." : stats.pending}
70+
</div>
71+
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
72+
Events awaiting approval
73+
</p>
74+
</CardContent>
75+
</Card>
76+
77+
<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">
78+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
79+
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
80+
Approved Today
81+
</CardTitle>
82+
<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">
83+
<CheckCircle className="h-5 w-5 text-green-600" />
84+
</div>
85+
</CardHeader>
86+
<CardContent>
87+
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
88+
{loading ? "..." : stats.approved}
89+
</div>
90+
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
91+
Events approved in last 24h
92+
</p>
93+
</CardContent>
94+
</Card>
95+
96+
<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">
97+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
98+
<CardTitle className="text-sm font-bold text-zinc-900 dark:text-zinc-100">
99+
Rejected Today
100+
</CardTitle>
101+
<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">
102+
<AlertCircle className="h-5 w-5 text-red-600" />
103+
</div>
104+
</CardHeader>
105+
<CardContent>
106+
<div className="text-2xl font-extrabold text-zinc-900 dark:text-white">
107+
{loading ? "..." : stats.rejected}
108+
</div>
109+
<p className="text-xs text-zinc-500 dark:text-zinc-300 mt-1">
110+
Events rejected in last 24h
111+
</p>
112+
</CardContent>
113+
</Card>
114+
</div>
115+
116+
{/* Moderation Queue */}
117+
<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">
118+
<CardHeader>
119+
<div className="flex items-center justify-between">
120+
<div>
121+
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
122+
<Filter className="h-5 w-5 text-purple-400" />
123+
Pending Events
124+
</CardTitle>
125+
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
126+
Review events and take action
127+
</CardDescription>
128+
</div>
129+
</div>
130+
</CardHeader>
131+
<CardContent>
132+
<ModerationQueue />
133+
</CardContent>
134+
</Card>
135+
</div>
136+
)
137+
}

0 commit comments

Comments
 (0)