Skip to content

[#398] COMPONENTS - Added creation/edit dialog component#421

Open
mchdich wants to merge 10 commits intomainfrom
issue-dialog-component
Open

[#398] COMPONENTS - Added creation/edit dialog component#421
mchdich wants to merge 10 commits intomainfrom
issue-dialog-component

Conversation

@mchdich
Copy link
Contributor

@mchdich mchdich commented Mar 13, 2026

Why

For the Calendar/Task Manager sprint.

What

Issue(s): #398
Dialog to create or edit a new issue.

Test Plan

Merge me twin.

Checklist

  • Database: No schema changes, OR I have contacted the Development Lead to run db:push before merging
  • Environment Variables: No environment variables changed, OR I have contacted the Development Lead to modify them on Coolify BEFORE merging.

Summary by CodeRabbit

  • New Features
    • New modal dialog for creating/editing issues with dual modes (Event vs Task) and multi-section form (Details, Room & Requirements, Links & Notes)
    • Status selector with visual indicators, team/priority controls, date/time/due handling, and all-day support
    • Link management (add/update/remove), room booking and requirements toggles, notes, and visibility controls
    • Keyboard/overlay close, body-scroll locking, and delete for existing items
    • Public types for issue/event form values and dialog props added

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new client-side CreateEditDialog React component implementing a portal modal for creating/editing Issues in Event or Task modes, plus related types/constants (CreateEditDialogProps, IssueFormValues, EventFormValues, DetailSectionKey, SECTION_TABS, and status/priority types) consumed by the dialog.

Changes

Cohort / File(s) Summary
Create/Edit dialog component
apps/blade/src/app/_components/issues/create-edit-dialog.tsx
New modal component: portal overlay, body scroll lock, Escape/overlay close, reinitialize form state on open/mode change, Event vs Task form modes (event-specific fields vs task-specific due-date normalization), top-level section tabs (Details, Room & Requirements, Links & Notes), status/team/priority controls, link add/remove/update UI, submit and delete handlers, exported CreateEditDialog.
Issue types & UI constants
packages/consts/src/issue.ts
Adds exported interfaces/types and UI constants for the dialog: CreateEditDialogProps, IssueFormValues, EventFormValues, DetailSectionKey, SECTION_TABS, plus status/priority related types/constants and a STATUS_COLORS mapping.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant Dialog as CreateEditDialog
    participant Parent as ParentComponent
    participant API as onSubmit/onDelete

    User->>Dialog: open(intent=create|edit, initialValues?)
    Dialog-->>User: render modal (tabs, Event/Task fields, controls)
    User->>Dialog: change fields / toggle isEvent / add/remove links
    Dialog->>Dialog: update internal form state
    User->>Dialog: Submit
    Dialog->>Parent: onSubmit(formValues)
    Parent->>API: persist create/update
    API-->>Parent: success/failure
    Parent-->>Dialog: onClose() or surface errors
    User->>Dialog: Delete (when editing)
    Dialog->>Parent: onDelete(formValues)
    Parent->>API: delete
    API-->>Parent: success
    Parent-->>Dialog: onClose()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested labels

Blade, Constants, Feature, Major

🚥 Pre-merge checks | ✅ 5 | ❌ 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 (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title meets all specified requirements: starts with issue number [#398], includes a concise description, and is 56 characters (well under the 72-character limit).
No Hardcoded Secrets ✅ Passed Repository search found no hardcoded secrets, API keys, passwords, or authentication tokens in the added files.
Validated Env Access ✅ Passed Pull request introduces new files without direct process.env references, properly adhering to validated environment access pattern via env.ts.
No Typescript Escape Hatches ✅ Passed No TypeScript escape hatches found. All type annotations use proper interfaces, optional chaining, and nullish coalescing instead of any, @ts-ignore, @ts-expect-error, or non-null assertions.

✏️ 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 issue-dialog-component
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

Copy link

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

🧹 Nitpick comments (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)

706-718: Add accessible labels to dynamic link inputs.

The link label and URL inputs lack associated <Label> elements or aria-label attributes. Screen reader users won't know what these fields are for.

Add aria-labels to link inputs
 <Input
   className={cn(baseField, "h-11")}
   placeholder="Label (e.g., Notion doc)"
+  aria-label={`Link ${formValues.links.indexOf(link) + 1} label`}
   value={link.label}
   onChange={(event) =>
     handleLinkUpdate(
       link.id,
       "label",
       event.target.value,
     )
   }
 />
 <Input
   className={cn(baseField, "h-11 pl-9")}
   placeholder="URL"
   type="url"
+  aria-label={`Link ${formValues.links.indexOf(link) + 1} URL`}
   value={link.url}
   onChange={(event) =>
     handleLinkUpdate(
       link.id,
       "url",
       event.target.value,
     )
   }
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
706 - 718, The dynamic link inputs rendered inside the create-edit dialog are
missing accessible labels; update the Input elements (the ones using
value={link.label} and value={link.url} and onChange calling
handleLinkUpdate(link.id,...)) to include accessible labeling — either wrap each
input with the existing Label component or add descriptive aria-label attributes
(e.g., aria-label="Link label" and aria-label="Link URL") and keep the existing
className={cn(baseField, "h-11")}; ensure the label text or aria-label uniquely
identifies the field when iterating multiple links so screen readers can
distinguish them (you can include the link.id or index in the aria-label if
needed).

300-331: Add aria-labelledby to improve screen reader accessibility.

The dialog has role="dialog" and aria-modal, but screen readers benefit from aria-labelledby pointing to the title. This helps users understand the dialog's purpose when it opens.

Add aria-labelledby
+      const titleId = `${baseId}-dialog-title`;
+      // ... in JSX:
       <div
         aria-modal
         role="dialog"
+        aria-labelledby={titleId}
         className="fixed inset-0 z-50 ..."
         onMouseDown={handleOverlayPointerDown}
       >
         ...
-          <h2 className="text-2xl font-semibold text-white">
+          <h2 id={titleId} className="text-2xl font-semibold text-white">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
300 - 331, The dialog container with role="dialog" lacks aria-labelledby; add
aria-labelledby to the outer div returned by createPortal (the element with
role="dialog") and give the title h2 a matching id (e.g.,
id="create-edit-dialog-title") so screen readers announce the dialog title;
update the h2 inside the header (the element rendering "Update the event details
below" / "Enter the event details below") and the createPortal div (the element
with aria-modal and role="dialog") accordingly.

285-288: Consider validating date range before submission.

The form allows endDate/endTime to be before startDate/startTime. Adding client-side validation prevents users from submitting logically invalid date ranges.

Example validation in handleSubmit
 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
   event.preventDefault();
+  
+  if (!formValues.allDay) {
+    const start = new Date(`${formValues.startDate}T${formValues.startTime}`);
+    const end = new Date(`${formValues.endDate}T${formValues.endTime}`);
+    if (end <= start) {
+      // Handle error - could add error state and display message
+      return;
+    }
+  }
+  
   onSubmit?.(formValues);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
285 - 288, The handleSubmit function currently submits formValues without
checking that the end date/time is after the start; update handleSubmit to
validate the date range by constructing comparable Date objects from
formValues.startDate/formValues.startTime and
formValues.endDate/formValues.endTime, and if the end is not after the start
prevent submission (call event.preventDefault() already present), set a
validation error state or invoke existing error handler to show a user-facing
message, and only call onSubmit(formValues) when the date-range check passes;
reference the handleSubmit function, formValues fields (startDate, startTime,
endDate, endTime), and onSubmit when implementing this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 737-744: The delete button that renders the Trash2 icon lacks an
accessible name; update the Button element that calls handleRemoveLink(link.id)
to include an aria-label (for example aria-label={`Delete link${link.title ? `:
${link.title}` : ''}`} or a static "Delete link") so screen readers get context
when they encounter the icon-only button.
- Around line 821-827: formatDateForInput currently uses date.toISOString()
(UTC) while formatTimeForInput uses date.toTimeString() (local), causing
timezone drift; change formatDateForInput to produce a local YYYY-MM-DD string
(e.g., via date.getFullYear()/getMonth()+1/getDate with zero-padding or
date.toLocaleDateString('sv')/ 'en-CA') so both formatDateForInput and
formatTimeForInput use the same local time basis and remain consistent across
timezones.
- Around line 31-49: IssueFormValues fields and STATUS_OPTIONS values do not
match the Issue DB enum (ISSUE.ISSUE_STATUS) and other form fields don't map to
the Issue model; before calling createIssue you must transform the form payload
to the DB shape: map form status values (from STATUS_OPTIONS) to the DB enum
values (e.g., "confirmed"/"tentative"/"draft"/"cancelled" -> one of
BACKLOG/PLANNING/IN_PROGRESS/FINISHED), combine startDate+startTime and
endDate+endTime into the single timestamp column (date) or otherwise produce the
expected timestamp(s), convert LinkItem[] into a string[] (text[])
representation, and drop or persist the extra properties (priority,
requiresRoom, needsDesignAssets, needsOutreach, requirements, notes) either by
removing them from the payload sent to createIssue or by adding corresponding DB
columns if intended; update the code path that builds the mutation payload
(where IssueFormValues is serialized for createIssue) to perform these mappings
and validations.

---

Nitpick comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 706-718: The dynamic link inputs rendered inside the create-edit
dialog are missing accessible labels; update the Input elements (the ones using
value={link.label} and value={link.url} and onChange calling
handleLinkUpdate(link.id,...)) to include accessible labeling — either wrap each
input with the existing Label component or add descriptive aria-label attributes
(e.g., aria-label="Link label" and aria-label="Link URL") and keep the existing
className={cn(baseField, "h-11")}; ensure the label text or aria-label uniquely
identifies the field when iterating multiple links so screen readers can
distinguish them (you can include the link.id or index in the aria-label if
needed).
- Around line 300-331: The dialog container with role="dialog" lacks
aria-labelledby; add aria-labelledby to the outer div returned by createPortal
(the element with role="dialog") and give the title h2 a matching id (e.g.,
id="create-edit-dialog-title") so screen readers announce the dialog title;
update the h2 inside the header (the element rendering "Update the event details
below" / "Enter the event details below") and the createPortal div (the element
with aria-modal and role="dialog") accordingly.
- Around line 285-288: The handleSubmit function currently submits formValues
without checking that the end date/time is after the start; update handleSubmit
to validate the date range by constructing comparable Date objects from
formValues.startDate/formValues.startTime and
formValues.endDate/formValues.endTime, and if the end is not after the start
prevent submission (call event.preventDefault() already present), set a
validation error state or invoke existing error handler to show a user-facing
message, and only call onSubmit(formValues) when the date-range check passes;
reference the handleSubmit function, formValues fields (startDate, startTime,
endDate, endTime), and onSubmit when implementing this change.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: 5cc81bde-1b80-4cdc-9cd4-6d1246535d03

📥 Commits

Reviewing files that changed from the base of the PR and between 0194403 and 51b5700.

📒 Files selected for processing (1)
  • apps/blade/src/app/_components/issues/create-edit-dialog.tsx

Comment on lines +737 to +744
<Button
variant="ghost"
type="button"
className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
onClick={() => handleRemoveLink(link.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add aria-label to the delete link button.

The button only contains an icon. Screen readers will announce it as "button" with no context.

Add aria-label
 <Button
   variant="ghost"
   type="button"
   className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
   onClick={() => handleRemoveLink(link.id)}
+  aria-label={`Remove link ${link.label || "item"}`}
 >
   <Trash2 className="h-4 w-4" />
 </Button>
📝 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
<Button
variant="ghost"
type="button"
className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
onClick={() => handleRemoveLink(link.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
type="button"
className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
onClick={() => handleRemoveLink(link.id)}
aria-label={`Remove link ${link.label || "item"}`}
>
<Trash2 className="h-4 w-4" />
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
737 - 744, The delete button that renders the Trash2 icon lacks an accessible
name; update the Button element that calls handleRemoveLink(link.id) to include
an aria-label (for example aria-label={`Delete link${link.title ? `:
${link.title}` : ''}`} or a static "Delete link") so screen readers get context
when they encounter the icon-only button.

Comment on lines +821 to +827
function formatDateForInput(date: Date) {
return date.toISOString().slice(0, 10);
}

function formatTimeForInput(date: Date) {
return date.toTimeString().slice(0, 5);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Timezone inconsistency between date and time formatting.

formatDateForInput uses toISOString() which returns UTC, but formatTimeForInput uses toTimeString() which returns local time. This mismatch can cause the displayed date to be off by one day near midnight in certain timezones.

Use consistent local time formatting
 function formatDateForInput(date: Date) {
-  return date.toISOString().slice(0, 10);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
 }
📝 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
function formatDateForInput(date: Date) {
return date.toISOString().slice(0, 10);
}
function formatTimeForInput(date: Date) {
return date.toTimeString().slice(0, 5);
}
function formatDateForInput(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function formatTimeForInput(date: Date) {
return date.toTimeString().slice(0, 5);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
821 - 827, formatDateForInput currently uses date.toISOString() (UTC) while
formatTimeForInput uses date.toTimeString() (local), causing timezone drift;
change formatDateForInput to produce a local YYYY-MM-DD string (e.g., via
date.getFullYear()/getMonth()+1/getDate with zero-padding or
date.toLocaleDateString('sv')/ 'en-CA') so both formatDateForInput and
formatTimeForInput use the same local time basis and remain consistent across
timezones.

Copy link

@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 (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)

854-861: ⚠️ Potential issue | 🟡 Minor

Add an accessible name to the icon-only delete button.

At Line 854, the trash-icon button has no aria-label, so assistive tech lacks context.

Suggested patch
 <Button
   variant="ghost"
   type="button"
   className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
   onClick={() => handleRemoveLink(link.id)}
+  aria-label={`Remove link ${link.label || "item"}`}
 >
   <Trash2 className="h-4 w-4" />
 </Button>

As per coding guidelines, apps/blade/** requires accessibility-safe UI patterns (including ARIA coverage for actionable controls).

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
854 - 861, The trash-icon-only Button (Button with onClick={() =>
handleRemoveLink(link.id)} rendering <Trash2 />) is missing an accessible name;
add an aria-label (or aria-labelledby) to the Button so screen readers have
context (e.g., aria-label={`Delete link`} or a dynamic label like
aria-label={`Delete link ${link.title || link.url || link.id}`}) while keeping
the existing onClick handler and classes; update the Button that wraps <Trash2
className="h-4 w-4" /> to include this aria attribute so the control is
accessible.

938-944: ⚠️ Potential issue | 🟡 Minor

Use a consistent local-time basis for date/time formatting.

Line 939 uses UTC (toISOString) while Line 943 uses local time (toTimeString), which can shift the displayed date near midnight/timezone boundaries.

Suggested patch
 function formatDateForInput(date: Date) {
-  return date.toISOString().slice(0, 10);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
 }
In JavaScript, does Date.prototype.toISOString() always output UTC while Date.prototype.toTimeString() outputs local time?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
938 - 944, formatDateForInput uses UTC via toISOString() while
formatTimeForInput uses local time via toTimeString(), causing inconsistent
results near timezone boundaries; change both to use the same local-time basis
by deriving date and time from the Date object's local getters (e.g.,
getFullYear, getMonth, getDate for formatDateForInput and getHours, getMinutes
for formatTimeForInput) and zero-pad as needed so both functions consistently
format based on local time; update the implementations of formatDateForInput and
formatTimeForInput accordingly.

68-93: ⚠️ Potential issue | 🟠 Major

Align status/priority option values with @forge/consts enums.

The select options use values outside ISSUE_STATUS/PRIORITY (confirmed, tentative, lowercase priorities), and casts at Line 463 mask the mismatch. This can submit values the backend does not accept.

Suggested patch
-const STATUS_OPTIONS = [
-  { value: "confirmed", label: "Confirmed", caption: "Everything is locked in", dotClass: "bg-emerald-400" },
-  { value: "tentative", label: "Tentative", caption: "Waiting on a few details", dotClass: "bg-amber-400" },
-  { value: "draft", label: "Draft", caption: "Still being scoped", dotClass: "bg-slate-400" },
-  { value: "cancelled", label: "Cancelled", caption: "No longer happening", dotClass: "bg-rose-400" },
-] as const;
+const STATUS_OPTIONS: { value: IssueStatus; label: string; caption: string; dotClass: string }[] = [
+  { value: "BACKLOG", label: "Backlog", caption: "Not started yet", dotClass: "bg-slate-400" },
+  { value: "PLANNING", label: "Planning", caption: "Being scoped", dotClass: "bg-amber-400" },
+  { value: "IN_PROGRESS", label: "In Progress", caption: "Actively underway", dotClass: "bg-sky-400" },
+  { value: "FINISHED", label: "Finished", caption: "Completed", dotClass: "bg-emerald-400" },
+];
@@
-const PRIORITY_OPTIONS = ["High", "Medium", "Low"];
+const PRIORITY_OPTIONS: IssuePriority[] = [...PRIORITY];
@@
-                onValueChange={(value) =>
-                  updateForm("status", value as IssueStatus)
-                }
+                onValueChange={(value) => updateForm("status", value as IssueStatus)}
@@
-                            {PRIORITY_OPTIONS.map((priority) => (
-                              <SelectItem
-                                key={priority}
-                                value={priority.toLowerCase()}
-                              >
+                            {PRIORITY_OPTIONS.map((priority) => (
+                              <SelectItem key={priority} value={priority}>
                                 {priority}
                               </SelectItem>
                             ))}
#!/bin/bash
# Verify enum sources vs local options (read-only).
echo "=== Canonical enums ==="
sed -n '1,40p' packages/consts/src/issue.ts

echo
echo "=== Local options and select value wiring ==="
sed -n '68,120p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
sed -n '455,705p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

Also applies to: 110-111, 461-464, 677-693

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 68
- 93, STATUS_OPTIONS (and the Priority options referenced around
create-edit-dialog.tsx) use non-canonical string values and then get force-cast
when submitting; update the option objects so their value fields match the
canonical enums exported from `@forge/consts` (ISSUE_STATUS and PRIORITY) instead
of "confirmed"/"tentative" or lowercase priorities, and update any select wiring
that currently casts values (see the cast near the submit handler and the SELECT
value props around create-edit-dialog.tsx) to rely on the real enum types; in
short, replace the value strings in STATUS_OPTIONS (and the priority options)
with the corresponding ISSUE_STATUS/PRIORITY enum members and remove the unsafe
type casts so submitted payloads use the backend-accepted enum values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Line 927: The submit button copy currently uses intent to render "Create
Event"/"Update Event" but doesn't respect task mode; update the text generation
in the CreateEditDialog component to also consider the task vs event flag (e.g.,
use isTask, mode, or itemType used in this component) and return "Create
Task"/"Update Task" when in task mode and "Create Event"/"Update Event"
otherwise, keeping the existing intent-based switch for create vs update.

---

Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 854-861: The trash-icon-only Button (Button with onClick={() =>
handleRemoveLink(link.id)} rendering <Trash2 />) is missing an accessible name;
add an aria-label (or aria-labelledby) to the Button so screen readers have
context (e.g., aria-label={`Delete link`} or a dynamic label like
aria-label={`Delete link ${link.title || link.url || link.id}`}) while keeping
the existing onClick handler and classes; update the Button that wraps <Trash2
className="h-4 w-4" /> to include this aria attribute so the control is
accessible.
- Around line 938-944: formatDateForInput uses UTC via toISOString() while
formatTimeForInput uses local time via toTimeString(), causing inconsistent
results near timezone boundaries; change both to use the same local-time basis
by deriving date and time from the Date object's local getters (e.g.,
getFullYear, getMonth, getDate for formatDateForInput and getHours, getMinutes
for formatTimeForInput) and zero-pad as needed so both functions consistently
format based on local time; update the implementations of formatDateForInput and
formatTimeForInput accordingly.
- Around line 68-93: STATUS_OPTIONS (and the Priority options referenced around
create-edit-dialog.tsx) use non-canonical string values and then get force-cast
when submitting; update the option objects so their value fields match the
canonical enums exported from `@forge/consts` (ISSUE_STATUS and PRIORITY) instead
of "confirmed"/"tentative" or lowercase priorities, and update any select wiring
that currently casts values (see the cast near the submit handler and the SELECT
value props around create-edit-dialog.tsx) to rely on the real enum types; in
short, replace the value strings in STATUS_OPTIONS (and the priority options)
with the corresponding ISSUE_STATUS/PRIORITY enum members and remove the unsafe
type casts so submitted payloads use the backend-accepted enum values.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: dd1ecf33-f938-403d-ae85-54fa8e50ccf6

📥 Commits

Reviewing files that changed from the base of the PR and between 51b5700 and 11cdf63.

📒 Files selected for processing (1)
  • apps/blade/src/app/_components/issues/create-edit-dialog.tsx

Comment on lines +44 to +57
export interface IssueFormValues {
status: IssueStatus;
name: string;
description: string;
links: LinkItem[];
date: string; // ISO string
priority: IssuePriority;
team: string; // UUID or name
parent?: string;
// UI only fields
isEvent: boolean;
// For event, we store event form values
event?: EventFormValues;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify missing type and key drift inside the dialog file (read-only).
echo "=== IssueFormValues declaration ==="
sed -n '44,90p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== updateForm key usage ==="
rg -nP 'updateForm\("([^"]+)"' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== Direct formValues accesses for UI-only fields ==="
rg -nP 'formValues\.(details|notes|requiresRoom|isHackathonCritical|needsDesignAssets|needsOutreach)' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== LinkItem declaration check ==="
rg -nP '\b(interface|type)\s+LinkItem\b' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

Repository: KnightHacks/forge

Length of output: 2150


🏁 Script executed:

#!/bin/bash
echo "=== LinkItem import/type in file ==="
head -50 apps/blade/src/app/_components/issues/create-edit-dialog.tsx | grep -E "import|LinkItem"

echo
echo "=== Search for needsDesignAssets usage ==="
rg -n 'needsDesignAssets' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== Delete button around line 854-861 ==="
sed -n '854,870p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== Check STATUS enum from consts ==="
rg -A10 'export.*STATUS|export.*enum.*Status' packages/consts/src/issue.ts

echo
echo "=== Check PRIORITY enum from consts ==="
rg -A10 'export.*PRIORITY|export.*enum.*Priority' packages/consts/src/issue.ts

Repository: KnightHacks/forge

Length of output: 1888


🏁 Script executed:

#!/bin/bash
echo "=== Full imports section (first 80 lines) ==="
sed -n '1,80p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== EventFormValues interface (around 113-127) ==="
sed -n '110,135p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== PRIORITY_OPTIONS definition ==="
rg -B2 -A15 'const PRIORITY_OPTIONS' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== Form submission - what gets sent to API ==="
rg -B5 -A10 'onSubmit\?.*values' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -40

Repository: KnightHacks/forge

Length of output: 3943


🏁 Script executed:

#!/bin/bash
echo "=== Search for LinkItem definition across codebase ==="
rg -n 'interface LinkItem|type LinkItem' --type ts --type tsx

echo
echo "=== Submit handler and form value processing ==="
sed -n '900,950p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== How STATUS_OPTIONS is used in form ==="
rg -B3 -A3 'SelectItem.*value.*confirmed' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -30

Repository: KnightHacks/forge

Length of output: 1538


🏁 Script executed:

#!/bin/bash
echo "=== Search for LinkItem definition (no type filter) ==="
rg -n 'LinkItem' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -5

echo
echo "=== Check if LinkItem is in shared types ==="
find . -path ./node_modules -prune -o -name '*.ts' -o -name '*.tsx' | xargs grep -l 'interface LinkItem\|type LinkItem' 2>/dev/null | head -10

echo
echo "=== Check handleSubmit and formValues usage ==="
rg -B5 -A8 'const handleSubmit|form.handleSubmit' apps/blade/src/app/_components/issues/create-edit-dialog.tsx

echo
echo "=== Check the default/initial formValues structure ==="
rg -B2 -A15 'const getDefaultValues|initialValues' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -50

Repository: KnightHacks/forge

Length of output: 2037


Define LinkItem type and sync IssueFormValues with actual form field usage—currently missing required fields causing TypeScript errors and form data loss.

The interface declares links: LinkItem[] without defining LinkItem, and is missing five fields actively bound in the form: requiresRoom, isHackathonCritical, notes, needsDesignAssets, and needsOutreach. Additionally, the form binds to formValues.details but the interface defines description—the submitted data will lack a description field.

Also, fix enum value mismatches: STATUS_OPTIONS values ("confirmed", "tentative", "draft", "cancelled") don't match the imported ISSUE_STATUS constants ("BACKLOG", "PLANNING", "IN_PROGRESS", "FINISHED"), and PRIORITY_OPTIONS (["High", "Medium", "Low"]) don't match PRIORITY (["LOWEST", "LOW", "MEDIUM", "HIGH", "HIGHEST"]). These mismatches will cause API validation failures at runtime.

Finally, the submit button always shows "Create Event" / "Update Event" regardless of isEvent mode, and the delete-link icon button (line 854–861) lacks an accessible name.

Suggested fixes
+ interface LinkItem {
+   id: string;
+   label: string;
+   url: string;
+ }
+ 
  export interface IssueFormValues {
    status: IssueStatus;
    name: string;
    description: string;
    links: LinkItem[];
    date: string; // ISO string
    priority: IssuePriority;
    team: string; // UUID or name
    parent?: string;
+   requiresRoom: boolean;
+   isHackathonCritical: boolean;
+   notes: string;
+   needsDesignAssets: boolean;
+   needsOutreach: boolean;
    // UI only fields
    isEvent: boolean;
    // For event, we store event form values
    event?: EventFormValues;
  }

At form binding (line 716–719):

- value={formValues.details}
+ value={formValues.description}
  onChange={(event) =>
-   updateForm("details", event.target.value)
+   updateForm("description", event.target.value)
  }

For enums, align STATUS_OPTIONS and PRIORITY_OPTIONS with the imported constants rather than hardcoded strings.

Delete button (line 854–861):

  <Button
    variant="ghost"
    type="button"
    className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
+   aria-label="Remove link"
    onClick={() => handleRemoveLink(link.id)}
  >

Submit button text (line 927):

  {intent === "edit" 
-   ? "Update Event" 
-   : "Create Event"
+   ? formValues.isEvent ? "Update Event" : "Update Issue"
+   : formValues.isEvent ? "Create Event" : "Create Issue"
  }

className="w-full disabled:opacity-40 sm:w-auto"
disabled={isSubmitDisabled}
>
{intent === "edit" ? "Update Event" : "Create Event"}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use task/event-aware submit copy.

Line 927 always renders event text, which is incorrect in task mode.

Suggested patch
-                {intent === "edit" ? "Update Event" : "Create Event"}
+                {intent === "edit"
+                  ? formValues.isEvent
+                    ? "Update Event"
+                    : "Update Task"
+                  : formValues.isEvent
+                    ? "Create Event"
+                    : "Create Task"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` at line 927,
The submit button copy currently uses intent to render "Create Event"/"Update
Event" but doesn't respect task mode; update the text generation in the
CreateEditDialog component to also consider the task vs event flag (e.g., use
isTask, mode, or itemType used in this component) and return "Create
Task"/"Update Task" when in task mode and "Create Event"/"Update Event"
otherwise, keeping the existing intent-based switch for create vs update.

Copy link

@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 (2)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (1)

534-553: ⚠️ Potential issue | 🟠 Major

Use the enum value as the select value.

formValues.priority stores ISSUE.PRIORITY members like "LOWEST", but each SelectItem publishes priority.toLowerCase(). After the first selection, the state contains a value outside IssuePriority, the current option no longer matches, and any upstream enum validation will fail. Keep the value uppercase and only format the label for display.

Suggested patch
 {ISSUE.PRIORITY.map((priority) => (
-  <SelectItem key={priority} value={priority.toLowerCase()}>
-    {priority}
+  <SelectItem key={priority} value={priority}>
+    {getStatusLabel(priority)}
   </SelectItem>
 ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
534 - 553, The Select is storing lowercase strings (priority.toLowerCase())
while formValues.priority expects ISSUE.PRIORITY enum members (e.g., "LOWEST"),
causing mismatches; update the SelectItem value to use the enum member
(priority) instead of priority.toLowerCase(), keep the displayed label formatted
(e.g., render priority.toLowerCase() or humanize it inside the SelectItem
children) and ensure updateForm("priority", value) continues to receive the
uppercase enum value so formValues.priority and any upstream IssuePriority
validation remain consistent.
packages/consts/src/issue.ts (1)

39-50: ⚠️ Potential issue | 🔴 Critical

Sync IssueFormValues with what the dialog actually edits.

CreateEditDialog reads/writes details, notes, requiresRoom, isHackathonCritical, needsDesignAssets, and needsOutreach in apps/blade/src/app/_components/issues/create-edit-dialog.tsx (for example Lines 574-576, 594-596, 616-618, and 718-720), but those keys do not exist here. Because updateForm is keyed off this interface, the component will not type-check, and the current details binding also means the dialog never populates the existing description field. Either add the missing state keys here or rename the component bindings to the existing schema before merge.

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

In `@packages/consts/src/issue.ts` around lines 39 - 50, The IssueFormValues
interface is missing fields that CreateEditDialog reads/writes (details, notes,
requiresRoom, isHackathonCritical, needsDesignAssets, needsOutreach) causing
type errors and incorrect binding of description; update the IssueFormValues
interface to include these properties with appropriate types (e.g., details:
string, notes: string, requiresRoom: boolean, isHackathonCritical: boolean,
needsDesignAssets: boolean, needsOutreach: boolean and keep existing fields like
description) so that updateForm and CreateEditDialog bindings compile and the
dialog correctly populates description, or alternatively change
CreateEditDialog’s bindings to the existing schema (e.g., use description
instead of details) but be sure to update all references in CreateEditDialog and
any callers (including updateForm) to match the chosen schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 53-70: The event form currently leaves required fields tag and
description empty because defaultEventForm sets them but the Event branch UI (in
create-edit-dialog.tsx) never renders inputs for them and handleSubmit forwards
formValues unchanged to onSubmit; update the Event branch to either (a) render
controlled inputs for tag and description and validate them before submission
(modify the JSX where event mode is handled to include these fields and their
handlers) or (b) if you intend defaults, populate formValues.tag and
formValues.description with sensible non-empty values just prior to calling
handleSubmit/onSubmit (update the handleSubmit path that prepares the payload),
and add client-side validation in the submit flow to reject empty
tag/description so the required server schema is satisfied (referencing
defaultEventForm, handleSubmit, and onSubmit in this file).

In `@packages/consts/src/issue.ts`:
- Around line 52-65: EventFormValues currently requires discordId and googleId
even though the dialog never edits them and createEvent omits them; update the
form model by making discordId and googleId optional (discordId?: string;
googleId?: string) or removing them from EventFormValues and defaultEventForm
entirely, and adjust any usages (e.g., defaultEventForm and form initialization)
so they don't fabricate empty strings; also ensure the API shape in createEvent
(packages/api/src/routers/event.ts, createEvent handler) remains consistent with
the revised EventFormValues so there is no mismatch between the form type and
the create payload.

---

Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 534-553: The Select is storing lowercase strings
(priority.toLowerCase()) while formValues.priority expects ISSUE.PRIORITY enum
members (e.g., "LOWEST"), causing mismatches; update the SelectItem value to use
the enum member (priority) instead of priority.toLowerCase(), keep the displayed
label formatted (e.g., render priority.toLowerCase() or humanize it inside the
SelectItem children) and ensure updateForm("priority", value) continues to
receive the uppercase enum value so formValues.priority and any upstream
IssuePriority validation remain consistent.

In `@packages/consts/src/issue.ts`:
- Around line 39-50: The IssueFormValues interface is missing fields that
CreateEditDialog reads/writes (details, notes, requiresRoom,
isHackathonCritical, needsDesignAssets, needsOutreach) causing type errors and
incorrect binding of description; update the IssueFormValues interface to
include these properties with appropriate types (e.g., details: string, notes:
string, requiresRoom: boolean, isHackathonCritical: boolean, needsDesignAssets:
boolean, needsOutreach: boolean and keep existing fields like description) so
that updateForm and CreateEditDialog bindings compile and the dialog correctly
populates description, or alternatively change CreateEditDialog’s bindings to
the existing schema (e.g., use description instead of details) but be sure to
update all references in CreateEditDialog and any callers (including updateForm)
to match the chosen schema.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: 7d55970f-f0d2-4c1d-b439-169511cd1df2

📥 Commits

Reviewing files that changed from the base of the PR and between 11cdf63 and 5229fa8.

📒 Files selected for processing (2)
  • apps/blade/src/app/_components/issues/create-edit-dialog.tsx
  • packages/consts/src/issue.ts

Comment on lines +53 to +70
const defaultEventForm = (): ISSUE.EventFormValues => {
const now = new Date();
const end = new Date(now.getTime() + 60 * 60 * 1000);
return {
discordId: "",
googleId: "",
name: "",
tag: "",
description: "",
startDate: formatDateForInput(now),
startTime: formatTimeForInput(now),
endDate: formatDateForInput(end),
endTime: formatTimeForInput(end),
location: "",
dues_paying: false,
points: undefined,
hackathonId: undefined,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Event mode never collects a complete event payload.

defaultEventForm initializes tag and description, but the event branch only renders name/date/time/location controls, and handleSubmit forwards formValues unchanged. That means a new event can reach onSubmit with event.tag === "", and event.description is never updated anywhere in this component even though the Event schema/API treat both fields as required in packages/db/src/schemas/knight-hacks.ts:165-189 and packages/api/src/routers/event.ts:168-200. Add event-specific inputs for these fields or populate them before calling onSubmit.

As per coding guidelines apps/blade/**: Form validation on both client and server.

Also applies to: 224-231, 370-459

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 53
- 70, The event form currently leaves required fields tag and description empty
because defaultEventForm sets them but the Event branch UI (in
create-edit-dialog.tsx) never renders inputs for them and handleSubmit forwards
formValues unchanged to onSubmit; update the Event branch to either (a) render
controlled inputs for tag and description and validate them before submission
(modify the JSX where event mode is handled to include these fields and their
handlers) or (b) if you intend defaults, populate formValues.tag and
formValues.description with sensible non-empty values just prior to calling
handleSubmit/onSubmit (update the handleSubmit path that prepares the payload),
and add client-side validation in the submit flow to reject empty
tag/description so the required server schema is satisfied (referencing
defaultEventForm, handleSubmit, and onSubmit in this file).

Copy link

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

♻️ Duplicate comments (4)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (4)

692-699: ⚠️ Potential issue | 🟡 Minor

Add aria-label to the delete link button for accessibility.

The button only contains an icon (Trash2). Screen readers will announce "button" without context.

Fix: Add accessible label
 <Button
   variant="ghost"
   type="button"
   className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
   onClick={() => handleRemoveLink(i)}
+  aria-label="Remove link"
 >
   <Trash2 className="h-4 w-4" />
 </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
692 - 699, The delete button that renders the Trash2 icon lacks an accessible
label; update the Button rendered in create-edit-dialog.tsx (the Button with
onClick={() => handleRemoveLink(i)}) to include an aria-label (for example
aria-label={`Remove link ${i + 1}`} or a static "Remove link") so screen readers
receive context about the button's purpose while keeping the existing onClick
and styling.

774-780: ⚠️ Potential issue | 🟡 Minor

Timezone inconsistency between date and time formatting.

formatDateForInput uses toISOString() (UTC), but formatTimeForInput uses toTimeString() (local). Near midnight, this can cause the displayed date to be off by one day.

Example: At 11 PM in UTC-5, toISOString() returns tomorrow's date while toTimeString() returns today's time.

Fix: Use consistent local time formatting
 function formatDateForInput(date: Date) {
-  return date.toISOString().slice(0, 10);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
774 - 780, The date/time formatting is inconsistent: formatDateForInput uses UTC
via toISOString() while formatTimeForInput uses local time via toTimeString(),
which can produce off-by-one-day dates near midnight; update formatDateForInput
to use local date fields (e.g., getFullYear(), getMonth()+1, getDate()) and
zero-pad to produce a "YYYY-MM-DD" string consistent with formatTimeForInput (or
alternatively make both use toLocaleDateString('en-CA') and a local time
formatter) so both functions operate in the same timezone (functions:
formatDateForInput and formatTimeForInput).

758-764: ⚠️ Potential issue | 🟡 Minor

Submit button always shows "Event" text regardless of mode.

Line 763 hardcodes "Event" but should adapt based on formValues.isEvent.

Fix: Use task/event-aware copy
 <Button
   type="submit"
   className="w-full disabled:opacity-40 sm:w-auto"
   disabled={isSubmitDisabled}
 >
-  {intent === "edit" ? "Update Event" : "Create Event"}
+  {intent === "edit"
+    ? formValues.isEvent ? "Update Event" : "Update Task"
+    : formValues.isEvent ? "Create Event" : "Create Task"}
 </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
758 - 764, The submit button currently hardcodes "Event" in its label; update
the Button render (inside create-edit-dialog.tsx) to pick "Task" vs "Event"
based on formValues.isEvent and still respect intent. Replace the current label
expression ({intent === "edit" ? "Update Event" : "Create Event"}) with a
conditional that uses formValues.isEvent to produce "Update Task"/"Create Task"
when false and "Update Event"/"Create Event" when true, referencing the existing
intent and formValues.isEvent variables so the button text adapts to both mode
and type.

71-89: ⚠️ Potential issue | 🟠 Major

Event mode lacks inputs for required tag and description fields.

The defaultEventForm() initializes tag and description to empty strings, but the event UI branch (lines 372-459) doesn't render inputs for these fields. Per EventFormValues, both are required—submitting with empty values will fail server validation.

Why it matters: The API expects non-empty tag and description for events. Users have no way to fill these fields.

Suggested fix: Add inputs for tag and description in event mode

Add these fields inside the event date/time section or in the Details tab when isEvent is true:

+              {formValues.isEvent && (
+                <div className="space-y-4">
+                  <div className="space-y-2">
+                    <Label className="text-sm text-white/70">Event Tag</Label>
+                    <Input
+                      className={cn(baseField, "h-11")}
+                      placeholder="e.g., workshop, social, hackathon"
+                      value={formValues.event?.tag ?? ""}
+                      onChange={(e) => updateEventForm("tag", e.target.value)}
+                    />
+                  </div>
+                  <div className="space-y-2">
+                    <Label className="text-sm text-white/70">Event Description</Label>
+                    <Textarea
+                      className={cn(baseField, "min-h-[100px] resize-none")}
+                      placeholder="Describe the event..."
+                      value={formValues.event?.description ?? ""}
+                      onChange={(e) => updateEventForm("description", e.target.value)}
+                    />
+                  </div>
+                </div>
+              )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 71
- 89, defaultEventForm() sets required EventFormValues.tag and .description to
empty strings but the event UI branch (the isEvent branch in
create-edit-dialog.tsx that renders the event date/time and details) does not
render inputs for these fields; add form inputs for "tag" and "description" to
the event mode UI (either inside the event date/time section or the Details tab
shown when isEvent is true) and wire them to the same form state handlers used
elsewhere (the form field names must match EventFormValues.tag and
EventFormValues.description and use the existing onChange/onBlur or form
register functions so server validation can receive non-empty values on submit).
🧹 Nitpick comments (1)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (1)

677-680: Consider stable keys for link items.

Using array index as key (line 679) can cause React to misassociate DOM state when items are removed mid-list. Since links are simple strings, you could generate stable IDs when adding items.

Why it matters: If a user deletes link 2 of 3, the input for link 3 might retain link 2's focus/cursor state.

Optional: Use stable IDs
- const createLinkItem = (): string => "";
+ interface LinkEntry {
+   id: string;
+   url: string;
+ }
+ const createLinkItem = (): LinkEntry => ({ id: crypto.randomUUID(), url: "" });

Then update the links array type and map by link.id.

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
677 - 680, The map over formValues.links is using the array index as key
(key={i}) which can break React reconciliation; change the links items to
objects with a stable id and value (e.g., {id: string, url: string}) when
creating/initializing links, generate the id in the add-link handler (use
nanoid/uuid or a simple incrementer), update all consumers (the form state
initialization, onChange handler, and remove handler) to read/write link.url and
link.id, and then map using key={link.id} instead of the index so inputs keep
stable identity when items are inserted/removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 547-557: The SelectItem values use priority.toLowerCase(), which
mismatches ISSUE.PRIORITY and formValues.priority (uppercase) and breaks
selection and onValueChange typing; update the SelectItem value to use the
original priority casing (value={priority}) or alternatively normalize values
consistently by converting incoming onValueChange to the enum casing inside the
handler (update the handler used by onValueChange in the create/edit dialog),
ensuring SelectItem, formValues.priority and onValueChange all use the same enum
casing (reference SelectItem, SelectContent, ISSUE.PRIORITY,
formValues.priority, and the onValueChange handler).

---

Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 692-699: The delete button that renders the Trash2 icon lacks an
accessible label; update the Button rendered in create-edit-dialog.tsx (the
Button with onClick={() => handleRemoveLink(i)}) to include an aria-label (for
example aria-label={`Remove link ${i + 1}`} or a static "Remove link") so screen
readers receive context about the button's purpose while keeping the existing
onClick and styling.
- Around line 774-780: The date/time formatting is inconsistent:
formatDateForInput uses UTC via toISOString() while formatTimeForInput uses
local time via toTimeString(), which can produce off-by-one-day dates near
midnight; update formatDateForInput to use local date fields (e.g.,
getFullYear(), getMonth()+1, getDate()) and zero-pad to produce a "YYYY-MM-DD"
string consistent with formatTimeForInput (or alternatively make both use
toLocaleDateString('en-CA') and a local time formatter) so both functions
operate in the same timezone (functions: formatDateForInput and
formatTimeForInput).
- Around line 758-764: The submit button currently hardcodes "Event" in its
label; update the Button render (inside create-edit-dialog.tsx) to pick "Task"
vs "Event" based on formValues.isEvent and still respect intent. Replace the
current label expression ({intent === "edit" ? "Update Event" : "Create Event"})
with a conditional that uses formValues.isEvent to produce "Update Task"/"Create
Task" when false and "Update Event"/"Create Event" when true, referencing the
existing intent and formValues.isEvent variables so the button text adapts to
both mode and type.
- Around line 71-89: defaultEventForm() sets required EventFormValues.tag and
.description to empty strings but the event UI branch (the isEvent branch in
create-edit-dialog.tsx that renders the event date/time and details) does not
render inputs for these fields; add form inputs for "tag" and "description" to
the event mode UI (either inside the event date/time section or the Details tab
shown when isEvent is true) and wire them to the same form state handlers used
elsewhere (the form field names must match EventFormValues.tag and
EventFormValues.description and use the existing onChange/onBlur or form
register functions so server validation can receive non-empty values on submit).

---

Nitpick comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 677-680: The map over formValues.links is using the array index as
key (key={i}) which can break React reconciliation; change the links items to
objects with a stable id and value (e.g., {id: string, url: string}) when
creating/initializing links, generate the id in the add-link handler (use
nanoid/uuid or a simple incrementer), update all consumers (the form state
initialization, onChange handler, and remove handler) to read/write link.url and
link.id, and then map using key={link.id} instead of the index so inputs keep
stable identity when items are inserted/removed.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: e745c88b-6e8c-4626-bbda-a945a5324001

📥 Commits

Reviewing files that changed from the base of the PR and between 5229fa8 and 9a83954.

📒 Files selected for processing (2)
  • apps/blade/src/app/_components/issues/create-edit-dialog.tsx
  • packages/consts/src/issue.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/consts/src/issue.ts

Comment on lines +547 to +557
<SelectContent>
{ISSUE.PRIORITY.map((priority) => (
<SelectItem
key={priority}
value={priority.toLowerCase()}
>
{priority}
</SelectItem>
))}
</SelectContent>
</Select>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Priority select value casing mismatch will break selection state.

SelectItem values are set to priority.toLowerCase() (line 551), but formValues.priority stores uppercase values from ISSUE.PRIORITY. This causes two problems:

  1. The current selection won't display (values don't match)
  2. onValueChange receives lowercase but the type expects uppercase

Fix: Keep values consistent with the enum:

Proposed fix
 <SelectContent>
   {ISSUE.PRIORITY.map((priority) => (
     <SelectItem
       key={priority}
-      value={priority.toLowerCase()}
+      value={priority}
     >
       {priority}
     </SelectItem>
   ))}
 </SelectContent>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
547 - 557, The SelectItem values use priority.toLowerCase(), which mismatches
ISSUE.PRIORITY and formValues.priority (uppercase) and breaks selection and
onValueChange typing; update the SelectItem value to use the original priority
casing (value={priority}) or alternatively normalize values consistently by
converting incoming onValueChange to the enum casing inside the handler (update
the handler used by onValueChange in the create/edit dialog), ensuring
SelectItem, formValues.priority and onValueChange all use the same enum casing
(reference SelectItem, SelectContent, ISSUE.PRIORITY, formValues.priority, and
the onValueChange handler).

Copy link

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

♻️ Duplicate comments (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)

534-551: ⚠️ Potential issue | 🟠 Major

Keep priority option values in enum casing.

formValues.priority stores ISSUE.PRIORITY entries, but each SelectItem lowercases its value. That breaks the selected state and feeds non-enum strings back through onValueChange.

Suggested patch
                             {ISSUE.PRIORITY.map((priority) => (
                               <SelectItem
                                 key={priority}
-                                value={priority.toLowerCase()}
+                                value={priority}
                               >
                                 {priority}
                               </SelectItem>
                             ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
534 - 551, The SelectItems are lowercasing values, causing mismatch with the
stored enum entries in formValues.priority and breaking selection; change each
SelectItem to use the original enum casing (use value={priority} instead of
value={priority.toLowerCase()}) and ensure the Select onValueChange still casts
to (typeof ISSUE.PRIORITY)[number] in updateForm so the component stores and
emits the exact ISSUE.PRIORITY enum values; locate SelectItem, SelectTrigger,
the onValueChange handler, updateForm call, and ISSUE.PRIORITY to make this
change.

560-577: ⚠️ Potential issue | 🟠 Major

Persist the textarea into description.

This field is labeled “Description”, but it only mutates formValues.details. formValues.description stays at the default "", so any downstream code reading the canonical field loses the user’s text.

Suggested patch
-                        value={formValues.details}
+                        value={formValues.description}
                         onChange={(event) =>
-                          updateForm("details", event.target.value)
+                          updateForm("description", event.target.value)
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
560 - 577, The Description textarea is currently bound to formValues.details so
formValues.description remains empty; change the binding and update to use the
canonical field (use value={formValues.description} and call
updateForm("description", event.target.value) in the Textarea's onChange) so the
Label/Textarea pair (identified by baseId, Label, Textarea,
formValues.details/formValues.description, updateForm) persists the user's input
into formValues.description for downstream consumers.

70-87: ⚠️ Potential issue | 🟠 Major

Event mode still submits blank required fields.

tag and description start as "", the event branch never edits them, and the submit gate only checks name before passing formValues through unchanged. Since ISSUE.EventFormValues still requires both fields, create-event flows can pass client-side and still send an invalid payload. Either collect those fields in the event UI or hydrate formValues.event from existing dialog inputs inside handleSubmit before calling onSubmit.

As per coding guidelines apps/blade/**: Form validation on both client and server.

Also applies to: 153-155, 246-253, 370-459

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 70
- 87, The event creation flow allows submitting blank required fields because
defaultEventForm sets tag and description to empty strings and the event branch
never updates them; modify handleSubmit to read the dialog's event-specific
inputs (tag and description) and hydrate formValues.event
(ISSUE.EventFormValues) with those values before calling onSubmit, and ensure
the submit gate validates that formValues.name, formValues.event.tag, and
formValues.event.description are non-empty so invalid payloads are blocked
client-side; reference defaultEventForm, handleSubmit, onSubmit and formValues
when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 184-205: The dialog effect that currently handles scroll locking
and Escape (inside the React.useEffect using previousOverflow and handleKeydown)
must also manage focus for accessibility: capture and store
document.activeElement before opening, move focus into the dialog container when
open, set role="dialog" with aria-modal="true" and a programmatic name via
aria-labelledby or aria-label on the dialog element, ensure the first name input
has an explicit id and its label uses htmlFor so it serves as the dialog's
labelledby target, restore focus to the previously focused element on close (in
the effect cleanup), and add a small keyboard focus trap inside the dialog to
keep Tab/Shift+Tab cycling within the dialog while open; update the dialog
container element and the useEffect cleanup to perform these focus/restore/trap
responsibilities alongside the existing overflow/keydown logic.
- Around line 468-478: The date picker is using new Date(e.target.value) (which
parses YYYY-MM-DD as UTC) and then setHours in local time, causing day shifts;
change the onChange for the Input with id `${baseId}-task-due-date` to parse
e.target.value by splitting into year, month, day and construct the Date via new
Date(year, month-1, day, 23, 0, 0, 0) before calling updateForm("date",
d.toISOString()); also replace the value expression that uses
formValues.date.slice(0,10) with a local-time formatter (or add a helper like
formatDateForInput(formValues.date)) that returns the YYYY-MM-DD string in local
time so the input and stored ISO string remain consistent.
- Around line 130-148: The form is being reset whenever buildInitialFormValues
(which depends on the entire initialValues object) changes, causing unwanted
resets when the parent recreates initialValues; modify the effect that calls
reset to only run when the dialog transitions from closed→open by tracking
previous open state with a ref (e.g., wasOpenRef) instead of depending on
buildInitialFormValues, and if the dialog can switch entities while open also
include a stable entity identifier (like initialValues.id) in the open
transition check; keep buildInitialFormValues as-is for computing values but
remove it from the reset effect dependencies and use the ref-based open
transition to trigger reset.

---

Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 534-551: The SelectItems are lowercasing values, causing mismatch
with the stored enum entries in formValues.priority and breaking selection;
change each SelectItem to use the original enum casing (use value={priority}
instead of value={priority.toLowerCase()}) and ensure the Select onValueChange
still casts to (typeof ISSUE.PRIORITY)[number] in updateForm so the component
stores and emits the exact ISSUE.PRIORITY enum values; locate SelectItem,
SelectTrigger, the onValueChange handler, updateForm call, and ISSUE.PRIORITY to
make this change.
- Around line 560-577: The Description textarea is currently bound to
formValues.details so formValues.description remains empty; change the binding
and update to use the canonical field (use value={formValues.description} and
call updateForm("description", event.target.value) in the Textarea's onChange)
so the Label/Textarea pair (identified by baseId, Label, Textarea,
formValues.details/formValues.description, updateForm) persists the user's input
into formValues.description for downstream consumers.
- Around line 70-87: The event creation flow allows submitting blank required
fields because defaultEventForm sets tag and description to empty strings and
the event branch never updates them; modify handleSubmit to read the dialog's
event-specific inputs (tag and description) and hydrate formValues.event
(ISSUE.EventFormValues) with those values before calling onSubmit, and ensure
the submit gate validates that formValues.name, formValues.event.tag, and
formValues.event.description are non-empty so invalid payloads are blocked
client-side; reference defaultEventForm, handleSubmit, onSubmit and formValues
when making this change.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: 8bfbe032-f578-46ff-883e-d128672e93b5

📥 Commits

Reviewing files that changed from the base of the PR and between 9a83954 and e83876c.

📒 Files selected for processing (1)
  • apps/blade/src/app/_components/issues/create-edit-dialog.tsx

Comment on lines +130 to +148
const buildInitialFormValues = React.useCallback(() => {
const defaults = defaultForm();
if (initialValues?.isEvent) {
return {
...defaults,
...initialValues,
isEvent: true,
event: initialValues.event ?? defaultEventForm(),
links: initialValues?.links ?? defaults.links,
};
}
return {
...defaults,
...initialValues,
isEvent: false,
event: undefined,
links: initialValues?.links ?? defaults.links,
};
}, [initialValues]);
Copy link

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:

fd "create-edit-dialog.tsx" --type f

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | head -200

Repository: KnightHacks/forge

Length of output: 6321


🏁 Script executed:

wc -l "apps/blade/src/app/_components/issues/create-edit-dialog.tsx"

Repository: KnightHacks/forge

Length of output: 125


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '175,220p'

Repository: KnightHacks/forge

Length of output: 1456


🏁 Script executed:

rg "CreateEditDialog" --type tsx -B 2 -A 5 apps/blade/src/app

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

rg "initialValues" apps/blade/src/app/_components/issues/ --type tsx -B 3 -A 3

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

rg "CreateEditDialog" -A 5 apps/blade/src/app/

Repository: KnightHacks/forge

Length of output: 572


🏁 Script executed:

rg "initialValues" apps/blade/src/app/_components/issues/ -B 3 -A 3

Repository: KnightHacks/forge

Length of output: 2688


🏁 Script executed:

fd "page.tsx" apps/blade/src/app | head -20

Repository: KnightHacks/forge

Length of output: 1138


🏁 Script executed:

rg "CreateEditDialog" --type js --type ts -l apps/blade/src/app

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

grep -r "CreateEditDialog" apps/blade/src/app --include="*.tsx" --include="*.ts" -l

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

rg "CreateEditDialog" --type js --type ts -B 2 -A 8

Repository: KnightHacks/forge

Length of output: 1509


🏁 Script executed:

find apps/blade/src -name "*.tsx" -o -name "*.ts" | xargs grep -l "CreateEditDialog" 2>/dev/null

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

rg "import.*CreateEditDialog" -A 5 -B 2

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

find apps/blade -name "*.tsx" -exec grep -l "CreateEditDialog" {} \;

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '1,160p'

Repository: KnightHacks/forge

Length of output: 5227


🏁 Script executed:

rg "issues" apps/blade/src/app --include="*.tsx" -l | head -10

Repository: KnightHacks/forge

Length of output: 502


🏁 Script executed:

find apps/blade/src/app -type f \( -name "*.tsx" -o -name "*.ts" \) | xargs grep -l "issue" | head -10

Repository: KnightHacks/forge

Length of output: 321


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | tail -100

Repository: KnightHacks/forge

Length of output: 4149


🏁 Script executed:

cat -n "apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx" | head -100

Repository: KnightHacks/forge

Length of output: 3761


🏁 Script executed:

rg "initialValues" apps/blade/src/app/_components/dashboard/ -B 3 -A 3

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

grep -r "CreateEditDialog" apps/blade/src/app --include="*.tsx" -B 5 -A 10

Repository: KnightHacks/forge

Length of output: 1365


🏁 Script executed:

find apps/blade -name "*.tsx" -exec grep -l "<CreateEditDialog" {} \;

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

grep -r "CreateEditDialog" . --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v node_modules

Repository: KnightHacks/forge

Length of output: 266


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '140,185p'

Repository: KnightHacks/forge

Length of output: 1441


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '128,182p'

Repository: KnightHacks/forge

Length of output: 1895


🏁 Script executed:

rg "open.*initialValues|initialValues.*open" apps/blade/src/app -B 3 -A 3

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

git log --all --oneline --decorate -- "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | head -10

Repository: KnightHacks/forge

Length of output: 117


🏁 Script executed:

grep -r "CreateEditDialog\|create-edit-dialog" apps/blade --include="*.tsx" -l

Repository: KnightHacks/forge

Length of output: 121


🏁 Script executed:

fd "admin" apps/blade/src/app --type d | xargs grep -l "issue" 2>/dev/null | head -5

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

find apps/blade/src/app -type f -name "*.tsx" | xargs grep -l "issue\|Issue" | head -10

Repository: KnightHacks/forge

Length of output: 619


🏁 Script executed:

rg "from.*create-edit-dialog\|import.*CreateEditDialog" apps/blade --type ts --type tsx

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

find . -name "*.tsx" -o -name "*.ts" | xargs grep "CreateEditDialog" 2>/dev/null

Repository: KnightHacks/forge

Length of output: 266


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '148,182p'

Repository: KnightHacks/forge

Length of output: 1152


🏁 Script executed:

cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '116,150p'

Repository: KnightHacks/forge

Length of output: 1314


🏁 Script executed:

ast-grep --pattern 'const buildInitialFormValues = React.useCallback'

Repository: KnightHacks/forge

Length of output: 43


Reset form only when dialog opens, not when initialValues object identity changes.

Lines 175-182: The reset effect depends on buildInitialFormValues (line 182), which depends on the whole initialValues object (line 148). If the parent recreates that object while the dialog is open—even with the same data—this triggers a form reset and snaps the user back to Details section, wiping in-progress edits.

Use a ref to track the open→closed transition instead:

Suggested patch
+  const previousOpenRef = React.useRef(false);
+
   React.useEffect(() => {
-    if (!open) {
-      return;
+    const justOpened = open && !previousOpenRef.current;
+    if (justOpened) {
+      setFormValues(buildInitialFormValues());
+      setActiveSection("details");
     }
-
-    setFormValues(buildInitialFormValues());
-    setActiveSection("details");
+    previousOpenRef.current = open;
   }, [buildInitialFormValues, open]);

If this dialog can switch between different entities while open, include a stable entity key (e.g., ID) in the condition as well.

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
130 - 148, The form is being reset whenever buildInitialFormValues (which
depends on the entire initialValues object) changes, causing unwanted resets
when the parent recreates initialValues; modify the effect that calls reset to
only run when the dialog transitions from closed→open by tracking previous open
state with a ref (e.g., wasOpenRef) instead of depending on
buildInitialFormValues, and if the dialog can switch entities while open also
include a stable entity identifier (like initialValues.id) in the open
transition check; keep buildInitialFormValues as-is for computing values but
remove it from the reset effect dependencies and use the ref-based open
transition to trigger reset.

Comment on lines +184 to +205
React.useEffect(() => {
if (!open) {
return;
}

const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";

const handleKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
onClose?.();
}
};

window.addEventListener("keydown", handleKeydown);

return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", handleKeydown);
};
}, [open, onClose]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Finish the custom dialog accessibility contract.

This portal locks scrolling and handles Escape, but it still never moves focus into the modal, restores focus on close, or gives role="dialog" a programmatic name. The first name field is also visually labeled only, so assistive tech lands in an unnamed dialog with an unlabeled primary input.

Suggested patch
+  const closeButtonRef = React.useRef<HTMLButtonElement>(null);
+  const previousActiveElementRef = React.useRef<HTMLElement | null>(null);
+
   React.useEffect(() => {
     if (!open) {
       return;
     }
 
+    previousActiveElementRef.current =
+      document.activeElement instanceof HTMLElement
+        ? document.activeElement
+        : null;
     const previousOverflow = document.body.style.overflow;
     document.body.style.overflow = "hidden";
+    closeButtonRef.current?.focus();
 
     const handleKeydown = (event: KeyboardEvent) => {
       if (event.key === "Escape") {
@@
     return () => {
       document.body.style.overflow = previousOverflow;
       window.removeEventListener("keydown", handleKeydown);
+      previousActiveElementRef.current?.focus();
     };
   }, [open, onClose]);
@@
     <div
       aria-modal
       role="dialog"
+      aria-labelledby={`${baseId}-dialog-title`}
       className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-10"
       onMouseDown={handleOverlayPointerDown}
     >
@@
-        <button
+        <button
+          ref={closeButtonRef}
           type="button"
           className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md border border-input text-muted-foreground transition hover:text-foreground"
           onClick={onClose}
@@
-          <h2 className="mt-1 text-lg font-semibold">
+          <h2 id={`${baseId}-dialog-title`} className="mt-1 text-lg font-semibold">
@@
-              <Label className="text-right">
+              <Label htmlFor={`${baseId}-name`} className="text-right">
               {formValues.isEvent ? "Event Name" : "Task Name"}
               </Label>
 
               <Input
+                id={`${baseId}-name`}
                 className={cn(baseField, "col-span-3")}

You’ll still want a small focus trap here so Tab cannot escape behind the overlay.

As per coding guidelines apps/blade/**: Accessibility (alt text, ARIA, semantic HTML).

Also applies to: 266-329

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
184 - 205, The dialog effect that currently handles scroll locking and Escape
(inside the React.useEffect using previousOverflow and handleKeydown) must also
manage focus for accessibility: capture and store document.activeElement before
opening, move focus into the dialog container when open, set role="dialog" with
aria-modal="true" and a programmatic name via aria-labelledby or aria-label on
the dialog element, ensure the first name input has an explicit id and its label
uses htmlFor so it serves as the dialog's labelledby target, restore focus to
the previously focused element on close (in the effect cleanup), and add a small
keyboard focus trap inside the dialog to keep Tab/Shift+Tab cycling within the
dialog while open; update the dialog container element and the useEffect cleanup
to perform these focus/restore/trap responsibilities alongside the existing
overflow/keydown logic.

Comment on lines +468 to +478
<Input
id={`${baseId}-task-due-date`}
type="date"
className={cn(baseField, "col-span-3")}
value={formValues.date.slice(0, 10)}
onChange={(e) => {
// Always set to 11:00 PM
const d = new Date(e.target.value);
d.setHours(23, 0, 0, 0);
updateForm("date", d.toISOString());
}}
Copy link

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:

cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -500 | tail -100

Repository: KnightHacks/forge

Length of output: 4538


🏁 Script executed:

cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '460,490p'

Repository: KnightHacks/forge

Length of output: 1406


🏁 Script executed:

rg "formValues\.date" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 2 -A 2

Repository: KnightHacks/forge

Length of output: 286


🏁 Script executed:

cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '1,100p'

Repository: KnightHacks/forge

Length of output: 3242


🏁 Script executed:

rg "formValues\s*=" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 3 -A 3

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

rg "updateForm" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 1 -A 1 | head -50

Repository: KnightHacks/forge

Length of output: 1589


🏁 Script executed:

rg "formatDateForInput|formatTimeForInput" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 2 -A 5

Repository: KnightHacks/forge

Length of output: 527


🏁 Script executed:

cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '100,150p'

Repository: KnightHacks/forge

Length of output: 1725


🏁 Script executed:

python3 << 'EOF'
# Verify JavaScript Date parsing behavior
from datetime import datetime, timezone

# Demonstrate the issue:
# When you parse "2025-03-15" with new Date() in JavaScript, it's treated as UTC midnight
# When you set local hours to 23 with setHours(23, 0, 0, 0), it depends on timezone

print("=== JavaScript Date Behavior Analysis ===\n")

print("1. String '2025-03-15' parsed with new Date():")
print("   - Treated as UTC midnight: 2025-03-15T00:00:00Z")
print("   - This is timezone-dependent interpretation\n")

print("2. When setHours(23, 0, 0, 0) is called:")
print("   - Sets LOCAL hours to 23")
print("   - In EST (UTC-5): 23:00 local = 04:00 UTC next day")
print("   - In UTC: 23:00 local = 23:00 UTC same day")
print("   - In JST (UTC+9): 23:00 local = 14:00 UTC same day\n")

print("3. The rendering issue:")
print("   - formValues.date.slice(0, 10) extracts UTC date portion")
print("   - But user's input picker shows local calendar")
print("   - Mismatch: stored as UTC, displayed as if it were local\n")

print("Example scenario (EST timezone):")
print("- User wants to set task for March 15")
print("- Stored value: 2025-03-16T04:00:00Z (11 PM EST = 4 AM UTC)")
print("- Displayed: 2025-03-16 (wrong day!)")
print("- When edited, the day can shift again")
EOF

Repository: KnightHacks/forge

Length of output: 875


🏁 Script executed:

rg "getFullYear|getMonth|getDate" apps/blade/src --type ts --type tsx -l

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

cat CONTRIBUTING.md 2>/dev/null | grep -i "date\|time\|timezone" -A 3 -B 1 || echo "No date/time guidance in CONTRIBUTING.md"

Repository: KnightHacks/forge

Length of output: 364


🏁 Script executed:

rg "date-fns|dayjs|luxon" apps/blade/package.json

Repository: KnightHacks/forge

Length of output: 43


Fix date picker to work with local calendar, not UTC.

Parsing YYYY-MM-DD with new Date() treats it as UTC midnight, but setHours(23) operates in local time. This creates a day shift: in EST, setting a task for March 15 at 11 PM stores it as March 16, 4 AM UTC—then displays "2025-03-16" when read back via slice(0, 10).

Instead, construct the Date from local year/month/day components and format the input consistently in local time:

Suggested fix
onChange={(e) => {
  const [year, month, day] = e.target.value.split('-').map(Number);
  const d = new Date(year, month - 1, day, 23, 0, 0, 0);
  updateForm("date", d.toISOString());
}}

And update the value to use a local date formatter:

value={formValues.date.split('T')[0].split('-').length === 3 
  ? new Date(formValues.date).toLocaleDateString('sv-SE')
  : ''}

Or create a helper similar to formatDateForInput() that works bidirectionally with local dates.

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

In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
468 - 478, The date picker is using new Date(e.target.value) (which parses
YYYY-MM-DD as UTC) and then setHours in local time, causing day shifts; change
the onChange for the Input with id `${baseId}-task-due-date` to parse
e.target.value by splitting into year, month, day and construct the Date via new
Date(year, month-1, day, 23, 0, 0, 0) before calling updateForm("date",
d.toISOString()); also replace the value expression that uses
formValues.date.slice(0,10) with a local-time formatter (or add a helper like
formatDateForInput(formValues.date)) that returns the YYYY-MM-DD string in local
time so the input and stored ISO string remain consistent.

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