Skip to content

feat: add project favorites with star icon and sort-to-top#147

Open
R-Hart80 wants to merge 2 commits into
CatholicOS:devfrom
R-Hart80:feat/project-favorites
Open

feat: add project favorites with star icon and sort-to-top#147
R-Hart80 wants to merge 2 commits into
CatholicOS:devfrom
R-Hart80:feat/project-favorites

Conversation

@R-Hart80
Copy link
Copy Markdown
Contributor

@R-Hart80 R-Hart80 commented Apr 13, 2026

Summary

  • Project interface: adds is_favorited?: boolean field
  • projectApi: adds favorite() and unfavorite() calling POST/DELETE /api/v1/projects/{id}/favorite
  • ProjectCard: star button shown only to authenticated users; clicking toggles the favorite state with optimistic UI (immediate visual feedback, rollback on API error); fully accessible with aria-label and aria-pressed
  • HomePage: tracks favorited IDs in local state and sorts favorited projects to the top within any active filter tab, maintaining existing creation-date order within each group

Note: requires backend issue ontokit-api#30 for the /favorite endpoints to be available.

Behavior

Scenario Behavior
Unauthenticated user Star icon hidden
Click star (unfavorited) Star fills yellow instantly; project moves to top
Click star (favorited) Star empties; project returns to normal order
API error Star reverts to previous state

Test plan

  • Star icon appears on cards when signed in, hidden when signed out
  • Clicking the star fills it yellow and moves the project to the top of the list
  • Clicking again removes the favorite and restores the order
  • Favorited state persists across page refreshes (returned by API in is_favorited)
  • Works across all filter tabs (My Projects, Public, Private, All)

Closes #78

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • Users can now favorite and unfavorite projects directly from project cards using a star button (requires authentication)
  • Favorited projects are sorted to appear first in the listing
  • Optimistic UI updates with automatic error recovery ensure a smooth user experience

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This PR implements project favorites for authenticated users. The backend API provides favorite/unfavorite endpoints; ProjectCard displays a conditional star button that performs optimistic updates with rollback on error; and HomePage maintains session-local favorite state and sorts projects to surface favorited items first across all views.

Changes

Project Favorites Feature

Layer / File(s) Summary
Favorites API contract and Project type
lib/api/projects.ts
The Project interface adds optional is_favorited field. projectApi adds favorite() and unfavorite() methods that POST/DELETE to /api/v1/projects/:id/favorite, both authenticated and returning the updated Project.
ProjectCard star-button and optimistic updates
components/projects/project-card.tsx
ProjectCard accepts optional accessToken and onFavoriteChange callback. A new star button calls handleFavoriteClick, which optimistically updates parent state, calls the API, and rolls back on error. Conditional star UI renders only when authenticated.
HomePage session state and project sorting
app/page.tsx
HomePage maintains favoriteOverrides map for session-local favorite state and sorts projects so favorited items (from override or server) appear first. Projects are passed to ProjectCard with overridden is_favorited and wired callbacks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🌟 A star now shines on cards so bright,
With toggles quick and rollback might,
Favorites float to the top, in sight,
User hearts and projects unite,
Optimism lights the way tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main feature: adding project favorites with a star icon and sorting to top, matching the primary change across all modified files.
Linked Issues check ✅ Passed All coding objectives from issue #78 are met: star icon added to ProjectCard (visible to authenticated users), favorite/unfavorite API methods implemented, optimistic UI with rollback, is_favorited field added to Project type, sorting implemented within tabs preserving creation_at order, and no separate tab created.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing project favorites: API methods and type definitions, ProjectCard star UI and callbacks, and HomePage sorting/state management are all essential to the feature requirement.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@JohnRDOrazio JohnRDOrazio changed the base branch from main to dev April 13, 2026 19:58
@JohnRDOrazio JohnRDOrazio added enhancement New feature or request UX User experience improvements labels Apr 13, 2026
R-Hart80 and others added 2 commits May 23, 2026 16:25
- Project interface: add is_favorited?: boolean field
- projectApi: add favorite() and unfavorite() methods calling
  POST/DELETE /api/v1/projects/{id}/favorite
- ProjectCard: star button visible to authenticated users only;
  optimistic toggle with rollback on error; accessible with
  aria-label and aria-pressed attributes
- HomePage: track favorited IDs in local state, sort favorited
  projects to the top within any active filter/tab;
  pass accessToken and onFavoriteChange to ProjectCard

Closes CatholicOS#78

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…overrides Map

Previously favoritedIds was a Set initialized once from the first page
load, causing newly paginated or refetched projects to miss their
is_favorited values from the server.

Replace it with a favoriteOverrides Map that only stores changes made
this session. The sort and display logic merges server data (source of
truth) with the override map, so all pages stay correct. On API error,
the rollback now calls onFavoriteChange back to the previous value,
keeping the override map as the single state location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@R-Hart80 R-Hart80 force-pushed the feat/project-favorites branch from 29cfe12 to bad9a80 Compare May 23, 2026 19:36
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 27.27273% with 16 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
components/projects/project-card.tsx 30.00% 13 Missing and 1 partial ⚠️
lib/api/projects.ts 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/projects/project-card.tsx (1)

55-98: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix invalid interactive nesting: <button> inside <Link>
In components/projects/project-card.tsx, the favorite <button> (lines 78-97) is rendered inside the clickable <Link> wrapper (lines 55-64), which results in a <button> inside an <a>—invalid HTML and can break keyboard/screen-reader behavior. Move the favorite control outside the Link clickable area (e.g., sibling/overlay in the card wrapper) and keep only the card body as the link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/projects/project-card.tsx` around lines 55 - 98, The favorite
button is nested inside the Link (invalid <button> inside <a>); refactor the JSX
so the outer card wrapper (a div) contains the Link for the card body and the
favorite control as a sibling element (not a child). Specifically, wrap the
existing Link props/content (href, aria-label={`Open project ${project.name}`},
the block classes and the project title/description JSX) in a Link that only
covers the left/main area, then move the conditional favorite button
(accessToken, onClick={handleFavoriteClick}, disabled={isToggling}, aria-label,
aria-pressed, className and <Star/> usage) out of the Link as a sibling inside
the same card container and preserve the visual classes/positioning (adjust
container flex/gap/shrink-0 classes if needed) so behavior and accessibility
remain identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/page.tsx`:
- Around line 69-80: favoriteOverrides persists across sessions and needs to be
cleared when the authenticated identity changes; add an effect that watches the
auth identity (e.g., accessToken or userId from your auth context/props) and
calls setFavoriteOverrides(new Map()) when that identity changes, so stale
overrides aren't applied to a different user. Locate the state favoriteOverrides
and setter setFavoriteOverrides and the handler handleFavoriteChange, then
implement a useEffect that depends on the chosen identity value and resets
favoriteOverrides on change. Ensure the dependency uses the exact auth value
available in this component (accessToken or userId).

---

Outside diff comments:
In `@components/projects/project-card.tsx`:
- Around line 55-98: The favorite button is nested inside the Link (invalid
<button> inside <a>); refactor the JSX so the outer card wrapper (a div)
contains the Link for the card body and the favorite control as a sibling
element (not a child). Specifically, wrap the existing Link props/content (href,
aria-label={`Open project ${project.name}`}, the block classes and the project
title/description JSX) in a Link that only covers the left/main area, then move
the conditional favorite button (accessToken, onClick={handleFavoriteClick},
disabled={isToggling}, aria-label, aria-pressed, className and <Star/> usage)
out of the Link as a sibling inside the same card container and preserve the
visual classes/positioning (adjust container flex/gap/shrink-0 classes if
needed) so behavior and accessibility remain identical.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43f1997b-d699-4b70-a1c0-168d943644dd

📥 Commits

Reviewing files that changed from the base of the PR and between fbeccd8 and bad9a80.

📒 Files selected for processing (3)
  • app/page.tsx
  • components/projects/project-card.tsx
  • lib/api/projects.ts

Comment thread app/page.tsx
Comment on lines +69 to +80
const [favoriteOverrides, setFavoriteOverrides] = useState<Map<string, boolean>>(new Map());

const handleFavoriteChange = (projectId: string, isFavorited: boolean) => {
setFavoriteOverrides((prev) => new Map(prev).set(projectId, isFavorited));
};

const rawProjects = data?.pages.flatMap((page) => page.items) ?? [];
const projects = [...rawProjects].sort((a, b) => {
const aFav = favoriteOverrides.has(a.id) ? favoriteOverrides.get(a.id)! : (a.is_favorited ?? false);
const bFav = favoriteOverrides.has(b.id) ? favoriteOverrides.get(b.id)! : (b.is_favorited ?? false);
return (bFav ? 1 : 0) - (aFav ? 1 : 0);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset favoriteOverrides when auth identity changes.

favoriteOverrides persists across session changes, so signing out/in as another user can apply stale overrides to the wrong account’s list ordering/state. Clear overrides on access-token (or user-id) change.

Suggested fix
   const [favoriteOverrides, setFavoriteOverrides] = useState<Map<string, boolean>>(new Map());
+
+  useEffect(() => {
+    setFavoriteOverrides(new Map());
+  }, [session?.accessToken]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/page.tsx` around lines 69 - 80, favoriteOverrides persists across
sessions and needs to be cleared when the authenticated identity changes; add an
effect that watches the auth identity (e.g., accessToken or userId from your
auth context/props) and calls setFavoriteOverrides(new Map()) when that identity
changes, so stale overrides aren't applied to a different user. Locate the state
favoriteOverrides and setter setFavoriteOverrides and the handler
handleFavoriteChange, then implement a useEffect that depends on the chosen
identity value and resets favoriteOverrides on change. Ensure the dependency
uses the exact auth value available in this component (accessToken or userId).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request UX User experience improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add project favorites with star icon and sort-to-top

2 participants