Skip to content

feat(builder): redesign CV creator UI for typing comfort and optimize ATS PDF template#3

Merged
DongDuong2001 merged 6 commits into
mainfrom
feat/cv-builder-ux-ats-optimization
Jun 28, 2026
Merged

feat(builder): redesign CV creator UI for typing comfort and optimize ATS PDF template#3
DongDuong2001 merged 6 commits into
mainfrom
feat/cv-builder-ux-ats-optimization

Conversation

@DongDuong2001

@DongDuong2001 DongDuong2001 commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

This Pull Request delivers key UX/UI visual improvements to the CV Creator section to make typing and form navigation highly comfortable, while reformatting the ATS PDF template to a single-column structure to maximize parseability for Applicant Tracking Systems.

Proposed Changes

UI/UX & Focus States Comfort

  • Redesigned Focus States: Replaced the jarring blackout input focus state (which flipped input backgrounds to pitch black and text to white) with a gentle border highlight and shadow ring (box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1)) inside globals.css.
  • Cleaned form components: Stripped hardcoded inline focus:bg-black and focus:text-white classes across all form files (personal-info, experience, education, skills, projects, certifications, competitions, languages) and builder-header.tsx, allowing them to cleanly inherit the new global comfort-focused styling.

Desktop Sidebar Navigation & Progress Tracking

  • Responsive Vertical Sidebar: Introduced a clean, space-efficient vertical navigation sidebar on desktop (md and lg screens) in builder-form.tsx, leaving the main editing form with ample vertical focus.
  • Progress Badges & Score Tracking:
    • Displays dynamic item count badges on list-based tabs (e.g., Experience (3), Projects (2)).
    • Displays required status badges for essential sections (Personal Info).
    • Displays the live Resume Score dynamically as a color-coded percentage badge (green/yellow/red) directly on the Score tab.
  • Section Reordering & Adjustments: Nested the reorder (Move Up/Move Down) and visibility controls directly inside the sidebar layout.

ATS Optimization

  • Single-Column Conversion: Restructured the ATS PDF template (ats-pdf.tsx) from a two-column flex layout to a standardized single-column layout flowing from top to bottom. This prevents automated parser engines from reading left/right columns out of order.
  • Competition Alignment: Added competitions support to the ATS PDF template, matching the schema rendered by the HTML preview.

Verification Plan

Automated Tests

  • Verified the Next.js production build compiles successfully:
    npm run build

Manual Checks

  1. Verified input fields no longer invert color/background to black upon focus.
  2. Verified the desktop sidebar layout adapts responsively to tablet/mobile viewports.
  3. Exported the ATS PDF and confirmed single-column structure with sequential text selection flow.

Summary by CodeRabbit

  • New Features

    • Added resume score badges and improved section indicators in the builder for quicker progress at a glance.
    • Expanded PDF exports with a cleaner single-column layout and support for competitions.
  • Bug Fixes

    • Updated form field focus styling for a more consistent editing experience across builder sections.
    • Changed focused inputs to display with clearer contrast for better readability.

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lab68cvbuilder Ready Ready Preview, Comment Jun 24, 2026 8:23am

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Input focus styling is changed globally from black-background/white-text to white-background/black-text with border and box-shadow, propagated across all builder form components. scoreResume and Check are exported from resume-score.tsx and consumed by a new getSectionBadge helper in BuilderForm, which also gains a restructured desktop sidebar and snap-scrolling mobile tab strip. AtsPDF is fully restyled to a centered single-column ATS layout with a new Competitions section.

Changes

Builder UI Styling, Badges, and ATS PDF Overhaul

Layer / File(s) Summary
Input focus style: white bg with border/shadow
src/app/globals.css, src/components/builder/builder-header.tsx, src/components/builder/forms/certifications-form.tsx, src/components/builder/forms/competitions-form.tsx, src/components/builder/forms/education-form.tsx, src/components/builder/forms/experience-form.tsx, src/components/builder/forms/languages-form.tsx, src/components/builder/forms/personal-info-form.tsx, src/components/builder/forms/projects-form.tsx, src/components/builder/forms/skills-form.tsx
globals.css changes input, textarea, select:focus from black background/white text to white background/black text with added border-color and box-shadow. Every builder form component removes focus:bg-black/focus:text-white from input/textarea/select className strings, leaving only focus:border-black.
Export scoreResume and add getSectionBadge to BuilderForm
src/components/builder/resume-score.tsx, src/components/builder/builder-form.tsx
Check interface and scoreResume function are exported from resume-score.tsx. BuilderForm imports scoreResume, wires data/hiddenSections/toggleSectionVisibility from the store, and adds a getSectionBadge(id) helper that renders per-section badge JSX including item counts and a tier-colored score percentage.
BuilderForm desktop sidebar, mobile tabs, and section manager
src/components/builder/builder-form.tsx
Desktop sidebar integrates getSectionBadge and restructures the ⚙ Sections dropdown. Mobile section tabs become a snap-scrolling horizontal strip with aria-current and per-tab badges. Section-manager dropdown items show checkmarks, strikethrough for hidden sections, and a "Required" indicator for the personal section. Main content wraps ActiveComponent in a centered container with a top-sticky move-feedback toast.
BuilderHeader and all form component reformatting
src/components/builder/builder-header.tsx, src/components/builder/forms/*
BuilderHeader save-status branches, button class strings, and quick-access/bullet-symbol controls are reformatted. All form components have handler signatures, state initializations, and JSX reflowed to multi-line form with no behavioral changes beyond the focus styling layer.
ATS PDF: new centered layout, styles, and Competitions section
src/components/pdf/ats-pdf.tsx
Entire StyleSheet replaced with a centered single-column ATS design. Sequential section rendering added for Summary, Skills, Experience, Projects, a new Competitions section, Certifications, Education, and Languages. Header uses bullet-separated contact links; Languages render bolded names with normalized proficiency capitalization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A bunny hopped through every form today,
Swapped black-on-focus for a lighter way,
Badges bloom on sidebar tabs with cheer,
The PDF gleams in columns crisp and clear.
Competitions join the ATS parade—
All tidied up by this small rabbit's trade! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main changes: builder UI redesign for typing comfort and ATS PDF template optimization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cv-builder-ux-ats-optimization

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@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: 8

🧹 Nitpick comments (1)
src/components/pdf/ats-pdf.tsx (1)

284-287: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Hardcoded English connectors bypass localization.

While section headers and most strings come from l.*, the connectors at {exp.company}, {edu.degree} in {edu.field}, and Relevant Coursework: are hardcoded English. For non-English locales these will render mixed-language output. Consider sourcing them from PdfLabels.

Also applies to: 491-496, 506-512

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/pdf/ats-pdf.tsx` around lines 284 - 287, The PDF ATS component
still renders hardcoded English connectors in the experience, education, and
coursework sections, which causes mixed-language output in localized builds.
Update the affected rendering in ats-pdf.tsx to use PdfLabels-backed strings
instead of inline literals for the “at”, “in”, and “Relevant Coursework” text,
and wire the existing JSX in the exp/edu/coursework blocks to read from the
locale labels consistently with the rest of the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/globals.css`:
- Line 44: The CSS declaration in the global styles uses a keyword casing that
breaks stylelint’s value-keyword-case rule. Update the text-rendering value in
the globals stylesheet to use the expected lowercase keyword form, and keep the
change localized to the same declaration so the lint error is resolved without
affecting the rest of the styles.
- Around line 197-200: The select focus styling in select:focus uses white
gradient caret indicators that disappear against the new white focus background.
Update the caret/background-image colors in the select:focus rule so the
dropdown indicator remains visible on focus, and keep the change aligned with
the existing select focus styling in globals.css.

In `@src/components/builder/builder-form.tsx`:
- Around line 241-246: The score badge thresholds in builder-form.tsx are
inconsistent with the ScoreRing display, causing 80–84 scores to be shown
differently in the navigation and panel. Update the scoreColor logic in the
builder-form component to match the ScoreRing green threshold at 80 instead of
85, and keep the yellow/red cutoffs aligned with the same scoring rules.
- Around line 178-246: The switch branches in getSectionBadge declare bindings
directly under case labels, which violates noSwitchDeclarations. Wrap the
personal, skills, and score case bodies in braces so their const declarations
are scoped safely, and keep the existing logic and return values unchanged.
- Around line 56-59: The outside-click check in builder-form uses a single
sectionManagerRef for both desktop and mobile wrappers, so it can resolve to
only one layout and misclassify clicks in the other as outside. Split this into
separate refs for the desktop and mobile section-manager wrappers, then update
the click containment logic to treat a click as inside if it lands within either
ref. Use the existing sectionManagerRef usage site and the wrapper components in
builder-form to replace the shared ref with breakpoint-specific refs.

In `@src/components/builder/forms/personal-info-form.tsx`:
- Around line 684-687: The invalid input styling in the personal info form loses
the red border on focus because the conditional class string only sets the red
border by default, so update the field class logic in personal-info-form.tsx to
keep the error state visibly red while focused by adding the focused red border
class alongside the existing error styles in the relevant input rendering path.
Use the conditional class block that checks errors[field] to ensure the focus
styling for invalid fields overrides the global input focus border rule.

In `@src/components/pdf/ats-pdf.tsx`:
- Around line 420-432: The competition link in the ATS PDF is using the wrong
visible label, so update the `Link` inside `ats-pdf.tsx` to use a
competition-appropriate text instead of `l.project`. Use an existing
localization key that matches a competition action such as `l.website` or a
certificate/view label, or render the competition name/URL directly, and keep
the `comp.url` and `ensureHref` logic unchanged.
- Around line 205-250: The contact separator logic in ats-pdf.tsx is using
pairwise conditions, so bullets disappear whenever a middle field is missing.
Refactor the contact row rendering around the contact item list in the ats-pdf
component: collect the visible fields for personalInfo.email, phone, location,
website, linkedin, and github, then render them in order with separators
inserted between every adjacent rendered item. Keep ensureHref and the existing
labels (like l.portfolio and LinkedIn) when building each item.

---

Nitpick comments:
In `@src/components/pdf/ats-pdf.tsx`:
- Around line 284-287: The PDF ATS component still renders hardcoded English
connectors in the experience, education, and coursework sections, which causes
mixed-language output in localized builds. Update the affected rendering in
ats-pdf.tsx to use PdfLabels-backed strings instead of inline literals for the
“at”, “in”, and “Relevant Coursework” text, and wire the existing JSX in the
exp/edu/coursework blocks to read from the locale labels consistently with the
rest of the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0768d17-ad96-4d1c-8434-0c8e603727d1

📥 Commits

Reviewing files that changed from the base of the PR and between d6b3b15 and 4ace1cf.

📒 Files selected for processing (13)
  • src/app/globals.css
  • src/components/builder/builder-form.tsx
  • src/components/builder/builder-header.tsx
  • src/components/builder/forms/certifications-form.tsx
  • src/components/builder/forms/competitions-form.tsx
  • src/components/builder/forms/education-form.tsx
  • src/components/builder/forms/experience-form.tsx
  • src/components/builder/forms/languages-form.tsx
  • src/components/builder/forms/personal-info-form.tsx
  • src/components/builder/forms/projects-form.tsx
  • src/components/builder/forms/skills-form.tsx
  • src/components/builder/resume-score.tsx
  • src/components/pdf/ats-pdf.tsx

Comment thread src/app/globals.css
font-feature-settings:
"kern" 1,
"liga" 1;
text-rendering: optimizeLegibility;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the lint-breaking keyword casing.

Line 44 will keep stylelint failing under value-keyword-case.

Suggested fix
-  text-rendering: optimizeLegibility;
+  text-rendering: optimizelegibility;
📝 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
text-rendering: optimizeLegibility;
text-rendering: optimizelegibility;
🧰 Tools
🪛 Stylelint (17.13.0)

[error] 44-44: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/globals.css` at line 44, The CSS declaration in the global styles
uses a keyword casing that breaks stylelint’s value-keyword-case rule. Update
the text-rendering value in the globals stylesheet to use the expected lowercase
keyword form, and keep the change localized to the same declaration so the lint
error is resolved without affecting the rest of the styles.

Source: Linters/SAST tools

Comment thread src/app/globals.css
Comment on lines 197 to +200
select:focus {
background-image:
linear-gradient(45deg, transparent 50%, #FFFFFF 50%),
linear-gradient(135deg, #FFFFFF 50%, transparent 50%);
linear-gradient(45deg, transparent 50%, #ffffff 50%),
linear-gradient(135deg, #ffffff 50%, transparent 50%);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Focused select caret becomes invisible on the new white focus background.

With white focus background (Lines 97-98), keeping white caret gradients at Line 199-200 removes the visible dropdown indicator.

Suggested fix
 select:focus {
   background-image:
-    linear-gradient(45deg, transparent 50%, `#ffffff` 50%),
-    linear-gradient(135deg, `#ffffff` 50%, transparent 50%);
+    linear-gradient(45deg, transparent 50%, `#000000` 50%),
+    linear-gradient(135deg, `#000000` 50%, transparent 50%);
 }
📝 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
select:focus {
background-image:
linear-gradient(45deg, transparent 50%, #FFFFFF 50%),
linear-gradient(135deg, #FFFFFF 50%, transparent 50%);
linear-gradient(45deg, transparent 50%, #ffffff 50%),
linear-gradient(135deg, #ffffff 50%, transparent 50%);
select:focus {
background-image:
linear-gradient(45deg, transparent 50%, `#000000` 50%),
linear-gradient(135deg, `#000000` 50%, transparent 50%);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/globals.css` around lines 197 - 200, The select focus styling in
select:focus uses white gradient caret indicators that disappear against the new
white focus background. Update the caret/background-image colors in the
select:focus rule so the dropdown indicator remains visible on focus, and keep
the change aligned with the existing select focus styling in globals.css.

Comment on lines +56 to +59
if (
sectionManagerRef.current &&
!sectionManagerRef.current.contains(e.target as Node)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there is only one assignment per section manager ref after the fix.
rg -n -C2 'sectionManagerRef|desktopSectionManagerRef|mobileSectionManagerRef' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 1049


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding component structure and the click-outside logic.
sed -n '1,120p' src/components/builder/builder-form.tsx
printf '\n---\n'
sed -n '240,520p' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 17928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether desktop/mobile sections are both rendered in the same component tree.
rg -n -C3 'lg:|md:|Mobile|Desktop|sectionManagerRef|showSectionManager' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 4132


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the surrounding component structure and the click-outside logic.
sed -n '1,120p' src/components/builder/builder-form.tsx
printf '\n---\n'
sed -n '240,520p' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 17928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether desktop/mobile sections are both rendered in the same component tree.
rg -n -C3 'lg:|md:|Mobile|Desktop|sectionManagerRef|showSectionManager' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 4132


Split the section-manager refs by breakpoint. The same ref is attached to both desktop and mobile wrappers, so it can point at the later one in the tree and make clicks in the other layout register as outside clicks. Use separate refs and treat either wrapper as inside.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/builder/builder-form.tsx` around lines 56 - 59, The
outside-click check in builder-form uses a single sectionManagerRef for both
desktop and mobile wrappers, so it can resolve to only one layout and
misclassify clicks in the other as outside. Split this into separate refs for
the desktop and mobile section-manager wrappers, then update the click
containment logic to treat a click as inside if it lands within either ref. Use
the existing sectionManagerRef usage site and the wrapper components in
builder-form to replace the shared ref with breakpoint-specific refs.

Comment on lines +178 to +246
const getSectionBadge = (id: string) => {
switch (id) {
case "personal":
const hasContact = Boolean(
data.personalInfo.fullName.trim() && data.personalInfo.email.trim(),
);
return hasContact ? (
<span className="text-[9px] font-bold px-1.5 py-0.5 uppercase tracking-wider bg-green-100 text-green-800 border border-green-200">
OK
</span>
) : (
<span className="text-[9px] font-bold px-1.5 py-0.5 uppercase tracking-wider bg-amber-100 text-amber-800 border border-amber-200">
Required
</span>
);
case "experience":
return data.experience.length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{data.experience.length}
</span>
) : null;
case "education":
return data.education.length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{data.education.length}
</span>
) : null;
case "skills":
const skillCount = data.skills.reduce(
(acc, cat) => acc + cat.items.length,
0,
);
return skillCount > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{skillCount}
</span>
) : null;
case "projects":
return data.projects.length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{data.projects.length}
</span>
) : null;
case "certifications":
return data.certifications.length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{data.certifications.length}
</span>
) : null;
case "competitions":
return (data.competitions ?? []).length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{(data.competitions ?? []).length}
</span>
) : null;
case "languages":
return data.languages.length > 0 ? (
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-800 border border-gray-300">
{data.languages.length}
</span>
) : null;
case "score":
const { total } = scoreResume(data);
const scoreColor =
total >= 85
? "bg-green-100 text-green-800 border-green-200"
: total >= 50
? "bg-yellow-100 text-yellow-800 border-yellow-200"
: "bg-red-100 text-red-800 border-red-200";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Inspect the target file and the surrounding switch cases
git ls-files src/components/builder/builder-form.tsx
wc -l src/components/builder/builder-form.tsx
sed -n '160,270p' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 4749


🏁 Script executed:

# Inspect the target file and the surrounding switch cases
git ls-files src/components/builder/builder-form.tsx
wc -l src/components/builder/builder-form.tsx
sed -n '160,270p' src/components/builder/builder-form.tsx

Repository: lab68dev/lab68cvbuilder

Length of output: 4749


🏁 Script executed:

nl -ba src/components/builder/builder-form.tsx | sed -n '175,250p'

Repository: lab68dev/lab68cvbuilder

Length of output: 197


🏁 Script executed:

nl -ba src/components/builder/builder-form.tsx | sed -n '175,250p'

Repository: lab68dev/lab68cvbuilder

Length of output: 197


Wrap the case bodies that declare bindings. personal, skills, and score each introduce const values directly under case; wrap those branches in braces to satisfy lint/correctness/noSwitchDeclarations.

🧰 Tools
🪛 Biome (2.5.0)

[error] 181-183: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 206-209: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 240-240: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 241-246: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/builder/builder-form.tsx` around lines 178 - 246, The switch
branches in getSectionBadge declare bindings directly under case labels, which
violates noSwitchDeclarations. Wrap the personal, skills, and score case bodies
in braces so their const declarations are scoped safely, and keep the existing
logic and return values unchanged.

Source: Linters/SAST tools

Comment on lines +241 to +246
const scoreColor =
total >= 85
? "bg-green-100 text-green-800 border-green-200"
: total >= 50
? "bg-yellow-100 text-yellow-800 border-yellow-200"
: "bg-red-100 text-red-800 border-red-200";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the score badge threshold with the score panel.

Line 242 uses green at 85%, while ScoreRing turns green at 80%; scores from 80–84 will show conflicting colors between navigation and the score panel.

-          total >= 85
+          total >= 80
📝 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
const scoreColor =
total >= 85
? "bg-green-100 text-green-800 border-green-200"
: total >= 50
? "bg-yellow-100 text-yellow-800 border-yellow-200"
: "bg-red-100 text-red-800 border-red-200";
const scoreColor =
total >= 80
? "bg-green-100 text-green-800 border-green-200"
: total >= 50
? "bg-yellow-100 text-yellow-800 border-yellow-200"
: "bg-red-100 text-red-800 border-red-200";
🧰 Tools
🪛 Biome (2.5.0)

[error] 241-246: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/builder/builder-form.tsx` around lines 241 - 246, The score
badge thresholds in builder-form.tsx are inconsistent with the ScoreRing
display, causing 80–84 scores to be shown differently in the navigation and
panel. Update the scoreColor logic in the builder-form component to match the
ScoreRing green threshold at 80 instead of 85, and keep the yellow/red cutoffs
aligned with the same scoring rules.

Comment on lines +684 to +687
`w-full border bg-transparent px-4 py-3 transition-all duration-150 ${
errors[field]
? "border-red-500 focus:bg-red-50 focus:text-red-900"
: "border-black focus:border-black"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep error border red while focused.

Line 686 misses focus:border-red-500; with global input:focus { border-color: #000000; }, invalid fields can look valid while focused.

Suggested fix
 const inputClass = (field: string) =>
   `w-full border bg-transparent px-4 py-3 transition-all duration-150 ${
       errors[field]
-        ? "border-red-500 focus:bg-red-50 focus:text-red-900"
+        ? "border-red-500 focus:border-red-500 focus:bg-red-50 focus:text-red-900"
         : "border-black focus:border-black"
   }`;
📝 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
`w-full border bg-transparent px-4 py-3 transition-all duration-150 ${
errors[field]
? "border-red-500 focus:bg-red-50 focus:text-red-900"
: "border-black focus:border-black"
`w-full border bg-transparent px-4 py-3 transition-all duration-150 ${
errors[field]
? "border-red-500 focus:border-red-500 focus:bg-red-50 focus:text-red-900"
: "border-black focus:border-black"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/builder/forms/personal-info-form.tsx` around lines 684 - 687,
The invalid input styling in the personal info form loses the red border on
focus because the conditional class string only sets the red border by default,
so update the field class logic in personal-info-form.tsx to keep the error
state visibly red while focused by adding the focused red border class alongside
the existing error styles in the relevant input rendering path. Use the
conditional class block that checks errors[field] to ensure the focus styling
for invalid fields overrides the global input focus border rule.

Comment on lines 205 to 250
<View style={s.contactRow}>
{personalInfo.email && <Text style={s.contactText}>{personalInfo.email}</Text>}
{personalInfo.phone && <Text style={s.contactText}>{personalInfo.phone}</Text>}
{personalInfo.location && <Text style={s.contactText}>{personalInfo.location}</Text>}
{personalInfo.website && <Link src={ensureHref(personalInfo.website)} style={{ ...s.contactText, color: "#666666", textDecoration: "none" }}>{l.portfolio}</Link>}
{personalInfo.linkedin && <Link src={ensureHref(personalInfo.linkedin)} style={{ ...s.contactText, color: "#666666", textDecoration: "none" }}>LinkedIn</Link>}
{personalInfo.github && <Link src={ensureHref(personalInfo.github)} style={{ ...s.contactText, color: "#666666", textDecoration: "none" }}>GitHub</Link>}
{personalInfo.email && (
<Text style={s.contactItem}>{personalInfo.email}</Text>
)}
{personalInfo.email && personalInfo.phone && (
<Text style={s.bulletSeparator}>•</Text>
)}
{personalInfo.phone && (
<Text style={s.contactItem}>{personalInfo.phone}</Text>
)}
{personalInfo.phone && personalInfo.location && (
<Text style={s.bulletSeparator}>•</Text>
)}
{personalInfo.location && (
<Text style={s.contactItem}>{personalInfo.location}</Text>
)}
{personalInfo.location && personalInfo.website && (
<Text style={s.bulletSeparator}>•</Text>
)}
{personalInfo.website && (
<Link
src={ensureHref(personalInfo.website)}
style={s.contactItem}
>
{l.portfolio}
</Link>
)}
{(personalInfo.website || personalInfo.location) &&
personalInfo.linkedin && <Text style={s.bulletSeparator}>•</Text>}
{personalInfo.linkedin && (
<Link
src={ensureHref(personalInfo.linkedin)}
style={s.contactItem}
>
LinkedIn
</Link>
)}
{personalInfo.linkedin && personalInfo.github && (
<Text style={s.bulletSeparator}>•</Text>
)}
{personalInfo.github && (
<Link src={ensureHref(personalInfo.github)} style={s.contactItem}>
GitHub
</Link>
)}
</View>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Contact separators collapse when an intermediate field is missing.

The bullet separators are gated on adjacent-pair presence, so any absent middle field drops the separator between the surviving neighbors. Examples that render with no separator:

  • email + location (no phone) → joined directly.
  • email + linkedin only → (website || location) && linkedin is false.
  • website + github (no linkedin) → linkedin && github is false.
  • email + github only.

Build the visible items into a list and interleave separators instead of pairwise conditions.

🐛 Suggested approach: derive items, then join with separators
const contactItems = [
  personalInfo.email && (
    <Text style={s.contactItem}>{personalInfo.email}</Text>
  ),
  personalInfo.phone && (
    <Text style={s.contactItem}>{personalInfo.phone}</Text>
  ),
  personalInfo.location && (
    <Text style={s.contactItem}>{personalInfo.location}</Text>
  ),
  personalInfo.website && (
    <Link src={ensureHref(personalInfo.website)} style={s.contactItem}>
      {l.portfolio}
    </Link>
  ),
  personalInfo.linkedin && (
    <Link src={ensureHref(personalInfo.linkedin)} style={s.contactItem}>
      LinkedIn
    </Link>
  ),
  personalInfo.github && (
    <Link src={ensureHref(personalInfo.github)} style={s.contactItem}>
      GitHub
    </Link>
  ),
].filter(Boolean);

// ...
<View style={s.contactRow}>
  {contactItems.map((item, i) => (
    <React.Fragment key={i}>
      {i > 0 && <Text style={s.bulletSeparator}></Text>}
      {item}
    </React.Fragment>
  ))}
</View>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/pdf/ats-pdf.tsx` around lines 205 - 250, The contact separator
logic in ats-pdf.tsx is using pairwise conditions, so bullets disappear whenever
a middle field is missing. Refactor the contact row rendering around the contact
item list in the ats-pdf component: collect the visible fields for
personalInfo.email, phone, location, website, linkedin, and github, then render
them in order with separators inserted between every adjacent rendered item.
Keep ensureHref and the existing labels (like l.portfolio and LinkedIn) when
building each item.

Comment on lines +420 to +432
{comp.url && (
<Link
src={ensureHref(comp.url)}
style={{
fontSize: 7.5,
color: "#666666",
textDecoration: "none",
marginBottom: 2,
}}
>
{l.project}
</Link>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Competition link is labeled "Project".

The competition URL uses l.project as its visible text, which mislabels the link. Use a competition-appropriate label (e.g. l.website / l.viewCertificate-style key) or render the URL/competition name instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/pdf/ats-pdf.tsx` around lines 420 - 432, The competition link
in the ATS PDF is using the wrong visible label, so update the `Link` inside
`ats-pdf.tsx` to use a competition-appropriate text instead of `l.project`. Use
an existing localization key that matches a competition action such as
`l.website` or a certificate/view label, or render the competition name/URL
directly, and keep the `comp.url` and `ensureHref` logic unchanged.

@DongDuong2001 DongDuong2001 merged commit f5942d2 into main Jun 28, 2026
4 checks passed
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