Skip to content
Draft
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
16 changes: 5 additions & 11 deletions app/admin/applications/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
UsersRoundIcon,
} from "lucide-react";
import { getApplicationReviewLeaderboard } from "@/lib/queries/application-review";
import { formatShortDateTime } from "@/lib/format/dates";
import type { ReviewLeaderboardRow } from "@/lib/types/application-reviews";
import {
Card,
Expand All @@ -22,16 +23,6 @@ import { Meter } from "../components/meter";
import { SummaryBar } from "../components/summary-bar";
import { AuditActivityFeed } from "./audit-activity-feed";

function formatDate(value: string | null) {
if (!value) return "No activity yet";
return new Date(value).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

function rankIcon(index: number) {
if (index === 0) {
return <CrownIcon className="size-4 text-amber-500 dark:text-amber-300" />;
Expand Down Expand Up @@ -87,7 +78,10 @@ function LeaderboardRow({
<div className="min-w-0">
<p className="truncate font-medium">{row.reviewerEmail}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Last completed {formatDate(row.lastActivityAt)}
Last completed{" "}
{row.lastActivityAt
? formatShortDateTime(row.lastActivityAt)
: "No activity yet"}
</p>
</div>
</div>
Expand Down
65 changes: 14 additions & 51 deletions app/admin/applications/review-event-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import type {
ReviewAuditEventRecord,
ReviewEventRecord,
} from "@/lib/types/application-reviews";
import {
applicationStatusBadgeClass,
reviewEventTypeBadgeClass,
} from "@/lib/utils/badge-classes";
import {
formatShortDateTime,
formatTimelineTimestamp,
} from "@/lib/format/dates";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { paginateSlice } from "@/lib/pagination";
Expand All @@ -31,56 +39,11 @@ function isAuditEvent(event: TimelineEvent): event is ReviewAuditEventRecord {
return "applicationName" in event;
}

function formatEventTimestamp(value: string, compact: true): string;
function formatEventTimestamp(
value: string,
compact?: false,
): { date: string; time: string };
function formatEventTimestamp(value: string, compact = false) {
const date = new Date(value);
if (compact) {
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

return {
date: date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
}),
time: date.toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
}),
};
}

function eventTypeBadgeClass(eventType: ReviewEventRecord["eventType"]) {
if (eventType === "review_completed") {
return "border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-950/50 dark:text-green-300";
}
return "border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300";
}

function eventTypeLabel(eventType: ReviewEventRecord["eventType"]) {
if (eventType === "review_completed") return "Completed";
return "Draft saved";
}

function statusBadgeClass(status: ReviewAuditEventRecord["applicationStatus"]) {
if (status === "reviewed") {
return "border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-950/50 dark:text-green-300";
}
if (status === "flagged") {
return "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/70 dark:bg-amber-950/50 dark:text-amber-300";
}
return "border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300";
}

function ChangeChip({
field,
from,
Expand Down Expand Up @@ -125,22 +88,22 @@ function ChangeChip({

function CompactReviewEventRow({ event }: { event: TimelineEvent }) {
const changes = Object.entries(event.changes);
const timestamp = formatEventTimestamp(event.createdAt, true);
const timestamp = formatShortDateTime(event.createdAt);

return (
<div className="min-w-0 space-y-2 py-3">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<Badge
variant="outline"
className={eventTypeBadgeClass(event.eventType)}
className={reviewEventTypeBadgeClass(event.eventType)}
>
{eventTypeLabel(event.eventType)}
</Badge>
{isAuditEvent(event) ? (
<Badge
variant="outline"
className={statusBadgeClass(event.applicationStatus)}
className={applicationStatusBadgeClass(event.applicationStatus)}
>
{applicationStatusLabel(event.applicationStatus)}
</Badge>
Expand Down Expand Up @@ -187,7 +150,7 @@ export function ReviewEventRow({
}

const changes = Object.entries(event.changes);
const timestamp = formatEventTimestamp(event.createdAt);
const timestamp = formatTimelineTimestamp(event.createdAt);

return (
<div className="relative flex min-w-0 gap-3 px-4 py-3">
Expand Down Expand Up @@ -220,14 +183,14 @@ export function ReviewEventRow({
<div className="flex flex-wrap items-center gap-1.5">
<Badge
variant="outline"
className={eventTypeBadgeClass(event.eventType)}
className={reviewEventTypeBadgeClass(event.eventType)}
>
{eventTypeLabel(event.eventType)}
</Badge>
{isAuditEvent(event) ? (
<Badge
variant="outline"
className={statusBadgeClass(event.applicationStatus)}
className={applicationStatusBadgeClass(event.applicationStatus)}
>
{applicationStatusLabel(event.applicationStatus)}
</Badge>
Expand Down
Loading
Loading