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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Loader2, Minus, ThumbsDown, ThumbsUp } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";

interface GradingActionButtonsProps {
disabled: boolean;
onReject: () => void;
onWaitlist: () => void;
onAccept: () => void;
label?: string;
}

export function GradingActionButtons({
disabled,
onReject,
onWaitlist,
onAccept,
label = "Cast your vote",
}: GradingActionButtonsProps) {
return (
<div>
<Label className="text-xs text-muted-foreground">{label}</Label>
<div className="flex flex-col gap-2 mt-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
className="w-full cursor-pointer hover:bg-red-50 hover:text-red-700 hover:border-red-300 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onReject}
disabled={disabled}
>
{disabled ? (
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
) : (
<ThumbsDown className="h-4 w-4 mr-1.5" />
)}
Reject
<kbd className="ml-auto px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
⌘J
</kbd>
</Button>
</TooltipTrigger>
<TooltipContent>Reject (⌘J)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
className="w-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 hover:border-yellow-300 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onWaitlist}
disabled={disabled}
>
{disabled ? (
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
) : (
<Minus className="h-4 w-4 mr-1.5" />
)}
Waitlist
<kbd className="ml-auto px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
⌘K
</kbd>
</Button>
</TooltipTrigger>
<TooltipContent>Waitlist (⌘K)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
className="w-full cursor-pointer hover:bg-green-50 hover:text-green-700 hover:border-green-300 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={onAccept}
disabled={disabled}
>
{disabled ? (
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
) : (
<ThumbsUp className="h-4 w-4 mr-1.5" />
)}
Accept
<kbd className="ml-auto px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
⌘L
</kbd>
</Button>
</TooltipTrigger>
<TooltipContent>Accept (⌘L)</TooltipContent>
</Tooltip>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import { memo } from "react";

import { Skeleton } from "@/components/ui/skeleton";
Expand All @@ -13,18 +14,16 @@ import {
} from "@/pages/admin/all-applicants/components/detail-sections";
import type { Application } from "@/types";

import type { Review } from "../../types";

interface GradingDetailsPanelProps {
application: Application | null;
review: Review | null;
loading: boolean;
children?: ReactNode;
}

export const GradingDetailsPanel = memo(function GradingDetailsPanel({
application,
review,
loading,
children,
}: GradingDetailsPanelProps) {
if (loading) {
return (
Expand Down Expand Up @@ -52,20 +51,7 @@ export const GradingDetailsPanel = memo(function GradingDetailsPanel({
<EventPreferencesSection application={application} />
<LinksSection application={application} />
<TimelineSection application={application} />

{review && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">
Review Details
</h3>
<div className="grid grid-cols-2 gap-y-2 text-sm">
<span className="text-muted-foreground">Application ID</span>
<span className="font-mono text-xs">{review.application_id}</span>
<span className="text-muted-foreground">Assigned at</span>
<span>{new Date(review.assigned_at).toLocaleString()}</span>
</div>
</div>
)}
{children}
</div>
);
});
108 changes: 108 additions & 0 deletions client/web/src/pages/admin/_shared/grading/GradingPageLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { ArrowLeft, ChevronLeft, ChevronRight } from "lucide-react";
import type { ReactNode } from "react";
import { useNavigate } from "react-router-dom";

import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";

interface GradingPageLayoutProps {
backUrl: string;
loading: boolean;
headerContent: ReactNode;
currentIndex: number;
totalCount: number;
onNavigateNext: () => void;
onNavigatePrev: () => void;
canNavigatePrev: boolean;
canNavigateNext: boolean;
detailsPanel: ReactNode;
actionPanel: ReactNode;
emptyState: ReactNode;
}

export function GradingPageLayout({
backUrl,
loading,
headerContent,
currentIndex,
totalCount,
onNavigateNext,
onNavigatePrev,
canNavigatePrev,
canNavigateNext,
detailsPanel,
actionPanel,
emptyState,
}: GradingPageLayoutProps) {
const navigate = useNavigate();

if (!loading && totalCount === 0) {
return <>{emptyState}</>;
}

return (
<div className="-m-4 flex flex-col h-[calc(100%+2rem)] min-h-0">
{/* Header */}
<div className="shrink-0 flex items-center gap-3 bg-gray-50 border-b px-4 py-3">
<Button
variant="ghost"
size="icon-sm"
className="cursor-pointer"
onClick={() => navigate(backUrl)}
>
<ArrowLeft className="h-4 w-4" />
</Button>

{loading ? <Skeleton className="h-5 w-40" /> : headerContent}

<div className="ml-auto flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
className="cursor-pointer"
onClick={onNavigatePrev}
disabled={!canNavigatePrev}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm text-muted-foreground tabular-nums">
{totalCount > 0 ? `${currentIndex + 1} of ${totalCount}` : "-"}
</span>
<Button
variant="ghost"
size="icon-sm"
className="cursor-pointer"
onClick={onNavigateNext}
disabled={!canNavigateNext}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>

{/* Content */}
<div className="flex flex-1 min-h-0">
{/* Left panel - Application details (75%) */}
<div className="w-3/4 overflow-auto border-r">{detailsPanel}</div>

{/* Right panel (25%) */}
<div className="w-1/4 flex flex-col bg-gray-50/50">
<div className="flex-1 overflow-auto">{actionPanel}</div>
{/* Navigation hint */}
<div className="shrink-0 border-t bg-gray-50 p-4 pt-2">
<p className="text-xs text-muted-foreground text-center mt-2">
Use{" "}
<kbd className="px-1 py-0.5 bg-muted rounded text-[10px] font-mono">
</kbd>{" "}
<kbd className="px-1 py-0.5 bg-muted rounded text-[10px] font-mono">
</kbd>{" "}
arrow keys to navigate &middot; Esc to go back
</p>
</div>
</div>
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions client/web/src/pages/admin/_shared/grading/ReviewerNotesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MessageSquare } from "lucide-react";

import { Label } from "@/components/ui/label";
import type { ReviewNote } from "@/pages/admin/assigned/types";

interface ReviewerNotesListProps {
notes: ReviewNote[];
loading: boolean;
}

export function ReviewerNotesList({ notes, loading }: ReviewerNotesListProps) {
return (
<div>
<div className="flex items-center gap-1.5 mb-2.5">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm text-muted-foreground">
Reviewer Notes ({notes.length})
</Label>
</div>
{loading ? (
<div className="text-xs text-muted-foreground">Loading notes...</div>
) : notes.length > 0 ? (
<div className="space-y-2.5">
{notes.map((note, idx) => (
<div
key={`${note.admin_id}-${idx}`}
className="bg-white border rounded-md p-3 text-sm"
>
<div className="flex items-center justify-between mb-1.5">
<span className="text-sm font-medium text-muted-foreground">
{note.admin_email}
</span>
<span className="text-xs text-muted-foreground">
{new Date(note.created_at).toLocaleDateString()}
</span>
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{note.notes}
</p>
</div>
))}
</div>
) : (
<p className="text-xs text-muted-foreground italic">
No reviewer notes
</p>
)}
</div>
);
}
5 changes: 5 additions & 0 deletions client/web/src/pages/admin/_shared/grading/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { GradingActionButtons } from "./GradingActionButtons";
export { GradingDetailsPanel } from "./GradingDetailsPanel";
export { GradingPageLayout } from "./GradingPageLayout";
export { ReviewerNotesList } from "./ReviewerNotesList";
export { useGradingKeyboardShortcuts } from "./useGradingKeyboardShortcuts";
Loading