Skip to content

Conversation

@mikeallisonJS
Copy link
Collaborator

@mikeallisonJS mikeallisonJS commented Jan 3, 2026

Summary by CodeRabbit

  • New Features

    • Added type declarations for static asset imports (images, SVGs)
    • Enhanced video player page with improved error handling using proper not-found responses
    • Added new audio and download management components
  • Dependency Updates

    • Upgraded Next.js to 16.0.10
    • Upgraded React to 19.2.3
    • Upgraded Storybook to 10.1.3
    • Updated Nx and related tooling packages
  • Build Improvements

    • Enabled React Compiler at top-level configuration
    • Enabled webpack support for Next.js builds
    • Enhanced TypeScript module resolution with esnext and bundler settings
    • Enforced ESLint during builds across all applications

✏️ Tip: You can customize this high-level summary in your review settings.

mikeallisonJS and others added 30 commits October 31, 2025 18:05
- Updated @playwright/test and playwright packages to version 1.57.0.
- Added @t3-oss/env-core dependency at version ^0.13.8.
- Updated all @nx packages to version 22.1.3.
- Updated @storybook/nextjs and storybook to version 10.1.3.
- Updated browserslist to version 4.27.0.
- Updated various other dependencies in pnpm-lock.yaml.
…ultiple apps

- Updated output paths and enabled webpack in project.json files for arclight, journeys, journeys-admin, and watch apps.
- Modified TypeScript configurations to use "esnext" module and "bundler" module resolution for arclight, journeys, journeys-admin, and videos-admin.
- Adjusted next-env.d.ts files to reference updated routes paths.
- Refactored route.ts in arclight to import GET_SHORT_LINK_QUERY from a separate file.
- Enhanced error handling in videoPlayerUrl/page.tsx by utilizing notFound() for missing parameters.
- Updated `react-dom`, `react-is`, and `nx` to version 19.2.3 and 22.1.3 respectively in `package.json`.
- Adjusted `pnpm-lock.yaml` to reflect the updated versions and dependencies.
- Refactored `.kube/config` for better clarity and structure, including updates to context and user configurations.
- Modified `next-env.d.ts` files across multiple apps to use import statements for route types.
- Simplified `next.config.js` and `next.config.mjs` by removing experimental flags and ensuring consistent configuration settings.
- Converted cluster and context entries to a list format for better readability.
- Updated current context to 'prod' for immediate use.
- Maintained user authentication configurations for both production and staging clusters.
- Converted cluster and context entries to a list format for better readability.
- Updated current context to 'prod' for immediate use.
- Maintained user authentication configurations for both production and staging clusters.
- Changed import statements from 'playwright-core' to '@playwright/test' across multiple files for consistency.
- Updated package version of '@playwright/test' from 1.57.0 to 1.56.1 in `package.json` and `pnpm-lock.yaml` to align with the new import structure.
- Added `useParams` to retrieve `videoId` and `variantId` in the VariantDialog component for improved routing.
- Updated test mocks to include `useParams` for consistent testing of route parameters.
…types

- Replaced import statements in multiple next-env.d.ts files to use a consistent format.
- Added eslint-disable comments to prevent linting errors related to module boundaries and path segments.
- Added a rule to allow literal strings in the ESLint configuration for the arclight app.
- Updated eslint-disable comments in next-env.d.ts to include 'import/no-relative-packages'.
- Removed unnecessary blank lines in getShortLinkQuery.ts.
- Added a missing import statement for graphql in page.tsx.
- Improved code organization in route.ts by adding a blank line for better readability.
- Removed unnecessary next-env.d.ts files from multiple apps.
- Added typedRoutes configuration to next.config.js and next.config.mjs for arclight, journeys, player, watch, and watch-modern apps.
- Updated .gitignore to exclude next-env.d.ts files and added .next/types/**/*.ts to tsconfig.json for relevant apps.
…@nx/vitest dependency

- Changed the test executor in the jf-proxy project configuration to use @nx/vitest:test.
- Added @nx/vitest as a dependency in package.json.
- Updated pnpm-lock.yaml to include @nx/vitest version 22.1.3.
- Updated the journey object in OnboardingTemplateCard.spec.tsx to include additional properties: website, journeyCustomizationDescription, and journeyCustomizationFields.
- Ensured the journey object is correctly typed as Journey for better type safety in tests.
- Deleted addByFileUtils.ts and addByFileUtils.spec.ts as they are no longer needed.
- Removed testFile.mp4, which was used in the deleted tests.
…ariantDownloadDialogClient components

- Reformatted function parameters for better alignment and clarity.
- Removed unnecessary blank lines to streamline the code structure.
@blacksmith-sh
Copy link
Contributor

blacksmith-sh bot commented Jan 3, 2026

Found 8 test failures on Blacksmith runners:

Failures

Test View Logs
AddVideoVariantDownloadDialog/changes button text based on selected quality option View Logs
AddVideoVariantDownloadDialog/navigates back when dialog is closed View Logs
AddVideoVariantDownloadDialog/renders the add download dialog with form elements View Logs
AddVideoVariantDownloadDialog/shows auto generate option when Mux video is available View Logs
DeleteAudio/calls delete mutation when delete button is clicked View Logs
DeleteAudio/navigates back when cancel button is clicked View Logs
DeleteAudio/renders the delete audio dialog with confirmation message View Logs
DeleteAudio/shows loading state when mutation is in progress View Logs

Fix in Cursor

@github-actions github-actions bot temporarily deployed to Preview - resources January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - short-links January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - journeys-admin January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - player January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - videos-admin January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - watch-modern January 3, 2026 00:10 Inactive
@github-actions github-actions bot temporarily deployed to Preview - journeys January 3, 2026 00:10 Inactive
@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
docs ✅ Ready docs preview Sat Jan 3 13:12:20 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
short-links ✅ Ready short-links preview Sat Jan 3 13:12:23 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
player ✅ Ready player preview Sat Jan 3 13:12:33 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch-modern ✅ Ready watch-modern preview Sat Jan 3 13:12:33 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
resources ✅ Ready resources preview Sat Jan 3 13:12:39 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Sat Jan 3 13:12:41 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
videos-admin ✅ Ready videos-admin preview Sat Jan 3 13:57:12 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Sat Jan 3 13:12:50 NZDT 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys ✅ Ready journeys preview Sat Jan 3 13:16:06 NZDT 2026

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: 10

Caution

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

⚠️ Outside diff range comments (3)
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/layout.tsx (1)

129-132: Missing dependencies in useEffect.

The effect references refetch and reloadOnPathChange but only lists pathname in the dependency array. This can lead to stale closures.

🔎 Proposed fix
   useEffect(() => {
     if (reloadOnPathChange) void refetch()
     setReloadOnPathChange(pathname?.includes('download') ?? false)
-  }, [pathname])
+  }, [pathname, refetch, reloadOnPathChange])

Note: If refetch identity changes on each render, you may need to wrap this logic differently or memoize refetch. Verify with your Apollo Client setup.

apps/videos-admin/src/app/(dashboard)/videos/[videoId]/editions/layout.tsx (2)

59-64: Missing dependency in useEffect.

The useEffect uses refetch but doesn't include it in the dependency array, which violates the exhaustive-deps rule and can cause stale closures.

🔎 Proposed fix
   useEffect(() => {
     if (reloadOnPathChange) void refetch()
     setReloadOnPathChange(
       (pathname?.includes('add') || pathname?.includes('delete')) ?? false
     )
-  }, [pathname])
+  }, [pathname, refetch, reloadOnPathChange])

87-104: Add accessibility attributes to interactive Box.

The clickable Box lacks keyboard navigation and ARIA attributes required by the coding guidelines. Interactive elements should support both mouse and keyboard interactions.

🔎 Proposed fix
               <Box
                 key={edition.id}
                 sx={{
                   border: '1px solid',
                   borderColor: 'divider',
                   borderRadius: 1,
                   minWidth: 200,
                   bgcolor: 'background.paper',
                   '&:hover': {
                     borderColor: 'action.hover',
                     cursor: 'pointer'
                   }
                 }}
                 onClick={() =>
                   router.push(`/videos/${videoId}/editions/${edition.id}`, {
                     scroll: false
                   })
                 }
+                onKeyDown={(e) => {
+                  if (e.key === 'Enter' || e.key === ' ') {
+                    e.preventDefault()
+                    router.push(`/videos/${videoId}/editions/${edition.id}`, {
+                      scroll: false
+                    })
+                  }
+                }}
+                role="button"
+                tabIndex={0}
+                aria-label={`View ${edition.name} edition`}
                 data-testid="EditionCard"
               >

As per coding guidelines, implement accessibility features on interactive elements.

♻️ Duplicate comments (1)
apps/journeys/project.json (1)

31-32: Verify the purpose of the webpack: true flag.

Same concern as raised in apps/arclight/project.json: this flag's purpose in the @nx/next:build executor needs verification to ensure it aligns with the intended build configuration.

🧹 Nitpick comments (11)
apps/watch/src/components/PageVideos/FilterList/LanguagesFilter/LanguagesFilter.stories.tsx (1)

71-74: Consider removing the no-op play function.

The play function currently returns undefined and serves no purpose. If interactive testing has been intentionally removed (as indicated by the removal of screen and userEvent imports), consider removing the play property entirely for cleaner code.

🔎 Proposed cleanup
 export const Default = {
-  ...Template,
-  play: async () => undefined
+  ...Template
 }
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/layout.spec.tsx (1)

310-313: Unused params prop being passed to component.

The params prop passed here is never consumed by VariantDialog — the component uses useParams() hook instead (line 91-95 in layout.tsx). This prop can be removed from the test, or the component should be updated to actually use the prop.

Since the component relies solely on useParams(), the test correctness depends on the mock at lines 202-205, not this prop.

🔎 Proposed simplification
     return render(
       <MockedProvider>
         <SnackbarProvider>
-          <VariantDialog
-            params={Promise.resolve({
-              variantId: mockVariantId,
-              videoId: mockVideoId
-            })}
-          >
+          <VariantDialog>
             <div>Child content</div>
           </VariantDialog>
         </SnackbarProvider>
       </MockedProvider>
     )

This requires also removing the params prop from VariantDialogProps in layout.tsx.

apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/layout.tsx (1)

91-95: Unsafe type assertion on useParams() result.

The type assertion as { variantId: string; videoId: string } assumes params always exist and are strings. If the component renders outside its expected route, this could cause runtime errors when accessing variantId or videoId.

🔎 Proposed fix with runtime guard
   const routeParams = useParams()
-  const { variantId, videoId } = routeParams as {
-    variantId: string
-    videoId: string
-  }
+  const variantId = routeParams.variantId as string | undefined
+  const videoId = routeParams.videoId as string | undefined
+
+  if (!variantId || !videoId) {
+    return null
+  }

Alternatively, use a type guard or early return to handle missing params gracefully.

apps/journeys-admin/proxy.spec.ts (1)

5-5: Update describe block to match the rename.

The describe block still references 'middleware' but the function under test is now proxy.

🔎 Proposed fix
-describe('middleware', () => {
+describe('proxy', () => {
apps/watch/proxy.spec.ts (1)

36-36: Update describe block to match the rename.

The describe block still references 'middleware' but the function under test is now proxy.

🔎 Proposed fix
-describe('middleware', () => {
+describe('proxy', () => {
apps/resources/proxy.spec.ts (1)

36-36: Update describe block to match the rename.

The describe block still references 'middleware' but the function under test is now proxy.

🔎 Proposed fix
-describe('middleware', () => {
+describe('proxy', () => {
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/editions/layout.tsx (1)

118-137: Consider simplifying the conditional delete logic.

The nested ternary for the delete action could be more readable by computing the actions object beforehand or using a clearer expression.

🔎 Proposed refactor
                   <ActionButton
                     actions={{
                       edit: () =>
                         router.push(
                           `/videos/${videoId}/editions/${edition.id}`,
                           {
                             scroll: false
                           }
                         ),
-                      delete: () =>
-                        edition.name === 'base'
-                          ? undefined
-                          : router.push(
-                              `/videos/${videoId}/editions/${edition.id}/delete`,
-                              {
-                                scroll: false
-                              }
-                            )
+                      delete:
+                        edition.name === 'base'
+                          ? undefined
+                          : () =>
+                              router.push(
+                                `/videos/${videoId}/editions/${edition.id}/delete`,
+                                {
+                                  scroll: false
+                                }
+                              )
                     }}
                   />
apps/videos-admin/src/app/layout.tsx (2)

21-23: Consider adding a loading fallback for better UX.

The fallback={null} means no UI is rendered while UserProviders fetches user data via getUser(). On slow connections or during auth initialization, this could result in a blank screen or flash of unstyled content.

🔎 Consider adding a minimal loading state
-<Suspense fallback={null}>
+<Suspense fallback={<div style={{ minHeight: '100vh' }} />}>
  <UserProviders>{children}</UserProviders>
</Suspense>

Or use a proper loading component if available:

-<Suspense fallback={null}>
+<Suspense fallback={<RootLayoutSkeleton />}>
  <UserProviders>{children}</UserProviders>
</Suspense>

29-48: Add return type annotation for clarity.

The async UserProviders component should have an explicit return type annotation for better type safety and code clarity.

🔎 Proposed type annotation
-async function UserProviders({ children }: { children: React.ReactNode }) {
+async function UserProviders({ 
+  children 
+}: { 
+  children: React.ReactNode 
+}): Promise<ReactElement> {
  const user = await getUser()

You'll need to import ReactElement:

-import { ReactNode, Suspense } from 'react'
+import { ReactElement, ReactNode, Suspense } from 'react'
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/download/[downloadId]/add/AddVideoVariantDownloadDialogClient.tsx (2)

189-230: Consider parallelizing download creation for better performance.

The three createVideoVariantDownload mutations are executed sequentially. If these operations are independent, consider using Promise.all() to execute them concurrently, which would reduce the total time for the auto-generation flow.

🔎 Proposed refactor
-          await createVideoVariantDownload({
+          await Promise.all([
+            createVideoVariantDownload({
-            variables: {
-              input: {
-                videoVariantId: variantId,
-                quality: 'high',
-                size: 0,
-                height: 720,
-                width: 1280,
-                url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/720p.mp4`,
-                version: 0,
-                assetId: null
-              }
-            }
-          })
-          await createVideoVariantDownload({
-            variables: {
-              input: {
-                videoVariantId: variantId,
-                quality: 'sd',
-                size: 0,
-                height: 360,
-                width: 640,
-                url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/360p.mp4`,
-                version: 0,
-                assetId: null
-              }
-            }
-          })
-          await createVideoVariantDownload({
-            variables: {
-              input: {
-                videoVariantId: variantId,
-                quality: 'low',
-                size: 0,
-                height: 270,
-                width: 480,
-                url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/270p.mp4`,
-                version: 0,
-                assetId: null
-              }
-            }
-          })
+              variables: {
+                input: {
+                  videoVariantId: variantId,
+                  quality: 'high',
+                  size: 0,
+                  height: 720,
+                  width: 1280,
+                  url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/720p.mp4`,
+                  version: 0,
+                  assetId: null
+                }
+              }
+            }),
+            createVideoVariantDownload({
+              variables: {
+                input: {
+                  videoVariantId: variantId,
+                  quality: 'sd',
+                  size: 0,
+                  height: 360,
+                  width: 640,
+                  url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/360p.mp4`,
+                  version: 0,
+                  assetId: null
+                }
+              }
+            }),
+            createVideoVariantDownload({
+              variables: {
+                input: {
+                  videoVariantId: variantId,
+                  quality: 'low',
+                  size: 0,
+                  height: 270,
+                  width: 480,
+                  url: `https://stream.mux.com/${data.videoVariant.muxVideo.playbackId}/270p.mp4`,
+                  version: 0,
+                  assetId: null
+                }
+              }
+            })
+          ])

338-352: Consider simplifying the custom validation logic.

The custom submit handler at lines 338-352 has complex conditional logic to skip file validation for 'auto' mode. While functional, this approach bypasses Formik's standard validation flow and may be confusing to maintainers.

Consider explicitly setting file validation to .nullable().optional() when quality is 'auto' (which is already done in the schema at lines 158-162), and rely on Formik's built-in validation instead of custom filtering.

message: 'Invalid start time parameter',
status: 400
}
return <p>Invalid start time parameter</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace HTML <p> tags with MUI components.

The error messages use plain HTML <p> elements, which violates the coding guidelines that require using MUI components instead of HTML elements in apps/**/*.{js,jsx,ts,tsx} files. Consider using MUI's Typography or Alert component for better consistency and user experience.

As per coding guidelines: "Always use MUI over HTML elements; avoid using CSS or tags."

🔎 Suggested refactor using MUI Typography
+import { Typography } from '@mui/material'

Then update the error returns:

  if (startTime != null && (isNaN(startTime) || startTime < 0)) {
-    return <p>Invalid start time parameter</p>
+    return <Typography>Invalid start time parameter</Typography>
  }

  if (endTime != null && (isNaN(endTime) || endTime < 0)) {
-    return <p>Invalid end time parameter</p>
+    return <Typography>Invalid end time parameter</Typography>
  }

  if (startTime != null && endTime != null && endTime <= startTime) {
-    return <p>End time must be greater than start time</p>
+    return <Typography>End time must be greater than start time</Typography>
  }

Alternatively, consider using Alert component for more prominent error display:

+import { Alert } from '@mui/material'
  if (startTime != null && (isNaN(startTime) || startTime < 0)) {
-    return <p>Invalid start time parameter</p>
+    return <Alert severity="error">Invalid start time parameter</Alert>
  }

Also applies to: 122-122, 126-126

🤖 Prompt for AI Agents
In apps/arclight/src/app/videoPlayerUrl/page.tsx around lines 118, 122 and 126,
replace the plain HTML <p> error messages with MUI components (e.g., import
Typography or Alert from @mui/material) and return those components instead;
update imports at the top to include the chosen MUI component, replace each
return <p>...</p> with <Typography variant="body1"
color="error">...</Typography> or <Alert severity="error">...</Alert>, keeping
the original text content and any translations, and ensure any necessary props
(variant/color/severity) match surrounding UI conventions.

Comment on lines +49 to +58
const [deleteVideoVariant, { loading }] = useMutation(DELETE_VIDEO_VARIANT, {
variables: { id: variantId },
onCompleted: () => {
enqueueSnackbar('Audio language deleted successfully', {
variant: 'success'
})

router.push(returnUrl, { scroll: false })
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for the mutation.

The mutation lacks an onError handler. If the delete operation fails (e.g., network error, permission denied), the user receives no feedback and the dialog remains open in an unclear state.

🔎 Proposed fix
   const [deleteVideoVariant, { loading }] = useMutation(DELETE_VIDEO_VARIANT, {
     variables: { id: variantId },
     onCompleted: () => {
       enqueueSnackbar('Audio language deleted successfully', {
         variant: 'success'
       })
 
       router.push(returnUrl, { scroll: false })
-    }
+    },
+    onError: (error) => {
+      enqueueSnackbar(error.message || 'Failed to delete audio language', {
+        variant: 'error'
+      })
+    }
   })
📝 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.

Suggested change
const [deleteVideoVariant, { loading }] = useMutation(DELETE_VIDEO_VARIANT, {
variables: { id: variantId },
onCompleted: () => {
enqueueSnackbar('Audio language deleted successfully', {
variant: 'success'
})
router.push(returnUrl, { scroll: false })
}
})
const [deleteVideoVariant, { loading }] = useMutation(DELETE_VIDEO_VARIANT, {
variables: { id: variantId },
onCompleted: () => {
enqueueSnackbar('Audio language deleted successfully', {
variant: 'success'
})
router.push(returnUrl, { scroll: false })
},
onError: (error) => {
enqueueSnackbar(error.message || 'Failed to delete audio language', {
variant: 'error'
})
}
})
🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/delete/DeleteAudioClient.tsx
around lines 49–58, the DELETE_VIDEO_VARIANT mutation is missing an onError
handler so failures provide no user feedback and the dialog stays in an unclear
state; add an onError callback to the useMutation options that captures the
GraphQL/network error, calls enqueueSnackbar(error.message || 'Failed to delete
audio language') with variant: 'error', optionally logs the error for debugging,
and avoid calling router.push so the dialog remains open for retry or
cancellation. Ensure the onError handler handles both ApolloError and generic
Error shapes and keeps UI state consistent.

}}
loading={loading}
>
{`Are you sure you want to delete the ${data.videoVariant.language.name[0].value} audio language? This action cannot be undone.`}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against null/undefined in nested data access.

Accessing data.videoVariant.language.name[0].value without null checks can throw if any property in the chain is null/undefined or if the name array is empty.

🔎 Proposed fix with optional chaining and fallback
-      {`Are you sure you want to delete the ${data.videoVariant.language.name[0].value} audio language? This action cannot be undone.`}
+      {`Are you sure you want to delete the ${data.videoVariant?.language?.name?.[0]?.value ?? 'selected'} audio language? This action cannot be undone.`}
📝 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.

Suggested change
{`Are you sure you want to delete the ${data.videoVariant.language.name[0].value} audio language? This action cannot be undone.`}
{`Are you sure you want to delete the ${data.videoVariant?.language?.name?.[0]?.value ?? 'selected'} audio language? This action cannot be undone.`}
🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/delete/DeleteAudioClient.tsx
around line 75, the template literal directly accesses
data.videoVariant.language.name[0].value which can throw if any part is
null/undefined or name is empty; change this to safely read the value using
optional chaining and a fallback (e.g., const lang =
data?.videoVariant?.language?.name?.[0]?.value ?? 'unknown language') and use
that fallback variable in the message so the UI never crashes when data is
missing.

Comment on lines +12 to +15
export default async function Page({ params }: PageProps) {
const { videoId, variantId } = await params

return (
<Dialog
open={true}
onClose={() =>
router.push(returnUrl, {
scroll: false
})
}
dialogTitle={{
title: 'Delete Audio Language',
closeButton: true
}}
dialogAction={{
onSubmit: deleteVideoVariant,
submitLabel: 'Delete',
closeLabel: 'Cancel'
}}
loading={loading}
>
{`Are you sure you want to delete the ${data.videoVariant.language.name[0].value} audio language? This action cannot be undone.`}
</Dialog>
)
return <DeleteAudioClient videoId={videoId} variantId={variantId} />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check for Suspense in parent layout files and parent directories
rg -n "Suspense" "apps/videos-admin/src/app/(dashboard)/videos/\[videoId\]/audio" --type tsx --type ts

# Check the root dashboard layout and parent layouts
fd -t f -e tsx -e ts "layout\." "apps/videos-admin/src/app/(dashboard)" 

# Check parent directories structure
fd -t d "audio" "apps/videos-admin/src/app/(dashboard)/videos"

Repository: JesusFilm/core

Length of output: 924


🏁 Script executed:

# Inspect the DeleteAudioClient component to verify useSuspenseQuery usage
fd -t f "DeleteAudioClient" "apps/videos-admin/src" --exec cat {}

# If not found, search for where it might be defined
rg -n "export.*DeleteAudioClient" "apps/videos-admin/src" -A 5

Repository: JesusFilm/core

Length of output: 2780


🏁 Script executed:

# Check the immediate parent layout for the delete page
cat -n "apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/layout.tsx"

Repository: JesusFilm/core

Length of output: 13327


🏁 Script executed:

# Check the audio parent layout
cat -n "apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/layout.tsx"

Repository: JesusFilm/core

Length of output: 9818


🏁 Script executed:

# Check the video parent layout
cat -n "apps/videos-admin/src/app/(dashboard)/videos/[videoId]/layout.tsx"

Repository: JesusFilm/core

Length of output: 6545


🏁 Script executed:

# Check the dashboard layout
cat -n "apps/videos-admin/src/app/(dashboard)/layout.tsx"

Repository: JesusFilm/core

Length of output: 1948


Correct Next.js async params pattern.

The page correctly follows the Next.js 15+ pattern for async params, awaiting the promise before passing values to the client component.

However, ensure a <Suspense> boundary exists in a parent layout wrapping this page. DeleteAudioClient uses useSuspenseQuery, and the immediate parent layout (VariantDialog) also uses useSuspenseQuery without a boundary. Without a Suspense wrapper, suspense will bubble up to the framework's error boundary, which may cause unexpected behavior.

🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/delete/page.tsx
around lines 12 to 15, the page correctly awaits async params but the reviewer
notes there is no Suspense boundary in a parent layout: both DeleteAudioClient
and the immediate parent layout (VariantDialog) use useSuspenseQuery so you must
add a React.Suspense boundary in the parent layout that wraps VariantDialog (or
higher) with a sensible fallback to prevent suspense from bubbling to the
framework error boundary; update the parent layout file to wrap the component
tree that renders VariantDialog (and thus this page) in <Suspense
fallback={...}>... </Suspense> ensuring the fallback is appropriate for loading
states.

Comment on lines +277 to +283
const uploadUrl = r2Asset.data?.cloudflareR2Create?.uploadUrl
if (uploadUrl == null) throw new Error('Upload URL is null')

await uploadAssetFile(uploadedFile, uploadUrl)

const publicUrl = r2Asset.data?.cloudflareR2Create?.publicUrl
if (publicUrl == null) return
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing loading state cleanup on early return.

If publicUrl is null, the function returns early without setting isLoading back to false, potentially leaving the UI stuck in a loading state. Additionally, no error feedback is shown to the user.

🔎 Proposed fix
 const publicUrl = r2Asset.data?.cloudflareR2Create?.publicUrl
-if (publicUrl == null) return
+if (publicUrl == null) {
+  enqueueSnackbar('Failed to retrieve public URL', { variant: 'error' })
+  setIsLoading(false)
+  return
+}
🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/download/[downloadId]/add/AddVideoVariantDownloadDialogClient.tsx
around lines 277 to 283, the function returns early when publicUrl is null
without clearing isLoading or surfacing an error; modify the control flow so
that when publicUrl is null you set isLoading back to false and provide user
feedback (e.g., set an error state or show a toast), and better yet wrap the
async flow in try/catch/finally and move isLoading=false into finally so it
always runs while catching/reporting the error before returning.

Comment on lines +403 to +408
{uploadedFile != null ? (
<LinkFile
name={uploadedFile.name}
link={URL.createObjectURL(uploadedFile)}
/>
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential memory leak from unreleased object URL.

The object URL created by URL.createObjectURL(uploadedFile) at line 406 is never revoked. Unlike the temporary URL in handleUpload, this one persists for the lifetime of the component, potentially causing memory leaks if users upload multiple files before submitting.

Consider storing the object URL in state and revoking it in a cleanup effect, or revoke it after the form submission completes.

🔎 Proposed fix

Add cleanup logic to revoke the object URL:

  const [uploadedFile, setUploadedFile] = useState<File | null>(null)
+  const [fileObjectUrl, setFileObjectUrl] = useState<string | null>(null)
  const [videoDimensions, setVideoDimensions] = useState<{
    width: number
    height: number
  } | null>(null)

+  // Cleanup object URL on unmount or when file changes
+  useEffect(() => {
+    return () => {
+      if (fileObjectUrl != null) {
+        URL.revokeObjectURL(fileObjectUrl)
+      }
+    }
+  }, [fileObjectUrl])

  const handleUpload = async (file: File): Promise<void> => {
    if (file == null) return

+    // Revoke previous object URL if exists
+    if (fileObjectUrl != null) {
+      URL.revokeObjectURL(fileObjectUrl)
+    }
+
    const video = document.createElement('video')
    function cleanup() {
      URL.revokeObjectURL(video.src)
      video.remove()
    }

    video.onerror = function () {
      cleanup()
      enqueueSnackbar('Failed to load video', {
        variant: 'error'
      })
    }

    video.src = URL.createObjectURL(file)
    video.onloadedmetadata = () => {
      setVideoDimensions({ width: video.videoWidth, height: video.videoHeight })
      cleanup()
    }

    setUploadedFile(file)
+    setFileObjectUrl(URL.createObjectURL(file))
  }

  // In the JSX, use the stored URL
  {uploadedFile != null ? (
    <LinkFile
      name={uploadedFile.name}
-      link={URL.createObjectURL(uploadedFile)}
+      link={fileObjectUrl ?? ''}
    />
  ) : null}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/download/[downloadId]/add/AddVideoVariantDownloadDialogClient.tsx
around lines 403 to 408, the temporary object URL created by
URL.createObjectURL(uploadedFile) is never revoked which can leak memory; store
the object URL in component state (e.g. setPreviewUrl) instead of calling
URL.createObjectURL inline, use that state value for the LinkFile link, and
revoke the previous URL whenever a new file is set and in a useEffect cleanup
(and also revoke after successful form submission) to ensure URLs are released.

Comment on lines +15 to +19
<AddVideoVariantDownloadDialogClient
videoId={videoId}
variantId={variantId}
languageId={downloadId}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prop name mismatch: downloadId passed as languageId.

The route parameter downloadId is passed to the client component's languageId prop. This naming inconsistency makes the code confusing and suggests either:

  • The parameter should be renamed to languageId in the route, or
  • The prop in the client component should be renamed to downloadId

This is the same issue flagged in the client component. Please align the naming throughout the flow.

🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/download/[downloadId]/add/page.tsx
lines 15-19, the route param downloadId is being passed into
AddVideoVariantDownloadDialogClient as languageId which is a naming mismatch;
change the prop to downloadId (and update the client component's
prop/interface/type and all its usages to accept downloadId instead of
languageId) so the route param and prop name align, and update any TypeScript
types, tests or imports that reference languageId to use downloadId.

Comment on lines 39 to 45
interface VariantDialogProps {
children: ReactNode
params: {
params: Promise<{
variantId: string
videoId: string
}
}>
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Dead code: params prop is defined but never used.

The params prop is declared in VariantDialogProps (lines 41-44) but the component extracts only children from props (line 90) and retrieves route parameters via useParams() hook instead (lines 91-95).

For client components ('use client'), using useParams() is the correct approach. The Promise-based params prop pattern is intended for Server Components.

🔎 Proposed fix — remove unused params prop
 interface VariantDialogProps {
   children: ReactNode
-  params: Promise<{
-    variantId: string
-    videoId: string
-  }>
 }

Also applies to: 87-95

🤖 Prompt for AI Agents
In
apps/videos-admin/src/app/(dashboard)/videos/[videoId]/audio/[variantId]/layout.tsx
around lines 39 to 45 (and related usage at 87–95), the VariantDialogProps
interface declares a Promise-based params prop that is never used because the
component reads route params via useParams() (client component). Remove the
unused params entry from VariantDialogProps, update the component's prop typing
to only expect children, and delete any unused params references/imports so the
type matches runtime usage.

Comment on lines 35 to +49
export default function EditionsPage({ children }: EditionsPageProps) {
return (
<Suspense
fallback={
<Section title="Editions">
<Box sx={{ display: 'grid', placeItems: 'center', height: 200 }}>
<Typography>Loading...</Typography>
</Box>
</Section>
}
>
<EditionsContent>{children}</EditionsContent>
</Suspense>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/videos-admin/src/app/\(dashboard\)/videos/\[videoId\]/editions/layout.tsx

Repository: JesusFilm/core

Length of output: 5755


🏁 Script executed:

# Check Apollo Client configuration
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*apps/videos-admin*" | grep -i apollo | head -20

Repository: JesusFilm/core

Length of output: 365


🏁 Script executed:

# Search for ApolloClient setup and configuration
rg -n "ApolloClient|createApolloClient|ApolloProvider" --type=ts --type=tsx -g "apps/videos-admin/**" -A 5 -B 2

Repository: JesusFilm/core

Length of output: 85


🏁 Script executed:

# Search for useSuspenseQuery usage in the codebase
rg -n "useSuspenseQuery" --type=ts --type=tsx -g "apps/**"

Repository: JesusFilm/core

Length of output: 85


🏁 Script executed:

cat -n apps/videos-admin/src/libs/apollo/makeClient.ts

Repository: JesusFilm/core

Length of output: 930


🏁 Script executed:

cat -n apps/videos-admin/src/libs/apollo/apolloClient.ts

Repository: JesusFilm/core

Length of output: 1749


🏁 Script executed:

# Search for suspense configuration in Apollo setup
rg -n "suspense" --type ts -g "apps/videos-admin/src/libs/apollo/**"

Repository: JesusFilm/core

Length of output: 40


🏁 Script executed:

# Check if there are other Suspense + useQuery patterns in the codebase
rg -n "Suspense" --type ts -A 3 -B 1 -g "apps/videos-admin/src/**" | head -50

Repository: JesusFilm/core

Length of output: 5183


Use useSuspenseQuery instead of useQuery for Suspense to work.

The Suspense wrapper won't display the loading fallback because useQuery doesn't suspend—it returns { data, loading, error } synchronously. To enable Suspense, replace useQuery with useSuspenseQuery:

-const { data, refetch } = useQuery(GET_EDITIONS, {
+const { data, refetch } = useSuspenseQuery(GET_EDITIONS, {
  variables: { videoId }
})

Also update the import to include useSuspenseQuery:

-import { useQuery } from '@apollo/client'
+import { useQuery, useSuspenseQuery } from '@apollo/client'

Note: Remove the unused loading state handling if present, as useSuspenseQuery throws promises that the Suspense boundary will catch.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/videos-admin/src/app/(dashboard)/videos/[videoId]/editions/layout.tsx
around lines 35 to 49, the Suspense fallback never shows because the
component(s) inside use useQuery (which does not suspend); replace useQuery with
useSuspenseQuery in the EditionsContent (and any child hooks) so those hooks
throw promises that the Suspense boundary can catch, update the import to pull
useSuspenseQuery from your data-fetching library instead of useQuery, and remove
any local loading boolean handling or conditional renders that duplicate the
Suspense fallback so the Suspense boundary handles loading states.

Comment on lines +12 to +16
export default function RootLayout({
children
}: {
children: React.ReactNode
}): Promise<ReactNode> {
const user = await getUser()

}): ReactNode {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and read the layout.tsx file
find apps/videos-admin -name "layout.tsx" -type f 2>/dev/null | head -5

Repository: JesusFilm/core

Length of output: 412


🏁 Script executed:

#!/bin/bash
# Get the actual content of the layout file
if [ -f "apps/videos-admin/src/app/layout.tsx" ]; then
  cat -n "apps/videos-admin/src/app/layout.tsx"
else
  echo "File not found at apps/videos-admin/src/app/layout.tsx"
  # Try to find it
  find . -name "layout.tsx" -path "*/videos-admin/*" 2>/dev/null
fi

Repository: JesusFilm/core

Length of output: 1739


🏁 Script executed:

#!/bin/bash
# Search for other layout files and async patterns in videos-admin
fd -e tsx -e ts 'layout\.(tsx|ts)$' apps/videos-admin/src 2>/dev/null

# Also check for Suspense usage patterns
rg -B 2 -A 5 'Suspense' apps/videos-admin/src --type tsx --type ts 2>/dev/null | head -50

Repository: JesusFilm/core

Length of output: 640


🏁 Script executed:

#!/bin/bash
# Check for UserProviders pattern
rg -B 2 -A 8 'UserProviders' apps/videos-admin/src --type tsx --type ts 2>/dev/null

Repository: JesusFilm/core

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check other layout files for async patterns
for file in apps/videos-admin/src/app/'(dashboard)'/layout.tsx apps/videos-admin/src/app/users/layout.tsx; do
  if [ -f "$file" ]; then
    echo "=== $file ==="
    cat -n "$file" | head -30
    echo ""
  fi
done

Repository: JesusFilm/core

Length of output: 1829


🏁 Script executed:

#!/bin/bash
# Check Next.js version in the project
if [ -f "package.json" ]; then
  rg '"next":\s*"[^"]*"' package.json
fi

# Also check if there's a .next or similar indicating Next.js version
if [ -f "apps/videos-admin/package.json" ]; then
  echo "=== apps/videos-admin/package.json ==="
  rg '"next"' apps/videos-admin/package.json
fi

Repository: JesusFilm/core

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Search for getUser implementation to understand what it does
rg -B 2 -A 10 'export.*function.*getUser|export const getUser' apps/videos-admin/src --type ts --type tsx

Repository: JesusFilm/core

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Search for getUser implementation
rg -B 2 -A 10 'export.*getUser|const getUser' apps/videos-admin/src --type ts

Repository: JesusFilm/core

Length of output: 486


🏁 Script executed:

#!/bin/bash
# Also try without type specification
rg -B 2 -A 10 'export.*getUser|const getUser' apps/videos-admin/src

Repository: JesusFilm/core

Length of output: 486


🏁 Script executed:

#!/bin/bash
# Check if getUser uses cookies() or similar async operations
find apps/videos-admin/src -name "*.ts" -o -name "*.tsx" | xargs grep -l "getUser" | head -5

Repository: JesusFilm/core

Length of output: 191


Add loading fallback for Suspense and return type for UserProviders component.

The Suspense + async child component pattern is correct for Next.js 16 and enables proper streaming; however, two improvements are needed:

  1. Replace fallback={null} — While fetching the user, no loading UI is shown. Add a loading skeleton or placeholder:

    <Suspense fallback={<LoadingFallback />}>
      <UserProviders>{children}</UserProviders>
    </Suspense>
  2. Add return type annotation to UserProviders — The async component is missing its return type:

    async function UserProviders({ children }: { children: React.ReactNode }): Promise<ReactElement> {
🤖 Prompt for AI Agents
In apps/videos-admin/src/app/layout.tsx around lines 12-16, replace the Suspense
fallback={null} with a real loading placeholder (e.g.,
fallback={<LoadingFallback />}) and ensure LoadingFallback is imported, and add
an explicit return type to the async UserProviders component (e.g., async
function UserProviders({ children }: { children: React.ReactNode }):
Promise<ReactElement>) and import ReactElement from React; this provides a
visible loading state while user data loads and fixes the missing return type on
the async component.

@github-actions github-actions bot temporarily deployed to Preview - videos-admin January 3, 2026 00:54 Inactive
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