Skip to content

Add candidate self-service: profile edit, invitation response, work log appeal, payments#5

Merged
BAWES merged 9 commits into
mainfrom
feature/STU-13-tailwind-setup
May 21, 2026
Merged

Add candidate self-service: profile edit, invitation response, work log appeal, payments#5
BAWES merged 9 commits into
mainfrom
feature/STU-13-tailwind-setup

Conversation

@BAWES
Copy link
Copy Markdown
Owner

@BAWES BAWES commented May 20, 2026

Summary

  • Profile edit — server action + client form + page route at /candidate/edit with full field coverage (name, email, phone, birth date, country, university, address, civil ID, objective, intro, profile URL) plus document uploads (photo, CV, video, civil ID)
  • Invitation accept/reject — form embedded on /candidate/invitations/[id] using useActionState
  • Work log appeal — form on /candidate/work-logs/[id] with reason textarea, creates appeal record + links it to the work log in a transaction
  • Payment history — new /candidate/payments page reading from transfer_candidate with format helpers
  • Build fix — exclude e2e/ and playwright.config.ts from tsconfig so Next.js build passes without Playwright types installed

Test plan

  • npm run build passes
  • Dev server smoke: visit /candidate/edit, save a field, verify it persists
  • Dev server smoke: accept/reject an invitation from /candidate/invitations/[uuid]
  • Dev server smoke: submit a work log appeal from /candidate/work-logs/[uuid]
  • Dev server smoke: visit /candidate/payments and verify transfer rows render

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Candidate profile edit page with multi-part profile, document upload, skills and experience editors.
    • Payments page showing transfer records, statuses and payment dates.
    • Invitation response UI to accept or reject invitations.
    • Work log appeal form to submit dispute reasons.
    • Candidate navigation updated with Edit Profile and Payments shortcuts.
    • Browser “work tabs” UI to track and switch recently opened records.
  • UX
    • Readiness view now lists missing fields and shows a detailed checklist.

Review Change Stack

BAWES and others added 4 commits May 21, 2026 04:38
`buildOSCommands()` never set the `shortcut` property on command objects,
so the chord handler never matched G+H, G+C, G+R, G+T, G+O chords. Added
a chord-to-href map keyed by role and applied shortcuts to both nav and
scope commands.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add tailwindcss, @tailwindcss/postcss, postcss, and autoprefixer as
devDependencies. Create postcss.config.mjs with the Tailwind v4 PostCSS
plugin. Add @import "tailwindcss" and @theme block to styles.css mapping
existing CSS custom properties to Tailwind design tokens.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…og appeal, payments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Walkthrough

This PR extends the candidate workspace with self-service profile management (including uploads, skills, experiences), invitation response and appeal forms, a payments/transfers view, workspace tabs UI, server actions and lookups, navigation updates, and Tailwind/CSS styling.

Changes

Candidate workspace enhancement

Layer / File(s) Summary
Profile edit page and form
src/app/candidate/edit/page.tsx, src/modules/candidates/CandidateEditForm.tsx, src/modules/candidates/actions.ts, src/modules/workspace/data.ts
CandidateEditPage enforces access, fetches candidate and option lists, maps data into CandidateEditForm props. CandidateEditForm renders personal-info inputs via useActionState wired to updateCandidateProfile. getCandidateDetail selection expanded with civil/profile fields and raw foreign ids; CandidateProfile readiness logic updated.
Document upload form and action
src/modules/candidates/CandidateEditForm.tsx, src/modules/candidates/actions.ts
DocumentUpload helper renders file inputs per document type with current-file display. acceptFor maps types to accept filters. saveUpload/uploadDocument validate and persist files to per-candidate UUID-named files under public/uploads and update candidate document columns.
Option lookups and skills/experience CRUD
src/modules/candidates/actions.ts, src/modules/candidates/CandidateEditForm.tsx
getCountryOptions, getUniversityOptions, getBankOptions query and map to {id,label}. CandidateEditForm renders skills and experiences lists; add/remove server actions insert or soft-delete rows and revalidate candidate pages.
Invitation response form and workflow
src/app/candidate/invitations/[id]/page.tsx, src/modules/candidates/InvitationRespondForm.tsx, src/modules/candidates/actions.ts
Invitation page imports and renders InvitationRespondForm with UUID and status. Form uses useActionState to submit accept/reject. respondToInvitation validates ownership, updates invitation status/timestamps via Prisma, revalidates routes, and redirects.
Work log appeal form and action
src/app/candidate/work-logs/[id]/page.tsx, src/modules/candidates/WorkLogAppealForm.tsx, src/modules/candidates/actions.ts
Work log page renders WorkLogAppealForm with work log UUID. Form collects required reason via useActionState. appealWorkLog validates inputs, creates appeal record and links the work log atomically, revalidates work-log routes and redirects.
Payments and transfers page
src/app/candidate/payments/page.tsx, src/modules/workspace/data.ts
CandidatePaymentsPage enforces candidate read capability, fetches transfer rows via getCandidateTransferRows, and renders a DataTable. getCandidateTransferRows queries non-deleted transfer_candidate rows, joins related data, and maps formatted period, totals, costs, status, and dates.
Navigation and action links
src/modules/workspace/navigation.ts, src/app/candidate/page.tsx
Navigation adds "Work Logs" and "Payments" to the candidate role menu. Candidate profile page adds "Edit profile" and "Payments" action links.
Workspace tabs, styling and config
src/modules/workspace/WorkTabs.tsx, src/app/styles.css, tsconfig.json
Adds WorkTabs hook/component persisting recent tabs to localStorage and a tab nav UI. Styles integrate Tailwind via @import "tailwindcss" and an @theme mapping; adds work-tab and candidate-edit form CSS and a mobile media query. tsconfig excludes e2e and playwright config.

Sequence Diagrams

sequenceDiagram
  participant CandidateEditPage
  participant getCandidateDetail
  participant CandidateEditForm
  participant updateCandidateProfile
  CandidateEditPage->>getCandidateDetail: fetch candidate, countries, universities
  getCandidateDetail-->>CandidateEditPage: candidate data with profile fields
  CandidateEditPage->>CandidateEditForm: render with candidate props
  CandidateEditForm->>updateCandidateProfile: submit profile form data
  updateCandidateProfile-->>CandidateEditForm: success or error
Loading
sequenceDiagram
  participant InvitationPage
  participant InvitationRespondForm
  participant respondToInvitation
  InvitationRespondForm->>respondToInvitation: submit accept/reject with invitationUuid
  respondToInvitation->>respondToInvitation: validate and update invitation status
  respondToInvitation-->>InvitationRespondForm: success or error
  respondToInvitation->>InvitationPage: redirect to updated invitation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BAWES/studenthub-codex#4: Implements Tailwind v4 CSS integration with the same @import and @theme wiring; this PR applies the same styling approach to the candidate workspace pages.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.82% 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
Title check ✅ Passed The title accurately summarizes the main changes: profile editing, invitation responses, work log appeals, and payment viewing for candidates.
Description check ✅ Passed The description covers summary and test plan adequately, though most smoke tests are unchecked and some required template sections like TypeScript/build validation status are incomplete.
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 feature/STU-13-tailwind-setup

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.

src/app/styles.css

Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (1:0)


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

…ate edit

Extends profile edit form with bank account details, inline skill/experience
add and remove actions, and exposes status values on request pipeline rows.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/modules/workspace/data.ts (1)

2-2: ⚡ Quick win

Switch internal import to @/ alias.

Use the project alias for the format import at Line 2 to match repository import rules.

As per coding guidelines, "Use @/ path alias for all internal imports".

🤖 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/modules/workspace/data.ts` at line 2, Update the import of the format
utilities to use the project alias: replace the current relative import of
formatDate and formatMoney (the import statement that references "./format")
with the "`@/`..." alias form so the module imports formatDate and formatMoney via
the project's path alias; ensure you only change the import path and keep the
imported symbols (formatDate, formatMoney) intact.
src/modules/candidates/CandidateEditForm.tsx (1)

11-11: ⚡ Quick win

Use the @/ alias for internal imports.

Replace the relative import at Line 11 with the project alias form for consistency and rule compliance.

As per coding guidelines, "Use @/ path alias for all internal imports".

🤖 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/modules/candidates/CandidateEditForm.tsx` at line 11, The import in
CandidateEditForm.tsx currently uses a relative path ("./actions"); update it to
use the project alias form (e.g., replace "./actions" with
"`@/modules/candidates/actions`") so internal imports follow the `@/` convention
and satisfy the linting rule; modify the import statement that imports from
"./actions" accordingly and ensure any named imports remain unchanged.
🤖 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/candidate/edit/page.tsx`:
- Around line 25-49: CandidateEditForm is being called without required fields
and props: the candidate object must include bankId, bankAccountName, and iban
(add them with appropriate values or safe defaults like null/"" using the same
pattern as other fields) and the component must be passed the required props
banks, skills, and experiences; update the call site to include candidate.bankId
/ candidate.bankAccountName / candidate.iban and add banks={banks}
skills={skills} experiences={experiences} (or the correct variables/empty arrays
if not yet available) so the prop contract for CandidateEditForm is satisfied.

In `@src/app/styles.css`:
- Around line 3-24: Stylelint is failing on the Tailwind v4 `@theme` at-rule used
in the CSS block; update the scss/at-rule-no-unknown rule in your stylelint
config to allow Tailwind at-rules by adding "theme", "source", and "utility" to
the ignoreAtRules list (i.e., modify the scss/at-rule-no-unknown rule in
.stylelintrc.json to include those at-rules so `@theme` and similar Tailwind
directives are accepted).

In `@src/modules/candidates/actions.ts`:
- Around line 105-115: The server currently only checks presence and logical
type before calling saveUpload, so harden validation by validating file MIME
type, extension, and size on the server side and sanitizing the filename before
saving; specifically, in the upload handler (the block that checks file
instanceof File, file.size, allowed array and then calls saveUpload) enforce
allowed MIME types and file extensions per document type (e.g., image/jpeg/png
for "photo", pdf/docx for "cv", mp4/webm for "video"), reject files over a
defined max size (e.g., 5MB for photos, larger for CV/video), verify the
uploaded file's actual content-type (not just client-provided) and/or inspect
magic bytes, sanitize or generate a safe filename, and ensure saveUpload writes
only into a safe directory under public/uploads with no path traversal; update
saveUpload to trust validated inputs or perform the same checks there if called
elsewhere.

In `@src/modules/candidates/CandidateEditForm.tsx`:
- Around line 147-167: The form currently renders multiple DocumentUpload
components that all submit the same field names ("type" and "file"), while the
server reads only one pair via formData.get(...), causing ambiguity; update the
client and/or server so each document uses unique field names: modify
DocumentUpload to set the file input name to a unique identifier (e.g.,
`file_${type}`) and, if you keep a single form with action={uploadAction},
include a hidden input name like `docType_${type}` or only submit one document
per request by moving DocumentUpload into its own <form> that posts a single
`type` and `file`; then update the upload handler to read files by those unique
names (e.g., loop supportedTypes and call formData.get(`file_${type}`) /
formData.get(`docType_${type}`)) or accept one-file-per-form when using separate
forms. Ensure you change references to the DocumentUpload prop `type` and the
server-side formData access to match the new naming scheme.

In `@src/modules/candidates/InvitationRespondForm.tsx`:
- Line 4: Replace the relative import of respondToInvitation in
InvitationRespondForm.tsx with the project alias import; locate the import
statement importing respondToInvitation from "./actions" and change it to use
the "`@/`..." alias path that points to the same module (e.g., import from
"`@/modules/candidates/actions`") so it conforms to the repo's internal import
conventions.

In `@src/modules/candidates/WorkLogAppealForm.tsx`:
- Line 4: Replace the relative import of the appealWorkLog symbol in
WorkLogAppealForm.tsx with the project alias form (using "`@/`...") to comply with
the internal import rule; locate the import statement that currently references
"./actions" and update it to import appealWorkLog from the equivalent "`@/`..."
module path so the same symbol is imported via the repository alias.

---

Nitpick comments:
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Line 11: The import in CandidateEditForm.tsx currently uses a relative path
("./actions"); update it to use the project alias form (e.g., replace
"./actions" with "`@/modules/candidates/actions`") so internal imports follow the
`@/` convention and satisfy the linting rule; modify the import statement that
imports from "./actions" accordingly and ensure any named imports remain
unchanged.

In `@src/modules/workspace/data.ts`:
- Line 2: Update the import of the format utilities to use the project alias:
replace the current relative import of formatDate and formatMoney (the import
statement that references "./format") with the "`@/`..." alias form so the module
imports formatDate and formatMoney via the project's path alias; ensure you only
change the import path and keep the imported symbols (formatDate, formatMoney)
intact.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fc8bdf80-1515-4b9d-9229-0057225f4b06

📥 Commits

Reviewing files that changed from the base of the PR and between a41a46a and 8ec7387.

📒 Files selected for processing (13)
  • src/app/candidate/edit/page.tsx
  • src/app/candidate/invitations/[id]/page.tsx
  • src/app/candidate/page.tsx
  • src/app/candidate/payments/page.tsx
  • src/app/candidate/work-logs/[id]/page.tsx
  • src/app/styles.css
  • src/modules/candidates/CandidateEditForm.tsx
  • src/modules/candidates/InvitationRespondForm.tsx
  • src/modules/candidates/WorkLogAppealForm.tsx
  • src/modules/candidates/actions.ts
  • src/modules/workspace/data.ts
  • src/modules/workspace/navigation.ts
  • tsconfig.json

Comment thread src/app/candidate/edit/page.tsx
Comment thread src/app/styles.css
Comment on lines +3 to +24
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted-shadcn);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius: var(--radius);
}
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find stylelint config files
fd -HI 'stylelint.config.*|.stylelintrc.*|.stylelintrc' .

# Verify current at-rule settings and whether `@theme` is allowed
rg -n --hidden -S 'scss/at-rule-no-unknown|at-rule-no-unknown|ignoreAtRules|`@theme`|`@source`|`@utility`'

Repository: BAWES/studenthub-codex

Length of output: 116


🏁 Script executed:

cat .stylelintrc.json

Repository: BAWES/studenthub-codex

Length of output: 1426


🏁 Script executed:

# Check Tailwind version and stylelint setup in package.json
grep -A 5 -B 5 'tailwind\|stylelint' package.json | head -30

Repository: BAWES/studenthub-codex

Length of output: 619


🏁 Script executed:

# Check if there are any other stylelint rules related to at-rules
rg -A 10 'rules.*:' .stylelintrc.json

Repository: BAWES/studenthub-codex

Length of output: 480


🏁 Script executed:

# Check if scss/at-rule-no-unknown is explicitly in the rules
rg -i 'scss/at-rule-no-unknown|ignoreAtRules' .stylelintrc.json

Repository: BAWES/studenthub-codex

Length of output: 48


🏁 Script executed:

# Get the full rules section from .stylelintrc.json to see all configured rules
cat .stylelintrc.json | jq '.rules | keys' 2>/dev/null || cat .stylelintrc.json

Repository: BAWES/studenthub-codex

Length of output: 1105


🏁 Script executed:

# Check if there's a postcss.config or tailwind.config that might handle this
fd -HI 'postcss.config.*|tailwind.config.*' . | head -10

Repository: BAWES/studenthub-codex

Length of output: 87


🏁 Script executed:

cat ./postcss.config.mjs

Repository: BAWES/studenthub-codex

Length of output: 182


🏁 Script executed:

# Also check if there's any reference to ignoreAtRules in docs or if it needs to be added
# Search entire codebase for any stylelint-related configuration
rg -i 'ignoreAtRules|ignoreUnknownAtRules' .

Repository: BAWES/studenthub-codex

Length of output: 48


Add scss/at-rule-no-unknown rule to allow Tailwind v4 at-rules.

Lines 3-24 use Tailwind v4's @theme at-rule, which is valid CSS but causes stylelint to fail on scss/at-rule-no-unknown (active in the inherited stylelint-config-standard-scss base config). Update .stylelintrc.json to ignore or allow Tailwind at-rules:

{
  "rules": {
    "scss/at-rule-no-unknown": [
      true,
      {
        "ignoreAtRules": ["theme", "source", "utility"]
      }
    ]
  }
}
🧰 Tools
🪛 Stylelint (17.11.1)

[error] 3-3: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 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/styles.css` around lines 3 - 24, Stylelint is failing on the Tailwind
v4 `@theme` at-rule used in the CSS block; update the scss/at-rule-no-unknown rule
in your stylelint config to allow Tailwind at-rules by adding "theme", "source",
and "utility" to the ignoreAtRules list (i.e., modify the
scss/at-rule-no-unknown rule in .stylelintrc.json to include those at-rules so
`@theme` and similar Tailwind directives are accepted).

Comment thread src/modules/candidates/actions.ts Outdated
Comment on lines +147 to +167
<form action={uploadAction} className="candidateEditForm">
<h2>Documents</h2>
{uploadState.error ? <p className="formError">{uploadState.error}</p> : null}

<DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} />

<DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} />

<DocumentUpload label="Video" type="video" current={candidate.video} />

<DocumentUpload
label="Civil ID (front)"
type="civilFront"
current={candidate.civilPhotoFront}
/>

<DocumentUpload
label="Civil ID (back)"
type="civilBack"
current={candidate.civilPhotoBack}
/>
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 | ⚡ Quick win

Document upload form cannot reliably map selected file to the intended document type.

At Line 147 and Line 191-193, all document sections submit repeated type and file fields in one form, but the server reads only one type/file pair via formData.get(...). This makes uploads ambiguous (wrong target field or rejected upload).

Also applies to: 191-193

🤖 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/modules/candidates/CandidateEditForm.tsx` around lines 147 - 167, The
form currently renders multiple DocumentUpload components that all submit the
same field names ("type" and "file"), while the server reads only one pair via
formData.get(...), causing ambiguity; update the client and/or server so each
document uses unique field names: modify DocumentUpload to set the file input
name to a unique identifier (e.g., `file_${type}`) and, if you keep a single
form with action={uploadAction}, include a hidden input name like
`docType_${type}` or only submit one document per request by moving
DocumentUpload into its own <form> that posts a single `type` and `file`; then
update the upload handler to read files by those unique names (e.g., loop
supportedTypes and call formData.get(`file_${type}`) /
formData.get(`docType_${type}`)) or accept one-file-per-form when using separate
forms. Ensure you change references to the DocumentUpload prop `type` and the
server-side formData access to match the new naming scheme.

Comment thread src/modules/candidates/InvitationRespondForm.tsx Outdated
Comment thread src/modules/candidates/WorkLogAppealForm.tsx Outdated
BAWES and others added 4 commits May 21, 2026 06:39
Expands readiness from 8 broad categories to 14 specific field-level
checks (name, email, phone, country, university, objective, civil ID,
profile photo, CV, bank info, skills, experience, approval) and surfaces
exact missing field names so candidates know precisely what to fill.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
…on types

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Use @/ path alias for all internal imports (CandidateEditForm,
  InvitationRespondForm, WorkLogAppealForm, data.ts)
- Add per-type MIME, extension, and size validation in uploadDocument
- Add try/catch around saveUpload for clean error UX

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (1)
src/modules/candidates/CandidateEditForm.tsx (1)

175-195: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document upload submission is still ambiguous and can target the wrong document type.

Line 301 and Line 302 are repeated for every document block inside one form, but the server action reads a single type/file pair. In practice this can submit the wrong file/type combination or fail when the first file input is empty.

Proposed fix
-<form action={uploadAction} className="candidateEditForm">
+<div className="candidateEditForm">
   <h2>Documents</h2>
   {uploadState.error ? <p className="formError">{uploadState.error}</p> : null}
-
-  <DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} />
-  <DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} />
-  ...
-
-  <div className="formActions">
-    <button type="submit" disabled={uploadPending}>
-      {uploadPending ? "Uploading..." : "Upload document"}
-    </button>
-  </div>
-</form>
+  <DocumentUpload label="Profile photo" type="photo" current={candidate.personalPhoto} action={uploadAction} pending={uploadPending} />
+  <DocumentUpload label="CV / Resume" type="cv" current={candidate.resume} action={uploadAction} pending={uploadPending} />
+  ...
+</div>
-function DocumentUpload({ label, type, current }: { ... }) {
+function DocumentUpload({ label, type, current, action, pending }: { ...; action: (formData: FormData) => void; pending: boolean }) {
   return (
-    <fieldset className="documentUploadField">
+    <form action={action} className="documentUploadField">
+      <fieldset>
         <legend>{label}</legend>
         <input type="hidden" name="type" value={type} />
         <input type="file" name="file" accept={acceptFor(type)} />
+        <button type="submit" disabled={pending}>{pending ? "Uploading..." : `Upload ${label}`}</button>
         ...
-    </fieldset>
+      </fieldset>
+    </form>
   );
}

Also applies to: 301-303

🤖 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/modules/candidates/CandidateEditForm.tsx` around lines 175 - 195, The
form currently wraps multiple DocumentUpload components but the server action
(uploadAction) expects a single type/file pair, causing ambiguous/mismatched
submissions; fix by making each DocumentUpload submit independently: either
change DocumentUpload to render its own <form action={uploadAction}> with a
hidden input for the document type and its file input + submit button, or split
the existing form into separate forms around each DocumentUpload so each
submission includes exactly one type/file pair; update any uploadState handling
to reflect per-document responses and keep uploadAction unchanged.
🤖 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/modules/candidates/CandidateEditForm.tsx`:
- Around line 204-217: The skills list nests a remove <form> inside the parent
addSkill form (using addSkillAction), which is invalid and causes the remove
action to submit the wrong form; fix by removing the inner <form> around the
"Remove" button and instead wire the remove action so it does not live inside
the parent form — for example create a standalone form element (per skill)
outside the parent add form or use a button with a form="{uniqueId}" attribute
that targets a separate form with action={removeSkillAction} and includes the
hidden skillId; use the same approach for the experience remove controls (the
removeSkillAction / removeSkillPending symbols and the corresponding experience
remove action/button) so no forms are nested.

In `@src/modules/workspace/WorkTabs.tsx`:
- Around line 89-104: closeTab currently calls setTabs and then reads the outer
tabs variable causing a stale-closure; move the navigation logic into the
setTabs updater so it uses the freshly computed next state: inside the updater
filter out the tab (as you already do), call writeTabs(next), and if pathname
=== path check remaining from next and call router.push(remaining[0].path as
Route) as needed; then return next and remove tabs from the useCallback
dependency list (keep pathname and router).

---

Duplicate comments:
In `@src/modules/candidates/CandidateEditForm.tsx`:
- Around line 175-195: The form currently wraps multiple DocumentUpload
components but the server action (uploadAction) expects a single type/file pair,
causing ambiguous/mismatched submissions; fix by making each DocumentUpload
submit independently: either change DocumentUpload to render its own <form
action={uploadAction}> with a hidden input for the document type and its file
input + submit button, or split the existing form into separate forms around
each DocumentUpload so each submission includes exactly one type/file pair;
update any uploadState handling to reflect per-document responses and keep
uploadAction unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ced9136-97e4-4487-b170-c29cc3fc66ab

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec7387 and 78bf011.

📒 Files selected for processing (10)
  • src/app/candidate/edit/page.tsx
  • src/app/styles.css
  • src/modules/candidates/CandidateEditForm.tsx
  • src/modules/candidates/CandidateProfile.tsx
  • src/modules/candidates/InvitationRespondForm.tsx
  • src/modules/candidates/WorkLogAppealForm.tsx
  • src/modules/candidates/actions.ts
  • src/modules/requests/application-actions.ts
  • src/modules/workspace/WorkTabs.tsx
  • src/modules/workspace/data.ts

Comment on lines +204 to +217
<form action={addSkillAction} className="candidateEditForm">
<h2>Skills</h2>

{skills.length ? (
<ul className="editableList">
{skills.map((s) => (
<li key={s.id}>
<span>{s.title}</span>
<form action={removeSkillAction}>
<input type="hidden" name="skillId" value={s.id} />
<button type="submit" disabled={removeSkillPending} className="removeButton">
Remove
</button>
</form>
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 | ⚡ Quick win

Nested <form> elements create invalid markup and break remove actions.

Line 212 and Line 245 place forms inside parent forms. Browsers do not support nested forms reliably, so remove actions may submit the outer add form instead of the intended remove action.

Proposed fix
-<form action={addSkillAction} className="candidateEditForm">
+<form action={addSkillAction} className="candidateEditForm">
   ...
   {skills.map((s) => (
     <li key={s.id}>
       <span>{s.title}</span>
-      <form action={removeSkillAction}>
-        <input type="hidden" name="skillId" value={s.id} />
-        <button type="submit" disabled={removeSkillPending} className="removeButton">
-          Remove
-        </button>
-      </form>
+      <button
+        type="submit"
+        formAction={removeSkillAction}
+        name="skillId"
+        value={String(s.id)}
+        disabled={removeSkillPending}
+        className="removeButton"
+      >
+        Remove
+      </button>
     </li>
   ))}

Apply the same pattern to the experience remove button (Line 245–250).

Also applies to: 237-250

🤖 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/modules/candidates/CandidateEditForm.tsx` around lines 204 - 217, The
skills list nests a remove <form> inside the parent addSkill form (using
addSkillAction), which is invalid and causes the remove action to submit the
wrong form; fix by removing the inner <form> around the "Remove" button and
instead wire the remove action so it does not live inside the parent form — for
example create a standalone form element (per skill) outside the parent add form
or use a button with a form="{uniqueId}" attribute that targets a separate form
with action={removeSkillAction} and includes the hidden skillId; use the same
approach for the experience remove controls (the removeSkillAction /
removeSkillPending symbols and the corresponding experience remove
action/button) so no forms are nested.

Comment on lines +89 to +104
const closeTab = useCallback(
(path: string) => {
setTabs((prev) => {
const next = prev.filter((t) => t.path !== path);
writeTabs(next);
return next;
});
if (pathname === path) {
const remaining = tabs.filter((t) => t.path !== path);
if (remaining.length > 0) {
router.push(remaining[0].path as Route);
}
}
},
[pathname, router, tabs]
);
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the stale-state pattern exists in closeTab
rg -n -C3 'const closeTab|tabs\.filter\(\(t\) => t\.path !== path\)|setTabs\(\(prev\)' src/modules/workspace/WorkTabs.tsx

Repository: BAWES/studenthub-codex

Length of output: 761


🏁 Script executed:

# Get the full context of the closeTab function and surrounding code
sed -n '70,110p' src/modules/workspace/WorkTabs.tsx

Repository: BAWES/studenthub-codex

Length of output: 1081


🏁 Script executed:

# Search for all usages of closeTab in the file to understand its usage pattern
rg -n 'closeTab' src/modules/workspace/WorkTabs.tsx

Repository: BAWES/studenthub-codex

Length of output: 190


🏁 Script executed:

# Check if there are other state dependencies or effects that interact with this function
rg -n 'useEffect|useCallback|useRef|useState' src/modules/workspace/WorkTabs.tsx | head -20

Repository: BAWES/studenthub-codex

Length of output: 304


Move tab navigation into the state updater to eliminate stale-state closure.

closeTab schedules setTabs then immediately reads the outer tabs snapshot to determine navigation, creating a stale-closure pattern. Move the navigation logic inside the setTabs updater to use the fresh next state, and remove tabs from dependencies—this is more efficient and aligns with how addTab structures its state updates.

Proposed fix
  const closeTab = useCallback(
    (path: string) => {
      setTabs((prev) => {
        const next = prev.filter((t) => t.path !== path);
        writeTabs(next);
+       if (pathname === path && next.length > 0) {
+         router.push(next[0].path as Route);
+       }
        return next;
      });
-     if (pathname === path) {
-       const remaining = tabs.filter((t) => t.path !== path);
-       if (remaining.length > 0) {
-         router.push(remaining[0].path as Route);
-       }
-     }
    },
-   [pathname, router, tabs]
+   [pathname, router]
  );
🤖 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/modules/workspace/WorkTabs.tsx` around lines 89 - 104, closeTab currently
calls setTabs and then reads the outer tabs variable causing a stale-closure;
move the navigation logic into the setTabs updater so it uses the freshly
computed next state: inside the updater filter out the tab (as you already do),
call writeTabs(next), and if pathname === path check remaining from next and
call router.push(remaining[0].path as Route) as needed; then return next and
remove tabs from the useCallback dependency list (keep pathname and router).

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