Skip to content

Comments

feat: Add keyboard shortcuts, export CSV, and last-updated indicator#1

Open
nikman21 wants to merge 1 commit intomainfrom
feature/keyboard-shortcuts-export
Open

feat: Add keyboard shortcuts, export CSV, and last-updated indicator#1
nikman21 wants to merge 1 commit intomainfrom
feature/keyboard-shortcuts-export

Conversation

@nikman21
Copy link
Contributor

@nikman21 nikman21 commented Feb 3, 2026

What I Built

Added productivity enhancements to Mission Control to help power users navigate and manage items faster:

Features Added

  1. Keyboard Navigation

    • j / Arrow Down - Move to next item
    • k / Arrow Up - Move to previous item
    • Enter - View item details
    • n - Open add new item modal
    • Esc - Close modals/drawers
    • ? - Show keyboard shortcuts help
  2. CSV Export

    • Export all filtered items to CSV
    • Includes all fields: title, description, category, status, dates, notes, link
    • Named with current date for easy organization
  3. Last Updated Indicator

    • Shows timestamp of last data refresh
    • Visual confirmation that polling is working
  4. Visual Selection

    • Highlighted ring around keyboard-selected item
    • Auto-scroll to keep selected item in view

How to Test

  1. Start the dev server: npm run dev
  2. Open http://localhost:3000
  3. Try keyboard navigation:
    • Press j/k or arrow keys to move between items
    • Press Enter to view item details
    • Press n to open the add modal
    • Press ? to see the shortcuts help
  4. Click the Export button to download items as CSV

Why It Matters

  • Keyboard shortcuts reduce mouse dependency for power users
  • CSV export enables backup and external analysis
  • Last updated indicator provides confidence in data freshness
  • These are common patterns in productivity tools that Mission Control users would expect

Built during Nightly Build Session (Feb 3, 2026)

Summary by CodeRabbit

  • New Features
    • Added CSV export to download items with timestamped filenames.
    • Introduced keyboard navigation shortcuts (j/k for up/down, Enter to view, n to add, Escape to close, ? for help).
    • Added visual highlighting to indicate the currently selected item.
    • Keyboard Shortcuts help modal showing all available keybindings.
    • Auto-scrolling to the selected item when navigating.
    • Last updated timestamp now visible in the header.

- Add keyboard navigation (j/k, arrows, Enter to view, n to add)
- Add keyboard shortcuts help modal (press ? to view)
- Add CSV export functionality for mission items
- Add last-updated timestamp with auto-refresh indicator
- Visual highlight for keyboard-selected items
@nikman21
Copy link
Contributor Author

nikman21 commented Feb 3, 2026

Summary

I built productivity enhancements for Mission Control that help power users navigate and manage items without reaching for the mouse.

Changes Made

File modified: src/app/page.tsx

New features:

  1. Keyboard navigation system (j/k, arrows, Enter, n, Esc, ?)
  2. CSV export functionality with date-stamped filename
  3. Last-updated timestamp display
  4. Visual highlighting for keyboard-selected items
  5. Keyboard shortcuts help modal

Technical Details

  • Used React useRef for item element references
  • Implemented scroll-into-view for keyboard navigation
  • CSV export uses native Blob API (no external dependencies)
  • All shortcuts work only when not typing in inputs
  • Visual ring indicator shows current keyboard selection

Testing Checklist

  • Keyboard navigation moves between items correctly
  • Selected item has visual ring highlight
  • Enter opens item detail drawer
  • n opens add item modal
  • Esc closes modal/drawer
  • ? opens keyboard help modal
  • Export button downloads CSV with correct data
  • Last updated timestamp shows current time after refresh

Build Status

✅ Build passes: npm run build completes successfully

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Added client-side interactivity to the page component with keyboard navigation (j/k/Enter/n/Escape/?), CSV export functionality, auto-scrolling to selected items, item selection highlighting, and a keyboard shortcuts modal displaying available shortcuts.

Changes

Cohort / File(s) Summary
Enhanced Page Component
src/app/page.tsx
Added useRef and useState hooks for item selection, keyboard navigation, and modal management. Implemented keyboard event handler for navigation and shortcuts (j/k/Enter/n/Escape/?). Added CSV export with date-stamped filename. Integrated auto-scroll on selection, visual item highlighting, "Last updated" timestamp display, and Keyboard Shortcuts modal with help documentation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With keyboard hops (j and k), we leap and bound,
CSV exports and modals all around,
Selection rings shine, shortcuts guide the way,
This rabbit's page dances brighter today! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the three main features added: keyboard shortcuts, CSV export, and last-updated indicator, matching the primary changes in the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/keyboard-shortcuts-export

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d5a633448

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +204 to +207
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;

Choose a reason for hiding this comment

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

P2 Badge Ignore keyboard shortcuts on interactive elements

The global keydown handler only exempts inputs and textareas, so shortcuts still fire when focus is on other interactive controls (e.g., the status <select>, buttons, or links). This prevents expected keyboard behavior like using Arrow keys inside the filter select or pressing Enter on the “Add Item” button, and instead moves the selection or opens a drawer. Consider skipping shortcuts when e.target is a select, button, a, or contentEditable, or when closest finds an interactive element.

Useful? React with 👍 / 👎.

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

🤖 Fix all issues with AI agents
In `@src/app/page.tsx`:
- Around line 234-238: The Escape key handler currently prevents default and
closes selected item/help but does not close the Add Item modal; update the
"Escape" case in the keyboard handler to also reset the add-modal state by
calling the modal setter (e.g., setShowAddModal(false)) alongside
setSelectedItem(null) and setKeyboardHelpOpen(false) so pressing Esc closes the
Add Item modal as well.
- Around line 203-208: The keyboard shortcut handler inside useEffect
(handleKeyDown) currently only ignores HTMLInputElement and HTMLTextAreaElement
and thus blocks normal interactions with <select> and contenteditable elements;
update the guard to also return early when e.target is an HTMLSelectElement or
when the event target (cast to HTMLElement) has isContentEditable === true (or
is inside a contenteditable) so arrow/Enter keys are not intercepted while
interacting with selects or contenteditable regions; modify the guard in
handleKeyDown to include these checks (use instanceof HTMLSelectElement and
(e.target as HTMLElement)?.isContentEditable or a closest('[contenteditable]')
check).
- Around line 509-511: Update the modal copy to match actual behavior: replace
the <p> element whose className is "mt-4 text-xs text-muted-foreground
text-center" (currently containing "Press any key to dismiss") with text that
accurately describes how to close the modal, e.g. "Press Esc to dismiss" (or
"Press Esc or click outside to dismiss" if you plan to add click-to-dismiss).
Ensure you only change the string content in the JSX where that <p> is rendered
(in the modal markup) so the UI copy matches the implemented Esc key handler.
- Around line 176-193: The CSV export in handleExportCSV currently quotes cells
but does not mitigate Excel formula injection; update the rows/CSV building so
any cell value that is a string and begins with one of the dangerous characters
(=, +, -, @) is prefixed (e.g., with a single quote) before being quoted.
Concretely, in handleExportCSV (where rows are created and csvContent is built)
add a small sanitizer that checks each cell (String(cell)) and, if it matches
the regex /^[=+\-@]/, prepends a safe prefix (such as "'") and then continues to
escape internal quotes and join — apply this sanitizer in the rows.map or right
before mapping row.map(...) so all exported cells are protected.
🧹 Nitpick comments (1)
src/app/page.tsx (1)

195-199: Revoke the object URL after download.

URL.createObjectURL is never revoked, which can leak memory on repeated exports.

♻️ Suggested fix
-    const link = document.createElement("a");
-    link.href = URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    const url = URL.createObjectURL(blob);
+    link.href = url;
     link.download = `mission-items-${new Date().toISOString().split("T")[0]}.csv`;
     link.click();
+    setTimeout(() => URL.revokeObjectURL(url), 0);

Comment on lines +176 to +193
// Export to CSV
const handleExportCSV = () => {
const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"];
const rows = filteredItems.map((item) => [
item.title,
item.description || "",
item.category,
item.status,
item.created,
item.updated,
item.notes || "",
item.link || "",
]);

const csvContent = [
headers.join(","),
...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")),
].join("\n");
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

Mitigate CSV formula injection.

User-supplied fields can start with =, +, -, or @, which may execute formulas when opened in Excel/Sheets. Prefix such cells before quoting.

🛡️ Suggested fix
   const handleExportCSV = () => {
+    const escapeCell = (cell: string) => {
+      const value = String(cell ?? "");
+      const safe = /^[=+\-@]/.test(value) ? `'${value}` : value;
+      return `"${safe.replace(/"/g, '""')}"`;
+    };
     const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"];
     const rows = filteredItems.map((item) => [
       item.title,
       item.description || "",
       item.category,
       item.status,
       item.created,
       item.updated,
       item.notes || "",
       item.link || "",
     ]);

     const csvContent = [
       headers.join(","),
-      ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")),
+      ...rows.map((row) => row.map(escapeCell).join(",")),
     ].join("\n");
📝 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
// Export to CSV
const handleExportCSV = () => {
const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"];
const rows = filteredItems.map((item) => [
item.title,
item.description || "",
item.category,
item.status,
item.created,
item.updated,
item.notes || "",
item.link || "",
]);
const csvContent = [
headers.join(","),
...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")),
].join("\n");
// Export to CSV
const handleExportCSV = () => {
const escapeCell = (cell: string) => {
const value = String(cell ?? "");
const safe = /^[=+\-@]/.test(value) ? `'${value}` : value;
return `"${safe.replace(/"/g, '""')}"`;
};
const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"];
const rows = filteredItems.map((item) => [
item.title,
item.description || "",
item.category,
item.status,
item.created,
item.updated,
item.notes || "",
item.link || "",
]);
const csvContent = [
headers.join(","),
...rows.map((row) => row.map(escapeCell).join(",")),
].join("\n");
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 176 - 193, The CSV export in handleExportCSV
currently quotes cells but does not mitigate Excel formula injection; update the
rows/CSV building so any cell value that is a string and begins with one of the
dangerous characters (=, +, -, @) is prefixed (e.g., with a single quote) before
being quoted. Concretely, in handleExportCSV (where rows are created and
csvContent is built) add a small sanitizer that checks each cell (String(cell))
and, if it matches the regex /^[=+\-@]/, prepends a safe prefix (such as "'")
and then continues to escape internal quotes and join — apply this sanitizer in
the rows.map or right before mapping row.map(...) so all exported cells are
protected.

Comment on lines +203 to +208
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
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

Guard shortcuts for select/contenteditable (currently breaks form controls).

Arrow keys and Enter are intercepted even when focus is on a <select> or contenteditable element, which blocks normal form interaction. Expand the editable-target check.

🛠️ Suggested fix
-      // Ignore if typing in an input
-      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
-        return;
-      }
+      // Ignore if typing/selecting in editable controls
+      const target = e.target as HTMLElement | null;
+      if (
+        target &&
+        (target.isContentEditable ||
+          target.closest("input, textarea, select, [contenteditable='true']"))
+      ) {
+        return;
+      }
📝 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
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing/selecting in editable controls
const target = e.target as HTMLElement | null;
if (
target &&
(target.isContentEditable ||
target.closest("input, textarea, select, [contenteditable='true']"))
) {
return;
}
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 203 - 208, The keyboard shortcut handler
inside useEffect (handleKeyDown) currently only ignores HTMLInputElement and
HTMLTextAreaElement and thus blocks normal interactions with <select> and
contenteditable elements; update the guard to also return early when e.target is
an HTMLSelectElement or when the event target (cast to HTMLElement) has
isContentEditable === true (or is inside a contenteditable) so arrow/Enter keys
are not intercepted while interacting with selects or contenteditable regions;
modify the guard in handleKeyDown to include these checks (use instanceof
HTMLSelectElement and (e.target as HTMLElement)?.isContentEditable or a
closest('[contenteditable]') check).

Comment on lines +234 to +238
case "Escape":
e.preventDefault();
setSelectedItem(null);
setKeyboardHelpOpen(false);
break;
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

Esc should also close the Add Item modal.

The PR objective states Esc closes modals/drawers, but showAddModal is not reset here.

🛠️ Suggested fix
         case "Escape":
           e.preventDefault();
           setSelectedItem(null);
           setKeyboardHelpOpen(false);
+          setShowAddModal(false);
           break;
📝 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
case "Escape":
e.preventDefault();
setSelectedItem(null);
setKeyboardHelpOpen(false);
break;
case "Escape":
e.preventDefault();
setSelectedItem(null);
setKeyboardHelpOpen(false);
setShowAddModal(false);
break;
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 234 - 238, The Escape key handler currently
prevents default and closes selected item/help but does not close the Add Item
modal; update the "Escape" case in the keyboard handler to also reset the
add-modal state by calling the modal setter (e.g., setShowAddModal(false))
alongside setSelectedItem(null) and setKeyboardHelpOpen(false) so pressing Esc
closes the Add Item modal as well.

Comment on lines +509 to +511
<p className="mt-4 text-xs text-muted-foreground text-center">
Press any key to dismiss
</p>
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

Modal copy doesn’t match behavior.

“Press any key to dismiss” isn’t implemented; only Esc closes it. Either update the copy or add an “any key” handler.

✏️ Suggested copy fix
-              <p className="mt-4 text-xs text-muted-foreground text-center">
-                Press any key to dismiss
-              </p>
+              <p className="mt-4 text-xs text-muted-foreground text-center">
+                Press Esc to dismiss
+              </p>
📝 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
<p className="mt-4 text-xs text-muted-foreground text-center">
Press any key to dismiss
</p>
<p className="mt-4 text-xs text-muted-foreground text-center">
Press Esc to dismiss
</p>
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 509 - 511, Update the modal copy to match
actual behavior: replace the <p> element whose className is "mt-4 text-xs
text-muted-foreground text-center" (currently containing "Press any key to
dismiss") with text that accurately describes how to close the modal, e.g.
"Press Esc to dismiss" (or "Press Esc or click outside to dismiss" if you plan
to add click-to-dismiss). Ensure you only change the string content in the JSX
where that <p> is rendered (in the modal markup) so the UI copy matches the
implemented Esc key handler.

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