Skip to content

UX fixes: completed-task ordering, editable links, Ctrl+K discoverability#9

Merged
Rupesh4604 merged 1 commit into
mainfrom
fix/ux-tweaks
May 31, 2026
Merged

UX fixes: completed-task ordering, editable links, Ctrl+K discoverability#9
Rupesh4604 merged 1 commit into
mainfrom
fix/ux-tweaks

Conversation

@Rupesh4604

Copy link
Copy Markdown
Owner

Addresses reported issues:

  1. All Tasks ordering — completed tasks now always sort below incomplete ones (was interleaving by the active sort).
  2. Project Notes links — links are now editable (inline form) and the edit/delete controls are always visible instead of hover-only (they were invisible on touch devices).
  3. Discoverability — added a Search… button in the sidebar showing the Ctrl/Cmd+K shortcut for the command palette, and a "drag the handle to reorder" hint when the Manual sort mode is selected.

Build is clean.

🤖 Generated with Claude Code

…iscoverability

- All Tasks: completed tasks now always sort below incomplete ones
- Project Notes: links are now editable (inline edit form) and the
  edit/delete controls are always visible (not hover-only, so they work on
  touch)
- Sidebar: add a Search… button showing the Ctrl/Cmd+K shortcut to surface
  the command palette
- ProjectDetail: show a 'drag the handle to reorder' hint when Manual sort
  mode is active

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Rupesh4604 Rupesh4604 merged commit e6c0f72 into main May 31, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 9b0cc0f):

https://my-productivity-hub-5a3ba--pr9-fix-ux-tweaks-97113ifu.web.app

(expires Sun, 07 Jun 2026 18:56:10 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 4ef75aef15076a8cc05555b91f31935d9a13db8e

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several enhancements, including inline editing for project resources, a search shortcut in the sidebar, manual task sorting guidance, and improved task sorting logic to keep completed tasks at the bottom. Feedback highlights opportunities to improve robustness: replacing the deprecated navigator.platform with navigator.userAgent for OS detection, coercing task completion states to booleans to avoid sorting bugs with undefined values, and tracking the resource editing state by URL instead of array index to prevent UI state mismatches.

Comment on lines +9 to +10
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
const SEARCH_SHORTCUT = isMac ? '⌘K' : 'Ctrl K';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The navigator.platform property is deprecated and may return spoofed or empty values in modern browsers. It is recommended to use navigator.userAgent with a case-insensitive regex instead to detect macOS/iOS devices reliably.

Suggested change
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
const SEARCH_SHORTCUT = isMac ? '⌘K' : 'Ctrl K';
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
const SEARCH_SHORTCUT = isMac ? '⌘K' : 'Ctrl K';

Comment on lines +23 to +24
// Always keep completed tasks below incomplete ones.
if (a.completed !== b.completed) return a.completed ? 1 : -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If some tasks have completed as undefined (e.g., legacy or partially initialized tasks) and others have completed as false, the comparison a.completed !== b.completed will evaluate to true even though both are incomplete. Coercing both values to booleans ensures a stable and correct sort order.

Suggested change
// Always keep completed tasks below incomplete ones.
if (a.completed !== b.completed) return a.completed ? 1 : -1;
// Always keep completed tasks below incomplete ones.
const aComp = !!a.completed;
const bComp = !!b.completed;
if (aComp !== bComp) return aComp ? 1 : -1;

Comment on lines +28 to +31
const [editingIndex, setEditingIndex] = useState(null);
const [editLabel, setEditLabel] = useState('');
const [editUrl, setEditUrl] = useState('');
const [editError, setEditError] = useState('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using the array index to track the editing state (editingIndex) can lead to correctness bugs if the list of resources is modified (e.g., an item is deleted or reordered) while editing. Since URLs are typically unique in a list of resources, tracking the editing state using the resource's URL (editingUrl) is much more robust and prevents UI state mismatch.

Suggested change
const [editingIndex, setEditingIndex] = useState(null);
const [editLabel, setEditLabel] = useState('');
const [editUrl, setEditUrl] = useState('');
const [editError, setEditError] = useState('');
const [editingUrl, setEditingUrl] = useState(null);
const [editLabel, setEditLabel] = useState('');
const [editUrl, setEditUrl] = useState('');
const [editError, setEditError] = useState('');

Comment on lines +96 to +128
const startEdit = (index) => {
setEditingIndex(index);
setEditLabel(resources[index].label || '');
setEditUrl(resources[index].url || '');
setEditError('');
};

const handleUpdateResource = async (e) => {
e.preventDefault();
setEditError('');
const url = normalizeUrl(editUrl);
if (!url) {
setEditError('Enter a URL.');
return;
}
try {
// eslint-disable-next-line no-new
new URL(url);
} catch {
setEditError('That doesn’t look like a valid URL.');
return;
}
if (!projectRef) return;
const label = editLabel.trim() || url.replace(/^https?:\/\//i, '');
const next = resources.map((r, i) => (i === editingIndex ? { label, url } : r));
try {
await updateDoc(projectRef, { resources: next });
setEditingIndex(null);
} catch (err) {
console.error('Error updating resource:', err);
setEditError('Could not save. Try again.');
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Refactor startEdit and handleUpdateResource to use the resource's URL (editingUrl) instead of the array index. This ensures that the correct resource is updated even if the array is modified concurrently.

  const startEdit = (res) => {
    setEditingUrl(res.url);
    setEditLabel(res.label || '');
    setEditUrl(res.url || '');
    setEditError('');
  };

  const handleUpdateResource = async (e) => {
    e.preventDefault();
    setEditError('');
    const url = normalizeUrl(editUrl);
    if (!url) {
      setEditError('Enter a URL.');
      return;
    }
    try {
      // eslint-disable-next-line no-new
      new URL(url);
    } catch {
      setEditError('That doesn’t look like a valid URL.');
      return;
    }
    if (!projectRef) return;
    const label = editLabel.trim() || url.replace(/^https?:\/\//i, '');
    const next = resources.map((r) => (r.url === editingUrl ? { label, url } : r));
    try {
      await updateDoc(projectRef, { resources: next });
      setEditingUrl(null);
    } catch (err) {
      console.error('Error updating resource:', err);
      setEditError('Could not save. Try again.');
    }
  };

Comment on lines +186 to +249
{resources.map((res, index) =>
editingIndex === index ? (
<li key={`edit-${index}`} className="p-2.5 bg-gray-900/40 border border-gray-700/40 rounded-xl">
<form onSubmit={handleUpdateResource} className="grid grid-cols-1 sm:grid-cols-[1fr_1.5fr_auto] gap-2">
<input
type="text"
value={editLabel}
onChange={(e) => setEditLabel(e.target.value)}
placeholder="Label (optional)"
className="bg-gray-800 border border-gray-700 rounded-lg px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
/>
<input
type="text"
value={editUrl}
onChange={(e) => setEditUrl(e.target.value)}
placeholder="https://…"
className="bg-gray-800 border border-gray-700 rounded-lg px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
autoFocus
/>
<div className="flex gap-1">
<button type="submit" aria-label="Save link" className="flex-1 sm:flex-none px-2.5 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded-lg text-xs font-semibold">
<Check size={14} />
</button>
<button type="button" onClick={() => setEditingIndex(null)} aria-label="Cancel" className="flex-1 sm:flex-none px-2.5 py-1.5 bg-gray-600 hover:bg-gray-700 rounded-lg text-xs font-semibold">
<X size={14} />
</button>
</div>
</form>
{editError && <p className="text-red-400 text-xs mt-1.5">{editError}</p>}
</li>
) : (
<li
key={`${res.url}-${index}`}
className="flex items-center gap-3 p-2.5 bg-gray-900/40 border border-gray-700/30 rounded-xl hover:border-gray-600/50 transition-colors"
>
<Trash2 size={14} />
</button>
</li>
))}
<ExternalLink size={14} className="text-gray-500 flex-shrink-0" />
<a
href={res.url}
target="_blank"
rel="noopener noreferrer"
className="flex-grow min-w-0 truncate text-sm text-blue-300 hover:text-blue-200 hover:underline"
title={res.url}
>
{res.label || res.url}
</a>
<div className="flex items-center gap-0.5 flex-shrink-0">
<button
onClick={() => startEdit(index)}
aria-label={`Edit ${res.label || res.url}`}
className="p-1 text-gray-500 hover:text-white rounded-md transition-colors"
>
<Edit2 size={14} />
</button>
<button
onClick={() => handleRemoveResource(index)}
aria-label={`Remove ${res.label || res.url}`}
className="p-1 text-gray-500 hover:text-red-400 rounded-md transition-colors"
>
<Trash2 size={14} />
</button>
</div>
</li>
)
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the list rendering to use editingUrl instead of editingIndex and pass the resource object res to startEdit.

            {resources.map((res, index) =>
              editingUrl === res.url ? (
                <li key={`edit-${res.url}`} className="p-2.5 bg-gray-900/40 border border-gray-700/40 rounded-xl">
                  <form onSubmit={handleUpdateResource} className="grid grid-cols-1 sm:grid-cols-[1fr_1.5fr_auto] gap-2">
                    <input
                      type="text"
                      value={editLabel}
                      onChange={(e) => setEditLabel(e.target.value)}
                      placeholder="Label (optional)"
                      className="bg-gray-800 border border-gray-700 rounded-lg px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
                    />
                    <input
                      type="text"
                      value={editUrl}
                      onChange={(e) => setEditUrl(e.target.value)}
                      placeholder="https://…"
                      className="bg-gray-800 border border-gray-700 rounded-lg px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
                      autoFocus
                    />
                    <div className="flex gap-1">
                      <button type="submit" aria-label="Save link" className="flex-1 sm:flex-none px-2.5 py-1.5 bg-emerald-600 hover:bg-emerald-700 rounded-lg text-xs font-semibold">
                        <Check size={14} />
                      </button>
                      <button type="button" onClick={() => setEditingUrl(null)} aria-label="Cancel" className="flex-1 sm:flex-none px-2.5 py-1.5 bg-gray-600 hover:bg-gray-700 rounded-lg text-xs font-semibold">
                        <X size={14} />
                      </button>
                    </div>
                  </form>
                  {editError && <p className="text-red-400 text-xs mt-1.5">{editError}</p>}
                </li>
              ) : (
                <li
                  key={`${res.url}-${index}`}
                  className="flex items-center gap-3 p-2.5 bg-gray-900/40 border border-gray-700/30 rounded-xl hover:border-gray-600/50 transition-colors"
                >
                  <ExternalLink size={14} className="text-gray-500 flex-shrink-0" />
                  <a
                    href={res.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="flex-grow min-w-0 truncate text-sm text-blue-300 hover:text-blue-200 hover:underline"
                    title={res.url}
                  >
                    {res.label || res.url}
                  </a>
                  <div className="flex items-center gap-0.5 flex-shrink-0">
                    <button
                      onClick={() => startEdit(res)}
                      aria-label={`Edit ${res.label || res.url}`}
                      className="p-1 text-gray-500 hover:text-white rounded-md transition-colors"
                    >
                      <Edit2 size={14} />
                    </button>
                    <button
                      onClick={() => handleRemoveResource(index)}
                      aria-label={`Remove ${res.label || res.url}`}
                      className="p-1 text-gray-500 hover:text-red-400 rounded-md transition-colors"
                    >
                      <Trash2 size={14} />
                    </button>
                  </div>
                </li>
              )
            )}

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