Skip to content

feat: implement search functionality and enhance job filtering in applications dashboard#47

Merged
JoachimLK merged 3 commits into
mainfrom
feat/applications-search
Feb 26, 2026
Merged

feat: implement search functionality and enhance job filtering in applications dashboard#47
JoachimLK merged 3 commits into
mainfrom
feat/applications-search

Conversation

@JoachimLK

@JoachimLK JoachimLK commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What does this PR change?
  • Why is this needed?

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features

    • Debounced global search (250ms) across candidates and jobs; single-status filter pills and a job dropdown; “Clear filters” action; expanded sort options.
  • Style

    • Redesigned applications list to clickable card grid with avatar initials, status colored dots/badges, score badges, timestamps, and improved empty/loading states.
  • Documentation

    • Clarified auth URL guidance and preview domain notes in architecture docs.

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-47 February 26, 2026 12:51 Destroyed
@railway-app

railway-app Bot commented Feb 26, 2026

Copy link
Copy Markdown

🚅 Deployed to the applirank-pr-47 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Web Feb 26, 2026 at 1:21 pm

@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The applications dashboard page is redesigned from a table to a card-based layout with a single active status filter, debounced client-side search, job dropdown filter, and consolidated sorting options. Server-side utilities update Better Auth URL/trusted origins resolution and environment parsing; swipe view guards current application binding with a resolved ID ref.

Changes

Cohort / File(s) Summary
Applications Dashboard UI & Filtering
app/pages/dashboard/applications/index.vue
Replaces multi-select status filter with single activeStatus; adds debounced search (searchInputdebouncedSearch), job dropdown (activeJobId, uniqueJobs), and outside-click handling. Introduces unified activeSort/sortOptions and recomputes filteredApplications by job → search → sort.
UI Layout & Presentation
app/pages/dashboard/applications/index.vue
Converts table into card-based grid; clickable application cards with avatar initials, colored status dots/badges, score badges, job & candidate info, and timestamps. Removes panel-based filters (column visibility, score range) and adds inline controls and a results bar. Updates empty/loading states and styles.
Helpers & Rendering Logic
app/pages/dashboard/applications/index.vue
Adds helper functions (candidateInitials, initialsColor, timeAgo, scoreClass, statusDotColors) and updates status badge rendering with color + label translations.
Swipe View: current application guard
app/pages/dashboard/jobs/[id]/swipe.vue
Replaces computed currentApplicationId with a ref('') and a watcher that populates it; adds resolvedCurrentApplication computed that returns the current application only when IDs match. Templates and fetch/loading logic now use the guarded resolvedCurrentApplication.
Auth URL & Trusted Origins
server/utils/auth.ts, server/utils/env.ts
Adds BETTER_AUTH_TRUSTED_ORIGINS parsing; implements resolveTrustedOrigins(baseUrl) and a stricter resolveBetterAuthUrl that distinguishes preview vs. production, prefers RAILWAY_PUBLIC_DOMAIN for previews, logs missing preview domains, and ensures trustedOrigins are passed to Better Auth. Updates getAuth to use resolved baseURL + trusted origins.
Docs: AUTH URL guidance
ARCHITECTURE.md
Expands BETTER_AUTH_URL documentation with production vs PR/preview guidance and notes for zero-manual-PR setups using Railway public domains.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 I hopped through cards where tables lay,
Filters curled and searches play.
Jobs and scores in colorful cheer,
Guards and origins now appear.
A little rabbit claps — hooray! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is essentially a template with no actual content filled in; the Summary section lacks implementation details explaining what changed and why, and all validation and DCO checklists remain unchecked. Complete the Summary section with concrete details of changes made and their rationale; check relevant boxes in Type of change, Validation, and DCO sections to document the testing and verification performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: implementing search functionality and enhancing job filtering in the applications dashboard, which aligns with the primary frontend modifications in index.vue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/applications-search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/dashboard/applications/index.vue`:
- Around line 21-27: debounceTimer is declared uninitialized and cleared
unguarded which can break TS and leave timeouts after unmount; change its
declaration to include undefined (e.g. let debounceTimer: ReturnType<typeof
setTimeout> | undefined = undefined), guard clearTimeout calls (if
(debounceTimer) clearTimeout(debounceTimer)), ensure assignments set
debounceTimer to the setTimeout return, and add an onBeforeUnmount(() => { if
(debounceTimer) clearTimeout(debounceTimer) }) to clean up the pending timeout
when the component is destroyed; references: debounceTimer variable and the
watch(...) callback.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab24d4 and 7092b05.

📒 Files selected for processing (1)
  • app/pages/dashboard/applications/index.vue

Comment on lines +21 to +27
let debounceTimer: ReturnType<typeof setTimeout>
watch(searchInput, (val) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim().toLowerCase()
}, 250)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the debounce timer initialization and usage around lines 20-32
echo "=== Lines 20-32 (debounce timer section) ==="
nl -ba app/pages/dashboard/applications/index.vue | sed -n '20,32p'

echo ""
echo "=== Lines 58-66 (mentioned as 'Also applies to: 61-63') ==="
nl -ba app/pages/dashboard/applications/index.vue | sed -n '58,66p'

echo ""
echo "=== Checking for onUnmounted imports and usage ==="
rg "onUnmounted" app/pages/dashboard/applications/index.vue

Repository: reqcore-inc/applirank

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the debounce timer initialization and usage around lines 20-32
echo "=== Lines 20-32 (debounce timer section) ==="
sed -n '20,32p' app/pages/dashboard/applications/index.vue | cat -n

echo ""
echo "=== Lines 58-66 (mentioned as 'Also applies to: 61-63') ==="
sed -n '58,66p' app/pages/dashboard/applications/index.vue | cat -n

echo ""
echo "=== Checking for onUnmounted imports and usage ==="
rg "onUnmounted" app/pages/dashboard/applications/index.vue

echo ""
echo "=== Checking for cleanup on unmount ==="
rg -A 5 "onUnmounted|onMounted" app/pages/dashboard/applications/index.vue

Repository: reqcore-inc/applirank

Length of output: 2166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All onUnmounted handlers in the file ==="
rg -B 2 -A 3 "onUnmounted" app/pages/dashboard/applications/index.vue

echo ""
echo "=== Total occurrences of onUnmounted ==="
rg -c "onUnmounted" app/pages/dashboard/applications/index.vue

echo ""
echo "=== Check if debounceTimer is mentioned anywhere else ==="
rg "debounceTimer" app/pages/dashboard/applications/index.vue

Repository: reqcore-inc/applirank

Length of output: 843


Initialize and clean up the debounce timer defensively.

Line 21 declares debounceTimer without initialization, and Line 23 calls clearTimeout(debounceTimer) unguarded. Additionally, there is no timeout cleanup on component unmount, risking stale callbacks after the component is destroyed. This breaks strict TypeScript builds and can cause runtime bugs.

🛠️ Proposed fix
-let debounceTimer: ReturnType<typeof setTimeout>
+let debounceTimer: ReturnType<typeof setTimeout> | undefined
 watch(searchInput, (val) => {
-  clearTimeout(debounceTimer)
+  if (debounceTimer) clearTimeout(debounceTimer)
   debounceTimer = setTimeout(() => {
     debouncedSearch.value = val.trim().toLowerCase()
   }, 250)
 })
+
+onUnmounted(() => {
+  if (debounceTimer) clearTimeout(debounceTimer)
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let debounceTimer: ReturnType<typeof setTimeout>
watch(searchInput, (val) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim().toLowerCase()
}, 250)
})
let debounceTimer: ReturnType<typeof setTimeout> | undefined
watch(searchInput, (val) => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
debouncedSearch.value = val.trim().toLowerCase()
}, 250)
})
onUnmounted(() => {
if (debounceTimer) clearTimeout(debounceTimer)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/applications/index.vue` around lines 21 - 27,
debounceTimer is declared uninitialized and cleared unguarded which can break TS
and leave timeouts after unmount; change its declaration to include undefined
(e.g. let debounceTimer: ReturnType<typeof setTimeout> | undefined = undefined),
guard clearTimeout calls (if (debounceTimer) clearTimeout(debounceTimer)),
ensure assignments set debounceTimer to the setTimeout return, and add an
onBeforeUnmount(() => { if (debounceTimer) clearTimeout(debounceTimer) }) to
clean up the pending timeout when the component is destroyed; references:
debounceTimer variable and the watch(...) callback.

Comment on lines +223 to +229
<input
v-model="searchInput"
type="text"
placeholder="Search by candidate name, email, or job title…"
class="w-full rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 pl-10 pr-3 py-2.5 text-sm text-surface-900 dark:text-surface-100 placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors"
/>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add accessibility semantics to search and dropdown controls.

The new controls are keyboard-clickable, but they’re missing key a11y semantics (programmatic label on search; expanded/menu relationships on custom dropdowns), which hurts screen-reader navigation.

♿ Proposed fix
+      <label for="applications-search" class="sr-only">Search applications</label>
       <input
+        id="applications-search"
         v-model="searchInput"
         type="text"
         placeholder="Search by candidate name, email, or job title…"
+        aria-label="Search applications"
         class="w-full rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 pl-10 pr-3 py-2.5 text-sm text-surface-900 dark:text-surface-100 placeholder:text-surface-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors"
       />
         <button
+          type="button"
+          aria-haspopup="menu"
+          :aria-expanded="jobDropdownOpen"
+          aria-controls="job-filter-menu"
           class="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors"
           :class="activeJobId
...
         <div
           v-if="jobDropdownOpen"
+          id="job-filter-menu"
+          role="menu"
           class="absolute left-0 top-full mt-1 z-20 w-64 max-h-56 overflow-y-auto rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 shadow-lg py-1"
         >
         <button
+          type="button"
+          aria-haspopup="menu"
+          :aria-expanded="sortDropdownOpen"
+          aria-controls="sort-menu"
           class="inline-flex items-center gap-1.5 text-xs text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 transition-colors"
           `@click`="sortDropdownOpen = !sortDropdownOpen"
         >
...
         <div
           v-if="sortDropdownOpen"
+          id="sort-menu"
+          role="menu"
           class="absolute right-0 top-full mt-1 z-20 w-48 rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 shadow-lg py-1"
         >

Also applies to: 235-333

@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-47 February 26, 2026 13:09 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / applirank-pr-47 February 26, 2026 13:18 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
server/utils/env.ts (1)

43-47: Consider simplifying the schema chain.

The .pipe(z.string()) is redundant since emptyToUndefined already validates as z.string() internally. Additionally, when using .default([]), the preceding .optional() is superfluous since .default() already handles undefined values.

♻️ Proposed simplification
     BETTER_AUTH_TRUSTED_ORIGINS: emptyToUndefined
-      .pipe(z.string())
       .transform(value => value.split(',').map(origin => origin.trim()).filter(Boolean))
-      .optional()
       .default([]),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/env.ts` around lines 43 - 47, Remove the redundant
.pipe(z.string()) and the unnecessary .optional() in the
BETTER_AUTH_TRUSTED_ORIGINS schema chain: keep emptyToUndefined, apply
.transform(value => value.split(',').map(origin =>
origin.trim()).filter(Boolean)) and then .default([]); ensure the transform
still expects a string input from emptyToUndefined and that the final schema
symbol is BETTER_AUTH_TRUSTED_ORIGINS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/utils/env.ts`:
- Around line 43-47: Remove the redundant .pipe(z.string()) and the unnecessary
.optional() in the BETTER_AUTH_TRUSTED_ORIGINS schema chain: keep
emptyToUndefined, apply .transform(value => value.split(',').map(origin =>
origin.trim()).filter(Boolean)) and then .default([]); ensure the transform
still expects a string input from emptyToUndefined and that the final schema
symbol is BETTER_AUTH_TRUSTED_ORIGINS.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7092b05 and f291a75.

📒 Files selected for processing (4)
  • ARCHITECTURE.md
  • app/pages/dashboard/jobs/[id]/swipe.vue
  • server/utils/auth.ts
  • server/utils/env.ts
✅ Files skipped from review due to trivial changes (1)
  • ARCHITECTURE.md

@JoachimLK JoachimLK merged commit 885674c into main Feb 26, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 1, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant