Skip to content

feat: activity#125

Merged
izakdvlpr merged 6 commits into
developfrom
feat/activity
Jul 10, 2026
Merged

feat: activity#125
izakdvlpr merged 6 commits into
developfrom
feat/activity

Conversation

@izakdvlpr

Copy link
Copy Markdown
Contributor

Description

Acknowledgements

Screenshots

Proof showing the creation/modification is working as expected

@izakdvlpr
izakdvlpr merged commit a31aa74 into develop Jul 10, 2026
4 checks passed
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 847ba133-9337-4f4c-acc5-5cc4ecc219fc

📥 Commits

Reviewing files that changed from the base of the PR and between 848f9f0 and 89e339b.

📒 Files selected for processing (66)
  • src/components/pages/details/review-item.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/feed/activity-item.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/feed/listFollowing.tsx
  • src/components/pages/feed/normalize.ts
  • src/components/pages/feed/review.tsx
  • src/components/pages/feed/user-activity.tsx
  • src/components/pages/feed/user-following.tsx
  • src/components/pages/notifications/item.tsx
  • src/components/pages/notifications/list-notification.tsx
  • src/components/pages/user/activity-tab.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/favorites-tab.tsx
  • src/components/pages/user/lists-tab.tsx
  • src/components/pages/user/overview-tab/about-card.tsx
  • src/components/pages/user/overview-tab/activity-card.tsx
  • src/components/pages/user/overview-tab/favorite-card.tsx
  • src/components/pages/user/overview-tab/favorites-card.tsx
  • src/components/pages/user/overview-tab/index.tsx
  • src/components/pages/user/overview-tab/medals-card.tsx
  • src/components/pages/user/overview-tab/statistics-card.tsx
  • src/components/pages/user/reviews-tab.tsx
  • src/components/pages/user/screenshots-tab.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/shared/cards/card.tsx
  • src/components/shared/comments/index.tsx
  • src/components/shared/header/user-dropdown.tsx
  • src/components/ui/card.tsx
  • src/hooks/activity.ts
  • src/hooks/notification.ts
  • src/lib/api.ts
  • src/lib/i18n/locales/en-US/common.json
  • src/lib/i18n/locales/en-US/feed.json
  • src/lib/i18n/locales/en-US/notifications.json
  • src/lib/i18n/locales/en-US/settings.json
  • src/lib/i18n/locales/en-US/user.json
  • src/lib/i18n/locales/pt-BR/notifications.json
  • src/lib/reactions.ts
  • src/routeTree.gen.ts
  • src/routes/_authenticated/notifications.tsx
  • src/routes/_authenticated/settings.tsx
  • src/routes/anime/$slug.tsx
  • src/routes/book/$slug.tsx
  • src/routes/feed.tsx
  • src/routes/game/$slug.tsx
  • src/routes/manga/$slug.tsx
  • src/routes/movie/$slug.tsx
  • src/routes/post/$id.tsx
  • src/routes/review/$id.tsx
  • src/routes/search.tsx
  • src/routes/tv/$slug.tsx
  • src/routes/user/$username/index.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • TrackGeek/api (manual) → reviewed against open PR #68 feat/activity instead of the default branch
💤 Files with no reviewable changes (21)
  • src/components/pages/feed/listFollowing.tsx
  • src/routes/review/$id.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/routes/post/$id.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/feed/review.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx

📝 Walkthrough

Walkthrough

This PR adds user activity feeds with normalization, pagination, infinite scrolling, and emoji reactions. It introduces an authenticated notifications center with read/delete actions and unread indicators. Review rendering moves to a dedicated component, while obsolete feed, composer, and comment-thread modules are removed. User profile tabs gain activity, favorites, lists, screenshots, and updated card layouts. Shared cards, comments, settings, and TV detail layouts are also reorganized around common Card components.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the change, but it is too vague to convey the main activity and notification feature work. Use a more specific title such as "feat: add activity and notifications features" to better describe the main change.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/activity

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
src/components/pages/user/favorites-tab.tsx (1)

82-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

isRemoving disables all cards during a single removal.

removeFavorite.isPending is a mutation-level flag. When one favorite is removed, every card's remove button becomes disabled, which is confusing UX. Compare the mutation's variables to scope the disabled state to the actively-removed item.

Proposed fix
-                  isRemoving={removeFavorite.isPending}
+                  isRemoving={
+                    removeFavorite.isPending &&
+                    removeFavorite.variables?.mediaId === item.mediaId
+                  }
🧹 Nitpick comments (8)
src/routes/_authenticated/notifications.tsx (1)

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unrelated paymentId validateSearch.

The validateSearch accepts a paymentId search parameter that is never used in this route — it appears to be a copy-paste from a billing/donation route. This adds an incorrect search param contract to the notifications route. Remove it entirely since no search params are needed.

♻️ Proposed fix
 export const Route = createFileRoute("/_authenticated/notifications")({
   head: () => ({
     meta: [...seo({ title: "Notifications" })],
   }),
-  validateSearch: (search): { paymentId?: string } => ({
-    ...(search.paymentId ? { paymentId: search.paymentId as string } : {}),
-  }),
   component: NotificationsRoute,
 });
src/components/shared/cards/card.tsx (1)

16-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the imageURL normalization.

(item.imageURL ?? null)?.replace(...) is redundant — ?? null converts undefined to null, but ?. already handles both. This can be simplified.

Proposed simplification
-  const imageURL =
-    (item.imageURL ?? null)?.replace(
-      "https://myanimelist.net/img/sp/icon/apple-touch-icon-256.png",
-      "/placeholder/cover.webp",
-    ) ?? "/placeholder/cover.webp";
+  const imageURL =
+    item.imageURL?.replace(
+      "https://myanimelist.net/img/sp/icon/apple-touch-icon-256.png",
+      "/placeholder/cover.webp",
+    ) ?? "/placeholder/cover.webp";
src/components/ui/card.tsx (1)

29-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CardTitle now has flex items-center gap-2 built in — check for redundant consumer classes.

The notifications route (src/routes/_authenticated/notifications.tsx:59) applies className="flex items-center gap-2" to CardTitle, which is now redundant since CardTitle includes those classes by default. While twMerge handles this gracefully, consumers passing these classes are now unnecessary. Consider cleaning up redundant classes in downstream consumers.

src/routes/tv/$slug.tsx (1)

768-775: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a sandbox attribute to the trailer iframe.

Static analysis flags this embed as missing a sandbox restriction, giving the embedded page full access to the site by default.

🔒 Proposed fix
 {item.trailerId && (
   <iframe
     src={`https://youtube.com/embed/${item.trailerId}`}
     allowFullScreen
+    sandbox="allow-scripts allow-same-origin allow-presentation"
     className="w-full aspect-video"
     title="Trailer"
   />
 )}

Source: Linters/SAST tools

src/components/pages/user/overview-tab/activity-card.tsx (2)

78-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline query duplicates the hook pattern established for activities.

src/hooks/activity.ts exports useUserActivities for the activities list, but the calendar query here is inlined directly in the component instead of following the same hook convention. Extracting a useUserActivityCalendar hook would keep the data-fetching layer consistent and testable.

♻️ Suggested refactor
-  const activityCalendarQuery = useQuery({
-    queryKey: ["user-activity-calendar", userId],
-    queryFn: () =>
-      api
-        .get<ApiTypes.GetUserActivityCalendarResponse>(apiEndpoints.getCalendarActivitiesByUserId(userId))
-        .then(({ data }) => data.activityCalendar),
-    enabled: !!userId,
-  });
+  const activityCalendarQuery = useUserActivityCalendar(userId);
// src/hooks/activity.ts
export function useUserActivityCalendar(userId: string) {
  return useQuery({
    queryKey: ["user-activity-calendar", userId],
    queryFn: () =>
      api
        .get<ApiTypes.GetUserActivityCalendarResponse>(apiEndpoints.getCalendarActivitiesByUserId(userId))
        .then(({ data }) => data.activityCalendar),
    enabled: !!userId,
  });
}

104-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate ResizeObserver effects.

The two effects only differ by ref and setter; extracting a small useElementWidth(ref, onResize) helper would remove the duplication.

♻️ Suggested refactor
+function useElementWidth<T extends HTMLElement>(ref: React.RefObject<T | null>, onResize: (width: number) => void) {
+  useEffect(() => {
+    const el = ref.current;
+    if (!el) return;
+    const observer = new ResizeObserver(([entry]) => onResize(entry.contentRect.width));
+    observer.observe(el);
+    return () => observer.disconnect();
+  }, [ref, onResize]);
+}
src/components/pages/feed/activity-item.tsx (2)

37-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract duplicated media rendering block.

The inner media content (div + Image/Icon) is identical in both the Link-wrapped and unwrapped branches. Extract it into a variable to eliminate the duplication.

♻️ Proposed refactor
   const hasMediaLink = Boolean(item.media);
   const mediaLink = item.media ? ({ to: item.media.to, params: { slug: item.media.slug } } as const) : null;
 
+  const mediaContent = (
+    <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
+      {item.coverURL ? (
+        <Image
+          src={item.coverURL}
+          layout="fullWidth"
+          aspectRatio={3 / 4}
+          alt={item.title}
+          className="w-full h-35 rounded-l-xl object-cover"
+        />
+      ) : (
+        <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
+      )}
+    </div>
+  );
+
   return (
     <Card className="p-0">
       <CardContent className="flex flex-row p-0 gap-0">
-        {hasMediaLink && mediaLink ? (
-          <Link {...mediaLink}>
-            <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
-              {item.coverURL ? (
-                <Image
-                  src={item.coverURL}
-                  layout="fullWidth"
-                  aspectRatio={3 / 4}
-                  alt={item.title}
-                  className="w-full h-35 rounded-l-xl object-cover"
-                />
-              ) : (
-                <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
-              )}
-            </div>
-          </Link>
+        {hasMediaLink && mediaLink ? (
+          <Link {...mediaLink}>{mediaContent}</Link>
         ) : (
-          <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
-            {item.coverURL ? (
-              <Image
-                src={item.coverURL}
-                layout="fullWidth"
-                aspectRatio={3 / 4}
-                alt={item.title}
-                className="w-full h-35 rounded-l-xl object-cover"
-              />
-            ) : (
-              <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
-            )}
-          </div>
+          mediaContent
         )}

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename interface to PascalCase.

TypeScript convention is PascalCase for interface names. feedItemProps should be FeedItemProps.

♻️ Proposed refactor
-interface feedItemProps {
+interface FeedItemProps {
   profile: FeedProfile;
   item: FeedItemData;
   onReact?: (emoji: string, currentReaction?: ApiTypes.ActivityReaction) => void;
   isReacting?: boolean;
 }

-export function ActivityItem({ profile, item, onReact, isReacting = false }: feedItemProps) {
+export function ActivityItem({ profile, item, onReact, isReacting = false }: FeedItemProps) {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 847ba133-9337-4f4c-acc5-5cc4ecc219fc

📥 Commits

Reviewing files that changed from the base of the PR and between 848f9f0 and 89e339b.

📒 Files selected for processing (66)
  • src/components/pages/details/review-item.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/feed/activity-item.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/feed/listFollowing.tsx
  • src/components/pages/feed/normalize.ts
  • src/components/pages/feed/review.tsx
  • src/components/pages/feed/user-activity.tsx
  • src/components/pages/feed/user-following.tsx
  • src/components/pages/notifications/item.tsx
  • src/components/pages/notifications/list-notification.tsx
  • src/components/pages/user/activity-tab.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/favorites-tab.tsx
  • src/components/pages/user/lists-tab.tsx
  • src/components/pages/user/overview-tab/about-card.tsx
  • src/components/pages/user/overview-tab/activity-card.tsx
  • src/components/pages/user/overview-tab/favorite-card.tsx
  • src/components/pages/user/overview-tab/favorites-card.tsx
  • src/components/pages/user/overview-tab/index.tsx
  • src/components/pages/user/overview-tab/medals-card.tsx
  • src/components/pages/user/overview-tab/statistics-card.tsx
  • src/components/pages/user/reviews-tab.tsx
  • src/components/pages/user/screenshots-tab.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/shared/cards/card.tsx
  • src/components/shared/comments/index.tsx
  • src/components/shared/header/user-dropdown.tsx
  • src/components/ui/card.tsx
  • src/hooks/activity.ts
  • src/hooks/notification.ts
  • src/lib/api.ts
  • src/lib/i18n/locales/en-US/common.json
  • src/lib/i18n/locales/en-US/feed.json
  • src/lib/i18n/locales/en-US/notifications.json
  • src/lib/i18n/locales/en-US/settings.json
  • src/lib/i18n/locales/en-US/user.json
  • src/lib/i18n/locales/pt-BR/notifications.json
  • src/lib/reactions.ts
  • src/routeTree.gen.ts
  • src/routes/_authenticated/notifications.tsx
  • src/routes/_authenticated/settings.tsx
  • src/routes/anime/$slug.tsx
  • src/routes/book/$slug.tsx
  • src/routes/feed.tsx
  • src/routes/game/$slug.tsx
  • src/routes/manga/$slug.tsx
  • src/routes/movie/$slug.tsx
  • src/routes/post/$id.tsx
  • src/routes/review/$id.tsx
  • src/routes/search.tsx
  • src/routes/tv/$slug.tsx
  • src/routes/user/$username/index.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • TrackGeek/api (manual) → reviewed against open PR #68 feat/activity instead of the default branch
💤 Files with no reviewable changes (21)
  • src/components/pages/feed/listFollowing.tsx
  • src/routes/review/$id.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/routes/post/$id.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/feed/review.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
src/components/pages/user/favorites-tab.tsx (1)

82-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

isRemoving disables all cards during a single removal.

removeFavorite.isPending is a mutation-level flag. When one favorite is removed, every card's remove button becomes disabled, which is confusing UX. Compare the mutation's variables to scope the disabled state to the actively-removed item.

Proposed fix
-                  isRemoving={removeFavorite.isPending}
+                  isRemoving={
+                    removeFavorite.isPending &&
+                    removeFavorite.variables?.mediaId === item.mediaId
+                  }
🧹 Nitpick comments (8)
src/routes/_authenticated/notifications.tsx (1)

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unrelated paymentId validateSearch.

The validateSearch accepts a paymentId search parameter that is never used in this route — it appears to be a copy-paste from a billing/donation route. This adds an incorrect search param contract to the notifications route. Remove it entirely since no search params are needed.

♻️ Proposed fix
 export const Route = createFileRoute("/_authenticated/notifications")({
   head: () => ({
     meta: [...seo({ title: "Notifications" })],
   }),
-  validateSearch: (search): { paymentId?: string } => ({
-    ...(search.paymentId ? { paymentId: search.paymentId as string } : {}),
-  }),
   component: NotificationsRoute,
 });
src/components/shared/cards/card.tsx (1)

16-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the imageURL normalization.

(item.imageURL ?? null)?.replace(...) is redundant — ?? null converts undefined to null, but ?. already handles both. This can be simplified.

Proposed simplification
-  const imageURL =
-    (item.imageURL ?? null)?.replace(
-      "https://myanimelist.net/img/sp/icon/apple-touch-icon-256.png",
-      "/placeholder/cover.webp",
-    ) ?? "/placeholder/cover.webp";
+  const imageURL =
+    item.imageURL?.replace(
+      "https://myanimelist.net/img/sp/icon/apple-touch-icon-256.png",
+      "/placeholder/cover.webp",
+    ) ?? "/placeholder/cover.webp";
src/components/ui/card.tsx (1)

29-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CardTitle now has flex items-center gap-2 built in — check for redundant consumer classes.

The notifications route (src/routes/_authenticated/notifications.tsx:59) applies className="flex items-center gap-2" to CardTitle, which is now redundant since CardTitle includes those classes by default. While twMerge handles this gracefully, consumers passing these classes are now unnecessary. Consider cleaning up redundant classes in downstream consumers.

src/routes/tv/$slug.tsx (1)

768-775: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a sandbox attribute to the trailer iframe.

Static analysis flags this embed as missing a sandbox restriction, giving the embedded page full access to the site by default.

🔒 Proposed fix
 {item.trailerId && (
   <iframe
     src={`https://youtube.com/embed/${item.trailerId}`}
     allowFullScreen
+    sandbox="allow-scripts allow-same-origin allow-presentation"
     className="w-full aspect-video"
     title="Trailer"
   />
 )}

Source: Linters/SAST tools

src/components/pages/user/overview-tab/activity-card.tsx (2)

78-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline query duplicates the hook pattern established for activities.

src/hooks/activity.ts exports useUserActivities for the activities list, but the calendar query here is inlined directly in the component instead of following the same hook convention. Extracting a useUserActivityCalendar hook would keep the data-fetching layer consistent and testable.

♻️ Suggested refactor
-  const activityCalendarQuery = useQuery({
-    queryKey: ["user-activity-calendar", userId],
-    queryFn: () =>
-      api
-        .get<ApiTypes.GetUserActivityCalendarResponse>(apiEndpoints.getCalendarActivitiesByUserId(userId))
-        .then(({ data }) => data.activityCalendar),
-    enabled: !!userId,
-  });
+  const activityCalendarQuery = useUserActivityCalendar(userId);
// src/hooks/activity.ts
export function useUserActivityCalendar(userId: string) {
  return useQuery({
    queryKey: ["user-activity-calendar", userId],
    queryFn: () =>
      api
        .get<ApiTypes.GetUserActivityCalendarResponse>(apiEndpoints.getCalendarActivitiesByUserId(userId))
        .then(({ data }) => data.activityCalendar),
    enabled: !!userId,
  });
}

104-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate ResizeObserver effects.

The two effects only differ by ref and setter; extracting a small useElementWidth(ref, onResize) helper would remove the duplication.

♻️ Suggested refactor
+function useElementWidth<T extends HTMLElement>(ref: React.RefObject<T | null>, onResize: (width: number) => void) {
+  useEffect(() => {
+    const el = ref.current;
+    if (!el) return;
+    const observer = new ResizeObserver(([entry]) => onResize(entry.contentRect.width));
+    observer.observe(el);
+    return () => observer.disconnect();
+  }, [ref, onResize]);
+}
src/components/pages/feed/activity-item.tsx (2)

37-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract duplicated media rendering block.

The inner media content (div + Image/Icon) is identical in both the Link-wrapped and unwrapped branches. Extract it into a variable to eliminate the duplication.

♻️ Proposed refactor
   const hasMediaLink = Boolean(item.media);
   const mediaLink = item.media ? ({ to: item.media.to, params: { slug: item.media.slug } } as const) : null;
 
+  const mediaContent = (
+    <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
+      {item.coverURL ? (
+        <Image
+          src={item.coverURL}
+          layout="fullWidth"
+          aspectRatio={3 / 4}
+          alt={item.title}
+          className="w-full h-35 rounded-l-xl object-cover"
+        />
+      ) : (
+        <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
+      )}
+    </div>
+  );
+
   return (
     <Card className="p-0">
       <CardContent className="flex flex-row p-0 gap-0">
-        {hasMediaLink && mediaLink ? (
-          <Link {...mediaLink}>
-            <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
-              {item.coverURL ? (
-                <Image
-                  src={item.coverURL}
-                  layout="fullWidth"
-                  aspectRatio={3 / 4}
-                  alt={item.title}
-                  className="w-full h-35 rounded-l-xl object-cover"
-                />
-              ) : (
-                <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
-              )}
-            </div>
-          </Link>
+        {hasMediaLink && mediaLink ? (
+          <Link {...mediaLink}>{mediaContent}</Link>
         ) : (
-          <div className="w-30 h-35 rounded-l-xl bg-muted/50 flex items-center justify-center">
-            {item.coverURL ? (
-              <Image
-                src={item.coverURL}
-                layout="fullWidth"
-                aspectRatio={3 / 4}
-                alt={item.title}
-                className="w-full h-35 rounded-l-xl object-cover"
-              />
-            ) : (
-              <Icon icon={item.icon ?? "lucide:activity"} className="size-10 text-muted-foreground" />
-            )}
-          </div>
+          mediaContent
         )}

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename interface to PascalCase.

TypeScript convention is PascalCase for interface names. feedItemProps should be FeedItemProps.

♻️ Proposed refactor
-interface feedItemProps {
+interface FeedItemProps {
   profile: FeedProfile;
   item: FeedItemData;
   onReact?: (emoji: string, currentReaction?: ApiTypes.ActivityReaction) => void;
   isReacting?: boolean;
 }

-export function ActivityItem({ profile, item, onReact, isReacting = false }: feedItemProps) {
+export function ActivityItem({ profile, item, onReact, isReacting = false }: FeedItemProps) {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 847ba133-9337-4f4c-acc5-5cc4ecc219fc

📥 Commits

Reviewing files that changed from the base of the PR and between 848f9f0 and 89e339b.

📒 Files selected for processing (66)
  • src/components/pages/details/review-item.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/feed/activity-item.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/feed/listFollowing.tsx
  • src/components/pages/feed/normalize.ts
  • src/components/pages/feed/review.tsx
  • src/components/pages/feed/user-activity.tsx
  • src/components/pages/feed/user-following.tsx
  • src/components/pages/notifications/item.tsx
  • src/components/pages/notifications/list-notification.tsx
  • src/components/pages/user/activity-tab.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/favorites-tab.tsx
  • src/components/pages/user/lists-tab.tsx
  • src/components/pages/user/overview-tab/about-card.tsx
  • src/components/pages/user/overview-tab/activity-card.tsx
  • src/components/pages/user/overview-tab/favorite-card.tsx
  • src/components/pages/user/overview-tab/favorites-card.tsx
  • src/components/pages/user/overview-tab/index.tsx
  • src/components/pages/user/overview-tab/medals-card.tsx
  • src/components/pages/user/overview-tab/statistics-card.tsx
  • src/components/pages/user/reviews-tab.tsx
  • src/components/pages/user/screenshots-tab.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/shared/cards/card.tsx
  • src/components/shared/comments/index.tsx
  • src/components/shared/header/user-dropdown.tsx
  • src/components/ui/card.tsx
  • src/hooks/activity.ts
  • src/hooks/notification.ts
  • src/lib/api.ts
  • src/lib/i18n/locales/en-US/common.json
  • src/lib/i18n/locales/en-US/feed.json
  • src/lib/i18n/locales/en-US/notifications.json
  • src/lib/i18n/locales/en-US/settings.json
  • src/lib/i18n/locales/en-US/user.json
  • src/lib/i18n/locales/pt-BR/notifications.json
  • src/lib/reactions.ts
  • src/routeTree.gen.ts
  • src/routes/_authenticated/notifications.tsx
  • src/routes/_authenticated/settings.tsx
  • src/routes/anime/$slug.tsx
  • src/routes/book/$slug.tsx
  • src/routes/feed.tsx
  • src/routes/game/$slug.tsx
  • src/routes/manga/$slug.tsx
  • src/routes/movie/$slug.tsx
  • src/routes/post/$id.tsx
  • src/routes/review/$id.tsx
  • src/routes/search.tsx
  • src/routes/tv/$slug.tsx
  • src/routes/user/$username/index.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • TrackGeek/api (manual) → reviewed against open PR #68 feat/activity instead of the default branch
💤 Files with no reviewable changes (21)
  • src/components/pages/feed/listFollowing.tsx
  • src/routes/review/$id.tsx
  • src/components/pages/feed/entry.tsx
  • src/components/pages/details/review.tsx
  • src/components/pages/user/tabs/overview/favorites-card.tsx
  • src/components/pages/feed/item.tsx
  • src/components/pages/user/cast-favorite-card.tsx
  • src/components/pages/user/studio-card.tsx
  • src/components/pages/user/tabs/overview/about-card.tsx
  • src/routes/post/$id.tsx
  • src/components/pages/user/tabs/game.tsx
  • src/components/pages/user/tabs/manga.tsx
  • src/components/pages/feed/composer.tsx
  • src/components/pages/user/tabs/anime.tsx
  • src/components/pages/user/tabs/activity.tsx
  • src/components/pages/feed/review.tsx
  • src/components/pages/user/tabs/movie.tsx
  • src/components/pages/feed/comment-thread.tsx
  • src/components/pages/user/tabs/serie.tsx
  • src/components/pages/user/tabs/book.tsx
  • src/components/pages/user/tabs/overview/activity-card.tsx
🛑 Comments failed to post (8)
src/components/pages/details/review-item.tsx (2)

127-134: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a concise image description instead of the full review text as alt.

alt={reviewText} puts the entire (potentially very long) review body into the cover image's alt attribute, which screen readers will read out in full instead of describing the image. Use reviewName or a generic label instead.

♻️ Proposed fix
                   <Image
                     src={coverURL}
                     layout="fullWidth"
                     aspectRatio={3 / 4}
-                    alt={reviewText}
+                    alt={reviewName ?? t("feed:cover")}
                     className="w-40 h-50 object-cover relative z-10"
                   />
📝 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.

                  <Image
                    src={coverURL}
                    layout="fullWidth"
                    aspectRatio={3 / 4}
                    alt={reviewName ?? t("feed:cover")}
                    className="w-40 h-50 object-cover relative z-10"
                  />
                </div>

266-271: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Likes" fallback affordance has no handler.

When reviewId is falsy, this block renders cursor-pointer and hover:text-red-500 styling implying it's clickable, but there's no onClick/keyboard handler — a dead UI affordance that misleads users.

src/components/pages/feed/user-following.tsx (1)

10-14: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trailing quote in avatar URL and missing username.

The avatar URL on line 12 ends with skinColor=Light' — the trailing ' is likely a typo that will produce a broken image. Additionally, username is omitted from the profile, so the user link in ActivityItem (line 78) navigates to /user/ with an empty string param.

🐛 Proposed fix
         profile={{
           avatarURL:
-            "https://avataaars.io/?avatarStyle=Circle&topType=LongHairStraight&accessoriesType=Blank&hairColor=BrownDark&facialHairType=Blank&clotheType=BlazerShirt&eyeType=Default&eyebrowType=Default&mouthType=Default&skinColor=Light'",
+            "https://avataaars.io/?avatarStyle=Circle&topType=LongHairStraight&accessoriesType=Blank&hairColor=BrownDark&facialHairType=Blank&clotheType=BlazerShirt&eyeType=Default&eyebrowType=Default&mouthType=Default&skinColor=Light",
           name: "John Doe",
+          username: "johndoe",
         }}
📝 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.

        profile={{
          avatarURL:
            "https://avataaars.io/?avatarStyle=Circle&topType=LongHairStraight&accessoriesType=Blank&hairColor=BrownDark&facialHairType=Blank&clotheType=BlazerShirt&eyeType=Default&eyebrowType=Default&mouthType=Default&skinColor=Light",
          name: "John Doe",
          username: "johndoe",
        }}
src/components/pages/user/overview-tab/favorite-card.tsx (1)

110-122: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove button is invisible on touch devices.

The remove button uses opacity-0 group-hover:opacity-100, relying on hover to reveal it. Touch devices have no hover state, so the button remains invisible — users can't see it and may accidentally tap it (it's still clickable at opacity-0).

Consider always showing the button on touch-capable devices or making it visible by default on smaller screens.

Proposed fix
-          className="absolute top-2 right-2 z-10 flex size-7 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100 disabled:opacity-50"
+          className="absolute top-2 right-2 z-10 flex size-7 items-center justify-center rounded-full bg-black/60 text-white opacity-100 md:opacity-0 transition-opacity hover:bg-black/80 md:group-hover:opacity-100 disabled:opacity-50"
📝 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.

      {onRemove && (
        <button
          type="button"
          disabled={isRemoving}
          onClick={(e) => {
            e.preventDefault();
            e.stopPropagation();
            onRemove(item);
          }}
          className="absolute top-2 right-2 z-10 flex size-7 items-center justify-center rounded-full bg-black/60 text-white opacity-100 md:opacity-0 transition-opacity hover:bg-black/80 md:group-hover:opacity-100 disabled:opacity-50"
        >
          <Icon icon="lucide:x" className="size-4" />
        </button>
src/components/pages/user/screenshots-tab.tsx (1)

4-42: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded mock data with identical screenshots across all games.

gamesWithScreenshots is hardcoded inside the component body with all four games sharing the exact same screenshot URLs (sc5rik.webp, sc5ril.webp). The component also takes no userId prop, unlike UserFavoritesTab and UserListsTab, so it cannot fetch real data.

Additionally, defining this array inside the component causes it to be reallocated on every render. At minimum, move it outside the component; ideally, wire it to a real data source.

Proposed fix: move data outside component
+const gamesWithScreenshots = [
+  {
+    id: "a1",
+    title: "Romeo is a Dead Man",
+    image: "https://images.igdb.com/igdb/image/upload/t_original/coakmt.webp",
+    images: [
+      "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
+      "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
+    ],
+  },
+  // ...
+];
+
 export function UserScreenshotsTab() {
-  const gamesWithScreenshots = [
-    {
-      id: "a1",
-      title: "Romeo is a Dead Man",
-      image: "https://images.igdb.com/igdb/image/upload/t_original/coakmt.webp",
-      images: [
-        "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
-        "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
-      ],
-    },
-    // ...
-  ];
-
   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.

const gamesWithScreenshots = [
  {
    id: "a1",
    title: "Romeo is a Dead Man",
    image: "https://images.igdb.com/igdb/image/upload/t_original/coakmt.webp",
    images: [
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
    ],
  },
  {
    id: "a2",
    title: "Soul Hackers 2",
    image: "https://images.igdb.com/igdb/image/upload/t_original/co4hzh.webp",
    images: [
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
    ],
  },
  {
    id: "a3",
    title: "Grand Theft Auto VI",
    image: "https://images.igdb.com/igdb/image/upload/t_original/co9rwo.webp",
    images: [
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
    ],
  },
  {
    id: "a4",
    title: "Call of Duty: Black Ops 7",
    image: "https://images.igdb.com/igdb/image/upload/t_original/co9xwv.webp",
    images: [
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5rik.webp",
      "https://images.igdb.com/igdb/image/upload/t_720p/sc5ril.webp",
    ],
  },
];

export function UserScreenshotsTab() {
  return (
src/components/ui/card.tsx (1)

9-9: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all Card consumers to assess regression scope
rg -l --type=ts,tsx '<Card[ >]' src/ | head -40

Repository: TrackGeek/web

Length of output: 182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== card component =="
cat -n src/components/ui/card.tsx

echo
echo "== card consumers =="
rg -n --glob '**/*.{ts,tsx}' '<Card[ >]' src || true

echo
echo "== card subcomponent usage =="
rg -n --glob '**/*.{ts,tsx}' 'Card(Header|Content|Footer|Action)' src || true

Repository: TrackGeek/web

Length of output: 11816


Keep the default Card spacing unchanged here unless this is a deliberate global redesign.
This gap-4 update affects every Card stack (CardHeader/CardContent/CardFooter/CardAction) in the app, so any cards not explicitly migrated in this PR will render tighter than before. If the intent is only to adjust a few screens, override spacing locally instead.

src/hooks/activity.ts (1)

40-58: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cache can go stale if delete succeeds but the follow-up post fails.

The delete-then-create sequence isn't atomic, and onSuccess only fires when the whole mutation resolves. If the reaction is deleted but the subsequent api.post fails, the query cache is never invalidated: the UI keeps the stale currentReaction, and a retry will re-issue deleteReaction for an ID that's already gone server-side.

♻️ Proposed fix: invalidate on settle instead of only on success
     onSuccess: () => queryClient.invalidateQueries({ queryKey: userActivitiesQueryKey(userId) }),
+    onSettled: () => queryClient.invalidateQueries({ queryKey: userActivitiesQueryKey(userId) }),
📝 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.

export function useToggleActivityReaction(userId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async ({ activityId, currentReaction, emoji }: ToggleActivityReactionArgs) => {
      if (currentReaction) {
        await api.delete(apiEndpoints.deleteReaction(currentReaction.id));
        if (currentReaction.emoji === emoji) return;
      }

      await api.post(apiEndpoints.addReaction, {
        type: "Activity",
        emoji,
        activityId,
      });
    },
    onSuccess: () => queryClient.invalidateQueries({ queryKey: userActivitiesQueryKey(userId) }),
    onSettled: () => queryClient.invalidateQueries({ queryKey: userActivitiesQueryKey(userId) }),
  });
}
src/routes/_authenticated/settings.tsx (1)

415-418: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix typo in placeholder text: "Jhon Doe" → "John Doe".

The name placeholder contains a typo. Since this is a user-facing string, it should be corrected. Ideally this should also use an i18n key for consistency with the rest of the file.

✏️ Proposed fix
-              placeholder="Jhon Doe"
+              placeholder="John Doe"
📝 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.

            <Input
              id="name"
              type="text"
              placeholder="John Doe"

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