Skip to content

#706 Show Position Deadlines On Draft Applications - #707

Open
cielbellerose wants to merge 5 commits into
devfrom
706-show-position-deadlines-on-draft-applications
Open

cielbellerose wants to merge 5 commits into
devfrom
706-show-position-deadlines-on-draft-applications

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #706

Summary

  • Adds a Deadline column (and matching mobile card line) to the My Applications table, and a deadline slot in the dashboard's My Applications widget, so a draft's position deadline is finally visible on the applicant's own surfaces.
  • Introduces getDeadlineInfo (lib/utils.ts) — a pure helper that classifies a position's window into upcoming / distant / soon / urgent / past and derives the countdown text from a single diffMs, so the tier and the displayed text can never disagree.
  • Floats at-risk drafts (closing within 7 days) to the top: client-side in the table (pre-sorted rows, sort-key stays null so a header click still takes over), and query-side in getRecentMyApplications (merges a bounded at-risk query with the existing recency query) — the dashboard widget's take 3 was silently hiding an old draft with an approaching deadline.
  • Adds getClosingSoonDraftCount and a N closing soon segment to the widget's subtitle.

Changes

  • lib/constants.tsDEADLINE_SOON_DAYS (7), DEADLINE_URGENT_HOURS (48): sole owners of the urgency boundaries, shared by the helper and the query.
  • lib/types.tsDeadlineTier, DeadlineInfo.
  • lib/utils.tsgetDeadlineInfo(position, now), placed directly below getPositionDateInfo (deliberately not merged into it — see the "Notes" section).
  • components/features/deadline-indicator.tsx — new; the only place a deadline is styled, full (table/card) and compact (widget) variants.
  • components/features/my-applications-table.tsxDeadline column, now prop, at-risk pre-sort, mobile card line.
  • app/(main)/(auth)/applications/page.tsx — resolves now server-side and passes it down (avoids a hydration mismatch near a tier boundary).
  • prisma/data/applications.tsbuildAtRiskDraftWhere (shared), the at-risk float in getRecentMyApplications, new getClosingSoonDraftCount.
  • components/features/my-applications-widget.tsx — deadline in the trailing slot for drafts, N closing soon subtitle segment, one shared now.
  • tests/unit/utils.test.ts — tier/countdown/boundary coverage for getDeadlineInfo.
  • tests/db/my-applications-deadline-ordering.test.ts — new; float order, dedupe, take cap, exclusions (past-due, not-yet-open, unpublished, soft-deleted), and count parity.
  • docs/WORKFLOWS.md — AP-10 (Deadline column, tiers, at-risk float) and AP-1 (widget trailing slot + N closing soon).

Deliberately not changed: getPositionDateInfo / PositionDateLine. Widening its emphasis union with a "closing soon" tier would repaint every position card and detail page amber/red, which this ticket doesn't ask for. getDeadlineInfo owns applicant urgency instead; both still classify through the shared getPositionAvailability, so they can never disagree about the underlying window state, only about how to present it.

Testing plan

  • /applications shows a Deadline cell for every row: a draft closing in ~12 hours gets a red badge ("Closes in N hours"), one closing in ~3 days gets an amber badge ("Closes in N days"), one closing in ~30 days shows a plain muted "Closes ", one with no closesAt shows "—", one past its close date shows a red "Closed " badge (with Continue still available), and one not yet open shows a muted "Opens " with no badge.
  • A submitted application's deadline always renders as plain muted text/date, never a badge, regardless of how close the date is.
  • At-risk drafts (soon/urgent) sort to the top of the table, nearest deadline first, ahead of more-recently-touched rows; a not-yet-open or past-due draft never floats.
  • Clicking any column header (Position, Status, Applied, Deadline) takes over sorting and the at-risk float does not reassert itself; ?sort=/?dir= round-trips on reload.
  • Narrower than md, the stacked mobile card shows the same deadline/tiering on its own line.
  • Hovering a deadline shows the exact instant and timezone in the tooltip.
  • The dashboard's My Applications widget lists an old-but-at-risk draft ahead of more-recent applications, with a compact "Nh left"/"Nd left" badge, and the subtitle appends N closing soon when at least one at-risk draft exists.
  • Submitting or deleting the last at-risk draft and reloading / drops the count and removes the closing soon segment.
  • An applicant with no applications still gets the table's and widget's existing empty states; an applicant with applications but no at-risk drafts sees the widget's prior ordering/subtitle unchanged.
  • Both the amber (warning) and red (destructive) badges stay legible in light and dark mode.
  • Unit tests cover getDeadlineInfo's tier boundaries (48h inclusive/exclusive, 7d inclusive/exclusive, past, upcoming, null-date) and singular/plural countdown text.
  • DB test covers getRecentMyApplications's float order, dedupe of a row qualifying for both queries, the take cap, and exclusion of past-due/not-yet-open/unpublished/soft-deleted positions from the float, plus getClosingSoonDraftCount parity.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — pass (409 tests)
  • tests/db/my-applications-deadline-ordering.test.ts — added but not executed in this environment: this worktree's Postgres port (5432) was already occupied by an unrelated container and I could not safely provision a second local database to run the db test project. The test logic was traced manually against the implementation (see PR description); please run npm run test (or the db project alone) in CI/review to confirm.

Notes

  • The countdown is only as fresh as the render; both surfaces are per-request dynamic and revalidated after every application mutation, so a long-lived open tab is the only staleness window — the alternative (a client-side ticking timer) would be a useEffect this codebase avoids.
  • formatCountdown in lib/utils.ts (the login OTP mm:ss timer) is unrelated and untouched.

🤖 Generated with Claude Code

Adds a Deadline column/mobile line and dashboard-widget trailing slot
with amber/red urgency badges for at-risk drafts, and floats at-risk
drafts ahead of recency order in both the table and
getRecentMyApplications so a stale-but-closing-soon draft can't be
hidden by a take-bounded query.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@cielbellerose cielbellerose added the claude Will be worked on by Claude label Sep 11, 2026
@cielbellerose cielbellerose self-assigned this Sep 11, 2026
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aplio Ready Ready Preview Sep 11, 2026 9:37pm UTC

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 11, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 1 · needs revision

4 open — 1 🟠 Medium, 3 🟡 Low (see inline)

Comment thread components/features/deadline-indicator.tsx
Comment thread prisma/data/applications.ts Outdated
Comment thread lib/utils.ts Outdated
Comment thread lib/utils.ts
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 11, 2026
Co-Authored-By: Claude Sonnet 5 <[email protected]>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-M1, R1-L1, R1-L2, R1-L3 · faf8184

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Sep 11, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 2 · approved

0 open

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision requested — UX feedback from review of the preview

Three changes, all in components/features/deadline-indicator.tsx and
components/features/my-applications-widget.tsx. Item 3 reverses a decision
from the approved plan — read its note before implementing.

1. Closes vs Closed is not distinguishable enough

On /applications, a future deadline and a past one currently differ by a
single character in identical styling. For any row where emphasizeUrgency is
false (every submitted row), distant and past both fall through to the same
mutedLine🗓 Closes Mar 28 next to 🗓 Closed Mar 02, same icon, same
muted colour, same weight. Scanning the column, they read as the same thing.

Make the past state unmistakable without shouting on rows the user can't act
on. Options worth considering — pick one and say why:

  • a different icon for past (the calendar-clock currently reads "scheduled",
    not "over")
  • clearer wording than a one-letter delta, e.g. Closed Mar 2 vs
    Closes Mar 28Closed · Mar 2 / Due Mar 28, or past-tense phrasing
    that can't be misread at a glance
  • a distinct (still muted) treatment for past — e.g. strikethrough or a
    deliberately dimmer token

Whatever you choose has to hold for submitted rows too, which is where the
ambiguity actually bites — those never get the urgency branch.

2. Widget: the deadline belongs to the left of the status badge

In ApplicationList, the row is currently
title (flex-1) · ApplicationStatusBadge · deadline (trailing), so the
deadline sits to the right of the Draft tag. Move the deadline before the
status badge.

The reason is the whole point of the ticket: someone with an unsubmitted draft
needs the closing date to land in the same glance as the word "Draft", not
pushed to the far edge of the row. Reorder for all rows so the column
rhythm stays consistent; urgency styling still applies only to drafts.

Keep the layout resilient — the row is flex flex-wrap, and the title holds
min-w-0 flex-1 truncate, so verify a long position title still truncates
rather than pushing the deadline off-row at narrow widths.

3. The countdown should be text, not a Badge

soon and urgent currently render <Badge variant="warning"> /
<Badge variant="destructive">. Drop the badge. The countdown should read as
emphasised text — red and/or bold — not as a pill. Two filled tags in one row
(status + deadline) competes with the status badge and reads heavier than it
should for what is a piece of metadata.

This reverses the approved plan, and it reopens a real accessibility
constraint — solve it, don't re-discover it.
The badges were chosen precisely
because plain coloured text failed AA:

  • --warning is oklch(0.7 0.15 85); as text on bg-card it measures
    ≈2.7:1 — far below AA. Every existing text-warning use in the repo is
    icon-only for this reason (warning-callout.tsx,
    profile-completeness-banner.tsx, position-date-line.tsx).
  • --destructive is oklch(0.55 0.22 28) and is identical in both themes
    (app/globals.css:75 and :120), while --card inverts from
    oklch(1 0 0) to oklch(0.19 0 0). So text-destructive on a card is
    ≈5.4:1 in light mode but only ≈3.4:1 in dark mode — passing in one
    theme, failing in the other.
  • Bold does not rescue it: WCAG's 3:1 large-text threshold needs ≥18.66px for
    bold, and these surfaces are text-sm (14px) and text-xs (12px).

So the fix is a text-safe colour, most likely a new theme-aware token
(a --destructive/--warning variant that lightens in dark mode, following
the per-theme pattern already in globals.css) rather than reusing a token
that was only ever validated as a fill or an icon. Weight and an icon can carry
additional emphasis, but the colour has to pass on its own at 12px and 14px, in
both themes.

State the computed ratios for whatever you land on, for light and dark, at both
sizes — and add a dark-mode contrast check to the manual test matrix.

Unchanged

Everything else stands: the single-diffMs helper, getDeadlineInfo taking a
PositionWindow and gating urgency on getPositionAvailability, the
server-resolved now, the sort.key-stays-null pre-sort, the two
take-bounded queries plus the separate count, and LocalTime wrapping every
rendered date so the exact instant stays in the tooltip. Do not touch
getPositionDateInfo or PositionDateLine — position cards must not change
appearance.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Sep 11, 2026
Distinguish past deadlines (icon + dimmer muted line), move the
deadline before the status badge in the widget, and replace the
soon/urgent/past Badge with AA-safe text tokens (--warning-text /
--destructive-text) instead of the fill-only --warning/--destructive.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — UX feedback follow-up

fixed all 3 items (past-deadline distinctness, deadline-before-badge ordering, Badge → AA-safe text tokens) · 2591a05

New tokens --warning-text / --destructive-text (app/globals.css): light 5.53:1 / 5.43:1, dark 5.66:1 / 5.36:1 against --card at 12px/14px (WCAG AA normal-text threshold, 4.5:1).

@cielbellerose cielbellerose removed the revising Revise agent working (in-flight) label Sep 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Cockpit note on Cycle 3 findings

R3-L2 is not a finding — dismiss it. The review reads the widget's
deadline-before-badge order as an unexplained departure from the mockup in the
issue. It is the opposite: the human explicitly requested that reorder in
this comment:

on the dash the closes on date should be to the left of the draft tag … we
want users who still have drafts to be very clear about that position about
to close if they havent submitted their draft

Do not revert the ordering. The issue's mockup predates that instruction
and is the stale artifact here.

The same comment is also why the Badge became plain text — another
intentional, human-requested change, not drift.

R3-M1 is valid and is the actual fix: docs/WORKFLOWS.md (AP-10, AP-1)
still describes the urgency tiers as badges and still reflects the old row
order. Update both entries to match shipped behaviour — plain
text-warning-text / text-destructive-text emphasis, and the deadline
preceding the status badge. While there, update the issue body's mockup so the
next reviewer isn't misled the same way.

R3-L1 is valid: trim the 3-line comment in deadline-indicator.tsx per
ENGINEERING §7.

@cielbellerose cielbellerose added revising Revise agent working (in-flight) and removed needs revision Review found issues that need fixing labels Sep 11, 2026
Co-Authored-By: Claude Sonnet 5 <[email protected]>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 3

fixed R3-M1, R3-L1 · skipped R3-L2 (false positive — human-requested order, see cockpit note) · be20d81

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 11, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 11, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 4 · approved

2 open — 2 🟡 Low (see inline)


// Floats at-risk drafts (closing within DEADLINE_SOON_DAYS) ahead of recency
// order — a `take`-bounded recency query alone can hide one entirely. Both
// queries stay take-bounded; never an unbounded fetch-then-sort.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R4-L1 🟡 Low — this comment is 3 lines; ENGINEERING.md §7 caps at two only rarely, three is always wrong. Fix: compress to one line, e.g. // Floats at-risk drafts (closing within DEADLINE_SOON_DAYS) ahead of recency order — a take-bounded recency query alone can hide one entirely.

describe('getClosingSoonDraftCount', () => {
// 2, not 6 — the other four drafts (past-due, not-yet-open, unpublished,
// soft-deleted) share buildAtRiskDraftWhere with the float above, so a
// count that disagreed with it would be a drift bug, not a design choice.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R4-L2 🟡 Low — this comment is 3 lines; ENGINEERING.md §7 caps at two only rarely, three is always wrong. Fix: compress to one line, e.g. // 2, not 6 — the other four drafts share buildAtRiskDraftWhere, so a mismatch here is a drift bug.

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision requested — copy simplification

Two changes, both in components/features/deadline-indicator.tsx.

1. Drop the Closes / Closed prefix

The Deadline column header already says what the date is, and the column is
sorted by it, so the word is redundant. Render the date (or countdown) alone.

Applies to the table cell, the mobile card, and the widget row — wherever the
prefix currently appears.

Keep the visual differentiation for past. The CalendarX icon, the
opacity-70 dimming and the · separator added last cycle stay — those are
what still distinguish a closed deadline from an upcoming one once the word is
gone. Do not drop them along with the label.

One exception: upcoming keeps its Opens label. That one is not
redundant, it is load-bearing — a bare Mar 20 sitting under a column headed
Deadline reads as the deadline when it is actually the opens date. Different
meaning, not a different tense of the same meaning. Keep Opens <date> exactly
as it renders today.

So, after this change:

tier renders
upcoming Opens Mar 20, 2026 (unchanged)
distant Mar 28, 2026
soon 5d left
urgent 8h left
past Mar 2, 2026 + CalendarX + dimming

2. One countdown format everywhere — the compact one

The dashboard renders 8h left while the table renders Closes in 8 hours.
Same information, two formats. Unify on the compact form, which is the
dashboard's: 8h left, 5d left.

In practice that means the full variant uses compactCountdown rather than
countdown for the soon / urgent tiers. If countdown ends up with no
remaining callers, delete it from DeadlineInfo and getDeadlineInfo rather
than leaving it dead — and drop its now-unused unit tests.

LocalTime must still wrap the countdown so the exact instant stays in the
tooltip; that was a prior review finding and it is the only thing carrying the
full date once the visible text is this terse.

Keep the docs and the plan in sync — same PR

Both have already drifted once this ticket and cost a review cycle on a false
positive:

  • update docs/WORKFLOWS.md AP-1 and AP-10 to describe the label-free copy
    and the single compact countdown format
  • add these two changes to the supersession comment on issue Show Position Deadlines On Draft Applications #706 (or post a
    follow-up comment there), so the next reviewer does not read correct code as
    drift from the plan

Unchanged

The two text-safe tokens and their measured ratios, the deadline-before-badge
row order, the at-risk float, the server-resolved now, the
getPositionAvailability gating, and the take-bounded queries all stand.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Sep 11, 2026
Deadline column header already names the field, so drop the redundant
Closes/Closed prefix (upcoming keeps Opens, which is load-bearing);
render soon/urgent as the compact Nd/Nh-left countdown everywhere
instead of a longer table-only form. Removes the now-unused
DeadlineInfo.countdown field and its tests.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — copy simplification follow-up

dropped Closes/Closed prefix (kept Opens, load-bearing) · unified soon/urgent countdown to compact Nd/Nh left everywhere · removed now-unused DeadlineInfo.countdown + its tests · docs/WORKFLOWS.md AP-1/AP-10 updated · follow-up posted on #706 · db45223

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 11, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 11, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 5 · approved

1 open — 1 🟡 Low (see inline)

}: MyApplicationsTableProps) {
const columns = useMemo(() => buildColumns(now), [now]);

// At-risk drafts float to the top, nearest deadline first; everything else

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R5-L1 🟡 Low — this comment is 3 lines; ENGINEERING.md §7 caps at one line by default, two only rarely, three is always wrong. Fix: compress to one line, e.g. // At-risk drafts float to the top, nearest deadline first; sort.key stays null so a header click still takes over.

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 11, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author
Screenshot 2026-09-11 at 6 22 34 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show Position Deadlines On Draft Applications

1 participant