Skip to content

Index based datesheet mapping#153

Merged
akshat-OwO merged 2 commits intomainfrom
datesheet-issue
Sep 22, 2025
Merged

Index based datesheet mapping#153
akshat-OwO merged 2 commits intomainfrom
datesheet-issue

Conversation

@ShouryaPal
Copy link
Collaborator

@ShouryaPal ShouryaPal commented Sep 22, 2025

Closing #152 issue

Summary by CodeRabbit

  • Refactor

    • Standardized a DateEntry type across hooks, timeline, and modal; renderItem now receives (date, index). Add/edit/remove APIs updated to operate by index and dates are sorted after changes/imports.
  • Bug Fixes

    • Fixed unstable list rendering and popover targeting by using composite unique IDs; ensured edits/deletes target the correct entry via index.
  • Style

    • “All Datesheets” button changed to full-width and redundant padding removed.

@changeset-bot
Copy link

changeset-bot bot commented Sep 22, 2025

🦋 Changeset detected

Latest commit: bfdb0ff

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
syllabusx-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Public API and internal types were unified around a new exported DateEntry ({ name: string; date: number }). The dates hook switched to index-based edit/remove and always sorts after mutations. Components (timeline and modal) updated props/signatures to accept DateEntry[], pass indices to renderers, and use composite string keys including index.

Changes

Cohort / File(s) Summary of Changes
Store & Type definition
src/hooks/use-datesheet.ts
Added exported DateEntry type. dates is now DateEntry[]. addDate appends then sorts; editDate and removeDate accept an index and operate by index, then sort. importDatesheet accepts list and sorts. Public store typings updated.
Modal: form, list, and popover behavior
src/components/modals/datesheet-modal.tsx
Import DateEntry. DatesheetModal.Form now accepts currentDate?: DateEntry and currentIndex?: number; edit/delete call editDate(index, ...) / removeDate(index). DatesheetModal.Dates accepts DateEntry[]; renderItem signature updated to (date, index) and uses composite uniqueId = \${date.date}-${date.name}-${index}`as key;openPopoverIdtype changed tostring
Timeline component prop/signature alignment
src/components/DatesheetTimeline.tsx
Import DateEntry. Component props updated: dates: DateEntry[], renderItem: (date: DateEntry, index: number) => ReactNode. Rendering uses composite key ${d.date}-${d.name}-${index} and calls renderItem(d, index).
Changeset metadata
.changeset/cold-pianos-carry.md
Added a changeset entry noting a patch with description "Fix datesheet issue". No code/API changes in the changeset file itself.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Modal as DatesheetModal
  participant Timeline as DatesheetTimeline
  participant Store as useDatesheet

  rect rgba(230,245,255,0.9)
    note right of Store: Exposes DateEntry[], addDate, editDate(index,...), removeDate(index)
  end

  User->>Modal: Open modal (reads dates)
  Modal->>Store: get dates (DateEntry[])
  Modal->>Modal: render items with uniqueId `${date.date}-${date.name}-${index}`
  User->>Modal: Open item -> Modal passes currentDate & currentIndex to Form

  alt Add
    User->>Modal: Submit new DateEntry
    Modal->>Store: addDate(entry)
    Store->>Store: append & sort by date
  else Edit
    User->>Modal: Submit edited DateEntry
    Modal->>Store: editDate(index, entry)
    Store->>Store: replace at index & sort
  else Delete
    User->>Modal: Delete entry
    Modal->>Store: removeDate(index)
    Store->>Store: remove at index & sort
  end

  Store-->>Modal: updated dates
  Store-->>Timeline: updated dates consumed
  Timeline->>Timeline: renderItem(date, index) with composite key
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hopped through dates in tidy rows,
Keys braided from time and name and toes.
I sorted moments, nudged each one in place—
A nibble of index, a carrot-clocked trace.
Hooray for DateEntry: neat, swift, and grace! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description "Closing #152 issue" is a single-line reference and does not follow the repository's required template; it omits the checklist, the "Type of change" section, a detailed summary of the changes, and any notes about breaking changes or required documentation updates. Because the template-required information is missing I cannot assess scope, risk, or whether the PR meets contribution guidelines, so the description check fails. Update the PR description to use the repository template: include the checklist, choose the appropriate "Type of change", provide a clear summary of code and behavioral changes (noting breaking API/typing changes), and add "closes #152" if this PR should auto-close the referenced issue.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: change openPopover action in datesheet timeline" accurately references a real change in the changeset (popover openId handling and uniqueId generation in the Datesheet timeline). However, the PR also includes broader API and typing changes (introduction of DateEntry, changes to renderItem signatures, and updates to the use-datesheet store) that the title does not reflect, so it is narrower than the full scope. Because it is related to an actual part of the changeset but omits other significant changes, the title is acceptable but could be more descriptive of the overall impact.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd66b33 and bfdb0ff.

📒 Files selected for processing (1)
  • .changeset/cold-pianos-carry.md (1 hunks)

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.

@akshat-OwO akshat-OwO changed the title feat: change openPopover action in datesheet timeline Index based datesheet mapping Sep 22, 2025
@akshat-OwO akshat-OwO merged commit dba3267 into main Sep 22, 2025
2 checks passed
@akshat-OwO akshat-OwO deleted the datesheet-issue branch September 22, 2025 18:20
@github-actions github-actions bot mentioned this pull request Sep 22, 2025
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: 4

🧹 Nitpick comments (12)
src/hooks/use-datesheet.ts (4)

4-7: Prefer stable IDs over index-based identity

Index-based edit/remove is brittle under re-sorting and concurrent mutations. Add a stable id on DateEntry to key items and target edits/removals by id.

 export type DateEntry = {
+   id: string;
    name: string;
    date: number;
 };

Outside this hunk (for context only):

  • Generate id on add/import: { id: crypto.randomUUID(), ...date }.
  • Switch edit/remove signatures to accept id: string instead of index: number.

33-35: Sorting after append is fine; duplicates now allowed—confirm intent

Previous uniqueness checks are gone; identical name/date pairs will be stored. If that’s not desired, add a dedupe guard.


38-45: Guard against out-of-bounds index in editDate

Prevent sparse arrays or silent no-ops when index is invalid.

 editDate: (index, newDate) => {
   set((state) => {
     const newDates = [...state.dates];
-    newDates[index] = newDate;
+    if (index < 0 || index >= newDates.length) return { dates: newDates };
+    newDates[index] = newDate;
     newDates.sort((a, b) => a.date - b.date);
     return { dates: newDates };
   });
 },

46-51: Guard against out-of-bounds index in removeDate

Avoid accidental negative-index splices and clarify behavior.

 removeDate: (index) => {
   set((state) => {
     const newDates = [...state.dates];
-    newDates.splice(index, 1);
+    if (index >= 0 && index < newDates.length) {
+      newDates.splice(index, 1);
+    }
     return { dates: newDates };
   });
 },
src/components/DatesheetTimeline.tsx (2)

33-38: Compute “now” once per render

Minor perf/consistency: reuse a single timestamp to avoid tiny intra-render mismatches.

 export const DatesheetTimeline: React.FC<DatesheetTimelineProps> = ({
   dates,
   interactive = false,
   renderItem,
 }) => {
-  return (
+  const now = Date.now();
+  return (
 ...
-  "bg-primary": d.date > new Date().getTime(),
+  "bg-primary": d.date > now,

(Apply the same replacement to other occurrences in this component.)

Also applies to: 41-45, 51-55, 63-67


89-95: Typo: text-muted-foreground

Fix class name to apply the intended color.

-<span className="text-muted-foregound text-xs font-medium">
+<span className="text-muted-foreground text-xs font-medium">
src/components/modals/datesheet-modal.tsx (6)

150-157: Avoid coupling to list index for edits

Passing both currentDate and currentIndex can desync after re-sorts. Prefer targeting by stable id on DateEntry.


175-181: Index guard in edit submit

If index is ever undefined, no-op silently. Consider an else path to surface an error or disable Edit when index is absent.


256-263: Ensure Delete button does not submit the form

Explicitly set type="button".

-<Button
+<Button
+  type="button"
   variant="outline"
   className="flex-1"
   onClick={() => handleDelete()}
>

314-319: Typo: text-muted-foreground

Fix class name so the date subtitle has the right color.

-<span className="text-muted-foregound text-xs font-medium">
+<span className="text-muted-foreground text-xs font-medium">

281-283: Open-popover state and re-sorts

Because openPopoverId is tied to a value including index, any list mutation/re-sort will close the popover unexpectedly. Prefer a stable id for both key and openPopoverId.

Also applies to: 289-297


107-145: Optional: wire Sheet onOpenChange to the boolean value

Current handler always closes. If Sheet can be opened from the UI, consider accepting the boolean.

-<Sheet open={isOpen} onOpenChange={onClose}>
+<Sheet open={isOpen} onOpenChange={(open) => (open ? undefined : onClose())}>

Or expose setOpen(open: boolean) in the store and pass it directly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a13929 and bd66b33.

📒 Files selected for processing (3)
  • src/components/DatesheetTimeline.tsx (3 hunks)
  • src/components/modals/datesheet-modal.tsx (8 hunks)
  • src/hooks/use-datesheet.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/modals/datesheet-modal.tsx (3)
src/hooks/use-datesheet.ts (1)
  • DateEntry (4-7)
src/components/DatesheetTimeline.tsx (1)
  • DatesheetTimeline (16-110)
src/lib/utils.ts (1)
  • cn (8-10)
src/components/DatesheetTimeline.tsx (1)
src/hooks/use-datesheet.ts (1)
  • DateEntry (4-7)
🔇 Additional comments (2)
src/components/modals/datesheet-modal.tsx (2)

88-91: LGTM: button layout tweak

Moving width styling to the Button simplifies the layout.


1-347: Fix typo: replace "text-muted-foregound" → "text-muted-foreground"

Found in:

  • src/components/DatesheetTimeline.tsx:89
  • src/components/modals/datesheet-modal.tsx:314

Repo-wide scans for key={index} usages and div-as-trigger checks failed due to ripgrep errors (unrecognized file type "tsx" and PCRE2 pattern error) — re-run those checks to verify there are no problematic keys or trigger elements.

Comment on lines +25 to +28
<div
key={`${d.date}-${d.name}-${index}`}
className="flex items-center"
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid index in React keys

Including index makes keys unstable across re-ordering and can remount items unexpectedly. Use a stable id (preferred) or fall back to content-only composite if ids are not yet available.

-  <div
-    key={`${d.date}-${d.name}-${index}`}
-    className="flex items-center"
-  >
+  <div
+    key={`${d.date}-${d.name}`}
+    className="flex items-center"
+  >

Best: key={d.id} once DateEntry carries an id.

📝 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
<div
key={`${d.date}-${d.name}-${index}`}
className="flex items-center"
>
<div
key={`${d.date}-${d.name}`}
className="flex items-center"
>
🤖 Prompt for AI Agents
In src/components/DatesheetTimeline.tsx around lines 25 to 28, the key uses the
array index which can cause unstable reconciliation; replace it with a stable
identifier: modify the DateEntry data shape to include an id and use key={d.id}.
If adding id isn't yet possible, use a stable composite that doesn't include the
index such as key={`${d.date}-${d.name}`} to avoid index-based keys.

Comment on lines +160 to 163
const [currDate, setCurrDate] = useState<DateEntry | undefined>(
currentDate
);
const form = useForm<TDatesheetSchema>({
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove unused currDate state; rely on react-hook-form

currDate isn’t read and the DateTimePicker’s setCurrDate does nothing user-visible. Simplify and avoid stale state.

-const [currDate, setCurrDate] = useState<DateEntry | undefined>(currentDate);
 const form = useForm<TDatesheetSchema>({
   resolver: zodResolver(datesheetSchema),
   defaultValues: {
-    name: currDate?.name || "",
-    date: currDate?.date ? new Date(currDate.date) : new Date(),
+    name: currentDate?.name || "",
+    date: currentDate?.date ? new Date(currentDate.date) : new Date(),
   },
 });
 ...
-<DateTimePicker
+<DateTimePicker
   hourCycle={12}
   value={field.value}
-  onChange={(e) => {
-    setCurrDate((prev) => {
-      if (prev) {
-        const timestamp = e?.getTime();
-        if (timestamp) {
-          return { ...prev, date: timestamp };
-        }
-      }
-      return prev;
-    });
-    field.onChange(e);
-  }}
+  onChange={field.onChange}
 />

Also applies to: 166-168, 220-239

🤖 Prompt for AI Agents
In src/components/modals/datesheet-modal.tsx around lines 160-163 (and also
touching 166-168 and 220-239), remove the unused currDate useState and any
setCurrDate calls and instead rely on react-hook-form for the date value;
initialize the form with currentDate as the defaultValue for the date field,
replace any local state updates with form.setValue or the Controller on the
DateTimePicker so the picker reads/writes the RHF field directly, and remove
related unused variables and handlers so there is no stale duplicate state.

Comment on lines +289 to +340
renderItem={(date, index) => {
const uniqueId = `${date.date}-${date.name}-${index}`;
return (
<Popover
key={uniqueId}
open={openPopoverId === uniqueId}
onOpenChange={(isOpen) =>
setOpenPopoverId(isOpen ? uniqueId : null)
}
>
<PopoverTrigger asChild>
<div
className={cn(
"group h-fit w-full whitespace-normal rounded-md border py-1.5 pl-3 pr-2",
{
"cursor-pointer transition-colors hover:bg-secondary/50":
true,
}
)}
>
<div className="flex flex-1 items-center justify-between">
<div className="flex flex-col">
<p className="line-clamp-1 text-sm font-semibold">
{date.name}
</p>
<span className="text-muted-foregound text-xs font-medium">
{format(
new Date(date.date),
"do MMMM yyyy hh:mm aaa"
)}
</span>
</div>
<div className="relative inline-flex items-center justify-center rounded-md border border-input p-1.5">
<CalendarIcon className="h-8 w-8" />
<span className="pointer-events-none absolute bottom-2.5 left-1/2 -translate-x-1/2 text-xs font-semibold">
{new Date(date.date).getDate()}
</span>
</div>
</div>
</div>
</div>
</PopoverTrigger>
<PopoverContent align="start">
<DatesheetModal.Form
isEditForm
currentDate={{
name: date.name,
date: date.date,
}}
onFormAction={() => setOpenPopoverId(null)}
/>
</PopoverContent>
</Popover>
)}
</PopoverTrigger>
<PopoverContent align="start">
<DatesheetModal.Form
isEditForm
currentDate={date}
currentIndex={index}
onFormAction={() => setOpenPopoverId(null)}
/>
</PopoverContent>
</Popover>
);
}}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Popover trigger should be an interactive element; stabilize identity

  • Accessibility: Using a div as PopoverTrigger breaks keyboard navigation. Use a button or add appropriate role/tabIndex/keypress handlers.
  • Identity: uniqueId includes index; it will change after re-sorts, breaking open state. Prefer a stable id.
-<PopoverTrigger asChild>
-  <div
+<PopoverTrigger asChild>
+  <button
+    type="button"
     className={cn(
       "group h-fit w-full whitespace-normal rounded-md border py-1.5 pl-3 pr-2",
       { "cursor-pointer transition-colors hover:bg-secondary/50": true }
     )}
+    aria-haspopup="dialog"
   >
 ...
-  </div>
+  </button>
 </PopoverTrigger>

And, if/when DateEntry has id:

-const uniqueId = `${date.date}-${date.name}-${index}`;
+const uniqueId = date.id;
📝 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
renderItem={(date, index) => {
const uniqueId = `${date.date}-${date.name}-${index}`;
return (
<Popover
key={uniqueId}
open={openPopoverId === uniqueId}
onOpenChange={(isOpen) =>
setOpenPopoverId(isOpen ? uniqueId : null)
}
>
<PopoverTrigger asChild>
<div
className={cn(
"group h-fit w-full whitespace-normal rounded-md border py-1.5 pl-3 pr-2",
{
"cursor-pointer transition-colors hover:bg-secondary/50":
true,
}
)}
>
<div className="flex flex-1 items-center justify-between">
<div className="flex flex-col">
<p className="line-clamp-1 text-sm font-semibold">
{date.name}
</p>
<span className="text-muted-foregound text-xs font-medium">
{format(
new Date(date.date),
"do MMMM yyyy hh:mm aaa"
)}
</span>
</div>
<div className="relative inline-flex items-center justify-center rounded-md border border-input p-1.5">
<CalendarIcon className="h-8 w-8" />
<span className="pointer-events-none absolute bottom-2.5 left-1/2 -translate-x-1/2 text-xs font-semibold">
{new Date(date.date).getDate()}
</span>
</div>
</div>
</div>
</div>
</PopoverTrigger>
<PopoverContent align="start">
<DatesheetModal.Form
isEditForm
currentDate={{
name: date.name,
date: date.date,
}}
onFormAction={() => setOpenPopoverId(null)}
/>
</PopoverContent>
</Popover>
)}
</PopoverTrigger>
<PopoverContent align="start">
<DatesheetModal.Form
isEditForm
currentDate={date}
currentIndex={index}
onFormAction={() => setOpenPopoverId(null)}
/>
</PopoverContent>
</Popover>
);
}}
renderItem={(date, index) => {
const uniqueId = date.id;
return (
<Popover
key={uniqueId}
open={openPopoverId === uniqueId}
onOpenChange={(isOpen) =>
setOpenPopoverId(isOpen ? uniqueId : null)
}
>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"group h-fit w-full whitespace-normal rounded-md border py-1.5 pl-3 pr-2",
{ "cursor-pointer transition-colors hover:bg-secondary/50": true }
)}
aria-haspopup="dialog"
>
<div className="flex flex-1 items-center justify-between">
<div className="flex flex-col">
<p className="line-clamp-1 text-sm font-semibold">
{date.name}
</p>
<span className="text-muted-foregound text-xs font-medium">
{format(
new Date(date.date),
"do MMMM yyyy hh:mm aaa"
)}
</span>
</div>
<div className="relative inline-flex items-center justify-center rounded-md border border-input p-1.5">
<CalendarIcon className="h-8 w-8" />
<span className="pointer-events-none absolute bottom-2.5 left-1/2 -translate-x-1/2 text-xs font-semibold">
{new Date(date.date).getDate()}
</span>
</div>
</div>
</button>
</PopoverTrigger>
<PopoverContent align="start">
<DatesheetModal.Form
isEditForm
currentDate={date}
currentIndex={index}
onFormAction={() => setOpenPopoverId(null)}
/>
</PopoverContent>
</Popover>
);
}}
🤖 Prompt for AI Agents
In src/components/modals/datesheet-modal.tsx around lines 289 to 340, the
PopoverTrigger currently uses a div and the uniqueId includes the mutable index;
replace the div trigger with a real interactive element (a <button
type="button">) preserving the same classes and asChild usage so keyboard
focus/activation works, and make the stable id/key use a persistent identifier
(prefer date.id if available, otherwise use a deterministic composite like
`${date.date}-${date.name}` without the index); update key, openPopoverId
assignment/comparison, and setOpenPopoverId to use that stableId so open state
doesn't break on reorders.

Comment on lines +15 to 20
dates: DateEntry[];
addDate: (date: { name: string; date: number }) => void;
editDate: (
date: { name: string; date: number },
newDate: { name: string; date: number }
) => void;
removeDate: (date: { name: string; date: number }) => void;
editDate: (index: number, newDate: { name: string; date: number }) => void;
removeDate: (index: number) => void;
importDatesheet: (dates: Array<{ name: string; date: number }>) => void;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unify function signatures on DateEntry

Use the exported DateEntry type consistently; also prefer DateEntry[] for import.

   dates: DateEntry[];
-  addDate: (date: { name: string; date: number }) => void;
-  editDate: (index: number, newDate: { name: string; date: number }) => void;
-  removeDate: (index: number) => void;
-  importDatesheet: (dates: Array<{ name: string; date: number }>) => void;
+  addDate: (date: DateEntry | Omit<DateEntry, "id">) => void;
+  editDate: (index: number, newDate: DateEntry | Omit<DateEntry, "id">) => void;
+  removeDate: (index: number) => void;
+  importDatesheet: (dates: DateEntry[]) => void;

If you add ids as suggested above, change edit/remove to (id: string, newDate: DateEntry) and (id: string).

📝 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
dates: DateEntry[];
addDate: (date: { name: string; date: number }) => void;
editDate: (
date: { name: string; date: number },
newDate: { name: string; date: number }
) => void;
removeDate: (date: { name: string; date: number }) => void;
editDate: (index: number, newDate: { name: string; date: number }) => void;
removeDate: (index: number) => void;
importDatesheet: (dates: Array<{ name: string; date: number }>) => void;
};
dates: DateEntry[];
addDate: (date: DateEntry | Omit<DateEntry, "id">) => void;
editDate: (index: number, newDate: DateEntry | Omit<DateEntry, "id">) => void;
removeDate: (index: number) => void;
importDatesheet: (dates: DateEntry[]) => void;
};
🤖 Prompt for AI Agents
In src/hooks/use-datesheet.ts around lines 15 to 20, the exported DateEntry type
should be used consistently in all function signatures: change addDate to accept
a DateEntry, change importDatesheet to accept DateEntry[] instead of
Array<{...}>, and (if you add ids to DateEntry as suggested) change editDate and
removeDate to use the id signature: editDate(id: string, newDate: DateEntry) and
removeDate(id: string). Update any call sites and types accordingly to match
these signatures.

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.

2 participants