Skip to content

Conversation

@ameer2468
Copy link
Contributor

@ameer2468 ameer2468 commented Aug 21, 2025

Summary by CodeRabbit

  • New Features

    • Unified video cards in folders to a single, consistent card.
    • Added visible loading state for analytics on cap cards.
    • Video thumbnails now accept numeric durations for improved compatibility.
  • Bug Fixes

    • More reliable analytics counts with resilient per-video fetching.
    • Proper handling when analytics are unavailable or disabled.
    • Ensured analytics refresh on page/view mount for up-to-date data.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 21, 2025

Walkthrough

Refactors analytics fetching in Caps to per-video concurrent requests with stable caching and loading state passed to CapCard. Unifies folder video cards to a single CapCard and derives user from context. Expands VideoThumbnail’s duration prop type. Adjusts folder data access to include public, branded IDs, and foldersData, and updates metadata typing.

Changes

Cohort / File(s) Summary
Analytics fetch refactor (Caps)
apps/web/app/(org)/dashboard/caps/Caps.tsx
Switch from single query to per-video fetch with Promise.allSettled, stable queryKey using sorted videoIds, early exits, per-video error handling, refetchOnMount true, isLoadingAnalytics surfaced to CapCard.
CapCard props and wiring
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx, apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
Add required isLoadingAnalytics prop to CapCard and CapCardAnalytics types; CapCard forwards prop to CapCardAnalytics; rendering logic otherwise unchanged.
Folder videos rendering simplification
apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx
Replace Shared/Client cards with single CapCard usage; remove userId prop, derive user from Dashboard context; adjust imports; tweak analytics query options (remove staleTime, add refetchOnMount).
Video thumbnail API
apps/web/components/VideoThumbnail.tsx
videoDuration prop widened to string
Folder data shape and typing
apps/web/lib/folder.ts
Add type-only Video import; change shared spaces map init; extend query to select/group by videos.public; return objects with branded id (VideoId), public, foldersData: [], stricter metadata typing, omit effectiveDate from result.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Caps as Caps.tsx
  participant RQ as React Query
  participant API as /api/analytics
  participant UI as CapCard

  User->>Caps: Open Caps page
  Caps->>RQ: useQuery(["analytics", sorted(videoIds)], queryFn)
  activate RQ
  RQ-->>Caps: isLoading = true
  Caps->>UI: Render CapCard(isLoadingAnalytics=true)
  par For each videoId
    RQ->>API: GET /api/analytics?videoId={id}
    API-->>RQ: { count } or error
  and
    RQ->>API: ... (concurrent per-video)
    API-->>RQ: ...
  end
  RQ-->>Caps: analyticsData { [videoId]: count }
  deactivate RQ
  Caps->>UI: Render CapCard(isLoadingAnalytics=false, analytics)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit taps keys with delighted flair,
Fetches per-video stats from the digital air.
One CapCard to rule them, tidy and bright,
Branded IDs hop in, durations set right.
In folders and caps, the numbers align—
Cache carrots crunchy, the dashboard divine. 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-view-analytics

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (1)

14-20: CapCardAnalytics: remove or implement unused props

Both isLoadingAnalytics and capId are declared in CapCardAnalyticsProps but never consumed by the component. Update the component to either use these props or remove them from the interface.

• File: apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx
– Lines 14–20:
ts interface CapCardAnalyticsProps { capId: string; ← declared but unused displayCount: number; totalComments: number; isLoadingAnalytics: boolean;← declared but unused totalReactions: number; }
• Component signature only destructures displayCount, totalComments, totalReactions; neither isLoadingAnalytics nor capId are referenced.

Refactor options (mandatory):

  1. Implement loading state using isLoadingAnalytics (e.g. render skeletons when true):
    export const CapCardAnalytics = Object.assign(
    -  ({ displayCount, totalComments, totalReactions }: CapCardAnalyticsProps) => (
    +  ({
    +    displayCount,
    +    totalComments,
    +    totalReactions,
    +    isLoadingAnalytics,
    +  }: CapCardAnalyticsProps) =>
    +    isLoadingAnalytics ? (
          <Shell>
            <SkeletonItem icon={faEye} />
            <SkeletonItem icon={faComment} />
            <SkeletonItem icon={faSmile} />
          </Shell>
    +    ) : (
            <Shell>
              <Tooltip content={`${displayCount} unique views`}>
                <IconItem icon={faEye}>
                  <span className="text-sm text-gray-12">{displayCount}</span>
                </IconItem>
              </Tooltip>
              {/* … */}
            </Shell>
    +    ),
      {
        Skeleton: () => (
          <Shell>
  2. Remove unused props if consumers don’t need them:
    • Delete isLoadingAnalytics and/or capId from CapCardAnalyticsProps and any parent callers.
apps/web/lib/folder.ts (1)

216-221: sharedOrganizations filter is ineffective; null IDs can leak through

Because the column is left-joined, MySQL can yield [ { id: null, name: null, iconUrl: null } ]. The current predicate organization.id !== null is a no-op under the stated TypeScript type (id: string) and won’t filter out nulls at runtime.

Harden with a type guard:

-      sharedOrganizations: Array.isArray(video.sharedOrganizations)
-        ? video.sharedOrganizations.filter(
-            (organization) => organization.id !== null,
-          )
-        : [],
+      sharedOrganizations: Array.isArray(video.sharedOrganizations)
+        ? (video.sharedOrganizations as Array<{
+            id: unknown;
+            name: unknown;
+            iconUrl: unknown;
+          }>).filter(
+            (o): o is { id: string; name: string; iconUrl: string } =>
+              !!o &&
+              typeof o.id === "string" &&
+              typeof o.name === "string" &&
+              typeof o.iconUrl === "string",
+          )
+        : [],

Optional (SQL-side): emit only non-null org rows by aggregating in a subquery that inner-joins sharedVideos -> organizations and left-joins that JSON aggregate back to videos, then COALESCE to JSON_ARRAY(). That removes the need for runtime filtering.

🧹 Nitpick comments (9)
apps/web/components/VideoThumbnail.tsx (3)

16-17: Prop now accepts number — good; consider making formatter accept number | string.

Broadening videoDuration to string | number is reasonable. Instead of coercing at the call site, update formatDuration to accept number | string to avoid repeated .toString() and to make the contract explicit.

Apply:

-const formatDuration = (duration: string) => {
-  const durationMs = parseFloat(duration);
+const formatDuration = (duration: string | number) => {
+  const durationMs =
+    typeof duration === "number" ? duration : Number.parseFloat(duration);

Then you can pass videoDuration directly without string conversion (see Line 113).


111-115: Remove unnecessary string conversion at render.

Once formatDuration accepts number | string, you can drop .toString(). This also avoids unnecessary allocations on every render.

-  {formatDuration(videoDuration.toString())}
+  {formatDuration(videoDuration)}

80-85: Prevent gradient “flicker” by memoizing the random fallback background.

randomGradient is re-generated on every render, causing visual flicker in the error state. Memoize once per mount.

-import { memo, useEffect, useRef, useState } from "react";
+import { memo, useEffect, useMemo, useRef, useState } from "react";

-const randomGradient = `linear-gradient(to right, ${generateRandomGrayScaleColor()}, ${generateRandomGrayScaleColor()})`;
+const randomGradient = useMemo(
+  () =>
+    `linear-gradient(to right, ${generateRandomGrayScaleColor()}, ${generateRandomGrayScaleColor()})`,
+  [],
+);
apps/web/app/(org)/dashboard/caps/Caps.tsx (2)

84-85: Stabilize and de-duplicate the analytics key with useMemo.

Sorting the IDs improves cache stability. Also memoize the sorted array to avoid re-sorting every render.

-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";

-const videoIds = data.map((video) => video.id).sort();
+const videoIds = useMemo(
+  () => data.map((video) => video.id).sort(),
+  [data],
+);

86-129: Make the query conditional and trim GET headers.

  • Use enabled to skip the query entirely when dubApiKeyEnabled is false or data is empty (avoids constructing promises and logging).
  • Content-Type on a GET request is unnecessary; drop it to avoid odd proxies/CORS behavior.
  • Optional: if data can be large, consider a batch endpoint or a small concurrency limit.
-const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
-  queryKey: ["analytics", videoIds],
-  queryFn: async () => {
-    if (!dubApiKeyEnabled || data.length === 0) {
-      return {};
-    }
+const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
+  queryKey: ["analytics", videoIds],
+  enabled: dubApiKeyEnabled && data.length > 0,
+  queryFn: async () => {
     const analyticsPromises = data.map(async (video) => {
       try {
-        const response = await fetch(`/api/analytics?videoId=${video.id}`, {
-          method: "GET",
-          headers: {
-            "Content-Type": "application/json",
-          },
-        });
+        const response = await fetch(`/api/analytics?videoId=${video.id}`);

If batching becomes feasible later, a single route like /api/analytics?ids=... will materially reduce request overhead.

apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (3)

31-32: Remove unused activeOrganization destructuring.

activeOrganization is not used in this component and can be dropped to appease linters and reduce noise.

-const { activeOrganization, user } = useDashboardContext();
+const { user } = useDashboardContext();

105-147: Align analytics query key with Caps.tsx and gate with enabled.

Use a sorted, memoized list of IDs to stabilize caching and add enabled to skip the query when not applicable.

+import { useMemo } from "react";
@@
-const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
-  queryKey: ["analytics", initialVideos.map((video) => video.id)],
+const videoIds = useMemo(
+  () => initialVideos.map((v) => v.id).sort(),
+  [initialVideos],
+);
+const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({
+  queryKey: ["analytics", videoIds],
+  enabled: dubApiKeyEnabled && initialVideos.length > 0,
   queryFn: async () => {
-    if (!dubApiKeyEnabled || initialVideos.length === 0) {
-      return {};
-    }
+    // fetch logic unchanged

Also consider removing the Content-Type header for GET requests here as in Caps.tsx.


146-147: refetchOnMount: true is fine; confirm intended UX.

This forces an analytics refresh whenever the section remounts. If the page often re-mounts this component, this may increase network churn; otherwise OK for freshness.

apps/web/lib/folder.ts (1)

116-119: Initialization is correct; consider DRY-ing with a small helper

The explicit initialization avoids ??= semantics—fine. You can reduce duplication across both loops with a tiny helper.

 async function getSharedSpacesForVideos(videoIds: string[]) {
+  const ensureBucket = <T,>(map: Record<string, T[]>, key: string) => {
+    if (!map[key]) map[key] = [];
+    return map[key]!;
+  };-  spaceSharing.forEach((space) => {
-    if (!sharedSpacesMap[space.videoId]) {
-      sharedSpacesMap[space.videoId] = [];
-    }
-    sharedSpacesMap[space.videoId].push({
+  spaceSharing.forEach((space) => {
+    ensureBucket(sharedSpacesMap, space.videoId).push({
       id: space.id,
       name: space.name,
       organizationId: space.organizationId,
       iconUrl: space.iconUrl || "",
       isOrg: false,
     });
   });
…
-  orgSharing.forEach((org) => {
-    if (!sharedSpacesMap[org.videoId]) {
-      sharedSpacesMap[org.videoId] = [];
-    }
-    sharedSpacesMap[org.videoId].push({
+  orgSharing.forEach((org) => {
+    ensureBucket(sharedSpacesMap, org.videoId).push({
       id: org.id,
       name: org.name,
       organizationId: org.organizationId,
       iconUrl: org.iconUrl || "",
       isOrg: true,
     });
   });

Also applies to: 130-134

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8277982 and f7df7ce.

📒 Files selected for processing (6)
  • apps/web/app/(org)/dashboard/caps/Caps.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (3 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (3 hunks)
  • apps/web/components/VideoThumbnail.tsx (2 hunks)
  • apps/web/lib/folder.ts (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx (4)
apps/web/app/(org)/dashboard/caps/Caps.tsx (1)
  • VideoData (27-47)
apps/web/app/(org)/dashboard/caps/UploadingContext.tsx (1)
  • useUploadingContext (21-29)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
  • useDashboardContext (42-42)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)
  • CapCard (79-474)
apps/web/lib/folder.ts (2)
packages/database/schema.ts (1)
  • videos (231-272)
packages/web-domain/src/Video.ts (3)
  • Video (13-35)
  • VideoId (9-9)
  • VideoId (10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
apps/web/app/(org)/dashboard/caps/Caps.tsx (1)

336-337: Prop pass-through LGTM.

isLoadingAnalytics={isLoadingAnalytics} aligns with the new analytics loading state.

apps/web/lib/folder.ts (3)

154-154: Including videos.public in both SELECT and GROUP BY is correct

Given the aggregates in this query, adding videos.public to the projection and GROUP BY is necessary for SQL correctness and aligns with the schema’s publicIndex. No issues here.

Also applies to: 191-191


225-231: Good move: switch metadata index signature to unknown

Changing [key: string]: any to [key: string]: unknown prevents accidental unsafe access and forces narrowing at use sites. Looks good.


233-233: Downstream foldersData usage confirmed safe
I’ve audited all occurrences of foldersData in lib/folder.ts, both dashboard space pages, the caps page, and the Caps component—the prop is strongly typed as FolderDataType[] and always passed as an array. No code assumes it can be undefined or non‐array, so defaulting to [] won’t introduce scattered runtime guards.

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.

2 participants