Skip to content

09 smart video preview with cloudinary ai - #8

Merged
CodingWithTushar merged 2 commits into
mainfrom
09-Smart-video-preview-with-cloudinary-AI
Aug 3, 2026
Merged

09 smart video preview with cloudinary ai#8
CodingWithTushar merged 2 commits into
mainfrom
09-Smart-video-preview-with-cloudinary-AI

Conversation

@CodingWithTushar

@CodingWithTushar CodingWithTushar commented Aug 3, 2026

Copy link
Copy Markdown
Owner
  • Introduces server-side logic to request Cloudinary AI-generated previews and thumbnails for uploaded videos.

  • Adds client-side components to display preview thumbnails and short preview clips in the course/video listing and player UI.

  • Stores preview metadata (preview URL, thumbnail URL, generation status, timestamps) alongside existing video records.

  • Adds background job / async task handling so preview generation is retried/resilient and does not block upload flow.

  • Includes configuration and environment variables for Cloudinary integration and feature toggles.

  • Adds basic tests for the new preview generation flow and a small set of UI tests for the preview display.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added video cards with thumbnails, hover previews, metadata, compression details, and download actions.
    • Added social media format presets for Instagram, Twitter, and Facebook.
    • Increased supported upload size limits for video files.
  • Bug Fixes

    • Improved upload authentication and unauthorized-access responses.
    • Added a fallback message when video previews cannot be played.
    • Removed stray debug output from authentication flows.

Walkthrough

The PR adds a Cloudinary-backed video card, centralizes social format data, and updates video upload authentication, middleware routing, persistence, and request body limits.

Changes

Video upload and display

Layer / File(s) Summary
Video display contracts and rendering
types/interfaces.ts, utils/constants.ts, app/page.tsx, components/videoCard.tsx, package.json
Adds VideoProps and centralized socialFormats data. Adds VideoCard rendering for thumbnails, previews, metadata, compression, and downloads. Adds dayjs, filesize, and the updated lucide-react version.
Upload routing and request limits
middleware.ts, next.config.ts
Middleware awaits Clerk authentication, changes API route handling, disables selected redirects and role checks, and passes through /api/upload-video. Next.js request limits are set to 70 MB and 100 MB.
Authenticated upload persistence
app/(app)/uploadvideo/page.tsx, app/api/upload-video/route.ts, utils/helpers.ts
The upload form removes unused router state and uses React.SubmitEvent. The route returns structured 401 responses, persists Cloudinary metadata, and removes authentication debug logging.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UploadPage
  participant ClerkMiddleware
  participant UploadRoute
  participant Cloudinary
  participant Prisma

  UploadPage->>ClerkMiddleware: Submit video upload
  ClerkMiddleware->>ClerkMiddleware: Await auth()
  ClerkMiddleware->>UploadRoute: Pass through /api/upload-video
  UploadRoute->>Cloudinary: Upload video
  Cloudinary-->>UploadRoute: Return video metadata
  UploadRoute->>Prisma: Persist video metadata
  Prisma-->>UploadRoute: Return saved video
  UploadRoute-->>UploadPage: Return upload response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided, so the changeset is not explained in the description. Add a brief description that summarizes the smart video preview, Cloudinary integration, authentication, and upload changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: adding a smart video preview with Cloudinary integration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@CodingWithTushar
CodingWithTushar merged commit 5b6e098 into main Aug 3, 2026
1 check was pending

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (2)
components/videoCard.tsx (1)

61-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset the preview error in the hover handler.

useEffect calls setpreviewError(false) synchronously on each hover state change, causing an extra render cycle. Handle the hover reset in the onMouseEnter callback instead.

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

In `@components/videoCard.tsx` around lines 61 - 63, Move the
setpreviewError(false) call from the isHover-dependent useEffect into the video
card’s onMouseEnter handler, so the preview error resets when hover begins
without an extra effect-driven render. Remove the now-unnecessary useEffect
while preserving the existing hover behavior.

Source: Linters/SAST tools

middleware.ts (1)

16-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the same path in the matcher and bypass.

Line 16 matches /api/uploadvideo, but the client calls /api/upload-video. Line 26 bypasses the actual route separately. Set the matcher to /api/upload-video and use isPublicApiRoutes(req) for the bypass.

Proposed fix
-const isPublicApiRoutes = createRouteMatcher(["/api/uploadvideo"]);
+const isPublicApiRoutes = createRouteMatcher(["/api/upload-video"]);

-if (req.nextUrl.pathname === "/api/upload-video") {
+if (isPublicApiRoutes(req)) {
   return NextResponse.next();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@middleware.ts` around lines 16 - 28, Update isPublicApiRoutes to match
"/api/upload-video", then replace the direct pathname check in clerkMiddleware
with isPublicApiRoutes(req) for the public-route bypass.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/upload-video/route.ts`:
- Line 29: Update the upload handling around the file persistence logic to
derive the stored original size from the uploaded file by using file.size
converted to a string, rather than reading the client-controlled originalSize
form field. Remove originalSize from the client payload and apply this change at
both persistence points near the form-data parsing and storage logic.
- Around line 26-39: Update the file validation in the upload handler before
uploadToCloudinary: require file to be an actual File instance and reject files
larger than 70 MiB with the existing 400-style validation response. Keep the
Cloudinary call reachable only after both checks pass.
- Around line 38-54: Update the upload flow around uploadToCloudinary and
prisma.video.create to retain result.public_id and delete the uploaded
Cloudinary asset if video persistence fails. Keep the cleanup within the failure
path before returning the existing 500 response, while preserving the normal
successful upload and database-save behavior.

In `@components/videoCard.tsx`:
- Around line 50-55: Update the remainingSeconds calculation in formatDuration
to use a 60-second remainder, so durations convert correctly into minutes and
seconds while preserving the existing formatting.
- Around line 135-137: Add an aria-label to the icon-only download button in the
video card, using the video title to identify which video will be downloaded.
Keep the existing onClick behavior and Download icon unchanged.
- Around line 76-80: Correct the misspelled Tailwind classes in the video card
markup: update ascept-video to aspect-video, bg-grey-200 to bg-gray-200, and
absoulte to absolute, preserving the surrounding layout and preview-error
behavior.

In `@middleware.ts`:
- Around line 43-46: Update isAdminRoute(req) to authorize authenticated users
using Clerk’s auth() role helper or configured admin claim, rather than checking
userId.role. Allow admin access to /admin, api/admin, and /dashboard/analytics;
return a 403 response for authenticated non-admin users while preserving
unauthenticated handling.

In `@next.config.ts`:
- Around line 26-34: Update the existing nextConfig object to include the
supported experimental serverActions body-size settings, preserving its images,
eslint, and typescript configuration. Remove the separate module.exports
assignment and retain only export default nextConfig so all settings are emitted
through one configuration object.
- Around line 26-34: Remove the experimental.serverActions configuration from
the Next config, including bodySizeLimit and proxyClientMaxBodySize, because the
upload uses the app/api/upload-video/route.ts Route Handler rather than a Server
Action. Enforce the upload size in the route handler’s request.formData() flow
or deployment ingress, accounting for multipart form-data overhead.

In `@utils/constants.ts`:
- Line 18: Correct the user-facing format labels in the constants definition:
update the key containing “Instagram Protrait (4:5)” to “Instagram Portrait
(4:5)” and fix the “Twtter” label to “Twitter,” preserving their existing format
values.

---

Nitpick comments:
In `@components/videoCard.tsx`:
- Around line 61-63: Move the setpreviewError(false) call from the
isHover-dependent useEffect into the video card’s onMouseEnter handler, so the
preview error resets when hover begins without an extra effect-driven render.
Remove the now-unnecessary useEffect while preserving the existing hover
behavior.

In `@middleware.ts`:
- Around line 16-28: Update isPublicApiRoutes to match "/api/upload-video", then
replace the direct pathname check in clerkMiddleware with isPublicApiRoutes(req)
for the public-route bypass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ab87755-c9a2-4905-ab5f-5cc8c87e0b9f

📥 Commits

Reviewing files that changed from the base of the PR and between eb4771e and e2cace3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • app/(app)/uploadvideo/page.tsx
  • app/api/upload-video/route.ts
  • app/page.tsx
  • components/videoCard.tsx
  • middleware.ts
  • next.config.ts
  • package.json
  • types/interfaces.ts
  • utils/constants.ts
  • utils/helpers.ts
💤 Files with no reviewable changes (1)
  • utils/helpers.ts

Comment on lines 26 to 39
const file = formData.get("file") as File | null;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const originalSize = formData.get("originalSize") as string;

if (!file) {
return NextResponse.json({ error: "File not found" }, { status: 400 });
return NextResponse.json(
{ error: "File not found" },
{ status: 400 }
);
}

// Upload to Cloudinary
const result = await uploadToCloudinary(file, videoOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the uploaded file on the server.

Line 26 only asserts that the multipart value is a File. A client can send a string, which makes uploadToCloudinary fail with a 500 response. The client-side 70 MiB limit can also be bypassed. Validate file instanceof File and reject files larger than 70 MiB before the Cloudinary call.

Proposed fix
-    const file = formData.get("file") as File | null;
+    const file = formData.get("file");
     const title = formData.get("title") as string;
     const description = formData.get("description") as string;
-    const originalSize = formData.get("originalSize") as string;
+    const MAX_FILE_SIZE = 70 * 1024 * 1024;

-    if (!file) {
+    if (!(file instanceof File)) {
       return NextResponse.json(
         { error: "File not found" },
         { status: 400 }
       );
     }
+    if (file.size > MAX_FILE_SIZE) {
+      return NextResponse.json({ error: "File size too large" }, { status: 413 });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/upload-video/route.ts` around lines 26 - 39, Update the file
validation in the upload handler before uploadToCloudinary: require file to be
an actual File instance and reject files larger than 70 MiB with the existing
400-style validation response. Keep the Cloudinary call reachable only after
both checks pass.

const file = formData.get("file") as File | null;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const originalSize = formData.get("originalSize") as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Derive originalSize from the uploaded file.

Lines 29 and 47 persist a client-controlled size value. An authenticated client can forge it. Store String(file.size) instead and remove originalSize from the client payload.

Also applies to: 47-47

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

In `@app/api/upload-video/route.ts` at line 29, Update the upload handling around
the file persistence logic to derive the stored original size from the uploaded
file by using file.size converted to a string, rather than reading the
client-controlled originalSize form field. Remove originalSize from the client
payload and apply this change at both persistence points near the form-data
parsing and storage logic.

Comment on lines +38 to 54
// Upload to Cloudinary
const result = await uploadToCloudinary(file, videoOptions);

// Save video
const video = await prisma.video.create({
data: {
Title: title,
description: description,
description,
publicId: result.public_id,
originalSize: originalSize,
originalSize,
compressedSize: String(result.bytes),
duration: result.duration || 0,
duration: result.duration?.toString() || "0",

// If your schema has user relation:
// userId: userId
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'destroy\(' node_modules/cloudinary --glob '*.{ts,d.ts,js}'

Repository: CodingWithTushar/learningsaas

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching upload-video route and Cloudinary deps:\n'
git ls-files | rg '(^app/api/upload-video/route\.ts$|package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true

printf '\nUpload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  nl -ba app/api/upload-video/route.ts | sed -n '1,140p'
fi

printf '\nSearch for uploadToCloudinary and destroy/delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\nPackage manifests / lock references to cloudinary:\n'
( [ -f package.json ] && cat package.json ) || true
( [ -f pnpm-lock.yaml ] && rg -n 'cloudinary' pnpm-lock.yaml || true ) || true
( [ -f package-lock.json ] && cat package-lock.json | jq -r '.. | objects | .dependencies??.cloudinary??.version // empty, .dependencies??.devDependencies??.cloudinary??.version // empty' --argjson file $(mktemp) 2>/dev/null || true ) || true

Repository: CodingWithTushar/learningsaas

Length of output: 360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching upload-video route and Cloudinary deps:\n'
git ls-files | rg '(^app/api/upload-video/route.ts$|package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true

printf '\nUpload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  nl -ba app/api/upload-video/route.ts | sed -n '1,140p'
fi

printf '\nSearch for uploadToCloudinary and destroy/delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\nPackage manifests / lock references to cloudinary:\n'
[ -f package.json ] && cat package.json || true
[ -f pnpm-lock.yaml ] && rg -n 'cloudinary' pnpm-lock.yaml || true
[ -f package-lock.json ] && rg -n '"cloudinary"' package-lock.json || true
[ -f yarn.lock ] && rg -n 'cloudinary' yarn.lock || true

Repository: CodingWithTushar/learningsaas

Length of output: 360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Upload-video route excerpt:\n'
if [ -f app/api/upload-video/route.ts ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' app/api/upload-video/route.ts | sed -n '1,160p'
fi

printf '\nSearch for uploadToCloudinary and delete usages:\n'
rg -n 'uploadToCloudinary|cloudinary|destroy\(|delete\(|public_id|publicId|bytes|duration' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

printf '\npackage.json cloudinary entries:\n'
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    for section in ('dependencies','devDependencies'):
        deps=data.get(section,{})
        if 'cloudinary' in deps:
            print(f'[{section}] cloudinary={deps["cloudinary"]}')
PY

printf '\nlockfile cloudinary entries:\n'
rg -n 'cloudinary|node_modules/cloudinary' package-lock.json || true

Repository: CodingWithTushar/learningsaas

Length of output: 10473


Clean up the Cloudinary asset when persistence fails.

uploadToCloudinary completes before prisma.video.create, and the catch block only returns 500. In app/api/upload-video/route.ts, keep result.public_id from the upload and delete that Cloudinary asset when prisma.video.create fails to avoid orphaned assets.

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

In `@app/api/upload-video/route.ts` around lines 38 - 54, Update the upload flow
around uploadToCloudinary and prisma.video.create to retain result.public_id and
delete the uploaded Cloudinary asset if video persistence fails. Keep the
cleanup within the failure path before returning the existing 500 response,
while preserving the normal successful upload and database-save behavior.

Comment thread components/videoCard.tsx
Comment on lines +50 to +55
const formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 6);

return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a 60-second remainder.

Line 52 uses % 6, so most durations display an incorrect seconds value. For example, 78 seconds renders as 1:00 instead of 1:18.

Proposed fix
-    const remainingSeconds = Math.round(seconds % 6);
+    const remainingSeconds = Math.floor(seconds % 60);
📝 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 formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 6);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);
const formatDuration = useCallback((seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/videoCard.tsx` around lines 50 - 55, Update the remainingSeconds
calculation in formatDuration to use a 60-second remainder, so durations convert
correctly into minutes and seconds while preserving the existing formatting.

Comment thread components/videoCard.tsx
Comment on lines +76 to +80
<figure className="ascept-video relative">
{isHover ? (
previewError ? (
<div className="w-full h-full flex items-center justify-center bg-grey-200">
<p className="text-red-500">Preview not available</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a -t f '^tailwind\.config\..*$' . || true
rg -n -C 2 'ascept-video|bg-grey-200|absoulte|aspect-video|bg-gray-200|absolute' components/videoCard.tsx

Repository: CodingWithTushar/learningsaas

Length of output: 795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\nNext docs availability:\n'
fd -t f . node_modules/next/dist/docs 2>/dev/null|sed -n '1,20p' || true
printf '\nPackage versions:\n'
if [ -f package.json ]; then
  node - "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k+': '+deps[k])"
fi
printf '\nCustom config theme colors/extensions:\n'
for f in $(fd -t f '^tailwind\.config\..*$' . 2>/dev/null); do
  echo "--- $f"
  rg -n -C 3 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|absolute|aspect-ratio" "$f" || true
done
printf '\nComponent relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

Repository: CodingWithTushar/learningsaas

Length of output: 874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Next docs availability:\n'
if [ -d node_modules/next/dist/docs ]; then
  fd -t f . node_modules/next/dist/docs | sed -n '1,20p'
else
  echo 'node_modules/next/dist/docs not present'
fi

printf '\nPackage versions:\n'
if [ -f package.json ]; then
  node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf八','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k + ': ' + deps[k]);"
fi

printf '\nTailwind/Daisy UI custom config extensions:\n'
while IFS= read -r f; do
  echo "--- $f"
  rg -n -C 3 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|base-100" "$f" || true
done < <(fd -t f '^tailwind\.config\..*$' . 2>/dev/null || true)

if [ -f tailwind.config.js ]; then
  echo "--- tailwind.config.js structure"
  sed -n '1,160p' tailwind.config.js
fi

printf '\nvideoCard relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

printf '\nOccurrences in tracked files:\n'
rg -n 'ascept-video|bg-grey-200|absoulte|absolute bottom-2|aspect-video' . || true

Repository: CodingWithTushar/learningsaas

Length of output: 986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Package versions:\n'
if [ -f package.json ]; then
  node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); const deps=Object.assign({}, p.dependencies, p.devDependencies); for (const k of ['next','tailwindcss','daisyui']) if (deps[k]) console.log(k + ': ' + deps[k]);"
fi

printf '\nTailwind/Daisy UI custom config extensions:\n'
while IFS= read -r f; do
  echo "--- $f"
  rg -n -C 4 "aspectVideo|aspect-video|aspect|grey|gray|gray-200|grey-200|base-100" "$f" || true
done < <(fd -t f '^tailwind\.config\..*$' . 2>/dev/null || true)

printf '\nvideoCard relevant section:\n'
sed -n '60,115p' components/videoCard.tsx

printf '\nOccurrences in tracked files:\n'
rg -n 'ascept-video|bg-grey-200|absoulte|absolute bottom-2|aspect-video' . || true

Repository: CodingWithTushar/learningsaas

Length of output: 334


Fix the Tailwind class typos.

ascept-video, bg-grey-200, and absoulte are misspelled classes, so Tailwind/DaisyUI will not generate the intended styles. Use aspect-video, bg-gray-200, and absolute.

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

In `@components/videoCard.tsx` around lines 76 - 80, Correct the misspelled
Tailwind classes in the video card markup: update ascept-video to aspect-video,
bg-grey-200 to bg-gray-200, and absoulte to absolute, preserving the surrounding
layout and preview-error behavior.

Comment thread components/videoCard.tsx
Comment on lines +135 to +137
<button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
<Download size={16}/>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add an accessible name to the download button.

Line 135 renders an icon-only button. Neither the button nor the icon receives an accessible name. Add an aria-label that identifies the download action and video. Interactive controls require an accessible name. (w3.org)

Proposed fix
-          <button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
+          <button
+            className="btn btn-primary btn-sm"
+            aria-label={`Download ${video.Title}`}
+            onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}
+          >
📝 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
<button className="btn btn-primary btn-sm" onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}>
<Download size={16}/>
</button>
<button
className="btn btn-primary btn-sm"
aria-label={`Download ${video.Title}`}
onClick={() => onDownload(getFullVideoUrl(video.publicId), video.Title)}
>
<Download size={16}/>
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/videoCard.tsx` around lines 135 - 137, Add an aria-label to the
icon-only download button in the video card, using the video title to identify
which video will be downloaded. Keep the existing onClick behavior and Download
icon unchanged.

Comment thread middleware.ts
Comment on lines +43 to 46
// if (userId.role !== "admin") {
// return NextResponse.redirect(new URL("/sign-in", req.url));
// }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the existing Clerk role-claim convention before adding the admin predicate.
rg -n -C3 'sessionClaims|publicMetadata|privateMetadata|orgRole|role' \
  --glob '*.{ts,tsx}' .

Repository: CodingWithTushar/learningsaas

Length of output: 495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline middleware.ts || true

echo "== middleware.ts =="
cat -n middleware.ts

echo "== package / clerk docs candidates =="
git ls-files 'node_modules/next/dist/docs/**' | sed -n '1,80p'
ls -1 node_modules/next/dist/docs 2>/dev/null | sed -n '1,80p' || true

echo "== imports/usages middleware.ts =="
rg -n -C2 'userId|isAdminRoute|userId\.role|role|sign-in|analytics|dashboard' middleware.ts

Repository: CodingWithTushar/learningsaas

Length of output: 4842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== auth/session claim patterns in tracked source =="
rm -f /tmp/claim_search_output.txt
rg -n -C2 'auth\(\)|userId|sessionClaims|publicMetadata|privateMetadata|orgRole|role' \
  --glob '*.{ts,tsx}' --glob '!**/node_modules/**' . > /tmp/claim_search_output.txt || true
cat /tmp/claim_search_output.txt

echo "== Clerk config/package files =="
rg -n '"`@clerk/`(nextjs|react|backend-api)"|clerk' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true

echo "== next version if present =="
node - <<'JS'
try {
  const pkg = JSON.parse(require('path').resolve('package.json'));
  const deps = {...(pkg.dependencies||{}), ...(pkg.devDependencies||{})};
  console.log(JSON.stringify({
    next: deps.next,
    clerkNextjs: deps['`@clerk/nextjs`'],
    clerkReact: deps['`@clerk/react`'],
    clerkBackend: deps['`@clerk/backend-api`'],
  }, null, 2));
} catch (e) {
  console.log(String(e));
}
JS

Repository: CodingWithTushar/learningsaas

Length of output: 4142


🌐 Web query:

Clerk Next.js auth() SessionClaims sessionClaims orgRole documentation

💡 Result:

In Clerk's Next.js integration, the auth() helper (used in the App Router) and the useAuth() hook (used on the client side) provide access to session information, including sessionClaims and orgRole. Auth and SessionClaims The auth() helper returns an Auth object [1][2]. The sessionClaims property within this object contains the user's JWT payload, which includes any custom claims you have configured in your Clerk dashboard or via custom session tokens [1][3]. Example (App Router): import { auth } from '@clerk/nextjs/server'; export default async function Page { const { sessionClaims } = await auth; const customClaim = sessionClaims?.metadata?.someKey; // Access your custom claims //... } OrgRole and Authorization orgRole represents the current user's role in their active organization [1][4]. It is available directly as a property on the object returned by auth() and useAuth() [1][4]. For robust authorization checks, it is recommended to use the has() method rather than checking orgRole directly [2][5]. The has() method is available on the Auth object and can verify roles, permissions, features, or plans [1][2][6]. Example (Authorization Check): import { auth } from '@clerk/nextjs/server'; export default async function Page { const { has } = await auth; // Check if the user has a specific role if (!has({ role: 'org:admin' })) { return

You do not have permission to access this page.

; } return

Welcome, Admin!

; } Key Distinctions - Use auth() for server-side logic (e.g., Server Components, API Routes) [1][3]. - Use useAuth() for client-side components [4][7]. - Prefer the has() helper for authorization to ensure your code remains flexible if you switch from role-based access control (RBAC) to permission-based access control later [2][5].

Citations:


Restore admin authorization before allowing admin routes.

isAdminRoute(req) currently only checks authentication, so any logged-in user can access /admin, api/admin, and /dashboard/analytics. Use the Clerk auth() authorization helper/claim (for example, has({ role: ... }) or the configured custom admin claim) and deny non-admin users with a 403. Do not use userId.role, because userId is the user identifier, not a role object.

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

In `@middleware.ts` around lines 43 - 46, Update isAdminRoute(req) to authorize
authenticated users using Clerk’s auth() role helper or configured admin claim,
rather than checking userId.role. Allow admin access to /admin, api/admin, and
/dashboard/analytics; return a 403 response for authenticated non-admin users
while preserving unauthenticated handling.

Comment thread next.config.ts
Comment on lines +26 to +34
module.exports = {
experimental: {
serverActions: {
bodySizeLimit: '70mb',
proxyClientMaxBodySize: '70mb',
middlewareClientMaxBodySize: 100 * 1024 * 1024,
},
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'module\.exports|export default|"type"' next.config.ts package.json

Repository: CodingWithTushar/learningsaas

Length of output: 284


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- package metadata ---\n'
sed -n '1,80p' package.json

printf '\n--- next.config.ts ---\n'
cat -n next.config.ts

printf '\n--- node_modules next docs availability ---\n'
if [ -d node_modules/next/dist/docs ]; then
  find node_modules/next/dist/docs -maxdepth 2 -type f | sed -n '1,40p'
else
  echo "node_modules/next/dist/docs not present"
fi

printf '\n--- next source snippets if present ---\n'
if [ -d node_modules/next/dist/build ]; then
  rg -n "serverActions|bodySizeLimit|proxyClientMaxBodySize|middlewareClientMaxBodySize|CONFIG_FILE" node_modules/next/dist/build node_modules/next/dist -g '*.js' -g '*.ts' | sed -n '1,120p'
else
  echo "node_modules/next/dist/build not present"
fi

Repository: CodingWithTushar/learningsaas

Length of output: 258


Keep one Next.js configuration export.

The file already defines nextConfig and exports it later. This module.exports object is separate and does not merge with nextConfig, so the effective config can drop existing images, eslint, and typescript settings. Add the supported options to nextConfig and keep only export default nextConfig.

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

In `@next.config.ts` around lines 26 - 34, Update the existing nextConfig object
to include the supported experimental serverActions body-size settings,
preserving its images, eslint, and typescript configuration. Remove the separate
module.exports assignment and retain only export default nextConfig so all
settings are emitted through one configuration object.

Source: Coding guidelines


🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n --glob 'package*.json' '"next"\s*:|proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit'

if [ -f node_modules/next/package.json ]; then
  rg -n '"version"' node_modules/next/package.json
fi

if [ -f node_modules/next/dist/server/config-schema.js ]; then
  rg -n 'proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit' \
    node_modules/next/dist/server/config-schema.js
fi

Repository: CodingWithTushar/learningsaas

Length of output: 260


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate config and route files =="
git ls-files | rg '(^|/)next\.config\.(ts|js|mjs|cjs)$' || true
git ls-files | rg '(^|/)app/api/upload-video/route\.(ts|js|mjs|cjs)$' || true

echo
echo "== package manifests and lockfiles =="
git ls-files | rg '(^|/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' || true

echo
echo "== config/route relevant text =="
for f in $(git ls-files | rg '(^|/)next\.config\.(ts|js|mjs|cjs)$' || true); do
  echo "--- $f"
  sed -n '1,120p' "$f"
done
for f in $(git ls-files | rg '(^|/)app/api/upload-video/route\.(ts|js|mjs|cjs)$' || true); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo
echo "== Next manifests in repo =="
for f in $(git ls-files | rg '(^|/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$' || true); do
  echo "--- $f"
  rg -n '"next"|next@|/next@|next:' "$f" | head -80 || true
done

echo
echo "== installed node_modules outline =="
git ls-files node_modules/next/package.json node_modules/next/dist/server/config-schema.js node_modules/next/docs node_modules/next/dist/docs 2>/dev/null || true
if [ -d node_modules/next ]; then
  if [ -f node_modules/next/package.json ]; then
    jq -r '.version' node_modules/next/package.json
  fi
  if [ -f node_modules/next/dist/server/config-schema.js ]; then
    rg -n 'proxyClientMaxBodySize|middlewareClientMaxBodySize|serverActions|bodySizeLimit' node_modules/next/dist/server/config-schema.js || true
  fi
fi

Repository: CodingWithTushar/learningsaas

Length of output: 3178


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== installed next package =="
if [ -f node_modules/next/package.json ]; then
  jq -r '{name, version, engines}' node_modules/next/package.json
else
  echo "node_modules/next/package.json not found"
fi

echo
echo "== installed next schema section =="
if [ -f node_modules/next/dist/server/config-schema.js ]; then
  sed -n '300,370p' node_modules/next/dist/server/config-schema.js
fi

echo
echo "== installed next docs for docs/config/deprecations =="
find node_modules/next/dist/docs node_modules/next/docs node_modules/next/dist -maxdepth 3 -type f \( -iname '*config*' -o -iname '*deprecat*' -o -iname '*server-actions*' \) 2>/dev/null | sort | head -200 || true

echo
echo "== installed next docs text for upload/body/serverActions =="
for f in $(find node_modules/next/dist/docs node_modules/next/docs node_modules/next/dist -maxdepth 3 -type f 2>/dev/null | rg -i 'config|deprecat|document|upload|server-action|route|middleware' | head -100); do
  echo "--- $f"
  rg -n -i 'serverActions|proxyClientMaxBodySize|middlewareClientMaxBodySize|bodySizeLimit|formData|Cloudinary|upload|route handler|request upload|multipart' "$f" | head -50 || true
done

Repository: CodingWithTushar/learningsaas

Length of output: 50385


Remove serverActions config for a Route Handler.

This app uses Next 15.5.22, where proxyClientMaxBodySize and bodySizeLimit are valid Next config keys, but app/api/upload-video/route.ts is not a Server Action. That Route Handler parses request.formData(), so bodySizeLimit does not enforce the upload limit there. Enforce the limit in the route handler or at deployment ingress, and account for multipart formData overhead.

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

In `@next.config.ts` around lines 26 - 34, Remove the experimental.serverActions
configuration from the Next config, including bodySizeLimit and
proxyClientMaxBodySize, because the upload uses the
app/api/upload-video/route.ts Route Handler rather than a Server Action. Enforce
the upload size in the route handler’s request.formData() flow or deployment
ingress, accounting for multipart form-data overhead.

Source: Coding guidelines

Comment thread utils/constants.ts

export const socialFormats = {
"Instagram Square (1:1)": { width: 1080, height: 1080, aspectRatio: "1:1" },
"Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the format labels.

Line 18 renders Protrait. Line 20 renders Twtter. These strings appear in the format selector.

Proposed fix
-  "Instagram Protrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },
+  "Instagram Portrait (4:5)": { width: 1080, height: 1350, aspectRatio: "4:5" },
-  "Twtter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" },
+  "Twitter Header (3:1)": { width: 1500, height: 500, aspectRatio: "3:1" },

Also applies to: 20-20

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

In `@utils/constants.ts` at line 18, Correct the user-facing format labels in the
constants definition: update the key containing “Instagram Protrait (4:5)” to
“Instagram Portrait (4:5)” and fix the “Twtter” label to “Twitter,” preserving
their existing format values.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant