-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #108 from MinaFoundation/feature/feedback-form
Feature/feedback form
- Loading branch information
Showing
16 changed files
with
1,267 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
prisma/migrations/20250114140328_add_user_feedback/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
-- CreateTable | ||
CREATE TABLE "UserFeedback" ( | ||
"id" UUID NOT NULL, | ||
"userId" UUID NOT NULL, | ||
"feedback" TEXT NOT NULL, | ||
"image" BYTEA, | ||
"metadata" JSONB NOT NULL, | ||
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMPTZ(6) NOT NULL, | ||
|
||
CONSTRAINT "UserFeedback_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "UserFeedback_userId_idx" ON "UserFeedback"("userId"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "UserFeedback_createdAt_idx" ON "UserFeedback"("createdAt"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "UserFeedback" ADD CONSTRAINT "UserFeedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
'use client'; | ||
|
||
import { useState, useEffect, useCallback } from "react"; | ||
import { FeedbackList, type FeedbackItem } from "@/components/admin/feedback/FeedbackList"; | ||
import { useToast } from "@/hooks/use-toast"; | ||
import { Card } from "@/components/ui/card"; | ||
import { Icons } from "@/components/ui/icons"; | ||
|
||
interface FeedbackResponse { | ||
items: FeedbackItem[]; | ||
total: number; | ||
page: number; | ||
limit: number; | ||
totalPages: number; | ||
} | ||
|
||
export default function AdminFeedbackPage() { | ||
const [isLoading, setIsLoading] = useState(true); | ||
const [data, setData] = useState<FeedbackResponse | null>(null); | ||
const [page, setPage] = useState(1); | ||
const [limit, setLimit] = useState(10); | ||
const [orderBy, setOrderBy] = useState<"asc" | "desc">("desc"); | ||
const { toast } = useToast(); | ||
|
||
const fetchFeedback = useCallback(async () => { | ||
try { | ||
setIsLoading(true); | ||
const response = await fetch( | ||
`/api/admin/feedback/list?page=${page}&limit=${limit}&orderBy=${orderBy}` | ||
); | ||
|
||
if (!response.ok) { | ||
throw new Error("Failed to fetch feedback"); | ||
} | ||
|
||
const result = await response.json(); | ||
setData(result); | ||
} catch (error) { | ||
toast({ | ||
title: "Error", | ||
description: "Failed to load feedback. Please try again.", | ||
variant: "destructive", | ||
}); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}, [page, limit, orderBy, toast]); | ||
|
||
useEffect(() => { | ||
fetchFeedback(); | ||
}, [fetchFeedback]); | ||
|
||
if (isLoading) { | ||
return ( | ||
<div className="container max-w-6xl mx-auto p-6"> | ||
<div className="flex items-center justify-center min-h-[400px]"> | ||
<div className="flex items-center gap-2"> | ||
<Icons.loader className="h-6 w-6 animate-spin" /> | ||
<span>Loading feedback...</span> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
if (!data) { | ||
return ( | ||
<div className="container max-w-6xl mx-auto p-6"> | ||
<Card className="p-6"> | ||
<div className="flex flex-col items-center justify-center min-h-[400px] text-center"> | ||
<Icons.messageSquare className="h-12 w-12 text-muted-foreground mb-4" /> | ||
<h3 className="text-lg font-medium">No Feedback Available</h3> | ||
<p className="text-sm text-muted-foreground"> | ||
There is no feedback data available at this time. | ||
</p> | ||
</div> | ||
</Card> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className="container max-w-6xl mx-auto p-6"> | ||
<div className="mb-8"> | ||
<h1 className="text-3xl font-bold">User Feedback</h1> | ||
<p className="text-muted-foreground mt-2"> | ||
View and manage user feedback submissions. Click on any feedback item to see more details. | ||
</p> | ||
</div> | ||
|
||
<FeedbackList | ||
items={data.items} | ||
total={data.total} | ||
page={data.page} | ||
limit={data.limit} | ||
totalPages={data.totalPages} | ||
onPageChange={setPage} | ||
onOrderChange={setOrderBy} | ||
onLimitChange={setLimit} | ||
/> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,112 @@ | ||
import { AdminDashboardComponent } from "@/components/AdminDashboard"; | ||
import { Metadata } from "next"; | ||
'use client'; | ||
|
||
export const metadata: Metadata = { | ||
title: "Admin Dashboard | MEF", | ||
description: "Admin dashboard for managing MEF platform", | ||
}; | ||
import { Card } from "@/components/ui/card"; | ||
import { Icons } from "@/components/ui/icons"; | ||
import Link from "next/link"; | ||
|
||
export default function AdminPage() { | ||
return <AdminDashboardComponent />; | ||
interface AdminOption { | ||
title: string; | ||
description: string; | ||
icon: keyof typeof Icons; | ||
href: string; | ||
color?: string; | ||
} | ||
|
||
const adminOptions: AdminOption[] = [ | ||
{ | ||
title: "Manage Reviewers", | ||
description: "Manage Reviewers and Users", | ||
icon: "users", | ||
href: "/admin/reviewers", | ||
color: "bg-blue-500/10 text-blue-500", | ||
}, | ||
{ | ||
title: "Manage Discussion Topics", | ||
description: "Manage Discussion Topics and Committees", | ||
icon: "messageSquare", | ||
href: "/admin/topics", | ||
color: "bg-purple-500/10 text-purple-500", | ||
}, | ||
{ | ||
title: "Manage Funding Rounds", | ||
description: "Manage Funding Rounds and Phases", | ||
icon: "link", | ||
href: "/admin/funding-rounds", | ||
color: "bg-green-500/10 text-green-500", | ||
}, | ||
{ | ||
title: "Manage Proposal Status", | ||
description: "Set/Override Proposal Status", | ||
icon: "fileText", | ||
href: "/admin/proposals", | ||
color: "bg-orange-500/10 text-orange-500", | ||
}, | ||
{ | ||
title: "Count Votes", | ||
description: "Count Votes for a Funding Round", | ||
icon: "barChart", | ||
href: "/admin/votes", | ||
color: "bg-yellow-500/10 text-yellow-500", | ||
}, | ||
{ | ||
title: "Worker Heartbeats", | ||
description: "Monitor background job statuses", | ||
icon: "activity", | ||
href: "/admin/workers", | ||
color: "bg-red-500/10 text-red-500", | ||
}, | ||
{ | ||
title: "Consideration OCV Votes", | ||
description: "Monitor OCV consideration votes", | ||
icon: "barChart2", | ||
href: "/admin/ocv-votes", | ||
color: "bg-indigo-500/10 text-indigo-500", | ||
}, | ||
{ | ||
title: "User Feedback", | ||
description: "View and manage user feedback submissions", | ||
icon: "messageCircle", | ||
href: "/admin/feedback", | ||
color: "bg-pink-500/10 text-pink-500", | ||
}, | ||
]; | ||
|
||
export default function AdminDashboard() { | ||
return ( | ||
<div className="container max-w-7xl mx-auto p-6 space-y-8"> | ||
<div className="space-y-2"> | ||
<h1 className="text-3xl font-bold tracking-tight">Admin Dashboard</h1> | ||
<p className="text-muted-foreground"> | ||
Welcome to the Admin Dashboard. Please select a category to manage. | ||
</p> | ||
</div> | ||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||
{adminOptions.map((option) => { | ||
const Icon = Icons[option.icon]; | ||
return ( | ||
<Link key={option.href} href={option.href}> | ||
<Card className="h-full p-6 hover:shadow-md transition-all hover:scale-[1.02] cursor-pointer"> | ||
<div className="flex flex-col h-full space-y-4"> | ||
<div className="flex items-center gap-4"> | ||
<div className={`p-2 rounded-lg ${option.color}`}> | ||
<Icon className="h-5 w-5" /> | ||
</div> | ||
<div className="space-y-1"> | ||
<h2 className="text-xl font-semibold tracking-tight"> | ||
{option.title} | ||
</h2> | ||
<p className="text-sm text-muted-foreground"> | ||
{option.description} | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</Card> | ||
</Link> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { NextRequest } from "next/server"; | ||
import { ApiResponse } from "@/lib/api-response"; | ||
import { getOrCreateUserFromRequest } from "@/lib/auth"; | ||
import prisma from "@/lib/prisma"; | ||
import { AdminFeedbackService } from "@/services/AdminFeedbackService"; | ||
import { AppError } from "@/lib/errors"; | ||
import { AuthErrors, HTTPStatus } from "@/constants/errors"; | ||
import { AdminService } from "@/services"; | ||
|
||
const adminService = new AdminService(prisma); | ||
|
||
export async function GET(req: NextRequest) { | ||
try { | ||
const user = await getOrCreateUserFromRequest(req); | ||
if (!user) { | ||
throw new AppError("Unauthorized", HTTPStatus.UNAUTHORIZED); | ||
} | ||
|
||
// Check if user is admin | ||
const isAdmin = await adminService.checkAdminStatus(user.id, user.linkId); | ||
if (!isAdmin) { | ||
throw AppError.forbidden(AuthErrors.FORBIDDEN); | ||
} | ||
|
||
const searchParams = req.nextUrl.searchParams; | ||
const page = parseInt(searchParams.get("page") || "1"); | ||
const limit = parseInt(searchParams.get("limit") || "10"); | ||
const orderBy = (searchParams.get("orderBy") || "desc") as "asc" | "desc"; | ||
|
||
const feedbackService = new AdminFeedbackService(prisma); | ||
const result = await feedbackService.getFeedbackList({ | ||
page, | ||
limit, | ||
orderBy, | ||
}); | ||
|
||
return ApiResponse.success(result); | ||
} catch (error) { | ||
return ApiResponse.error(error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { NextRequest } from "next/server"; | ||
import { ApiResponse } from "@/lib/api-response"; | ||
import { getOrCreateUserFromRequest } from "@/lib/auth"; | ||
import prisma from "@/lib/prisma"; | ||
import { FeedbackService } from "@/services/FeedbackService"; | ||
import { AppError } from "@/lib/errors"; | ||
import { HTTPStatus } from "@/constants/errors"; | ||
|
||
export async function POST(req: NextRequest) { | ||
try { | ||
const user = await getOrCreateUserFromRequest(req); | ||
if (!user) { | ||
throw new AppError("Unauthorized", HTTPStatus.UNAUTHORIZED); | ||
} | ||
|
||
const formData = await req.formData(); | ||
const feedback = formData.get("feedback") as string; | ||
const imageData = formData.get("image") as File | null; | ||
const metadataStr = formData.get("metadata") as string; | ||
|
||
if (!feedback) { | ||
throw new AppError("Feedback message is required", HTTPStatus.BAD_REQUEST); | ||
} | ||
|
||
let imageBuffer: Buffer | undefined; | ||
if (imageData) { | ||
const arrayBuffer = await imageData.arrayBuffer(); | ||
imageBuffer = Buffer.from(arrayBuffer); | ||
} | ||
|
||
const metadata = metadataStr ? JSON.parse(metadataStr) : { | ||
url: req.nextUrl.pathname, | ||
userAgent: req.headers.get("user-agent") || "unknown", | ||
timestamp: new Date().toISOString(), | ||
}; | ||
|
||
const feedbackService = new FeedbackService(prisma); | ||
const result = await feedbackService.submitFeedback({ | ||
userId: user.id, | ||
feedback, | ||
image: imageBuffer, | ||
metadata, | ||
}); | ||
|
||
return ApiResponse.success({ success: true, feedback: result }); | ||
} catch (error) { | ||
return ApiResponse.error(error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.