Skip to content

Commit a00499d

Browse files
authored
Merge pull request #90 from Ekene001/Feat/Implement-Sponsor-Review-Dashboard
feat:implement sponsor review dashboard.
2 parents 5772cad + 61444ae commit a00499d

4 files changed

Lines changed: 1203 additions & 1903 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { format, parseISO } from "date-fns"
5+
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
6+
import { Button } from "@/components/ui/button"
7+
import { ReviewSubmission } from "@/types/participation"
8+
9+
type Action = 'approve' | 'reject' | 'request_revision'
10+
11+
interface SponsorReviewDashboardProps {
12+
submissions: ReviewSubmission[]
13+
onAction?: (submissionId: string, action: Action) => Promise<void> | void
14+
}
15+
16+
export function SponsorReviewDashboard({ submissions, onAction }: SponsorReviewDashboardProps) {
17+
const [items, setItems] = React.useState<ReviewSubmission[]>(() => submissions)
18+
const [loadingIds, setLoadingIds] = React.useState<Record<string, boolean>>({})
19+
20+
React.useEffect(() => {
21+
setItems(curr => {
22+
const currIdMap = new Map(curr.map(it => [it.submissionId, it]))
23+
return submissions.map(sub => (currIdMap.get(sub.submissionId) ?? sub) as ReviewSubmission)
24+
})
25+
}, [submissions])
26+
27+
const handleAction = async (id: string, action: Action) => {
28+
setLoadingIds(s => ({ ...s, [id]: true }))
29+
let prevItem: ReviewSubmission | undefined
30+
31+
setItems(curr => curr.map(it => {
32+
if (it.submissionId === id) {
33+
prevItem = it
34+
return {
35+
...it,
36+
status: action === 'approve' ? 'approved' : action === 'reject' ? 'rejected' : 'revision_requested'
37+
}
38+
}
39+
return it
40+
}))
41+
42+
try {
43+
const maybe = onAction && onAction(id, action)
44+
if (maybe && maybe instanceof Promise) await maybe
45+
} catch (err) {
46+
if (prevItem) {
47+
setItems(curr => curr.map(it => (it.submissionId === id ? prevItem : it)) as ReviewSubmission[])
48+
}
49+
} finally {
50+
setLoadingIds(s => {
51+
const copy = { ...s }
52+
delete copy[id]
53+
return copy
54+
})
55+
}
56+
}
57+
58+
return (
59+
<div className="space-y-4">
60+
{items.length === 0 && <div className="text-sm text-muted-foreground">No submissions to review.</div>}
61+
62+
<ul className="space-y-2">
63+
{items.map(sub => (
64+
<li key={sub.submissionId} className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 rounded-md border p-3">
65+
<div className="flex items-center gap-3 min-w-0 w-full sm:w-auto">
66+
<Avatar className="shrink-0">
67+
{sub.contributor.avatarUrl ? (
68+
<AvatarImage src={sub.contributor.avatarUrl} alt={sub.contributor.username} />
69+
) : (
70+
<AvatarFallback>{sub.contributor.username?.charAt(0).toUpperCase() ?? "?"}</AvatarFallback>
71+
)}
72+
</Avatar>
73+
<div className="min-w-0 flex-1">
74+
<div className="font-medium truncate">{sub.contributor.username}</div>
75+
<div className="text-xs text-muted-foreground">Submitted {format(parseISO(sub.submittedAt), 'MM/dd/yyyy, hh:mm aa')}</div>
76+
{sub.milestoneId && <div className="text-xs text-muted-foreground">Milestone: {sub.milestoneId}</div>}
77+
</div>
78+
</div>
79+
80+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 w-full sm:w-auto">
81+
<div className="text-sm shrink-0">
82+
{sub.status === 'pending' && <span className="text-yellow-600 font-medium">Pending</span>}
83+
{sub.status === 'approved' && <span className="text-green-600 font-medium">Approved</span>}
84+
{sub.status === 'rejected' && <span className="text-red-600 font-medium">Rejected</span>}
85+
{sub.status === 'revision_requested' && <span className="text-blue-600 font-medium">Revision Requested</span>}
86+
</div>
87+
88+
<div className="flex flex-wrap gap-2 w-full sm:w-auto">
89+
<Button size="sm" variant="ghost" onClick={() => handleAction(sub.submissionId, 'request_revision')} disabled={!!loadingIds[sub.submissionId]} className="flex-1 sm:flex-none">
90+
Request revision
91+
</Button>
92+
<Button size="sm" variant="outline" onClick={() => handleAction(sub.submissionId, 'reject')} disabled={!!loadingIds[sub.submissionId]} className="text-red-600 flex-1 sm:flex-none">
93+
Reject
94+
</Button>
95+
<Button size="sm" onClick={() => handleAction(sub.submissionId, 'approve')} disabled={!!loadingIds[sub.submissionId]} className="text-green-600 flex-1 sm:flex-none">
96+
Approve
97+
</Button>
98+
</div>
99+
</div>
100+
</li>
101+
))}
102+
</ul>
103+
</div>
104+
)
105+
}
106+
107+
export default SponsorReviewDashboard

0 commit comments

Comments
 (0)