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
16 changes: 14 additions & 2 deletions app/(main)/(auth)/applications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Metadata } from 'next';

import { getMyApplications } from '@/prisma/data/applications';
import {
getApplicationCompletion,
getMyApplications,
} from '@/prisma/data/applications';

import { getCurrentUser } from '@/lib/auth/server';

Expand All @@ -13,14 +16,23 @@ export default async function MyApplicationsPage() {
const user = await getCurrentUser();
const applications = await getMyApplications(user.id);
const now = new Date();
const completion = await getApplicationCompletion(
applications
.filter((a) => a.status === 'draft')
.map((a) => ({ id: a.id, positionId: a.positionId, userId: user.id })),
);

return (
<div className="flex w-full flex-col gap-6">
<PageHeader
title="My Applications"
description="Track your drafts and submitted applications."
/>
<MyApplicationsTable applications={applications} now={now} />
<MyApplicationsTable
applications={applications}
now={now}
completion={completion}
/>
</div>
);
}
63 changes: 44 additions & 19 deletions components/features/applications-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
compareMergedApplicationRows,
getAllApplications,
getAllDraftApplications,
getApplicationCompletion,
getApplications,
getApplicationsCount,
getDraftApplications,
Expand Down Expand Up @@ -103,18 +104,27 @@ export async function ApplicationsResults({
filters,
page,
);
const completion = await getApplicationCompletion(
rows.map((r) => ({
id: r.id,
positionId: r.position.id,
userId: r.user.id,
})),
);

return (
<div className={DATA_TABLE_RESULTS_CLASS}>
<p className="text-muted-foreground flex items-start gap-2 text-sm">
<STATE_ICONS.hidden className="mt-0.5 size-4 shrink-0" />
You can see who started an application, not what they&apos;ve written.
Draft answers stay private until the applicant submits.
You can see who started an application and how far along it is, not
what they&apos;ve written. Draft answers stay private until the
applicant submits.
</p>

<ApplicationsTable
isDraftView
applications={rows}
completion={completion}
hasActiveFilters={hasActiveFilters}
sort={filters.sort}
isAdmin={user.isAdmin}
Expand Down Expand Up @@ -153,11 +163,21 @@ export async function ApplicationsResults({

const { rows, total, totalPages, currentPage, rangeStart, rangeEnd } =
paginateRows(merged, page);
const completion = await getApplicationCompletion(
rows
.filter((r) => r.isDraft)
.map((r) => ({
id: r.id,
positionId: r.position.id,
userId: r.user.id,
})),
);

return (
<div className={DATA_TABLE_RESULTS_CLASS}>
<ApplicationsTable
applications={rows}
completion={completion}
hasActiveFilters={hasActiveFilters}
sort={filters.sort}
isAdmin={user.isAdmin}
Expand All @@ -182,6 +202,7 @@ export async function ApplicationsResults({
<div className={DATA_TABLE_RESULTS_CLASS}>
<ApplicationsTable
applications={rows.map((a) => ({ ...a, isDraft: false as const }))}
completion={{}}
hasActiveFilters={hasActiveFilters}
sort={filters.sort}
isAdmin={user.isAdmin}
Expand All @@ -206,23 +227,27 @@ interface ApplicationsResultsSkeletonProps {
export function ApplicationsResultsSkeleton({
isDraftView = false,
}: ApplicationsResultsSkeletonProps) {
const columns: DataTableSkeletonColumn[] = [
...(isDraftView
? []
: ([
{
head: 'w-10',
shape: 'checkbox',
headClassName: 'w-10',
cellClassName: 'w-10',
mobile: 'leading',
},
] satisfies DataTableSkeletonColumn[])),
{ head: 'w-24', cell: 'w-36', subCell: 'w-48', mobile: 'primary' },
{ head: 'w-20', cell: 'w-28' },
{ head: 'w-16', cell: 'w-20', shape: 'badge', mobile: 'trailing' },
{ head: 'w-24', cell: 'w-20' },
];
const columns: DataTableSkeletonColumn[] = isDraftView
? [
{ head: 'w-24', cell: 'w-36', subCell: 'w-48', mobile: 'primary' },
{ head: 'w-20', cell: 'w-28' },
{ head: 'w-16', cell: 'w-16' },
{ head: 'w-20', cell: 'w-24' },
{ head: 'w-24', cell: 'w-20' },
]
: [
{
head: 'w-10',
shape: 'checkbox',
headClassName: 'w-10',
cellClassName: 'w-10',
mobile: 'leading',
},
{ head: 'w-24', cell: 'w-36', subCell: 'w-48', mobile: 'primary' },
{ head: 'w-20', cell: 'w-28' },
{ head: 'w-16', cell: 'w-20', shape: 'badge', mobile: 'trailing' },
{ head: 'w-24', cell: 'w-20' },
];

return (
<div className={DATA_TABLE_RESULTS_CLASS}>
Expand Down
47 changes: 29 additions & 18 deletions components/features/applications-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
STATE_ICONS,
} from '@/lib/icons';
import type {
ApplicationCompletion,
ApplicationSort,
ApplicationSortDirection,
ApplicationSortField,
Expand All @@ -35,11 +36,13 @@ import { Checkbox } from '@/components/ui/checkbox';
import { DataTable } from '@/components/ui/data-table';
import { EmptyState } from '@/components/ui/empty-state';
import { LocalTime } from '@/components/ui/local-time';
import { ProgressRing } from '@/components/ui/progress-ring';

interface BaseApplicationsTableProps {
hasActiveFilters: boolean;
sort?: ApplicationSort;
isAdmin: boolean;
completion: Record<string, ApplicationCompletion>;
}

// Discriminated on isDraftView: true is the explicit "Draft" filter (pure
Expand All @@ -59,7 +62,7 @@ function isAdminRow(
}

export function ApplicationsTable(props: ApplicationsTableProps) {
const { hasActiveFilters, sort, isAdmin } = props;
const { hasActiveFilters, sort, isAdmin, completion } = props;
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
Expand Down Expand Up @@ -172,6 +175,17 @@ export function ApplicationsTable(props: ApplicationsTableProps) {
</Link>
),
},
{
key: 'progress',
header: 'Progress',
// Server-side pagination sorts on ApplicationSortField, which progress isn't.
cell: (app) => {
const entry = completion[app.id];
return entry ? (
<ProgressRing percent={entry.percent} size="sm" />
) : null;
},
},
{
key: 'started',
header: 'Started',
Expand Down Expand Up @@ -274,16 +288,11 @@ export function ApplicationsTable(props: ApplicationsTableProps) {
sortAccessor: (a) => (a.isDraft ? 'draft' : a.status),
cell: (app) => {
if (app.isDraft) {
const entry = completion[app.id];
return (
<div className="flex items-center gap-1">
<ApplicationStatusBadge status="draft" />
<ApplicationStatusActions
applicationId={app.id}
currentStatus="draft"
applicantName={displayUserName(app.user)}
applicantEmail={app.user.email}
isAdmin={isAdmin}
/>
{entry && <ProgressRing percent={entry.percent} size="sm" />}
</div>
);
}
Expand Down Expand Up @@ -366,9 +375,14 @@ export function ApplicationsTable(props: ApplicationsTableProps) {
>
{app.position.title}
</Link>
<span className="text-muted-foreground text-xs">
Started <LocalTime date={app.createdAt} precision="date" /> ·
Updated <LocalTime date={app.updatedAt} precision="date" />
<span className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
<span>
Started <LocalTime date={app.createdAt} precision="date" /> ·
Updated <LocalTime date={app.updatedAt} precision="date" />
</span>
{completion[app.id] && (
<ProgressRing percent={completion[app.id]!.percent} size="sm" />
)}
</span>
</div>
)}
Expand Down Expand Up @@ -422,6 +436,7 @@ export function ApplicationsTable(props: ApplicationsTableProps) {
onSortToggle={(key) => toggleSort(key as ApplicationSortField)}
mobileCard={(app) => {
if (app.isDraft) {
const entry = completion[app.id];
return (
<div className="flex gap-3 p-4">
<div className="w-4 shrink-0" aria-hidden />
Expand All @@ -432,13 +447,9 @@ export function ApplicationsTable(props: ApplicationsTableProps) {
</span>
<div className="flex shrink-0 items-center gap-1">
<ApplicationStatusBadge status="draft" />
<ApplicationStatusActions
applicationId={app.id}
currentStatus="draft"
applicantName={displayUserName(app.user)}
applicantEmail={app.user.email}
isAdmin={isAdmin}
/>
{entry && (
<ProgressRing percent={entry.percent} size="sm" />
)}
</div>
</div>
{app.user.name && (
Expand Down
Loading
Loading