feat: activity#125
Conversation
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (66)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
💤 Files with no reviewable changes (21)
📝 WalkthroughWalkthroughThis 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)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
isRemovingdisables all cards during a single removal.
removeFavorite.isPendingis a mutation-level flag. When one favorite is removed, every card's remove button becomes disabled, which is confusing UX. Compare the mutation'svariablesto 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 valueRemove the unrelated
paymentIdvalidateSearch.The
validateSearchaccepts apaymentIdsearch 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 valueSimplify the
imageURLnormalization.
(item.imageURL ?? null)?.replace(...)is redundant —?? nullconvertsundefinedtonull, 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 valueCardTitle now has
flex items-center gap-2built in — check for redundant consumer classes.The notifications route (
src/routes/_authenticated/notifications.tsx:59) appliesclassName="flex items-center gap-2"toCardTitle, which is now redundant sinceCardTitleincludes those classes by default. WhiletwMergehandles 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 winAdd a
sandboxattribute 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 winInline query duplicates the hook pattern established for activities.
src/hooks/activity.tsexportsuseUserActivitiesfor the activities list, but the calendar query here is inlined directly in the component instead of following the same hook convention. Extracting auseUserActivityCalendarhook 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 winDuplicate 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 winExtract 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 valueRename interface to PascalCase.
TypeScript convention is PascalCase for interface names.
feedItemPropsshould beFeedItemProps.♻️ 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
📒 Files selected for processing (66)
src/components/pages/details/review-item.tsxsrc/components/pages/details/review.tsxsrc/components/pages/feed/activity-item.tsxsrc/components/pages/feed/comment-thread.tsxsrc/components/pages/feed/composer.tsxsrc/components/pages/feed/entry.tsxsrc/components/pages/feed/item.tsxsrc/components/pages/feed/listFollowing.tsxsrc/components/pages/feed/normalize.tssrc/components/pages/feed/review.tsxsrc/components/pages/feed/user-activity.tsxsrc/components/pages/feed/user-following.tsxsrc/components/pages/notifications/item.tsxsrc/components/pages/notifications/list-notification.tsxsrc/components/pages/user/activity-tab.tsxsrc/components/pages/user/cast-favorite-card.tsxsrc/components/pages/user/favorites-tab.tsxsrc/components/pages/user/lists-tab.tsxsrc/components/pages/user/overview-tab/about-card.tsxsrc/components/pages/user/overview-tab/activity-card.tsxsrc/components/pages/user/overview-tab/favorite-card.tsxsrc/components/pages/user/overview-tab/favorites-card.tsxsrc/components/pages/user/overview-tab/index.tsxsrc/components/pages/user/overview-tab/medals-card.tsxsrc/components/pages/user/overview-tab/statistics-card.tsxsrc/components/pages/user/reviews-tab.tsxsrc/components/pages/user/screenshots-tab.tsxsrc/components/pages/user/studio-card.tsxsrc/components/pages/user/tabs/activity.tsxsrc/components/pages/user/tabs/anime.tsxsrc/components/pages/user/tabs/book.tsxsrc/components/pages/user/tabs/game.tsxsrc/components/pages/user/tabs/manga.tsxsrc/components/pages/user/tabs/movie.tsxsrc/components/pages/user/tabs/overview/about-card.tsxsrc/components/pages/user/tabs/overview/activity-card.tsxsrc/components/pages/user/tabs/overview/favorites-card.tsxsrc/components/pages/user/tabs/serie.tsxsrc/components/shared/cards/card.tsxsrc/components/shared/comments/index.tsxsrc/components/shared/header/user-dropdown.tsxsrc/components/ui/card.tsxsrc/hooks/activity.tssrc/hooks/notification.tssrc/lib/api.tssrc/lib/i18n/locales/en-US/common.jsonsrc/lib/i18n/locales/en-US/feed.jsonsrc/lib/i18n/locales/en-US/notifications.jsonsrc/lib/i18n/locales/en-US/settings.jsonsrc/lib/i18n/locales/en-US/user.jsonsrc/lib/i18n/locales/pt-BR/notifications.jsonsrc/lib/reactions.tssrc/routeTree.gen.tssrc/routes/_authenticated/notifications.tsxsrc/routes/_authenticated/settings.tsxsrc/routes/anime/$slug.tsxsrc/routes/book/$slug.tsxsrc/routes/feed.tsxsrc/routes/game/$slug.tsxsrc/routes/manga/$slug.tsxsrc/routes/movie/$slug.tsxsrc/routes/post/$id.tsxsrc/routes/review/$id.tsxsrc/routes/search.tsxsrc/routes/tv/$slug.tsxsrc/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#68feat/activityinstead 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
There was a problem hiding this comment.
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
isRemovingdisables all cards during a single removal.
removeFavorite.isPendingis a mutation-level flag. When one favorite is removed, every card's remove button becomes disabled, which is confusing UX. Compare the mutation'svariablesto 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 valueRemove the unrelated
paymentIdvalidateSearch.The
validateSearchaccepts apaymentIdsearch 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 valueSimplify the
imageURLnormalization.
(item.imageURL ?? null)?.replace(...)is redundant —?? nullconvertsundefinedtonull, 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 valueCardTitle now has
flex items-center gap-2built in — check for redundant consumer classes.The notifications route (
src/routes/_authenticated/notifications.tsx:59) appliesclassName="flex items-center gap-2"toCardTitle, which is now redundant sinceCardTitleincludes those classes by default. WhiletwMergehandles 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 winAdd a
sandboxattribute 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 winInline query duplicates the hook pattern established for activities.
src/hooks/activity.tsexportsuseUserActivitiesfor the activities list, but the calendar query here is inlined directly in the component instead of following the same hook convention. Extracting auseUserActivityCalendarhook 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 winDuplicate 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 winExtract 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 valueRename interface to PascalCase.
TypeScript convention is PascalCase for interface names.
feedItemPropsshould beFeedItemProps.♻️ 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
📒 Files selected for processing (66)
src/components/pages/details/review-item.tsxsrc/components/pages/details/review.tsxsrc/components/pages/feed/activity-item.tsxsrc/components/pages/feed/comment-thread.tsxsrc/components/pages/feed/composer.tsxsrc/components/pages/feed/entry.tsxsrc/components/pages/feed/item.tsxsrc/components/pages/feed/listFollowing.tsxsrc/components/pages/feed/normalize.tssrc/components/pages/feed/review.tsxsrc/components/pages/feed/user-activity.tsxsrc/components/pages/feed/user-following.tsxsrc/components/pages/notifications/item.tsxsrc/components/pages/notifications/list-notification.tsxsrc/components/pages/user/activity-tab.tsxsrc/components/pages/user/cast-favorite-card.tsxsrc/components/pages/user/favorites-tab.tsxsrc/components/pages/user/lists-tab.tsxsrc/components/pages/user/overview-tab/about-card.tsxsrc/components/pages/user/overview-tab/activity-card.tsxsrc/components/pages/user/overview-tab/favorite-card.tsxsrc/components/pages/user/overview-tab/favorites-card.tsxsrc/components/pages/user/overview-tab/index.tsxsrc/components/pages/user/overview-tab/medals-card.tsxsrc/components/pages/user/overview-tab/statistics-card.tsxsrc/components/pages/user/reviews-tab.tsxsrc/components/pages/user/screenshots-tab.tsxsrc/components/pages/user/studio-card.tsxsrc/components/pages/user/tabs/activity.tsxsrc/components/pages/user/tabs/anime.tsxsrc/components/pages/user/tabs/book.tsxsrc/components/pages/user/tabs/game.tsxsrc/components/pages/user/tabs/manga.tsxsrc/components/pages/user/tabs/movie.tsxsrc/components/pages/user/tabs/overview/about-card.tsxsrc/components/pages/user/tabs/overview/activity-card.tsxsrc/components/pages/user/tabs/overview/favorites-card.tsxsrc/components/pages/user/tabs/serie.tsxsrc/components/shared/cards/card.tsxsrc/components/shared/comments/index.tsxsrc/components/shared/header/user-dropdown.tsxsrc/components/ui/card.tsxsrc/hooks/activity.tssrc/hooks/notification.tssrc/lib/api.tssrc/lib/i18n/locales/en-US/common.jsonsrc/lib/i18n/locales/en-US/feed.jsonsrc/lib/i18n/locales/en-US/notifications.jsonsrc/lib/i18n/locales/en-US/settings.jsonsrc/lib/i18n/locales/en-US/user.jsonsrc/lib/i18n/locales/pt-BR/notifications.jsonsrc/lib/reactions.tssrc/routeTree.gen.tssrc/routes/_authenticated/notifications.tsxsrc/routes/_authenticated/settings.tsxsrc/routes/anime/$slug.tsxsrc/routes/book/$slug.tsxsrc/routes/feed.tsxsrc/routes/game/$slug.tsxsrc/routes/manga/$slug.tsxsrc/routes/movie/$slug.tsxsrc/routes/post/$id.tsxsrc/routes/review/$id.tsxsrc/routes/search.tsxsrc/routes/tv/$slug.tsxsrc/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#68feat/activityinstead 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. UsereviewNameor 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
reviewIdis falsy, this block renderscursor-pointerandhover:text-red-500styling implying it's clickable, but there's noonClick/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,usernameis omitted from the profile, so the user link inActivityItem(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 atopacity-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.
gamesWithScreenshotsis hardcoded inside the component body with all four games sharing the exact same screenshot URLs (sc5rik.webp,sc5ril.webp). The component also takes nouserIdprop, unlikeUserFavoritesTabandUserListsTab, 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 -40Repository: 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 || trueRepository: TrackGeek/web
Length of output: 11816
Keep the default Card spacing unchanged here unless this is a deliberate global redesign.
Thisgap-4update affects everyCardstack (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
onSuccessonly fires when the whole mutation resolves. If the reaction is deleted but the subsequentapi.postfails, the query cache is never invalidated: the UI keeps the stalecurrentReaction, and a retry will re-issuedeleteReactionfor 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"
Description
Acknowledgements
npm run checkScreenshots
Proof showing the creation/modification is working as expected