Skip to content

Feat/multi-step-job-creation-form#44

Merged
JoachimLK merged 9 commits into
mainfrom
feat/multi-step-job-creation-form
Feb 26, 2026
Merged

Feat/multi-step-job-creation-form#44
JoachimLK merged 9 commits into
mainfrom
feat/multi-step-job-creation-form

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

    • Feature Catalog page with interactive outline, details sidebar, live stats and discussion widget
    • Multi-step job creation wizard (Details → Application Form → Candidate Search)
    • Enhanced candidate swipe view with in-list search and three‑panel detail view
    • Client-side discussion/comments component for in-page conversations
  • Documentation

    • Large catalog of new docs across AI, pipeline management, recruitment tools, collaboration, mobile, platform, and security/compliance
    • Added "catalog" content collection
  • Navigation

    • "Features" link added to site header
  • Removed

    • Tailwind v4 skill documentation deleted

…, pipeline management, recruitment tools, and security compliance

- Introduced AI Candidate Ranking (Glass Box) feature with detailed implementation plans and design notes.
- Added Local AI via Ollama for privacy-focused AI model execution.
- Implemented Resume Parsing functionality to automate candidate data extraction from resumes.
- Created Candidate Portal for self-service application status checks and updates.
- Developed Email Notifications system for key hiring events.
- Established Application Tracking and Candidate Profiles for better management of candidate data.
- Launched Kanban Pipeline Board for visual tracking of candidates through hiring stages.
- Enhanced Job Management with structured workflows and CRUD capabilities.
- Built Public Job Board for SEO-friendly job listings and application submissions.
- Implemented GDPR Data Management features for compliance with data protection regulations.
- Established API Rate Limiting and Server-Proxied Documents for enhanced security and data privacy.
@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@JoachimLK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 9035208 and ea7a8c9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json
📝 Walkthrough

Walkthrough

Adds a Feature Catalog page and content collection with many catalog docs, a client-side Giscus comments component, a three-step job-creation wizard, dashboard swipe UI enhancements, header links to /catalog, runtime config/prerender updates, and deletes the Tailwind v4 agent skill SKILL.md.

Changes

Cohort / File(s) Summary
Feature Catalog Page
app/pages/catalog/index.vue
New interactive catalog page: loads catalog collection, builds nested tree from flat paths, manages expand/collapse, computes stats, and shows a right-side detail sidebar with metadata, markdown body, competitor table, and Giscus discussion.
Catalog Content (many docs)
content/catalog/**, content/catalog/...
Adds ~30+ markdown feature docs across AI Intelligence, Collaboration, Pipeline Management, Platform, Recruitment Tools, Mobile Support, Security/Compliance, and related topics. Mostly descriptive frontmatter and planned implementation notes.
Content Config
content.config.ts
Adds a new catalog content collection schema (type: page) with fields: description, status, priority, complexity, competitors.
Giscus Discussion Component
app/components/GiscusComments.vue, nuxt.config.ts (runtimeConfig.public additions)
New client-only Vue component that injects the Giscus client script and reloads when term changes; runtime public config keys giscusRepoId and giscusCategoryId added for IDs.
Nuxt Config / Prerender
nuxt.config.ts
Adds public runtime config for Giscus IDs and a routeRules entry to prerender '/catalog'.
Job Creation Wizard
app/pages/dashboard/jobs/new.vue
Introduces a three-step job creation wizard with validation, dynamic question CRUD/reordering, skill/tag UI, application link generation/copy, submit flow creating job and posting questions. New local types: QuestionType, DraftQuestion.
Dashboard Swipe Enhancements
app/pages/dashboard/jobs/[id]/swipe.vue
Reworks swipe UI into three-panel layout with client-side search/filtering, filtered navigation, multi-tab candidate detail (overview/documents/responses), timeAgo/scoreClass helpers, and selection helpers.
Header Navigation Updates
app/pages/index.vue, app/pages/blog/index.vue, app/pages/blog/[...slug].vue, app/pages/roadmap.vue
Inserted a "Features" NuxtLink to /catalog in header nav on multiple pages (template-only changes).
Removed Agent Skill Doc
.agents/skills/tailwind-v4-skill/SKILL.md
Deleted the Tailwind CSS v4 skill documentation file previously used by agents.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CatalogPage
    participant ContentAPI
    participant TreeBuilder
    participant Sidebar
    participant GiscusWidget

    User->>CatalogPage: Request /catalog
    CatalogPage->>ContentAPI: fetchAll('catalog')
    ContentAPI-->>CatalogPage: catalogItems
    CatalogPage->>TreeBuilder: buildTree(catalogItems)
    TreeBuilder-->>CatalogPage: hierarchicalNodes
    CatalogPage-->>User: render outline + stats

    User->>CatalogPage: Click node
    CatalogPage->>Sidebar: loadFeatureDetails(nodeId)
    Sidebar->>Sidebar: render metadata + markdown
    Sidebar->>GiscusWidget: init(term=nodeId)
    GiscusWidget-->>Sidebar: display discussion
    Sidebar-->>User: show sidebar
Loading
sequenceDiagram
    participant User
    participant JobWizard
    participant Validator
    participant Server
    participant QuestionsAPI

    User->>JobWizard: Open /dashboard/jobs/new
    JobWizard->>JobWizard: show Step 1

    User->>JobWizard: Fill Step1, Click Next
    JobWizard->>Validator: validateStep1(data)
    Validator-->>JobWizard: ok

    JobWizard->>JobWizard: show Step 2 (questions)
    User->>JobWizard: add/edit questions
    JobWizard->>QuestionsAPI: (deferred) queue question posts

    JobWizard->>JobWizard: show Step 3 (targeting)
    User->>JobWizard: Click Create
    JobWizard->>Validator: validateStep1(data)
    Validator-->>JobWizard: ok
    JobWizard->>Server: POST /api/jobs (job payload)
    Server-->>JobWizard: createdJob
    JobWizard->>QuestionsAPI: POST /api/jobs/{id}/questions for each question
    QuestionsAPI-->>JobWizard: createdQuestions
    JobWizard-->>User: navigate to /dashboard/jobs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through docs where new features grow,
A catalog tree with details to show.
Widgets and wizards, a sidebar of light,
Discussions and questions keep pages alight.
I twitch my nose — the roadmap looks bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is completely unfilled; it contains only the template structure with no actual summary, rationale, or validation details, leaving reviewers without context about the changes. Complete the summary section with what was changed and why, mark applicable checkboxes (Feature, tested locally, documentation added/updated), and confirm DCO compliance with signed-off commits.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/multi-step-job-creation-form' clearly and specifically describes the main feature being added—a multi-step form for job creation, matching the substantial changes to the job creation workflow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/multi-step-job-creation-form

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: 15

🧹 Nitpick comments (1)
content/catalog/platform/deployment/index.md (1)

33-33: Avoid hardcoding cloud pricing in evergreen docs.

The ~€5/month figure will age quickly and can become inaccurate. Consider replacing it with “check current provider pricing” or adding an “as of” date.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/platform/deployment/index.md` at line 33, The line listing
"Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)" hardcodes a price that will
become outdated; update that entry (the text containing "Hetzner Cloud CX23 — 2
vCPU, 4GB RAM (~€5/month)") to remove the static price and either replace it
with a neutral note like "check current provider pricing" or append an "as of
<YYYY-MM-DD>" qualifier so the doc remains accurate over time.
🤖 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/components/GiscusComments.vue`:
- Around line 23-25: The Giscus widget fails because the script element in
GiscusComments.vue sets empty data-repo-id and data-category-id; populate those
values from runtime config instead: add giscusRepoId and giscusCategoryId to
your nuxt.config.ts runtimeConfig (using the GitHub repo node ID and Discussions
category node ID), then update the script.setAttribute calls in the component
(the script element created in GiscusComments.vue) to read
processRuntimeConfig.giscusRepoId and processRuntimeConfig.giscusCategoryId (or
use useRuntimeConfig()) so the data-repo-id and data-category-id are non-empty
and the widget can initialize.

In `@app/pages/catalog/index.vue`:
- Around line 352-365: The expand toggle is only clickable via the SVG inside
the row, blocking keyboard users; update the template so the expand control is
its own focusable button element (not just a clickable SVG inside the parent
<button>) and wire its handlers to accessible attributes and events: create a
dedicated toggle button (with aria-expanded bound to isExpanded(feature.path),
aria-controls pointing to the children region, and a visible focusable element)
that calls toggleExpand(feature.path) on click and on key activation
(Enter/Space), while keeping the row click on selectFeature(feature); reference
the existing selectFeature, isExpanded, toggleExpand, and feature.children
symbols when making the change.
- Around line 327-330: The category toggle buttons need ARIA state so screen
readers know whether the branch is open: update the button with a bound
aria-expanded attribute that reflects the current open state (e.g.,
:aria-expanded="isExpanded(category.path)" or
:aria-expanded="expanded.has(category.path)" depending on your state shape) and
add aria-controls pointing to the associated submenu id (e.g.,
:aria-controls="'category-'+category.path") while ensuring the submenu element
has the matching id; apply the same change to the other toggle button usages
(the one using toggleExpand(category.path) at the second location) and ensure
the component exposes an isExpanded helper or reads the same expanded state used
by toggleExpand to keep the attributes in sync.
- Around line 431-434: The icon-only close button that calls closeSidebar lacks
an accessible name; update the <button> that uses `@click`="closeSidebar" to
include an explicit accessible label (e.g., add aria-label="Close sidebar" or
bind a localized label via :aria-label="t('closeSidebar')"), or add visually
hidden text inside the button for screen readers, ensuring the label clearly
identifies the action and references the existing closeSidebar handler.
- Line 509: The external anchor element that opens in a new tab (the <a ...>
anchor with text "Giscus" and target="_blank") is missing rel attributes; update
that anchor to include rel="noopener noreferrer" (and audit other
target="_blank" anchors in the same component to add the same rel attributes) to
prevent tab-nabbing and ensure consistent external-link handling.

In `@app/pages/dashboard/jobs/new.vue`:
- Around line 512-514: The help text paragraph that reads "Our AI can help you
write a compelling job description. Click the magic wand icon in the editor."
(the <p> with classes "text-xs text-brand-700 dark:text-brand-300
leading-relaxed") references a non-existent feature; either remove that <p>
entirely or replace it with accurate guidance that matches the actual
description textarea (the job description textarea/component) — e.g., a neutral
tip about best practices for writing descriptions or a note linking to available
help — and ensure the copy matches existing UI features (do not mention a magic
wand or AI unless you add that editor feature).
- Around line 79-82: The computed canGoNext currently calls validateStep1(),
which mutates errors.value and causes side effects during reactivity; make the
computed pure by creating a non-mutating predicate (e.g., isStep1Valid or
validateStep1Pure) that returns boolean without touching errors.value and use
that in canGoNext, while keeping the existing validateStep1() (which sets
errors.value) for use inside nextStep() and handleSubmit() where showing errors
is intended; update calls in nextStep/handleSubmit to call validateStep1() and
update canGoNext to call the new pure predicate.
- Line 269: The static hint "80 characters left" is wrong; update the template
to show a live remaining count by adding a computed property (e.g.
titleCharsRemaining) that returns 200 - form.value.title.length (matching the
Zod schema max 200) and replace the hardcoded text with the computed value (use
titleCharsRemaining and form.value.title.length as needed); ensure the computed
is imported from Vue and referenced in the template where the current <p v-else>
is rendered.
- Around line 172-177: The "Save draft" button in new.vue currently has no click
handler; add a handler (e.g., `@click`="saveDraft") on the button and implement a
saveDraft method in this component (or setup() return) that persists the current
form state (call the existing job save API or local draft API, or emit an event)
and provides UX feedback (success/error). If you don't want saving now, remove
the button entirely to avoid a dead control; reference the button text "Save
draft" and the component new.vue when making the change.

In `@content/catalog/ai-intelligence/ai-ranking/index.md`:
- Around line 30-37: The table row claiming "EU AI Act compliant" is a
definitive legal claim; update the "EU AI Act compliant" cell in the table (the
row header "EU AI Act compliant") to a cautious phrasing such as "Designed for
compliance (pending legal review)" or "Aims for compliance — subject to formal
validation" to reflect that compliance is a goal pending legal/compliance review
rather than a confirmed fact.

In `@content/catalog/ai-intelligence/index.md`:
- Line 8: The sentence "Applirank eliminates that risk" overstates legal
certainty; update the copy so it avoids absolute guarantees — replace the phrase
"Applirank eliminates that risk" (in the sentence starting "This is not just a
design preference — the [EU AI Act]...") with a mitigated formulation such as
"Applirank helps mitigate that risk" or "Applirank reduces that risk" and ensure
surrounding wording remains grammatically correct and consistent with the
document's tone.

In `@content/catalog/collaboration/interview-scheduling/index.md`:
- Line 20: Fix the missing blank line after the "## Considerations" section
heading so the heading and its paragraph are separated: locate the "##
Considerations" heading in the document (the line starting with "##
Considerations") and insert a single newline/empty line between that heading and
the following paragraph text ("this is a complex feature...") so the heading
renders correctly.

In `@content/catalog/security-compliance/gdpr/index.md`:
- Around line 22-24: Update the "Right to deletion" bullet so it no longer
implies cross-tenant removal; replace the phrase "Remove all candidate data
across all organizations, including S3 documents" (under the "Right to deletion"
heading) with wording that scopes deletion to the requesting controller/tenant
only and references deletion of that tenant's copies (including that tenant's S3
objects) while preserving tenant boundaries and authorization checks.

In `@content/catalog/security-compliance/index.md`:
- Around line 6-8: Update the absolute claim that "input validation on every
endpoint" to accurately reflect implementation gaps by scoping it to validated
inputs (e.g., "request body and query parameter validation on most write
endpoints") and explicitly call out exceptions: list the read-only GET endpoints
(documents/[id]/preview.get.ts, documents/[id]/download.get.ts,
dashboard/stats.get.ts, applications/[id].get.ts, candidates/[id].get.ts,
jobs/[id].get.ts, jobs/[id]/questions/[questionId].delete.ts) and note that
auth/[...all].ts delegates validation to an external auth library;
alternatively, add a short note that validation coverage varies and where to
find the authoritative list. Ensure the revised sentence replaces the current
absolute line and includes a pointer to the exception list or documentation.

In `@content/catalog/security-compliance/rate-limiting/index.md`:
- Around line 14-19: The doc incorrectly says the in-memory limiter is "global"
— clarify that server/utils/rateLimit.ts implements a per-instance (in-memory)
sliding-window limiter applied via the server middleware to API routes, and
update the prose to state this limitation (limits are enforced per process and
can be bypassed across multiple instances); also suggest recommended
alternatives (use a centralized store like Redis/Memcached or an API
gateway/load‑balancer with shared rate-limiting) or note that a distributed rate
limiter is required for multi-node deployments.

---

Nitpick comments:
In `@content/catalog/platform/deployment/index.md`:
- Line 33: The line listing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)"
hardcodes a price that will become outdated; update that entry (the text
containing "Hetzner Cloud CX23 — 2 vCPU, 4GB RAM (~€5/month)") to remove the
static price and either replace it with a neutral note like "check current
provider pricing" or append an "as of <YYYY-MM-DD>" qualifier so the doc remains
accurate over time.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1483cbc and 1459bc6.

📒 Files selected for processing (38)
  • .agents/skills/tailwind-v4-skill/SKILL.md
  • app/components/GiscusComments.vue
  • app/pages/blog/[...slug].vue
  • app/pages/blog/index.vue
  • app/pages/catalog/index.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/index.vue
  • app/pages/roadmap.vue
  • content.config.ts
  • content/catalog/ai-intelligence/ai-ranking/index.md
  • content/catalog/ai-intelligence/index.md
  • content/catalog/ai-intelligence/local-ai/index.md
  • content/catalog/ai-intelligence/resume-parsing/index.md
  • content/catalog/collaboration/candidate-portal/index.md
  • content/catalog/collaboration/email-notifications/index.md
  • content/catalog/collaboration/index.md
  • content/catalog/collaboration/interview-scheduling/index.md
  • content/catalog/collaboration/team-comments/index.md
  • content/catalog/mobile-support/index.md
  • content/catalog/mobile-support/responsive-dashboard/index.md
  • content/catalog/pipeline-management/application-tracking/index.md
  • content/catalog/pipeline-management/candidate-profiles/index.md
  • content/catalog/pipeline-management/index.md
  • content/catalog/pipeline-management/job-management/index.md
  • content/catalog/pipeline-management/kanban-board/index.md
  • content/catalog/platform/dashboard/index.md
  • content/catalog/platform/deployment/index.md
  • content/catalog/platform/index.md
  • content/catalog/platform/multi-tenant/index.md
  • content/catalog/recruitment-tools/custom-application-forms/index.md
  • content/catalog/recruitment-tools/document-storage/index.md
  • content/catalog/recruitment-tools/index.md
  • content/catalog/recruitment-tools/public-job-board/index.md
  • content/catalog/security-compliance/gdpr/index.md
  • content/catalog/security-compliance/index.md
  • content/catalog/security-compliance/rate-limiting/index.md
  • content/catalog/security-compliance/server-proxied-documents/index.md
  • nuxt.config.ts
💤 Files with no reviewable changes (1)
  • .agents/skills/tailwind-v4-skill/SKILL.md

Comment thread app/components/GiscusComments.vue Outdated
Comment thread app/pages/catalog/index.vue
Comment thread app/pages/catalog/index.vue
Comment on lines +431 to +434
<button
class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white"
@click="closeSidebar"
>

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 | 🟡 Minor

Icon-only close button needs an accessible name.

Without an explicit label, screen readers announce an unnamed button.

Suggested change
 <button
+  type="button"
+  aria-label="Close feature details"
   class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white"
   `@click`="closeSidebar"
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 431 - 434, The icon-only close
button that calls closeSidebar lacks an accessible name; update the <button>
that uses `@click`="closeSidebar" to include an explicit accessible label (e.g.,
add aria-label="Close sidebar" or bind a localized label via
:aria-label="t('closeSidebar')"), or add visually hidden text inside the button
for screen readers, ensuring the label clearly identifies the action and
references the existing closeSidebar handler.

</h4>
<p class="text-[12px] text-white/30 mb-3">
Sign in with GitHub to comment. Discussions are powered by
<a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>.

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 | 🟡 Minor

Add rel on external link opened in a new tab.

Use rel="noopener noreferrer" consistently for _blank links.

Suggested change
- <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>.
+ <a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>.
📝 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
<a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>.
<a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` at line 509, The external anchor element that
opens in a new tab (the <a ...> anchor with text "Giscus" and target="_blank")
is missing rel attributes; update that anchor to include rel="noopener
noreferrer" (and audit other target="_blank" anchors in the same component to
add the same rel attributes) to prevent tab-nabbing and ensure consistent
external-link handling.


Applirank's AI features follow the **Glass Box** principle: every AI decision must be explainable and auditable. When the system ranks a candidate, it shows you exactly which skills matched, why the score was assigned, and what data drove the recommendation.

This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank eliminates that risk by making AI reasoning fully visible.

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

Avoid absolute legal guarantees (“eliminates that risk”).

Line 8 overstates legal certainty. Prefer “reduces risk” or “helps mitigate risk” to avoid compliance/marketing liability.

📝 Suggested wording
-Applirank eliminates that risk by making AI reasoning fully visible.
+Applirank helps mitigate that risk by making AI reasoning fully visible.
📝 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
This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank eliminates that risk by making AI reasoning fully visible.
This is not just a design preference — the [EU AI Act](https://artificialintelligenceact.eu/) classifies employment decisions as high-risk AI, meaning opaque ranking algorithms create real legal liability. Applirank helps mitigate that risk by making AI reasoning fully visible.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/ai-intelligence/index.md` at line 8, The sentence "Applirank
eliminates that risk" overstates legal certainty; update the copy so it avoids
absolute guarantees — replace the phrase "Applirank eliminates that risk" (in
the sentence starting "This is not just a design preference — the [EU AI
Act]...") with a mitigated formulation such as "Applirank helps mitigate that
risk" or "Applirank reduces that risk" and ensure surrounding wording remains
grammatically correct and consistent with the document's tone.


Interview scheduling is one of the most tedious parts of recruiting. Coordinators spend hours going back and forth between candidates and interviewers to find available slots. An integrated scheduler reduces this to a few clicks.

## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.

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 | 🟡 Minor

Fix missing space after section heading.

There's a missing line break between the "## Considerations" heading and the paragraph text, causing them to run together.

📝 Proposed fix
-## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.
+## Considerations
+
+This is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.
📝 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
## Considerationsthis is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.
## Considerations
This is a complex feature with many edge cases (timezone handling, calendar sync, rescheduling, panel interviews). We're evaluating whether to build a lightweight version in-house or integrate with existing scheduling tools (Calendly, Cal.com) via API.
🧰 Tools
🪛 LanguageTool

[grammar] ~20-~20: Ensure spelling is correct
Context: ...duler reduces this to a few clicks. ## Considerationsthis is a complex feature with many edge cas...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/collaboration/interview-scheduling/index.md` at line 20, Fix
the missing blank line after the "## Considerations" section heading so the
heading and its paragraph are separated: locate the "## Considerations" heading
in the document (the line starting with "## Considerations") and insert a single
newline/empty line between that heading and the following paragraph text ("this
is a complex feature...") so the heading renders correctly.

Comment on lines +22 to +24
- **Data export** — Generate a JSON/CSV bundle of all data associated with a candidate (profile, applications, documents, question responses)
- **Right to deletion** — Remove all candidate data across all organizations, including S3 documents
- **Consent tracking** — Record when and how consent was given for data processing

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

Avoid cross-tenant deletion semantics in the GDPR plan.

Line 23 currently implies erasing candidate data across all organizations. That would violate tenant boundaries and can create authorization/compliance issues. Scope deletion to the requesting controller/tenant only.

Suggested wording update
-- **Right to deletion** — Remove all candidate data across all organizations, including S3 documents
+- **Right to deletion** — Remove all candidate data for the requesting organization/workspace, including related S3 documents
+- **Tenant isolation** — Process deletion per controller/tenant; never allow one tenant’s request to erase another tenant’s data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/security-compliance/gdpr/index.md` around lines 22 - 24,
Update the "Right to deletion" bullet so it no longer implies cross-tenant
removal; replace the phrase "Remove all candidate data across all organizations,
including S3 documents" (under the "Right to deletion" heading) with wording
that scopes deletion to the requesting controller/tenant only and references
deletion of that tenant's copies (including that tenant's S3 objects) while
preserving tenant boundaries and authorization checks.

Comment on lines +6 to +8
Security in an ATS isn't optional — you're handling people's personal data, resumes, and employment history. Applirank ships with production-ready security defaults: API rate limiting, comprehensive security headers, server-proxied document access, and input validation on every endpoint.

Every security feature is on by default. You don't have to remember to enable it.

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
# Read-only reconnaissance: inspect endpoint definitions vs validation usage.

echo "=== Endpoint handlers ==="
fd -e ts -e js server | xargs rg -nP 'defineEventHandler|eventHandler|router\.(get|post|put|delete|patch)' -C2

echo "=== Validation patterns ==="
fd -e ts -e js server | xargs rg -nP 'readValidatedBody|getValidatedQuery|safeParse|\.parse\(|z\.object\(' -C2

Repository: reqcore-inc/applirank

Length of output: 34902


Scope the input validation claim to reflect actual implementation gaps.

The claim "input validation on every endpoint" overstates coverage. Several endpoints lack explicit validation for route parameters:

  • Read-only GET endpoints: documents/[id]/preview.get.ts, documents/[id]/download.get.ts, dashboard/stats.get.ts, applications/[id].get.ts, candidates/[id].get.ts, jobs/[id].get.ts, jobs/[id]/questions/[questionId].delete.ts
  • The auth/[...all].ts handler delegates validation to an external auth library without visible guardrails

Either add validation guardrails to these endpoints and document them consistently, or update the documentation to qualify which endpoints have validation (e.g., "request body and query parameter validation" rather than "every endpoint"). The claims as written are too absolute to maintain reliably.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/security-compliance/index.md` around lines 6 - 8, Update the
absolute claim that "input validation on every endpoint" to accurately reflect
implementation gaps by scoping it to validated inputs (e.g., "request body and
query parameter validation on most write endpoints") and explicitly call out
exceptions: list the read-only GET endpoints (documents/[id]/preview.get.ts,
documents/[id]/download.get.ts, dashboard/stats.get.ts,
applications/[id].get.ts, candidates/[id].get.ts, jobs/[id].get.ts,
jobs/[id]/questions/[questionId].delete.ts) and note that auth/[...all].ts
delegates validation to an external auth library; alternatively, add a short
note that validation coverage varies and where to find the authoritative list.
Ensure the revised sentence replaces the current absolute line and includes a
pointer to the exception list or documentation.

Comment on lines +14 to +19
Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations.

## What it does

- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
- Applied globally via server middleware to all API routes

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

Avoid calling an in-memory limiter “global” across deployments.

Line 14 and Line 18 describe global coverage, but an in-memory limiter is enforced per instance. In multi-instance deployments, limits can be bypassed by spreading requests across nodes.

📝 Suggested wording update
-Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations.
+Per-instance per-IP rate limiting on all `/api` endpoints, with stricter thresholds for authentication and write operations.

-- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
-- Applied globally via server middleware to all API routes
+- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
+- Applied via server middleware to all API routes in each app instance
+- For true cluster-wide/global enforcement, back the limiter with a shared store (e.g., Redis)
📝 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
Global per-IP rate limiting on all `/api` endpoints with stricter thresholds for authentication and write operations.
## What it does
- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
- Applied globally via server middleware to all API routes
Per-instance per-IP rate limiting on all `/api` endpoints, with stricter thresholds for authentication and write operations.
## What it does
- In-memory sliding window rate limiter (`server/utils/rateLimit.ts`)
- Applied via server middleware to all API routes in each app instance
- For true cluster-wide/global enforcement, back the limiter with a shared store (e.g., Redis)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@content/catalog/security-compliance/rate-limiting/index.md` around lines 14 -
19, The doc incorrectly says the in-memory limiter is "global" — clarify that
server/utils/rateLimit.ts implements a per-instance (in-memory) sliding-window
limiter applied via the server middleware to API routes, and update the prose to
state this limitation (limits are enforced per process and can be bypassed
across multiple instances); also suggest recommended alternatives (use a
centralized store like Redis/Memcached or an API gateway/load‑balancer with
shared rate-limiting) or note that a distributed rate limiter is required for
multi-node deployments.

@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

🧹 Nitpick comments (2)
app/pages/dashboard/jobs/new.vue (2)

258-274: Partial failure risk: questions may fail after job creation.

If createJob succeeds but posting questions fails, the job exists without its questions. Promise.all rejects on first failure, leaving the system in a partial state. Consider:

  • Catching question-posting errors separately and showing a warning (job created, questions partially saved)
  • Or using Promise.allSettled to attempt all questions and report individual failures
Option: Handle partial question failures gracefully
     if (applicationForm.value.questions.length > 0 && created?.id) {
-      await Promise.all(
+      const results = await Promise.allSettled(
         applicationForm.value.questions.map((question, index) => (
           $fetch(`/api/jobs/${created.id}/questions`, {
             method: 'POST',
             body: {
               label: question.label,
               type: question.type,
               description: question.description || undefined,
               required: question.required,
               options: question.options || undefined,
               displayOrder: index,
             },
           })
-        )),
+        ))
       )
+      const failures = results.filter((r) => r.status === 'rejected')
+      if (failures.length > 0) {
+        console.warn(`${failures.length} question(s) failed to save`)
+        // Optionally show user warning but still navigate
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 258 - 274, The current block
that posts applicationForm.value.questions after job creation uses Promise.all
which will abort on the first failed $fetch and can leave the job without some
or all questions; replace Promise.all with Promise.allSettled when mapping over
applicationForm.value.questions (and keep the POST to
/api/jobs/${created.id}/questions), then inspect the settled results to detect
failures and surface a concise warning to the user (e.g., "Job created but X
questions failed to save") while logging individual errors for debugging;
alternatively, wrap each $fetch in its own try/catch and collect failures to
report the same way—ensure you reference the questions mapping and created?.id
when implementing this change.

221-235: Consider case-insensitive duplicate skill detection.

The current check !findCandidates.value.skills.includes(skill) is case-sensitive, so "Vue" and "vue" would both be added. Consider normalizing for comparison:

Optional: Case-insensitive check
     .forEach((skill) => {
-      if (!findCandidates.value.skills.includes(skill)) {
+      const normalizedSkill = skill.toLowerCase()
+      const isDuplicate = findCandidates.value.skills.some(
+        (s) => s.toLowerCase() === normalizedSkill
+      )
+      if (!isDuplicate) {
         findCandidates.value.skills.push(skill)
       }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 221 - 235, The
addSkillFromInput function currently checks duplicates case-sensitively; update
the duplicate check to compare case-insensitively by normalizing both the
incoming skill and existing entries (e.g., use skillLower = skill.toLowerCase()
and check !findCandidates.value.skills.some(s => s.toLowerCase() ===
skillLower)) before pushing to findCandidates.value.skills so "Vue" and "vue"
are treated as duplicates while preserving the pushed value format you prefer.
🤖 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/jobs/new.vue`:
- Around line 162-169: The object spread into
applicationForm.value.questions[index] makes the id field become optional; fix
it by explicitly preserving the original id when replacing the question object:
read the existing question (applicationForm.value.questions[index]) and include
its id in the new object construction so id remains required (e.g., set id:
existingQuestion.id) while assigning label, type, description, required, and
options to retain type safety for the questions array.
- Around line 184-189: The swap in moveQuestion is failing TS because
list[index] and list[targetIndex] may be typed as possibly undefined; update the
swap to assert non-null (safe after the bounds check) or guard with explicit
local variables: compute const a = list[index]!, const b = list[targetIndex]!
(or return early if either is undefined) and then assign list[index] = b;
list[targetIndex] = a; reference moveQuestion, applicationForm.value.questions,
list, index and targetIndex when making the change.

---

Nitpick comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 258-274: The current block that posts
applicationForm.value.questions after job creation uses Promise.all which will
abort on the first failed $fetch and can leave the job without some or all
questions; replace Promise.all with Promise.allSettled when mapping over
applicationForm.value.questions (and keep the POST to
/api/jobs/${created.id}/questions), then inspect the settled results to detect
failures and surface a concise warning to the user (e.g., "Job created but X
questions failed to save") while logging individual errors for debugging;
alternatively, wrap each $fetch in its own try/catch and collect failures to
report the same way—ensure you reference the questions mapping and created?.id
when implementing this change.
- Around line 221-235: The addSkillFromInput function currently checks
duplicates case-sensitively; update the duplicate check to compare
case-insensitively by normalizing both the incoming skill and existing entries
(e.g., use skillLower = skill.toLowerCase() and check
!findCandidates.value.skills.some(s => s.toLowerCase() === skillLower)) before
pushing to findCandidates.value.skills so "Vue" and "vue" are treated as
duplicates while preserving the pushed value format you prefer.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1459bc6 and ff0f70e.

📒 Files selected for processing (1)
  • app/pages/dashboard/jobs/new.vue

Comment thread app/pages/dashboard/jobs/new.vue
Comment thread app/pages/dashboard/jobs/new.vue
…wipe component

- Implemented search feature to filter candidates by name and email.
- Updated candidate selection logic to work with filtered applications.
- Enhanced candidate detail view with additional information and improved layout.
- Added transition labels and classes for better status management.
- Improved navigation and action buttons for better user experience.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/dashboard/jobs/[id]/swipe.vue (1)

104-120: ⚠️ Potential issue | 🟠 Major

Index can go out of bounds when search filters the list.

The watcher at line 104 adjusts currentIndex when focusedApplications changes, but there's no equivalent logic when filteredApplications shrinks due to a search. If the user has currentIndex = 5 and then searches to get 3 results, filteredApplications[5] returns undefined, causing the detail panel to show "No candidates" while the candidate list displays matches.

🐛 Proposed fix: Add a watcher on filteredApplications
 watch(focusedApplications, () => {
   if (focusedApplications.value.length === 0) {
     currentIndex.value = 0
     return
   }

   if (currentIndex.value >= focusedApplications.value.length) {
     currentIndex.value = focusedApplications.value.length - 1
   }
 }, { immediate: true })

+watch(filteredApplications, () => {
+  if (filteredApplications.value.length === 0) {
+    currentIndex.value = 0
+    return
+  }
+  if (currentIndex.value >= filteredApplications.value.length) {
+    currentIndex.value = filteredApplications.value.length - 1
+  }
+})
+
 watch(focusStatus, () => {
   currentIndex.value = 0
   searchTerm.value = ''
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 104 - 120, Add a
watcher for filteredApplications that mirrors the focusedApplications watcher:
when filteredApplications.value is empty set currentIndex.value = 0; otherwise
if currentIndex.value >= filteredApplications.value.length clamp it to
filteredApplications.value.length - 1 so currentSummary (computed from
filteredApplications[currentIndex]) never becomes undefined; reference the
existing currentIndex, filteredApplications and currentSummary symbols and
follow the same { immediate: true } behavior as the focusedApplications watcher.
♻️ Duplicate comments (1)
app/pages/dashboard/jobs/new.vue (1)

804-809: ⚠️ Potential issue | 🟡 Minor

Help text still references non-existent magic wand feature.

This was previously marked as addressed, but the text "Click the magic wand icon in the editor" remains. No such icon exists in the description textarea. This may have been inadvertently reintroduced.

Suggested fix
           <div v-if="currentStep === 1" class="p-6 rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-100 dark:border-brand-900/30">
             <h4 class="text-sm font-semibold text-brand-900 dark:text-brand-100 mb-2">Need help?</h4>
             <p class="text-xs text-brand-700 dark:text-brand-300 leading-relaxed">
-              Our AI can help you write a compelling job description. Click the magic wand icon in the editor.
+              Write a clear, detailed description to attract qualified candidates.
             </p>
           </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 804 - 809, The help text
inside the template block shown when currentStep === 1 still mentions a
non-existent "magic wand" icon; update the paragraph text in
app/pages/dashboard/jobs/new.vue (the <div v-if="currentStep === 1"> block and
specifically the <p> help paragraph) to remove or replace the "Click the magic
wand icon in the editor" phrase with an accurate instruction (e.g., point to the
real AI helper or a generic "use the AI assistant in the editor" or simply
remove that clause) so the UI text matches existing editor controls.
🧹 Nitpick comments (4)
app/pages/dashboard/jobs/new.vue (2)

688-706: No limit on skills array in Step 3.

Users can add unlimited skills. Consider adding a reasonable cap (e.g., 20) with feedback when the limit is reached, similar to the 5-question limit in Step 2.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 688 - 706, The skills list
(findCandidates.skills) currently has no upper bound; update the add logic in
the addSkillFromInput method to enforce a max (e.g., MAX_SKILLS = 20) and
return/display a user-facing message when the cap is reached (disable adding on
`@keydown.enter` and `@blur` and avoid pushing new skills if length >= MAX_SKILLS).
Also update the template to show a small feedback element near the input (e.g.,
"You can add up to 20 skills") and optionally disable or hide the add button
behavior when findCandidates.skills.length >= MAX_SKILLS; keep removeSkill(idx)
unchanged so users can delete to free space. Ensure the limit constant and
checks are added where addSkillFromInput and the input event handlers are
defined.

566-569: GripVertical icon suggests drag-and-drop but only buttons exist.

The grip icon typically indicates draggable elements, but reordering is only possible via the up/down buttons. This could confuse users expecting drag functionality.

Consider either removing the grip icon or implementing actual drag-and-drop reordering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 566 - 569, The GripVertical
icon (GripVertical) suggests drag-and-drop but the UI only supports reordering
via the existing up/down buttons (e.g., the move-up/move-down handlers), so
either remove the GripVertical element to avoid misleading users or implement
real drag-and-drop: integrate a drag library (like Vue.Draggable), wrap the jobs
list in the draggable container, add handlers (onDragStart/onEnd or `@update`)
that call the existing reorder logic or update the jobs array directly, and
ensure the same persistence/validation used by the move-up/move-down methods is
invoked after reordering.
app/pages/dashboard/jobs/[id]/swipe.vue (2)

251-260: Consider handling edge cases for very recent or future timestamps.

When mins is 0 or negative (clock skew, future date), this returns "0m ago" or a negative value. A small guard would improve UX.

♻️ Suggested improvement
 function timeAgo(date: string | Date) {
   const diff = Date.now() - new Date(date).getTime()
   const mins = Math.floor(diff / 60_000)
+  if (mins < 1) return 'Just now'
   if (mins < 60) return `${mins}m ago`
   const hrs = Math.floor(mins / 60)
   if (hrs < 24) return `${hrs}h ago`
   const days = Math.floor(hrs / 24)
   if (days < 30) return `${days}d ago`
   return new Date(date).toLocaleDateString()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 251 - 260, The timeAgo
function should guard against zero or negative diffs and clearly handle future
timestamps: in the timeAgo implementation, detect when mins <= 0 and return a
friendly "just now" (or similar) string, and for negative diffs (new Date(date)
is in the future) optionally format as "in Xm" / "in Xh" or clamp to "just now"
to avoid showing "0m ago" or negative values; update the logic inside the
timeAgo function to check the computed diff/mins and branch accordingly before
returning minutes/hours/days or the date string.

829-848: Consider adding aria-labels for navigation buttons.

The navigation buttons use icons with text, which is generally accessible, but adding aria-label attributes would improve screen reader support, especially since the button state (e.g., "Previous candidate, 1 of 5") provides useful context.

♿ Optional accessibility enhancement
 <button
   :disabled="currentIndex === 0"
+  :aria-label="`Previous candidate`"
   class="flex flex-1 cursor-pointer items-center justify-center gap-1 rounded-lg border border-surface-200 px-2.5 py-1.5 text-xs text-surface-600 transition-colors hover:bg-surface-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-surface-700 dark:text-surface-400 dark:hover:bg-surface-800"
   `@click`="goToPreviousCard"
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/swipe.vue around lines 829 - 848, Add
descriptive aria-label attributes to the Prev and Next navigation buttons in the
template to improve screen reader context: update the button elements that call
goToPreviousCard and goToNextCard to include aria-labels like "Previous
candidate, {currentIndex + 1} of {filteredApplications.length}" and "Next
candidate, {currentIndex + 1} of {filteredApplications.length}" respectively
(use the existing currentIndex and filteredApplications bindings to build the
dynamic label); also ensure the disabled state remains intact (the existing
:disabled binding can stay) so screen readers know when the control is inactive.
🤖 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/jobs/new.vue`:
- Around line 203-207: The preview applicationLink computed property returns a
URL with a placeholder ID ("xxxxxxxx") which can confuse users if copied; update
the UI to prevent copying invalid links by either disabling the
copy-to-clipboard control until the job is created or by adding a clear
tooltip/label indicating the link is a non-functional preview. Locate the
applicationLink computed property and the copy button component used near the
preview display, then wire the copy button's disabled state or tooltip to a new
reactive flag (e.g., jobCreated or isPreview) derived from the form/job creation
status so the button is disabled or shows a tooltip when applicationLink
contains the placeholder.
- Around line 258-274: The current use of Promise.all when posting
applicationForm.value.questions for created?.id can leave some question POSTs
failed while others succeed and the user is still navigated away; change the
logic to use Promise.allSettled on the array returned from
applicationForm.value.questions.map(...) and then inspect the settled results to
collect any failures; if any entries are rejected, surface an error (e.g., set
an error state or show a notification) and avoid navigating to /dashboard/jobs
(or retry the failed posts), otherwise proceed normally—update the code
references around the Promise.all call and the created?.id handling to implement
this settled-result check and user-facing error handling.

---

Outside diff comments:
In `@app/pages/dashboard/jobs/`[id]/swipe.vue:
- Around line 104-120: Add a watcher for filteredApplications that mirrors the
focusedApplications watcher: when filteredApplications.value is empty set
currentIndex.value = 0; otherwise if currentIndex.value >=
filteredApplications.value.length clamp it to filteredApplications.value.length
- 1 so currentSummary (computed from filteredApplications[currentIndex]) never
becomes undefined; reference the existing currentIndex, filteredApplications and
currentSummary symbols and follow the same { immediate: true } behavior as the
focusedApplications watcher.

---

Duplicate comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 804-809: The help text inside the template block shown when
currentStep === 1 still mentions a non-existent "magic wand" icon; update the
paragraph text in app/pages/dashboard/jobs/new.vue (the <div v-if="currentStep
=== 1"> block and specifically the <p> help paragraph) to remove or replace the
"Click the magic wand icon in the editor" phrase with an accurate instruction
(e.g., point to the real AI helper or a generic "use the AI assistant in the
editor" or simply remove that clause) so the UI text matches existing editor
controls.

---

Nitpick comments:
In `@app/pages/dashboard/jobs/`[id]/swipe.vue:
- Around line 251-260: The timeAgo function should guard against zero or
negative diffs and clearly handle future timestamps: in the timeAgo
implementation, detect when mins <= 0 and return a friendly "just now" (or
similar) string, and for negative diffs (new Date(date) is in the future)
optionally format as "in Xm" / "in Xh" or clamp to "just now" to avoid showing
"0m ago" or negative values; update the logic inside the timeAgo function to
check the computed diff/mins and branch accordingly before returning
minutes/hours/days or the date string.
- Around line 829-848: Add descriptive aria-label attributes to the Prev and
Next navigation buttons in the template to improve screen reader context: update
the button elements that call goToPreviousCard and goToNextCard to include
aria-labels like "Previous candidate, {currentIndex + 1} of
{filteredApplications.length}" and "Next candidate, {currentIndex + 1} of
{filteredApplications.length}" respectively (use the existing currentIndex and
filteredApplications bindings to build the dynamic label); also ensure the
disabled state remains intact (the existing :disabled binding can stay) so
screen readers know when the control is inactive.

In `@app/pages/dashboard/jobs/new.vue`:
- Around line 688-706: The skills list (findCandidates.skills) currently has no
upper bound; update the add logic in the addSkillFromInput method to enforce a
max (e.g., MAX_SKILLS = 20) and return/display a user-facing message when the
cap is reached (disable adding on `@keydown.enter` and `@blur` and avoid pushing new
skills if length >= MAX_SKILLS). Also update the template to show a small
feedback element near the input (e.g., "You can add up to 20 skills") and
optionally disable or hide the add button behavior when
findCandidates.skills.length >= MAX_SKILLS; keep removeSkill(idx) unchanged so
users can delete to free space. Ensure the limit constant and checks are added
where addSkillFromInput and the input event handlers are defined.
- Around line 566-569: The GripVertical icon (GripVertical) suggests
drag-and-drop but the UI only supports reordering via the existing up/down
buttons (e.g., the move-up/move-down handlers), so either remove the
GripVertical element to avoid misleading users or implement real drag-and-drop:
integrate a drag library (like Vue.Draggable), wrap the jobs list in the
draggable container, add handlers (onDragStart/onEnd or `@update`) that call the
existing reorder logic or update the jobs array directly, and ensure the same
persistence/validation used by the move-up/move-down methods is invoked after
reordering.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff0f70e and a36fe91.

📒 Files selected for processing (2)
  • app/pages/dashboard/jobs/[id]/swipe.vue
  • app/pages/dashboard/jobs/new.vue

Comment on lines +203 to +207
const applicationLink = computed(() => {
const base = `${requestUrl.protocol}//${requestUrl.host}`
const slugBase = slugifyTitle(form.value.title) || 'new-job'
return `${base}/jobs/${slugBase}-xxxxxxxx/apply`
})

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 | 🟡 Minor

Preview link with placeholder may confuse users if copied.

applicationLink generates a URL with xxxxxxxx as a placeholder ID. The UI at line 481 mentions it's a preview, but allowing copy-to-clipboard for an invalid URL could mislead users.

Consider disabling the copy button until the job is created, or adding a tooltip clarifying the link isn't functional yet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 203 - 207, The preview
applicationLink computed property returns a URL with a placeholder ID
("xxxxxxxx") which can confuse users if copied; update the UI to prevent copying
invalid links by either disabling the copy-to-clipboard control until the job is
created or by adding a clear tooltip/label indicating the link is a
non-functional preview. Locate the applicationLink computed property and the
copy button component used near the preview display, then wire the copy button's
disabled state or tooltip to a new reactive flag (e.g., jobCreated or isPreview)
derived from the form/job creation status so the button is disabled or shows a
tooltip when applicationLink contains the placeholder.

Comment on lines +258 to +274
if (applicationForm.value.questions.length > 0 && created?.id) {
await Promise.all(
applicationForm.value.questions.map((question, index) => (
$fetch(`/api/jobs/${created.id}/questions`, {
method: 'POST',
body: {
label: question.label,
type: question.type,
description: question.description || undefined,
required: question.required,
options: question.options || undefined,
displayOrder: index,
},
})
)),
)
}

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

Partial failure in Promise.all leaves questions in inconsistent state.

If one question POST fails, Promise.all rejects immediately while other requests may still be in-flight. The job is already created (line 251), so the user is navigated to /dashboard/jobs unaware that some questions weren't saved.

Consider using Promise.allSettled to capture individual failures and report them:

Proposed fix using Promise.allSettled
     if (applicationForm.value.questions.length > 0 && created?.id) {
-      await Promise.all(
+      const results = await Promise.allSettled(
         applicationForm.value.questions.map((question, index) => (
           $fetch(`/api/jobs/${created.id}/questions`, {
             method: 'POST',
             body: {
               label: question.label,
               type: question.type,
               description: question.description || undefined,
               required: question.required,
               options: question.options || undefined,
               displayOrder: index,
             },
           })
         )),
       )
+      const failures = results.filter((r) => r.status === 'rejected')
+      if (failures.length > 0) {
+        console.error('Some questions failed to save:', failures)
+        // Optionally show a warning to the user before navigating
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 258 - 274, The current use of
Promise.all when posting applicationForm.value.questions for created?.id can
leave some question POSTs failed while others succeed and the user is still
navigated away; change the logic to use Promise.allSettled on the array returned
from applicationForm.value.questions.map(...) and then inspect the settled
results to collect any failures; if any entries are rejected, surface an error
(e.g., set an error state or show a notification) and avoid navigating to
/dashboard/jobs (or retry the failed posts), otherwise proceed normally—update
the code references around the Promise.all call and the created?.id handling to
implement this settled-result check and user-facing error handling.

@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

♻️ Duplicate comments (9)
app/pages/dashboard/jobs/new.vue (6)

261-277: ⚠️ Potential issue | 🟠 Major

Partial failure in Promise.all still leaves questions in inconsistent state.

If one question POST fails, Promise.all rejects immediately while other requests may still be in-flight or succeed. The job is already created, so the user is navigated away unaware that some questions weren't saved.

Suggested fix using Promise.allSettled
     if (applicationForm.value.questions.length > 0 && created?.id) {
-      await Promise.all(
+      const results = await Promise.allSettled(
         applicationForm.value.questions.map((question, index) => (
           $fetch(`/api/jobs/${created.id}/questions`, {
             method: 'POST',
             body: {
               label: question.label,
               type: question.type,
               description: question.description || undefined,
               required: question.required,
               options: question.options || undefined,
               displayOrder: index,
             },
           })
         )),
       )
+      const failures = results.filter((r) => r.status === 'rejected')
+      if (failures.length > 0) {
+        console.error('Some questions failed to save:', failures)
+        // Consider showing a warning toast before navigating
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 261 - 277, Replace the
Promise.all call that posts applicationForm.value.questions for created?.id with
Promise.allSettled, inspect each result, and handle failures explicitly: collect
failed question uploads (reference applicationForm.value.questions and
created?.id), retry or surface an error to the user and prevent silent
navigation away if any question POSTs failed; ensure successful responses are
kept and failed ones are reported/logged so the UI can show which questions need
retrying rather than leaving the job in a partially-updated state.

321-327: ⚠️ Potential issue | 🟠 Major

"Save draft" button still has no functionality.

This button lacks a click handler, so clicking it does nothing. Either implement draft saving or remove the button to avoid user confusion.

Option: Remove until implemented
-        <button
-          type="button"
-          class="px-4 py-2 text-sm font-medium text-surface-700 dark:text-surface-300 bg-white dark:bg-surface-900 border border-surface-300 dark:border-surface-700 rounded-lg hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors"
-        >
-          Save draft
-        </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 321 - 327, The "Save draft"
button in the new.vue template has no click handler and therefore does nothing;
either remove the button or wire it up to an action: add a `@click` handler (e.g.
`@click`="saveDraft") on the button and implement a saveDraft method in the
component's script that performs draft persistence (calling your existing save
method or an API, e.g. reuse createJob/updateJob logic or emit an event), or
remove the button markup entirely until the draft flow (saveDraft) is
implemented to avoid confusing users.

113-116: ⚠️ Potential issue | 🟠 Major

Computed property canGoNext still has side effects.

validateStep1() mutates errors.value, but it's called inside a computed property. This causes validation errors to appear/disappear reactively during render cycles, which is unexpected behavior.

Suggested fix: separate validation check from error-setting
+const isStep1Valid = computed(() => {
+  const result = formSchema.safeParse(form.value)
+  return result.success
+})
+
 const canGoNext = computed(() => {
-  if (currentStep.value === 1) return validateStep1()
+  if (currentStep.value === 1) return isStep1Valid.value
   return true
 })

Keep validateStep1() for use in nextStep() and handleSubmit() where showing errors is intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 113 - 116, The computed
canGoNext calls validateStep1(), which mutates errors.value during render;
extract a pure validator (e.g., isStep1Valid or validateStep1Silent) that
returns boolean without touching errors.value and use that in the computed
property canGoNext, while keeping the existing validateStep1() (which sets
errors.value) for use inside nextStep() and handleSubmit() where showing errors
is intended; update references so canGoNext -> isStep1Valid and
nextStep()/handleSubmit() still call validateStep1().

419-419: ⚠️ Potential issue | 🟡 Minor

Hardcoded character count is still misleading.

The text "80 characters left" is static and doesn't reflect actual input length. The Zod schema allows 200 characters (line 93), but this hint suggests a different limit.

Suggested fix: Dynamic character count

Add a computed property:

const titleCharsRemaining = computed(() => 200 - form.value.title.length)

Then update the template:

-                  <p v-else class="mt-1.5 text-xs text-surface-500">80 characters left. No special characters.</p>
+                  <p v-else class="mt-1.5 text-xs text-surface-500">{{ titleCharsRemaining }} characters left.</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` at line 419, The "80 characters left" hint
is hardcoded and inconsistent with the Zod 200-char rule; add a computed
property (e.g., titleCharsRemaining = computed(() => Math.max(0, 200 -
(form.value.title?.length || 0)))) and replace the static paragraph text with a
binding that displays the dynamic count and optionally pluralizes or clamps at
0; update references to use form.value.title length so the UI always reflects
the Zod max length of 200.

206-210: ⚠️ Potential issue | 🟡 Minor

Preview link with placeholder ID can mislead users.

The applicationLink computed generates a URL with xxxxxxxx as a placeholder. While the UI mentions it's a "preview", the copy button allows copying an invalid URL that won't work.

Consider disabling the copy button until the job is created, or adding a tooltip clarifying the link isn't functional yet.

Also applies to: 493-500

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 206 - 210, The preview URL
produced by the applicationLink computed uses a fake "xxxxxxxx" ID and can be
copied as if valid; change applicationLink (computed) to return either a
non-copyable placeholder (empty string or a clearly non-URL value) when there is
no real job id (e.g., check form.value.id or job.id/persisted flag) and return
the real URL only when the job id exists, and then update the copy button's
disabled binding to be true when the job is not yet created (disable copy if
!form.value.id) and add a tooltip on the copy button explaining "Preview — link
not active until job is created" so users cannot copy an invalid URL; locate and
modify the applicationLink computed and the copy button component usage to
implement these checks and the tooltip.

807-812: ⚠️ Potential issue | 🟡 Minor

Help text still references non-existent feature.

The tip mentions "Click the magic wand icon in the editor" but the description textarea (lines 464-470) has no such icon. This was previously flagged and marked as addressed, but the text remains.

Suggested fix
             <p class="text-xs text-brand-700 dark:text-brand-300 leading-relaxed">
-              Our AI can help you write a compelling job description. Click the magic wand icon in the editor.
+              Write a clear, detailed description to attract qualified candidates.
             </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 807 - 812, The help text
inside the v-if="currentStep === 1" panel still references a non-existent "magic
wand icon" in the editor; either update the copy or add the missing UI. Fix by
editing the help panel (the div with v-if="currentStep === 1") to remove or
replace "Click the magic wand icon in the editor" with accurate guidance (e.g.,
"Use the AI assist button above the editor" or "Use the GenerateDescription
action") or, if you intend to provide that feature, implement the icon/button
inside the description textarea component so the copy matches; ensure the change
touches the help panel text and/or the description textarea UI so they stay
consistent.
app/pages/catalog/index.vue (3)

441-446: ⚠️ Potential issue | 🟡 Minor

Close button still needs an accessible name.

The icon-only close button lacks an aria-label, leaving screen reader users unaware of its purpose.

Suggested fix
             <button
+              type="button"
+              aria-label="Close feature details"
               class="rounded-md p-1.5 text-white/40 transition hover:bg-white/[0.06] hover:text-white"
               `@click`="closeSidebar"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 441 - 446, The close icon button in
the template that calls closeSidebar (the <button `@click`="closeSidebar">
containing the <X /> icon) lacks an accessible name; add an aria-label (e.g.,
aria-label="Close sidebar") or aria-labelledby that references a visible label
so screen readers announce its purpose, and ensure the label text is localized
if the app uses i18n; update only the button attributes—do not change the click
handler or icon component.

367-372: ⚠️ Potential issue | 🟠 Major

Sub-feature expand toggle is still mouse-only.

The toggle action is bound via @click.stop on the SVG icon inside the button. Keyboard users who tab to the button and press Enter/Space will trigger selectFeature(feature) instead of expanding. They cannot independently expand sub-features.

Consider wrapping the chevron in its own focusable button:

Suggested change
-                  <component
-                    v-if="feature.children.length"
-                    :is="isExpanded(feature.path) ? ChevronDown : ChevronRight"
-                    class="size-3.5 text-white/25 shrink-0"
-                    `@click.stop`="toggleExpand(feature.path)"
-                  />
+                  <button
+                    v-if="feature.children.length"
+                    type="button"
+                    :aria-expanded="isExpanded(feature.path)"
+                    :aria-label="isExpanded(feature.path) ? 'Collapse sub-features' : 'Expand sub-features'"
+                    class="rounded p-0.5 hover:bg-white/[0.06] transition"
+                    `@click.stop`="toggleExpand(feature.path)"
+                  >
+                    <component
+                      :is="isExpanded(feature.path) ? ChevronDown : ChevronRight"
+                      class="size-3.5 text-white/25"
+                    />
+                  </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 367 - 372, The chevron toggle is
only bound to `@click` on the SVG so keyboard users trigger selectFeature when
pressing Enter/Space on the parent button; wrap the ChevronDown/ChevronRight
icon in its own focusable element (a button) and move the
`@click.stop`="toggleExpand(feature.path)" there, add keyboard handlers (e.g.,
`@keydown.space.prevent` and `@keydown.enter.prevent`) that call
toggleExpand(feature.path) to support Space/Enter, ensure the handler stops
propagation so selectFeature(feature) on the row/button isn't invoked, and add
appropriate ARIA attributes (aria-expanded bound to isExpanded(feature.path) and
aria-controls referencing the sub-feature region) to the new toggle button;
locate these changes around the component using isExpanded, toggleExpand,
selectFeature, feature.path, ChevronDown and ChevronRight.

519-519: ⚠️ Potential issue | 🟡 Minor

Add rel attribute to external Giscus link.

The link opens in a new tab but lacks rel="noopener noreferrer".

Suggested fix
-                <a href="https://giscus.app" target="_blank" class="text-brand-400 hover:underline">Giscus</a>.
+                <a href="https://giscus.app" target="_blank" rel="noopener noreferrer" class="text-brand-400 hover:underline">Giscus</a>.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` at line 519, The external anchor for "Giscus"
that uses target="_blank" should include rel="noopener noreferrer" to prevent
reverse tabnabbing and leak of window.opener; update the <a
href="https://giscus.app" target="_blank" ...>Giscus</a> element to add
rel="noopener noreferrer" (keeping the existing class and target attributes
intact).
🧹 Nitpick comments (4)
app/pages/catalog/index.vue (2)

395-413: Sub-feature buttons missing type="button".

For consistency with other buttons in this file and to prevent accidental form submissions if this component is ever nested differently, add type="button".

Suggested fix
                   <button
                     v-for="sub in feature.children"
                     :key="sub.path"
+                    type="button"
                     class="flex w-full items-center gap-3 px-5 py-2.5 pl-20 text-left transition hover:bg-white/[0.03]"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 395 - 413, The sub-feature <button>
elements lack an explicit type attribute which can cause accidental form
submissions; update the buttons rendered in the v-for (the ones using
selectFeature(sub), binding :key="sub.path", and referencing selectedNode and
getStatusConfig) to include type="button" on each <button> so they behave as
plain clickable controls rather than submit buttons.

118-124: Auto-expand logic may not trigger on subsequent data updates.

The watchEffect only auto-expands categories when expandedPaths.value.size === 0. If allItems is refetched or changes later, new categories won't be auto-expanded because the set is no longer empty.

If this is intentional (only expand on initial load), consider using watch with { once: true } for clarity:

Suggested change
-watchEffect(() => {
-  if (tree.value.length && expandedPaths.value.size === 0) {
-    for (const node of tree.value) {
-      expandedPaths.value.add(node.path)
-    }
-  }
-})
+watch(tree, (nodes) => {
+  if (nodes.length) {
+    for (const node of nodes) {
+      expandedPaths.value.add(node.path)
+    }
+  }
+}, { once: true })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 118 - 124, The current watchEffect
auto-expansion (watchEffect) only runs when expandedPaths is empty, so
subsequent updates to tree/allItems won't expand new categories; decide intended
behavior and change accordingly: if you want a one-time initial expand, replace
the watchEffect with a watch on tree (use { immediate: true }) and stop the
watcher after the first successful expansion (call the stop function) so intent
is explicit; if you want to re-expand on every tree update, keep watching tree
but remove the expandedPaths.value.size === 0 guard so new node.path entries are
added whenever tree changes (refer to watchEffect, tree, and expandedPaths to
locate the logic).
app/pages/dashboard/jobs/new.vue (1)

701-707: Skills input may add duplicates when blurring after Enter.

The input has both @keydown.enter.prevent="addSkillFromInput" and @blur="addSkillFromInput". If a user types a skill and presses Enter (which adds it and clears the input), then clicks elsewhere, @blur fires on an empty input - this is harmless due to the early return. However, if they type and click away without pressing Enter, it works correctly.

The current implementation is safe, but consider removing the @blur handler if Enter is the expected interaction to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 701 - 707, Remove the
`@blur`="addSkillFromInput" binding from the input element to avoid
double-triggering addSkillFromInput (the handler tied to
`@keydown.enter.prevent`="addSkillFromInput"); alternatively if blur behavior must
be kept, update addSkillFromInput to deduplicate and ignore empty/unchanged
values (use the addSkillFromInput function and the skills collection to check
for existing entries before pushing). Ensure references to addSkillFromInput
remain consistent after the change.
nuxt.config.ts (1)

73-76: Empty defaults for "required" Giscus config may cause silent failures.

The comments indicate these fields are "required for the comments widget", but the defaults are empty strings. If the environment variables are not set, GiscusComments.vue will likely render nothing or fail silently without warning.

Consider either:

  1. Logging a warning during build when these are empty, or
  2. Having the GiscusComments component show an informative placeholder when config is missing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nuxt.config.ts` around lines 73 - 76, The giscusRepoId and giscusCategoryId
in nuxt.config.ts are defaulting to empty strings which can silently break the
comments widget; update nuxt.config.ts to detect when
process.env.NUXT_PUBLIC_GISCUS_REPO_ID or
process.env.NUXT_PUBLIC_GISCUS_CATEGORY_ID are missing and emit a clear
build-time warning (e.g., using console.warn) naming
giscusRepoId/giscusCategoryId, and/or change their default from '' to
null/undefined so GiscusComments.vue can explicitly detect missing config and
render a helpful placeholder; also update GiscusComments.vue to check for
missing giscusRepoId/giscusCategoryId and show an informative message
instructing to set the env vars.
🤖 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/catalog/index.vue`:
- Around line 540-546: The footer GitHub anchor (the <a> element that currently
has href="https://github.com/applirank/applirank" and target="_blank") is
missing the rel attribute; update that anchor to include rel="noopener
noreferrer" to match the nav GitHub link and prevent reverse tabnabbing by
adding rel="noopener noreferrer" to the same <a> element in
app/pages/catalog/index.vue.

In `@app/pages/dashboard/jobs/new.vue`:
- Around line 591-626: The action buttons are only revealed on hover (opacity-0
group-hover:opacity-100) which makes them inaccessible to keyboard users; update
the parent container or the buttons to also reveal on focus by adding
focus-within:opacity-100 to the parent div (the container around the buttons) or
focus:opacity-100 on each button so keyboard tabbing triggers visibility, ensure
the interactive handlers (moveQuestion, editingQuestion assignment, showAddForm
toggle, handleDeleteQuestion) still point to the same elements and that disabled
states for the move buttons (index checks against
applicationForm.questions.length) remain unaffected.

---

Duplicate comments:
In `@app/pages/catalog/index.vue`:
- Around line 441-446: The close icon button in the template that calls
closeSidebar (the <button `@click`="closeSidebar"> containing the <X /> icon)
lacks an accessible name; add an aria-label (e.g., aria-label="Close sidebar")
or aria-labelledby that references a visible label so screen readers announce
its purpose, and ensure the label text is localized if the app uses i18n; update
only the button attributes—do not change the click handler or icon component.
- Around line 367-372: The chevron toggle is only bound to `@click` on the SVG so
keyboard users trigger selectFeature when pressing Enter/Space on the parent
button; wrap the ChevronDown/ChevronRight icon in its own focusable element (a
button) and move the `@click.stop`="toggleExpand(feature.path)" there, add
keyboard handlers (e.g., `@keydown.space.prevent` and `@keydown.enter.prevent`) that
call toggleExpand(feature.path) to support Space/Enter, ensure the handler stops
propagation so selectFeature(feature) on the row/button isn't invoked, and add
appropriate ARIA attributes (aria-expanded bound to isExpanded(feature.path) and
aria-controls referencing the sub-feature region) to the new toggle button;
locate these changes around the component using isExpanded, toggleExpand,
selectFeature, feature.path, ChevronDown and ChevronRight.
- Line 519: The external anchor for "Giscus" that uses target="_blank" should
include rel="noopener noreferrer" to prevent reverse tabnabbing and leak of
window.opener; update the <a href="https://giscus.app" target="_blank"
...>Giscus</a> element to add rel="noopener noreferrer" (keeping the existing
class and target attributes intact).

In `@app/pages/dashboard/jobs/new.vue`:
- Around line 261-277: Replace the Promise.all call that posts
applicationForm.value.questions for created?.id with Promise.allSettled, inspect
each result, and handle failures explicitly: collect failed question uploads
(reference applicationForm.value.questions and created?.id), retry or surface an
error to the user and prevent silent navigation away if any question POSTs
failed; ensure successful responses are kept and failed ones are reported/logged
so the UI can show which questions need retrying rather than leaving the job in
a partially-updated state.
- Around line 321-327: The "Save draft" button in the new.vue template has no
click handler and therefore does nothing; either remove the button or wire it up
to an action: add a `@click` handler (e.g. `@click`="saveDraft") on the button and
implement a saveDraft method in the component's script that performs draft
persistence (calling your existing save method or an API, e.g. reuse
createJob/updateJob logic or emit an event), or remove the button markup
entirely until the draft flow (saveDraft) is implemented to avoid confusing
users.
- Around line 113-116: The computed canGoNext calls validateStep1(), which
mutates errors.value during render; extract a pure validator (e.g., isStep1Valid
or validateStep1Silent) that returns boolean without touching errors.value and
use that in the computed property canGoNext, while keeping the existing
validateStep1() (which sets errors.value) for use inside nextStep() and
handleSubmit() where showing errors is intended; update references so canGoNext
-> isStep1Valid and nextStep()/handleSubmit() still call validateStep1().
- Line 419: The "80 characters left" hint is hardcoded and inconsistent with the
Zod 200-char rule; add a computed property (e.g., titleCharsRemaining =
computed(() => Math.max(0, 200 - (form.value.title?.length || 0)))) and replace
the static paragraph text with a binding that displays the dynamic count and
optionally pluralizes or clamps at 0; update references to use form.value.title
length so the UI always reflects the Zod max length of 200.
- Around line 206-210: The preview URL produced by the applicationLink computed
uses a fake "xxxxxxxx" ID and can be copied as if valid; change applicationLink
(computed) to return either a non-copyable placeholder (empty string or a
clearly non-URL value) when there is no real job id (e.g., check form.value.id
or job.id/persisted flag) and return the real URL only when the job id exists,
and then update the copy button's disabled binding to be true when the job is
not yet created (disable copy if !form.value.id) and add a tooltip on the copy
button explaining "Preview — link not active until job is created" so users
cannot copy an invalid URL; locate and modify the applicationLink computed and
the copy button component usage to implement these checks and the tooltip.
- Around line 807-812: The help text inside the v-if="currentStep === 1" panel
still references a non-existent "magic wand icon" in the editor; either update
the copy or add the missing UI. Fix by editing the help panel (the div with
v-if="currentStep === 1") to remove or replace "Click the magic wand icon in the
editor" with accurate guidance (e.g., "Use the AI assist button above the
editor" or "Use the GenerateDescription action") or, if you intend to provide
that feature, implement the icon/button inside the description textarea
component so the copy matches; ensure the change touches the help panel text
and/or the description textarea UI so they stay consistent.

---

Nitpick comments:
In `@app/pages/catalog/index.vue`:
- Around line 395-413: The sub-feature <button> elements lack an explicit type
attribute which can cause accidental form submissions; update the buttons
rendered in the v-for (the ones using selectFeature(sub), binding
:key="sub.path", and referencing selectedNode and getStatusConfig) to include
type="button" on each <button> so they behave as plain clickable controls rather
than submit buttons.
- Around line 118-124: The current watchEffect auto-expansion (watchEffect) only
runs when expandedPaths is empty, so subsequent updates to tree/allItems won't
expand new categories; decide intended behavior and change accordingly: if you
want a one-time initial expand, replace the watchEffect with a watch on tree
(use { immediate: true }) and stop the watcher after the first successful
expansion (call the stop function) so intent is explicit; if you want to
re-expand on every tree update, keep watching tree but remove the
expandedPaths.value.size === 0 guard so new node.path entries are added whenever
tree changes (refer to watchEffect, tree, and expandedPaths to locate the
logic).

In `@app/pages/dashboard/jobs/new.vue`:
- Around line 701-707: Remove the `@blur`="addSkillFromInput" binding from the
input element to avoid double-triggering addSkillFromInput (the handler tied to
`@keydown.enter.prevent`="addSkillFromInput"); alternatively if blur behavior must
be kept, update addSkillFromInput to deduplicate and ignore empty/unchanged
values (use the addSkillFromInput function and the skills collection to check
for existing entries before pushing). Ensure references to addSkillFromInput
remain consistent after the change.

In `@nuxt.config.ts`:
- Around line 73-76: The giscusRepoId and giscusCategoryId in nuxt.config.ts are
defaulting to empty strings which can silently break the comments widget; update
nuxt.config.ts to detect when process.env.NUXT_PUBLIC_GISCUS_REPO_ID or
process.env.NUXT_PUBLIC_GISCUS_CATEGORY_ID are missing and emit a clear
build-time warning (e.g., using console.warn) naming
giscusRepoId/giscusCategoryId, and/or change their default from '' to
null/undefined so GiscusComments.vue can explicitly detect missing config and
render a helpful placeholder; also update GiscusComments.vue to check for
missing giscusRepoId/giscusCategoryId and show an informative message
instructing to set the env vars.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a36fe91 and 9035208.

📒 Files selected for processing (4)
  • app/components/GiscusComments.vue
  • app/pages/catalog/index.vue
  • app/pages/dashboard/jobs/new.vue
  • nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/GiscusComments.vue

Comment on lines +540 to +546
<a
href="https://github.com/applirank/applirank"
target="_blank"
class="inline-flex items-center gap-1 text-white/50 hover:text-white transition"
>
GitHub <ArrowUpRight class="size-3" />
</a>

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 | 🟡 Minor

Footer GitHub link is missing rel attribute.

This target="_blank" link lacks rel="noopener noreferrer", inconsistent with the nav GitHub link at line 248.

Suggested fix
           <a
             href="https://github.com/applirank/applirank"
             target="_blank"
+            rel="noopener noreferrer"
             class="inline-flex items-center gap-1 text-white/50 hover:text-white transition"
           >
📝 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
<a
href="https://github.com/applirank/applirank"
target="_blank"
class="inline-flex items-center gap-1 text-white/50 hover:text-white transition"
>
GitHub <ArrowUpRight class="size-3" />
</a>
<a
href="https://github.com/applirank/applirank"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-white/50 hover:text-white transition"
>
GitHub <ArrowUpRight class="size-3" />
</a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/catalog/index.vue` around lines 540 - 546, The footer GitHub anchor
(the <a> element that currently has
href="https://github.com/applirank/applirank" and target="_blank") is missing
the rel attribute; update that anchor to include rel="noopener noreferrer" to
match the nav GitHub link and prevent reverse tabnabbing by adding rel="noopener
noreferrer" to the same <a> element in app/pages/catalog/index.vue.

Comment on lines +591 to +626
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
type="button"
:disabled="index === 0"
class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors disabled:opacity-30"
title="Move up"
@click="moveQuestion(index, 'up')"
>
<ChevronUp class="size-4" />
</button>
<button
type="button"
:disabled="index === applicationForm.questions.length - 1"
class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors disabled:opacity-30"
title="Move down"
@click="moveQuestion(index, 'down')"
>
<ChevronDown class="size-4" />
</button>
<button
type="button"
class="rounded p-1 text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
title="Edit"
@click="editingQuestion = q; showAddForm = false"
>
<Pencil class="size-4" />
</button>
<button
type="button"
class="rounded p-1 text-surface-400 hover:text-danger-600 dark:hover:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-950 transition-colors"
title="Delete"
@click="handleDeleteQuestion(q.id)"
>
<Trash2 class="size-4" />
</button>
</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 | 🟡 Minor

Action buttons hidden on hover are inaccessible to keyboard users.

The move/edit/delete buttons are hidden with opacity-0 group-hover:opacity-100. Keyboard users navigating via Tab won't see these buttons appear since hover doesn't trigger on focus.

Consider adding focus-within:opacity-100 to the parent or using focus:opacity-100 on buttons:

Suggested fix
-                    <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
+                    <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity shrink-0">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 591 - 626, The action buttons
are only revealed on hover (opacity-0 group-hover:opacity-100) which makes them
inaccessible to keyboard users; update the parent container or the buttons to
also reveal on focus by adding focus-within:opacity-100 to the parent div (the
container around the buttons) or focus:opacity-100 on each button so keyboard
tabbing triggers visibility, ensure the interactive handlers (moveQuestion,
editingQuestion assignment, showAddForm toggle, handleDeleteQuestion) still
point to the same elements and that disabled states for the move buttons (index
checks against applicationForm.questions.length) remain unaffected.

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