Skip to content

feat(auth): add Google OAuth provider and expand login options#44

Merged
creatorcluster merged 5 commits intocreatorcluster:mainfrom
Coder-soft:main
Mar 1, 2026
Merged

feat(auth): add Google OAuth provider and expand login options#44
creatorcluster merged 5 commits intocreatorcluster:mainfrom
Coder-soft:main

Conversation

@Coder-soft
Copy link

@Coder-soft Coder-soft commented Mar 1, 2026

  • Implement Google sign-in alongside existing GitHub and Discord providers
  • Introduce creator packs section with creation, viewing, and management pages
  • Add admin dashboard for creator packs oversight
  • Enable custom font preview text in resources browser
  • Update UI with feature announcement badges in hero and navigation

feat(resources): add folder organization for favorites with drag-and-drop

  • Add favorites sidebar with folder management, allowing users to create,

  • edit, delete, and organize favorite resources into folders. Implement

  • drag-and-drop functionality using https://github.com/dnd-kit to move resources between

  • folders. Add folder download as ZIP capability using jszip and file-saver.

  • Introduces new hooks (useFavoriteFolders) and extends useUserFavorites
    to support folder_id tracking in the database.

BREAKING CHANGE: user_favorites table now requires folder_id column
(migration needed: ALTER TABLE user_favorites ADD COLUMN folder_id UUID
REFERENCES user_favorite_folders(id) ON DELETE SET NULL)

Summary by CodeRabbit

  • New Features

    • Creator Packs: browse, upload, edit, manage, view details, and admin review workflow (including admin manager)
    • Favorites folders: create/manage folders, drag‑and‑drop organization, folder download, and persistent sidebar
    • Google sign‑in option
    • Customizable font preview text for font resources
    • Download confirmation warning for external links
    • Creator Packs tab added to Resources Hub
  • Style

    • New CTA banner promoting Creator Packs; updated Resources tab labels and badges

…drop

Add favorites sidebar with folder management, allowing users to create,
edit, delete, and organize favorite resources into folders. Implement
drag-and-drop functionality using @dnd-kit to move resources between
folders. Add folder download as ZIP capability using jszip and file-saver.

Introduces new hooks (useFavoriteFolders) and extends useUserFavorites
to support folder_id tracking in the database.

BREAKING CHANGE: user_favorites table now requires folder_id column
(migration needed: ALTER TABLE user_favorites ADD COLUMN folder_id UUID
REFERENCES user_favorite_folders(id) ON DELETE SET NULL)
- Implement Google sign-in alongside existing GitHub and Discord providers
- Introduce creator packs section with creation, viewing, and management pages
- Add admin dashboard for creator packs oversight
- Enable custom font preview text in resources browser
- Update UI with feature announcement badges in hero and navigation
@vercel
Copy link

vercel bot commented Mar 1, 2026

@Coder-soft is attempting to deploy a commit to the yamura3's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd0ad7a and bb6a227.

📒 Files selected for processing (1)
  • src/pages/CreateCreatorPackPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/CreateCreatorPackPage.tsx

📝 Walkthrough

Walkthrough

Adds a Creator Packs feature (models, hooks, pages, admin UI, components), folder-based Favorites with drag-and-drop, font-preview plumbing, Google OAuth, UI updates (Hero, Navbar, ResourcesHub), new dnd-kit dependency, and removes ffmpeg-static in favor of system ffmpeg.

Changes

Cohort / File(s) Summary
Package & Backend script
package.json, api/download.js
Added @dnd-kit/utilities; removed ffmpeg-static and simplified ffmpeg resolution to use the system ffmpeg binary.
Creator Packs Pages
src/pages/CreateCreatorPackPage.tsx, src/pages/ManageCreatorPacksPage.tsx, src/pages/EditCreatorPackPage.tsx, src/pages/CreatorPackPage.tsx
New pages for creating, managing, editing, and viewing Creator Packs with validation, Markdown support, cover upload, status/review handling, and routing.
Creator Packs Components & Admin
src/components/resources/CreatorPackCard.tsx, src/components/resources/CreatorPacksTab.tsx, src/components/admin/AdminCreatorPacksManager.tsx, src/components/resources/DownloadWarningDialog.tsx
New UI components: pack card, Creator Packs tab with filtering, admin manager for approval/rejection (reason dialog), and external-download warning dialog.
Creator Packs State & Types
src/hooks/useCreatorPacks.ts, src/integrations/supabase/types.ts, src/App.tsx, src/pages/Admin.tsx
New hook with CRUD, image upload, admin review flows; Supabase types for creator_packs; routes and admin page wired to manager.
Favorites: DnD & Folders
src/components/resources/FavoritesTab.tsx, src/components/resources/FavoritesSidebar.tsx, src/components/resources/FolderDialog.tsx, src/hooks/useFavoriteFolders.ts, src/hooks/useUserFavorites.ts
Adds folder model and hook, sidebar UI, drag-and-drop via dnd-kit to move favorites, folder CRUD, ZIP export, and related dialogs/UX.
Resources UI & Font Preview
src/components/resources/ResourceCard.tsx, src/components/resources/ResourceFilters.tsx, src/components/resources/ResourcesList.tsx, src/hooks/useResources.ts
Introduces optional fontPreviewText prop and handler across filters, list, and cards; useResources exposes fontPreviewText and setter.
Auth: Google OAuth
src/providers/AuthContext.tsx, src/providers/AuthProvider.tsx, src/components/auth/OAuthProviders.tsx
Adds signInWithGoogle to auth context/provider, implements Google OAuth flow and sign-in button; improves avatar sourcing for Google identities.
Hub & Navigation Updates
src/pages/ResourcesHub.tsx, src/components/Hero.tsx, src/components/Navbar.tsx, src/App.tsx
ResourcesHub adds creator-packs tab and activeTab state; Hero and Navbar updated for Creator Packs; routes wired for pack pages.
Misc / Content
src/pages/Community.tsx
Added a tutorial video entry ("How to Edit Minecraft Videos in Premiere Pro").

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CreatePage as CreateCreatorPackPage
    participant useCP as useCreatorPacks
    participant Storage as Supabase\ Storage
    participant DB as Supabase\ DB
    participant Router as React\ Router

    User->>CreatePage: Fill form & select cover
    User->>CreatePage: Click Publish
    CreatePage->>CreatePage: Validate fields
    alt cover provided
        CreatePage->>useCP: uploadCoverImage(file)
        useCP->>Storage: upload file
        Storage-->>useCP: return public URL
    end
    CreatePage->>useCP: createPack(input with imageUrl)
    useCP->>DB: INSERT creator_packs (status=pending)
    DB-->>useCP: return created pack
    useCP-->>CreatePage: created pack (slug)
    CreatePage->>Router: navigate to /creator-packs/:slug
Loading
sequenceDiagram
    actor Admin
    participant AdminUI as AdminCreatorPacksManager
    participant useCP as useCreatorPacks
    participant DB as Supabase\ DB
    participant Toast as UI\ Toast

    Admin->>AdminUI: Open admin manager
    AdminUI->>useCP: fetchPendingPacks()
    useCP->>DB: SELECT where status='pending'
    DB-->>useCP: pending packs
    useCP-->>AdminUI: packs list
    Admin->>AdminUI: Click Approve
    AdminUI->>useCP: reviewPack(id, 'approved')
    useCP->>DB: UPDATE status='approved'
    DB-->>useCP: confirm
    useCP-->>AdminUI: updated pack
    AdminUI->>Toast: show success
    Admin->>AdminUI: Click Reject (enter reason)
    AdminUI->>useCP: reviewPack(id, 'rejected', reason)
    useCP->>DB: UPDATE status='rejected', review_reason
    DB-->>useCP: confirm
    AdminUI->>Toast: show success
Loading
sequenceDiagram
    actor User
    participant FavTab as FavoritesTab
    participant DnD as dnd-kit
    participant useUF as useUserFavorites
    participant DB as Supabase\ DB
    participant Toast as UI\ Toast

    User->>FavTab: Drag resource card
    FavTab->>DnD: initiate drag, show overlay
    User->>FavTab: Drop onto folder target
    FavTab->>useUF: moveFavorite(resourceUrl, folderId)
    useUF->>DB: UPDATE user_favorites set folder_id
    DB-->>useUF: confirm
    useUF->>FavTab: invalidate cache / return
    FavTab->>Toast: show success
    FavTab->>User: UI updated with resource moved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰

I hopped through lines of code and cheer,
Creator Packs arrived — a brand new tier.
Folders twirl as cards take flight,
Google gates swing open bright.
A crunchy carrot for the build tonight!

🚥 Pre-merge checks | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title focuses on Google OAuth and login options, but the raw summary reveals the changeset is far more comprehensive, including creator packs features, favorites sidebar with drag-and-drop, folder management, and font preview enhancements. The title captures only one aspect of a much larger feature set. Revise the title to reflect the primary and most impactful changes, such as 'feat: add Google OAuth, creator packs, favorites organization, and font preview customization' or split into multiple PRs with distinct titles.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

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

🧹 Nitpick comments (4)
src/components/Navbar.tsx (1)

66-66: Consider removing the inline guidance comment from nav data.

The “You can use …” note is non-functional and can drift over time; keeping the nav config declarative improves readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Navbar.tsx` at line 66, Remove the inline guidance comment
attached to the icon property in the nav data inside the Navbar component:
locate the nav item object that contains icon: "tools" within
src/components/Navbar.tsx (in the Navbar component or the nav data array) and
delete the trailing comment "// You can use "tools" or any appropriate icon
name" so the nav configuration remains declarative and comment-free.
src/hooks/useUserFavorites.ts (1)

127-130: moveFavorite should mirror the same guard paths as toggleFavorite.

At Line 127, the wrapper silently returns for invalid states and skips the schema-readiness guard, so drag/drop can fail with no user feedback in edge cases.

♻️ Suggested guard parity with toggleFavorite
   const moveFavorite = (resourceUrl: string, folderId: string | null) => {
-    if (!user || !resourceUrl) return;
+    if (!user) {
+      toast.error('Please sign in to organize favorites');
+      return;
+    }
+    if (!resourceUrl) {
+      toast.error('Unable to move this favorite');
+      return;
+    }
+    if (!isSchemaReady) {
+      toast.error('Favorites storage needs a database update');
+      return;
+    }
     moveFavoriteMutation.mutate({ resourceUrl, folderId });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useUserFavorites.ts` around lines 127 - 130, The moveFavorite
wrapper currently only checks user and resourceUrl and skips the same guards
toggleFavorite enforces (causing silent failures); update moveFavorite to mirror
toggleFavorite's guard paths by checking favoritesSchemaReady (and any other
early-return/handler logic used in toggleFavorite), and only call
moveFavoriteMutation.mutate({ resourceUrl, folderId }) when those guards pass;
ensure you reuse the same handling behavior (e.g., invoking the same
disabled/early-return handling or error feedback) so drag/drop failures surface
consistently.
src/pages/CreatorPackPage.tsx (2)

47-47: Consider using React Router's useNavigate instead of window.location.href.

Using window.location.href causes a full page reload, losing client-side state and the SPA navigation benefits. Consider using useNavigate from react-router-dom for a smoother transition.

♻️ Proposed refactor
-import { useParams, Link } from 'react-router-dom';
+import { useParams, Link, useNavigate } from 'react-router-dom';

Then in the component:

 const CreatorPackPage = () => {
     const { slug } = useParams<{ slug: string }>();
+    const navigate = useNavigate();
     const { fetchPackBySlug, deletePack } = useCreatorPacks();

And in the handler:

-        window.location.href = '/resources?tab=creator-packs';
+        navigate('/resources?tab=creator-packs');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/CreatorPackPage.tsx` at line 47, Replace the direct full-page
navigation using window.location.href in the CreatorPackPage component with
React Router's useNavigate: import { useNavigate } from 'react-router-dom', call
const navigate = useNavigate() at the top of the CreatorPackPage component, and
update the handler that currently sets window.location.href =
'/resources?tab=creator-packs' to instead call
navigate('/resources?tab=creator-packs') (or
navigate('/resources?tab=creator-packs', { replace: true }) if you want to
replace history); this preserves SPA state and client-side routing.

163-165: Author link navigates to # when username is missing.

When pack.profiles?.username is falsy, the link navigates to #, which is a non-functional destination. Consider disabling the link or rendering a non-clickable element in this case.

♻️ Proposed refactor
-                            <Link
-                                to={pack.profiles?.username ? `/u/${pack.profiles.username}` : '#'}
-                                className="flex items-center gap-2 hover:text-primary transition-colors"
-                            >
+                            {pack.profiles?.username ? (
+                                <Link
+                                    to={`/u/${pack.profiles.username}`}
+                                    className="flex items-center gap-2 hover:text-primary transition-colors"
+                                >
+                                    {authorAvatar ? (
+                                        <img src={authorAvatar} alt={authorName} className="w-8 h-8 rounded-full" />
+                                    ) : (
+                                        <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
+                                            <IconUser size={16} />
+                                        </div>
+                                    )}
+                                    <span className="font-medium">{authorName}</span>
+                                </Link>
+                            ) : (
+                                <div className="flex items-center gap-2">
+                                    <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
+                                        <IconUser size={16} />
+                                    </div>
+                                    <span className="font-medium">{authorName}</span>
+                                </div>
+                            )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/CreatorPackPage.tsx` around lines 163 - 165, The Link currently
points to `#` when `pack.profiles?.username` is falsy which results in a
non-functional anchor; update the JSX in CreatorPackPage.tsx around the Link
that uses `pack.profiles?.username` so that if username is truthy you render the
existing <Link to={`/u/${pack.profiles.username}`} ...> otherwise render a
non-clickable element (e.g., a <span> or <div>) with the same styling and aria
attributes (preserve "flex items-center gap-2 hover:text-primary
transition-colors" and add aria-disabled or role="text" as appropriate) to
prevent navigation and keep visual consistency; ensure any onClick or
accessibility handlers on the Link are not attached when rendering the fallback
element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/admin/AdminCreatorPacksManager.tsx`:
- Line 19: The pendingPacks state is typed as any[]; import the CreatorPack
interface from '@/hooks/useCreatorPacks' and update the useState call to
useState<CreatorPack[]>(...) so pendingPacks and setPendingPacks use the correct
CreatorPack type; ensure the import line for CreatorPack is added at the top of
AdminCreatorPacksManager.tsx and replace any other any[] uses for pendingPacks
with the new CreatorPack type.

In `@src/components/resources/DownloadWarningDialog.tsx`:
- Around line 19-21: The DownloadWarningDialog component currently uses the
downloadUrl prop directly for navigation; validate its scheme before assigning
it to any href or using it in onClick to prevent unsafe protocols. In
DownloadWarningDialog (props: isOpen, onClose, downloadUrl) parse downloadUrl
with the URL constructor inside a try/catch and ensure url.protocol is "http:"
or "https:"; if validation fails, disable the download action or render a safe
fallback (e.g., no href, disabled button, or show an error) so the link/button
that consumes downloadUrl cannot navigate to non-http(s) schemes.
- Around line 44-49: Replace the invalid nested interactive elements in
DownloadWarningDialog by rendering the anchor as the Button's underlying element
instead of wrapping a Button inside an <a>; specifically, use the Button
component's "asChild" (or equivalent) prop so the anchor (href={downloadUrl},
target="_blank", rel="noopener noreferrer", onClick={onClose}) becomes the
actual element while preserving Button styling and IconExternalLink; update the
element that currently uses <a href={downloadUrl} ...><Button ...> to render
Button as the child wrapper around the anchor attributes to avoid a button
inside an anchor and keep keyboard/screen-reader behavior intact.

In `@src/components/resources/FavoritesSidebar.tsx`:
- Around line 116-129: The FolderItem rendering in FavoritesSidebar drops
recursive state by hardcoding isSelected={false} and childrenFolders={[]} which
prevents deeper selection/highlighting and hierarchy; update the map to pass the
real child properties: compute isSelected for each child (e.g., compare child.id
to the currently selectedFolderId or call an existing selection helper) and pass
the child's actual children array (e.g., child.children or
child.childrenFolders) instead of an empty array so FolderItem can recurse;
ensure you propagate level + 1, onSelect, onEdit, onDelete, onDownload unchanged
so deeper FolderItem instances receive the correct handlers and state.

In `@src/components/resources/FavoritesTab.tsx`:
- Around line 203-216: Sanitize the computed extension and ensure the ZIP isn't
empty before reporting success: when building archive entries in the loop (using
ext, resource.filetype, url and filename), normalize ext to a safe alphanumeric
subset (fallback to "file" if invalid) before calling zip.file, and ensure
filename parts (resource.title and resource.credit) remain sanitized as already
done; after attempting to add files, check the generated JSZip instance for the
actual number of files added (inspect zip files list before calling
zip.generateAsync/saveAs) and only call saveAs and toast.success if at least one
file exists, otherwise show an error toast or message; update the block that
uses zip.generateAsync, saveAs, and toast.success accordingly.

In `@src/components/resources/FolderDialog.tsx`:
- Line 18: The submit path in FolderDialog calls the onSave prop without
awaiting it, which can drop promise rejections; update the submit handler (e.g.,
handleSubmit or the inline submit function in FolderDialog) to be async, set a
local saving flag (e.g., isSaving via setIsSaving) before calling await
props.onSave(name, color), wrap the await in try/catch to handle/log errors and
only close the dialog (or reset state) after the awaited call completes, and
ensure the submit button is disabled while isSaving to prevent duplicate
submits.

In `@src/components/resources/ResourceFilters.tsx`:
- Around line 101-113: The font preview Input (rendered when selectedCategory
=== 'fonts' and onFontPreviewTextChange exists) lacks an accessible name; add
one by either providing an explicit label element tied to the Input via id
(e.g., render a <label> for "Font preview text" and give the Input an id like
"font-preview-input") or by adding an aria-label/aria-labelledby prop to the
Input component (e.g., aria-label="Font preview text"), ensuring the controlled
value fontPreviewText and onChange handler onFontPreviewTextChange are
unchanged.

In `@src/hooks/useCreatorPacks.ts`:
- Line 63: The code uses many explicit any casts (e.g., "(supabase as any)",
"(data as any)", and "catch (error: any)") in useCreatorPacks.ts which violates
lint rules; replace these with proper types: import and type the Supabase client
as SupabaseClient from '@supabase/supabase-js' and use it instead of casting
supabase to any, type query returns with PostgrestResponse<T> or the specific
row interface (e.g., CreatorPack or PackRow) for variables currently cast as
any, and change catch (error: any) to catch (error: unknown) then narrow it
(e.g., if (error instanceof Error) { /* use error.message */ }) or create a
typed helper to extract message; update all occurrences across useCreatorPacks
functions (the Supabase calls around the data/error destructures and all catch
blocks) so linting passes and type-safety is restored.
- Around line 51-52: The regex escapes are unnecessary and trigger
no-useless-escape; update the slug generation in useCreatorPacks (the .replace
calls that currently use .replace(/[^\w\-]+/g, '') and .replace(/\-\-+/g, '-'))
by removing the backslashes before hyphens so the first becomes
.replace(/[^\w-]+/g, '') and the second becomes .replace(/- -+/g, '-') (i.e.,
use unescaped '-' where it isn't forming a range or ambiguous), keeping flags
and behavior the same; adjust the .replace calls in the slugify logic
accordingly.
- Around line 78-84: fetchPackBySlug (and callers like EditCreatorPackPage),
fetchPendingPacks, and reviewPack currently trust frontend checks; enforce
backend RLS and add server-side role/ownership checks: update Supabase RLS on
creator_packs to only return public/approved packs to unauthenticated users and
allow SELECT/UPDATE/DELETE for owners (owner_id eq auth.uid()) and admins
(role='admin'), then modify fetchPackBySlug to include a server-side filter for
status when calling from public contexts and to only return packs if the current
session user is owner or has admin role; in reviewPack, validate the current
user's role server-side (confirm admin) before performing any mutation and
reject if not authorized; similarly ensure fetchPendingPacks only queries
pending status and is callable only by admins or via a privileged RPC or policy.

In `@src/pages/CreateCreatorPackPage.tsx`:
- Around line 49-66: The form handler handleSubmit (and the same validation area
later around the other submit/location) currently only checks that externalLink
is non-empty; ensure the external_link being saved in CreateCreatorPackInput
uses a safe web protocol by validating/parsing externalLink and rejecting or
normalizing values that do not start with "http://" or "https://" (or by using
the URL constructor to verify protocol is "http:" or "https:"); if invalid,
prevent submission (setIsSubmitting(false)) and surface an error to the user
instead of persisting; keep uploadCoverImage/coverFile logic unchanged and apply
the same validation in the other submit path referenced (the second submission
block).

In `@src/pages/CreatorPackPage.tsx`:
- Around line 41-48: The handleDelete function calls deletePack without error
handling, which causes a success toast and redirect even on failure; wrap the
await deletePack(pack.id) call in a try/catch/finally: setIsDeleting(true)
before the try, await deletePack inside the try and on success call
toast.success and perform the redirect (window.location.href =
'/resources?tab=creator-packs'), in the catch call toast.error with a helpful
message (and include error.message), and ensure setIsDeleting(false) runs in
finally so the UI resets whether the delete succeeds or fails; reference
handleDelete, deletePack, setIsDeleting, toast.success, toast.error, and
window.location.href when making the change.

In `@src/pages/EditCreatorPackPage.tsx`:
- Around line 84-87: When uploading a new cover, do not overwrite or null out
the existing cover URL on upload failure; check the return value of
uploadCoverImage and if it returns null/undefined, abort the submit (or preserve
existingCoverUrl) instead of setting coverUrl to null. In the submit flow that
sets coverUrl (the coverUrl/coverFile handling and subsequent payload assembly),
verify uploadCoverImage(coverFile) returned a valid URL before proceeding—if it
failed, stop the submit and surface an error to the user (or retain
existingCoverUrl). Apply the same check to the analogous block at the other
occurrence (lines ~93-94) to prevent accidental clearing of cover_image_url.
- Around line 40-45: loadPack currently fetches pack by slug and only sets
title, but updatePack in useCreatorPacks expects the numeric id (it queries
`.eq('id', id)`), so passing slug on save fails to update; modify loadPack (the
function that calls fetchPackBySlug and setTitle) to also capture and store the
returned pack id in component state (e.g., setPackId or setEditingPackId using
data.id) and then change the save/update handler to call updatePack with that
stored id instead of the route slug; ensure references to updatePack, loadPack,
fetchPackBySlug and the new pack id state are updated accordingly.

In `@src/pages/ManageCreatorPacksPage.tsx`:
- Around line 30-37: The component unconditionally removes the pack from UI
because deletePack() swallows errors, causing desync; update deletePack to
return a boolean success flag (true on backend deletion, false on failure) and
then change handleDelete to call const success = await deletePack(packId) and
only call setUserPacks(prev => prev.filter(p => p.id !== packId)) when success
is true; also ensure setDeletingId(packId) is set before the call and cleared in
a finally block (setDeletingId(null)) so the UI state is always cleared even on
failure.

In `@src/pages/ResourcesHub.tsx`:
- Around line 110-115: The effect only reads the ?tab= param but never updates
it when the user changes tabs; update the URL whenever activeTab changes (and
when tab button handlers call setActiveTab) by writing the current activeTab
into window.history (e.g., history.replaceState) using URLSearchParams so the
?tab= query stays in sync for deep links/back/refresh; modify the existing
useEffect/read-once block (and the analogous code around the second occurrence
referenced) to either add a dependent useEffect watching activeTab or call a
small helper that updates the query param after setActiveTab is invoked.

---

Nitpick comments:
In `@src/components/Navbar.tsx`:
- Line 66: Remove the inline guidance comment attached to the icon property in
the nav data inside the Navbar component: locate the nav item object that
contains icon: "tools" within src/components/Navbar.tsx (in the Navbar component
or the nav data array) and delete the trailing comment "// You can use "tools"
or any appropriate icon name" so the nav configuration remains declarative and
comment-free.

In `@src/hooks/useUserFavorites.ts`:
- Around line 127-130: The moveFavorite wrapper currently only checks user and
resourceUrl and skips the same guards toggleFavorite enforces (causing silent
failures); update moveFavorite to mirror toggleFavorite's guard paths by
checking favoritesSchemaReady (and any other early-return/handler logic used in
toggleFavorite), and only call moveFavoriteMutation.mutate({ resourceUrl,
folderId }) when those guards pass; ensure you reuse the same handling behavior
(e.g., invoking the same disabled/early-return handling or error feedback) so
drag/drop failures surface consistently.

In `@src/pages/CreatorPackPage.tsx`:
- Line 47: Replace the direct full-page navigation using window.location.href in
the CreatorPackPage component with React Router's useNavigate: import {
useNavigate } from 'react-router-dom', call const navigate = useNavigate() at
the top of the CreatorPackPage component, and update the handler that currently
sets window.location.href = '/resources?tab=creator-packs' to instead call
navigate('/resources?tab=creator-packs') (or
navigate('/resources?tab=creator-packs', { replace: true }) if you want to
replace history); this preserves SPA state and client-side routing.
- Around line 163-165: The Link currently points to `#` when
`pack.profiles?.username` is falsy which results in a non-functional anchor;
update the JSX in CreatorPackPage.tsx around the Link that uses
`pack.profiles?.username` so that if username is truthy you render the existing
<Link to={`/u/${pack.profiles.username}`} ...> otherwise render a non-clickable
element (e.g., a <span> or <div>) with the same styling and aria attributes
(preserve "flex items-center gap-2 hover:text-primary transition-colors" and add
aria-disabled or role="text" as appropriate) to prevent navigation and keep
visual consistency; ensure any onClick or accessibility handlers on the Link are
not attached when rendering the fallback element.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d8e538 and 65966cc.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • package.json
  • src/App.tsx
  • src/components/Hero.tsx
  • src/components/Navbar.tsx
  • src/components/admin/AdminCreatorPacksManager.tsx
  • src/components/auth/OAuthProviders.tsx
  • src/components/resources/CreatorPackCard.tsx
  • src/components/resources/CreatorPacksTab.tsx
  • src/components/resources/DownloadWarningDialog.tsx
  • src/components/resources/FavoritesSidebar.tsx
  • src/components/resources/FavoritesTab.tsx
  • src/components/resources/FolderDialog.tsx
  • src/components/resources/ResourceCard.tsx
  • src/components/resources/ResourceFilters.tsx
  • src/components/resources/ResourcesList.tsx
  • src/hooks/useCreatorPacks.ts
  • src/hooks/useFavoriteFolders.ts
  • src/hooks/useResources.ts
  • src/hooks/useUserFavorites.ts
  • src/pages/Admin.tsx
  • src/pages/Community.tsx
  • src/pages/CreateCreatorPackPage.tsx
  • src/pages/CreatorPackPage.tsx
  • src/pages/EditCreatorPackPage.tsx
  • src/pages/ManageCreatorPacksPage.tsx
  • src/pages/ResourcesHub.tsx
  • src/providers/AuthContext.tsx
  • src/providers/AuthProvider.tsx

Comment on lines 44 to 49
<a href={downloadUrl} target="_blank" rel="noopener noreferrer" className="w-full sm:w-auto" onClick={onClose}>
<Button className="pixel-corners bg-cow-purple hover:bg-cow-purple/90 w-full sm:w-auto">
<IconExternalLink className="mr-2 h-4 w-4" />
Continue to Download
</Button>
</a>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid nesting interactive elements (<button> inside <a>).

At Line 45, the current structure creates invalid interactive nesting and can break keyboard/screen-reader behavior.

♿ Suggested semantic fix using Button asChild
-                    <a href={downloadUrl} target="_blank" rel="noopener noreferrer" className="w-full sm:w-auto" onClick={onClose}>
-                        <Button className="pixel-corners bg-cow-purple hover:bg-cow-purple/90 w-full sm:w-auto">
+                    <Button asChild className="pixel-corners bg-cow-purple hover:bg-cow-purple/90 w-full sm:w-auto">
+                        <a href={downloadUrl} target="_blank" rel="noopener noreferrer" className="w-full sm:w-auto" onClick={onClose}>
                             <IconExternalLink className="mr-2 h-4 w-4" />
                             Continue to Download
-                        </Button>
-                    </a>
+                        </a>
+                    </Button>
📝 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
<a href={downloadUrl} target="_blank" rel="noopener noreferrer" className="w-full sm:w-auto" onClick={onClose}>
<Button className="pixel-corners bg-cow-purple hover:bg-cow-purple/90 w-full sm:w-auto">
<IconExternalLink className="mr-2 h-4 w-4" />
Continue to Download
</Button>
</a>
<Button asChild className="pixel-corners bg-cow-purple hover:bg-cow-purple/90 w-full sm:w-auto">
<a href={downloadUrl} target="_blank" rel="noopener noreferrer" className="w-full sm:w-auto" onClick={onClose}>
<IconExternalLink className="mr-2 h-4 w-4" />
Continue to Download
</a>
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/resources/DownloadWarningDialog.tsx` around lines 44 - 49,
Replace the invalid nested interactive elements in DownloadWarningDialog by
rendering the anchor as the Button's underlying element instead of wrapping a
Button inside an <a>; specifically, use the Button component's "asChild" (or
equivalent) prop so the anchor (href={downloadUrl}, target="_blank",
rel="noopener noreferrer", onClick={onClose}) becomes the actual element while
preserving Button styling and IconExternalLink; update the element that
currently uses <a href={downloadUrl} ...><Button ...> to render Button as the
child wrapper around the anchor attributes to avoid a button inside an anchor
and keep keyboard/screen-reader behavior intact.

Comment on lines +41 to +48
const handleDelete = async () => {
if (!pack) return;
if (!window.confirm('Are you sure you want to delete this creator pack?')) return;
setIsDeleting(true);
await deletePack(pack.id);
toast.success('Creator pack deleted.');
window.location.href = '/resources?tab=creator-packs';
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing error handling in delete operation.

The deletePack call has no error handling. If the deletion fails, the success toast is still shown and the user is redirected, leaving them unaware of the failure and the pack potentially still existing.

🛡️ Proposed fix to add error handling
     const handleDelete = async () => {
         if (!pack) return;
         if (!window.confirm('Are you sure you want to delete this creator pack?')) return;
         setIsDeleting(true);
-        await deletePack(pack.id);
-        toast.success('Creator pack deleted.');
-        window.location.href = '/resources?tab=creator-packs';
+        try {
+            await deletePack(pack.id);
+            toast.success('Creator pack deleted.');
+            window.location.href = '/resources?tab=creator-packs';
+        } catch (error) {
+            toast.error('Failed to delete creator pack.');
+            setIsDeleting(false);
+        }
     };
📝 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 handleDelete = async () => {
if (!pack) return;
if (!window.confirm('Are you sure you want to delete this creator pack?')) return;
setIsDeleting(true);
await deletePack(pack.id);
toast.success('Creator pack deleted.');
window.location.href = '/resources?tab=creator-packs';
};
const handleDelete = async () => {
if (!pack) return;
if (!window.confirm('Are you sure you want to delete this creator pack?')) return;
setIsDeleting(true);
try {
await deletePack(pack.id);
toast.success('Creator pack deleted.');
window.location.href = '/resources?tab=creator-packs';
} catch (error) {
toast.error('Failed to delete creator pack.');
setIsDeleting(false);
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/CreatorPackPage.tsx` around lines 41 - 48, The handleDelete
function calls deletePack without error handling, which causes a success toast
and redirect even on failure; wrap the await deletePack(pack.id) call in a
try/catch/finally: setIsDeleting(true) before the try, await deletePack inside
the try and on success call toast.success and perform the redirect
(window.location.href = '/resources?tab=creator-packs'), in the catch call
toast.error with a helpful message (and include error.message), and ensure
setIsDeleting(false) runs in finally so the UI resets whether the delete
succeeds or fails; reference handleDelete, deletePack, setIsDeleting,
toast.success, toast.error, and window.location.href when making the change.

Comment on lines +30 to +37
const handleDelete = async (packId: string) => {
if (!window.confirm('Are you sure you want to delete this creator pack? This action cannot be undone.')) return;

setDeletingId(packId);
await deletePack(packId);
setUserPacks(prev => prev.filter(p => p.id !== packId));
setDeletingId(null);
};
Copy link

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:

#!/bin/bash
set -euo pipefail

# Find the useCreatorPacks hook file
FILE=$(fd 'useCreatorPacks.ts$' | head -n1)
if [ -z "$FILE" ]; then
    echo "File not found. Searching for similar names..."
    fd -i 'creator.*pack' --type f
    exit 1
fi

echo "=== Inspecting: $FILE ==="
echo ""

# Show the deletePack function implementation
echo "--- deletePack function ---"
rg -nP 'const deletePack|function deletePack' "$FILE" -A 30 | head -50

echo ""
echo "--- Error handling patterns in file ---"
rg -n 'throw|catch|toast|error|fail' "$FILE" | head -20

Repository: creatorcluster/renderdragon.org

Length of output: 2526


🏁 Script executed:

#!/bin/bash
# Verify if setUserPacks is a separate state or synced with hook
FILE="src/pages/ManageCreatorPacksPage.tsx"
echo "=== Looking for state management in ManageCreatorPacksPage ==="
rg -n 'setUserPacks|useCreatorPacks' "$FILE" -B2 -A2

Repository: creatorcluster/renderdragon.org

Length of output: 1240


Remove local pack only if deletion succeeds; the proposed fix won't work as written.

The desync risk is real: deletePack() catches errors internally and returns successfully regardless of backend failure. The component then unconditionally removes the pack from UI after await deletePack(), leaving the UI and backend out of sync.

However, wrapping deletePack() in a try-catch won't fix this because deletePack() already catches exceptions and doesn't re-throw them—the catch block in the component would never execute.

Better solutions:

  • Modify deletePack() to return a success/failure boolean that the component can check before removing the pack
  • Skip removal in the component and rely solely on the hook's internal state management
  • Refactor deletePack() to throw exceptions instead of catching them internally, allowing the component to handle failures
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/ManageCreatorPacksPage.tsx` around lines 30 - 37, The component
unconditionally removes the pack from UI because deletePack() swallows errors,
causing desync; update deletePack to return a boolean success flag (true on
backend deletion, false on failure) and then change handleDelete to call const
success = await deletePack(packId) and only call setUserPacks(prev =>
prev.filter(p => p.id !== packId)) when success is true; also ensure
setDeletingId(packId) is set before the call and cleared in a finally block
(setDeletingId(null)) so the UI state is always cleared even on failure.

Comment on lines 110 to 115
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('tab') === 'favorites') {
setShowFavorites(true);
}
const tabParam = urlParams.get('tab');
if (tabParam === 'favorites') setActiveTab('favorites');
else if (tabParam === 'creator-packs') setActiveTab('creator-packs');
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Synchronize activeTab back to ?tab= to preserve deep links.

The tab is initialized from URL, but tab button clicks never write back to URL state. Refresh/share/back can reopen the wrong tab.

🛠️ Proposed fix
   useEffect(() => {
     const urlParams = new URLSearchParams(window.location.search);
     const tabParam = urlParams.get('tab');
     if (tabParam === 'favorites') setActiveTab('favorites');
     else if (tabParam === 'creator-packs') setActiveTab('creator-packs');
   }, []);
+
+  useEffect(() => {
+    const url = new URL(window.location.href);
+    if (activeTab === 'resources') {
+      url.searchParams.delete('tab');
+    } else {
+      url.searchParams.set('tab', activeTab);
+    }
+    const query = url.searchParams.toString();
+    const nextUrl = `${url.pathname}${query ? `?${query}` : ''}`;
+    window.history.replaceState({}, '', nextUrl);
+  }, [activeTab]);

Also applies to: 210-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/ResourcesHub.tsx` around lines 110 - 115, The effect only reads the
?tab= param but never updates it when the user changes tabs; update the URL
whenever activeTab changes (and when tab button handlers call setActiveTab) by
writing the current activeTab into window.history (e.g., history.replaceState)
using URLSearchParams so the ?tab= query stays in sync for deep
links/back/refresh; modify the existing useEffect/read-once block (and the
analogous code around the second occurrence referenced) to either add a
dependent useEffect watching activeTab or call a small helper that updates the
query param after setActiveTab is invoked.

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR introduces three major feature additions to the RenderDragon platform:

Google OAuth Integration

  • Added Google as a third OAuth provider alongside GitHub and Discord
  • Implemented proper avatar URL extraction and synchronization for Google users
  • Extended AuthProvider and AuthContext to support Google sign-in with appropriate OAuth parameters (access_type: 'offline', prompt: 'consent')

Creator Packs System

  • Implemented a complete creator packs feature allowing users to submit, manage, and browse curated resource collections
  • Added admin review workflow with approve/reject functionality and rejection reasons
  • Created dedicated pages for creating, editing, managing, and viewing creator packs
  • Includes markdown-based descriptions, cover image uploads to Supabase storage, tagging system, and external download links
  • Packs require admin approval before appearing publicly

Favorites Folder Organization

  • Added folder management system for organizing favorite resources
  • Implemented drag-and-drop functionality using @dnd-kit for moving resources between folders
  • Added folder download as ZIP capability using jszip and file-saver libraries
  • Includes folder CRUD operations, color customization, and hierarchical folder support

Additional Improvements

  • Added custom font preview text input in resources browser
  • Added UI badges and announcements for new features in Hero and Navbar
  • Added download warning dialog component

Breaking Change
The user_favorites table now requires a folder_id column. The PR description includes the migration SQL.

Confidence Score: 4/5

  • This PR is generally safe to merge with minor style improvements possible
  • The implementation is well-structured with proper error handling, loading states, and user feedback. The authentication changes follow OAuth best practices. The drag-and-drop implementation uses a mature library. However, there's extensive use of type assertions in useCreatorPacks.ts that could benefit from proper TypeScript types, and one minor typo in a toast message.
  • Check that the database migration for user_favorites.folder_id has been applied before deploying. Also verify Supabase storage bucket creator_packs_covers exists and has appropriate permissions.

Important Files Changed

Filename Overview
src/providers/AuthProvider.tsx Added Google OAuth provider with proper avatar URL handling and sync logic
src/hooks/useFavoriteFolders.ts New hook for managing favorite folders with CRUD operations and proper error handling
src/hooks/useUserFavorites.ts Extended to support folder_id tracking and added moveFavorite mutation for drag-and-drop
src/components/resources/FavoritesSidebar.tsx New sidebar component with folder tree, drag-and-drop zones, and folder management actions
src/components/resources/FavoritesTab.tsx Integrated drag-and-drop for favorites organization and ZIP download functionality
src/hooks/useCreatorPacks.ts New hook for creator packs with CRUD operations, admin review, and image upload
src/components/admin/AdminCreatorPacksManager.tsx New admin component for reviewing pending creator packs with approve/reject functionality
src/pages/CreateCreatorPackPage.tsx New page for creating creator packs with markdown preview and cover image upload

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User] -->|Sign In| B{Auth Provider}
    B -->|Email/Password| C[Supabase Auth]
    B -->|OAuth| D{OAuth Provider}
    D -->|GitHub| E[GitHub OAuth]
    D -->|Discord| F[Discord OAuth]
    D -->|Google NEW| G[Google OAuth]
    E --> H[Avatar Sync]
    F --> H
    G --> H
    H --> I[Profiles Table]
    
    A -->|Browse Resources| J[Resources Hub]
    J -->|Tab Selection| K{Resource Type}
    K -->|Resources| L[Resources List]
    K -->|Creator Packs NEW| M[Creator Packs Tab]
    K -->|Favorites| N[Favorites Tab NEW]
    
    N -->|Organize| O[Folders Sidebar]
    O -->|Create/Edit| P[Folder Dialog]
    O -->|Download| Q[ZIP Generation]
    O -->|Drag & Drop| R[Move to Folder]
    
    M -->|View Pack| S[Creator Pack Page]
    M -->|Create New| T[Create Pack Form]
    T -->|Submit| U{Review Status}
    U -->|Pending| V[Admin Review]
    V -->|Approve| W[Public Display]
    V -->|Reject| X[User Notification]
    X -->|Edit & Resubmit| T
    
    A -->|Manage Packs| Y[Manage Packs Page]
    Y -->|Edit| Z[Edit Pack Page]
    Z --> U
Loading

Last reviewed commit: 65966cc

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

30 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

}

setIsZipping(true);
toast.info(`Preparing zip map for ${folder.name}...`);
Copy link

Choose a reason for hiding this comment

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

Typo: "zip map" should be "zip file"

Suggested change
toast.info(`Preparing zip map for ${folder.name}...`);
toast.info(`Preparing zip file for ${folder.name}...`);

Add proper TypeScript types to useCreatorPacks hook with explicit interfaces and error handling. Add URL validation for external links in DownloadWarningDialog, CreateCreatorPackPage, and EditCreatorPackPage to ensure link integrity. Refactor FavoritesSidebar props structure and FolderDialog async handling. Add creator_packs table type definitions to supabase types.
Copy link

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

♻️ Duplicate comments (1)
src/components/resources/FavoritesTab.tsx (1)

203-205: ⚠️ Potential issue | 🟡 Minor

Further harden extension normalization for ZIP entry names.

Line 204 validates allowed chars, but extension length/case are still unconstrained. Extremely long alphanumeric values can still produce problematic archive names.

🧷 Suggested normalization
-          const rawExt = resource.filetype || url.split('.').pop()?.split('?')[0] || 'file';
-          const ext = /^[a-z0-9]+$/i.test(rawExt) ? rawExt : 'file';
+          const rawExt = resource.filetype || url.split('.').pop()?.split('?')[0] || 'file';
+          const ext = rawExt.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 10) || 'file';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/resources/FavoritesTab.tsx` around lines 203 - 205, The ext
normalization is too permissive; update the rawExt->ext handling so only short
alphanumeric extensions are allowed and stored lowercase (e.g., test
/^[a-z0-9]{1,8}$/i or truncate to 8 chars) and otherwise fall back to 'file';
specifically change the ext assignment around rawExt/ext to validate length and
set ext = rawExt.toLowerCase() (or ext = rawExt.slice(0,8).toLowerCase() if you
prefer truncation) and keep the fallback 'file'; also ensure the filename
normalization (resource.title -> filename) remains trimmed to a safe length
(e.g., limit to 255 chars) after replacing invalid chars to avoid overly long
ZIP entry names.
🧹 Nitpick comments (5)
src/pages/CreateCreatorPackPage.tsx (1)

29-41: Consider adding authentication guard.

The component accesses createPack without checking if the user is authenticated. While createPack in the hook handles the unauthenticated case with a toast, users can fill out the entire form before discovering they need to sign in.

♻️ Proposed early auth check
 const CreateCreatorPackPage = () => {
     const navigate = useNavigate();
     const { createPack, uploadCoverImage } = useCreatorPacks();
+    const { user } = useAuth();
+
+    useEffect(() => {
+        if (user === null) {
+            toast.error('Please sign in to create a creator pack.');
+            navigate('/resources?tab=creator-packs');
+        }
+    }, [user, navigate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/CreateCreatorPackPage.tsx` around lines 29 - 41, The component lets
unauthenticated users fill the form and only discovers auth failure when calling
createPack; add an early auth guard in CreateCreatorPackPage by checking the
authentication state (e.g., isAuthenticated or currentUser from your auth
hook/context) on mount and before rendering the form: if unauthenticated, either
navigate to the sign-in page with navigate('/signin') or render a signin prompt
and disable the form inputs and submit button; ensure this guard references the
same useAuth/useUser boolean you use elsewhere so createPack is only called when
authenticated.
src/pages/EditCreatorPackPage.tsx (1)

17-28: Consider extracting shared constants and utilities.

AVAILABLE_TAGS and isValidUrl are duplicated between EditCreatorPackPage and CreateCreatorPackPage. Consider extracting these to a shared location (e.g., src/constants/creatorPacks.ts and src/utils/validation.ts) to maintain DRY principles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/EditCreatorPackPage.tsx` around lines 17 - 28, AVAILABLE_TAGS and
isValidUrl are duplicated across EditCreatorPackPage and CreateCreatorPackPage;
extract them to shared modules and import them instead. Create a constants
export (e.g., export const AVAILABLE_TAGS = [...] ) in a new
src/constants/creatorPacks.ts and move isValidUrl into a utility file (e.g.,
export const isValidUrl = (url: string) => { ... }) in src/utils/validation.ts,
then replace the local definitions in EditCreatorPackPage (references:
AVAILABLE_TAGS, isValidUrl) and CreateCreatorPackPage with imports from those
new modules and update any tests/import paths accordingly.
src/hooks/useCreatorPacks.ts (1)

249-272: Consider validating file type and size before upload.

The uploadCoverImage function accepts any file matching image/*. Consider adding client-side validation for maximum file size and allowed MIME types to provide faster feedback and reduce unnecessary upload attempts.

♻️ Optional validation example
 const uploadCoverImage = useCallback(async (file: File): Promise<string | null> => {
     if (!user) return null;
+    
+    const MAX_SIZE_MB = 5;
+    const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+    
+    if (!ALLOWED_TYPES.includes(file.type)) {
+        toast.error('Please upload a valid image (JPEG, PNG, WebP, or GIF).');
+        return null;
+    }
+    
+    if (file.size > MAX_SIZE_MB * 1024 * 1024) {
+        toast.error(`Image must be smaller than ${MAX_SIZE_MB}MB.`);
+        return null;
+    }

     const fileExt = file.name.split('.').pop();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useCreatorPacks.ts` around lines 249 - 272, The uploadCoverImage
function currently uploads any file; add client-side validation in
uploadCoverImage to reject non-image MIME types and files exceeding a max size
before calling supabase.storage: check file.type (e.g., allow image/png,
image/jpeg, image/webp or file.type.startsWith('image/')) against an allowed
list and compare file.size to a MAX_COVER_SIZE_BYTES constant, call toast.error
and return null immediately on invalid input, and only proceed to build fileName
and call supabase.storage.upload when validation passes (keep existing error
handling around the upload).
src/components/resources/FavoritesTab.tsx (2)

59-60: Use one favorites source for both data and loading state.

Line 59 reads loading from useHeartedResources, but Line 60 reads data/actions from useUserFavorites. This can drift state and adds redundant hook work in this tab.

♻️ Suggested cleanup
-import { useHeartedResources } from '@/hooks/useHeartedResources';
@@
-  const { isLoading: favoritesLoading } = useHeartedResources();
-  const { favoritesData, moveFavorite } = useUserFavorites();
+  const { favoritesData, moveFavorite, isLoading: favoritesLoading } = useUserFavorites();

Also applies to: 86-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/resources/FavoritesTab.tsx` around lines 59 - 60, FavoritesTab
reads loading from useHeartedResources but data/actions from useUserFavorites,
which can create stale/drifting state and duplicate work; pick a single source
(either enhance useUserFavorites to return isLoading or switch to
useHeartedResources for data/mutations) and update FavoritesTab to import and
use that one hook for both favoritesData, moveFavorite and the loading flag
(replace favoritesLoading/favoritesData/moveFavorite usages accordingly) so
loading and data come from the same hook.

88-108: Pre-index favoritesData to avoid repeated O(n×m) scans.

Lines 88-108 and Lines 177-180 call .some/.find inside filters repeatedly. Build a Set/Map once and reuse it for render + download filtering.

⚡ Suggested indexing approach
+  const favoriteUrlSet = useMemo(() => {
+    return new Set(favoritesData.map(f => f.resource_url));
+  }, [favoritesData]);
+
+  const favoriteFolderByUrl = useMemo(() => {
+    const map = new Map<string, string | null>();
+    for (const f of favoritesData) {
+      map.set(f.resource_url, f.folder_id ?? null);
+    }
+    return map;
+  }, [favoritesData]);
+
   const favoriteResources = useMemo(() => {
     return resources.filter(resource => {
       const resourceUrl = getResourceUrl(resource);
-      return resourceUrl ? favoritesData.some(f => f.resource_url === resourceUrl) : false;
+      return resourceUrl ? favoriteUrlSet.has(resourceUrl) : false;
     });
-  }, [resources, favoritesData]);
+  }, [resources, favoriteUrlSet]);
@@
   const displayedResources = useMemo(() => {
     return favoriteResources.filter(resource => {
       const resourceUrl = getResourceUrl(resource);
-      const favData = favoritesData.find(f => f.resource_url === resourceUrl);
+      if (!resourceUrl) return false;
@@
-      return favData?.folder_id === selectedFolderId;
+      return favoriteFolderByUrl.get(resourceUrl) === selectedFolderId;
     });
-  }, [favoriteResources, favoritesData, selectedFolderId]);
+  }, [favoriteResources, favoriteFolderByUrl, selectedFolderId]);
@@
     const folderResources = favoriteResources.filter(r => {
       const url = getResourceUrl(r);
-      return url && favoritesData.find(f => f.resource_url === url)?.folder_id === folder.id;
+      return url ? favoriteFolderByUrl.get(url) === folder.id : false;
     });

Also applies to: 177-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/resources/FavoritesTab.tsx` around lines 88 - 108,
favoriteResources and displayedResources currently call favoritesData.some/find
repeatedly causing O(n×m) work; build an index (e.g., a Set of resource_url for
membership checks and a Map from resource_url to favorite record for lookups)
once from favoritesData and reuse it in the filters instead of calling
favoritesData.some/find inside the predicates — create the index above
favoriteResources (using favoritesData.map/reduce) and replace uses in
favoriteResources, displayedResources and the other filter at lines referenced
by getResourceUrl and selectedFolderId to perform O(1) lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/resources/FavoritesTab.tsx`:
- Around line 247-317: The UI only supports dragging to move resources (handled
by DndContext with handleDragStart/handleDragEnd and DraggableResourceCard),
leaving keyboard-only users unable to reassign folders; add a
keyboard-accessible fallback by exposing the same move logic used in
handleDragEnd as a callable function (e.g., moveResource(resourceId,
targetFolderId)) and wire a "Move" action on DraggableResourceCard (and/or the
ResourceCard overlay) that opens a focusable folder picker or modal from
FavoritesSidebar to select the destination folder; implement onKeyDown handlers
(Enter/Space) on DraggableResourceCard to open the picker, ensure the picker is
fully keyboard-navigable and calls moveResource, and reuse existing
state/updaters (selectedFolderId, displayedResources, getResourceUrl) so the UI
updates identical to a drag operation.

In `@src/pages/CreateCreatorPackPage.tsx`:
- Around line 70-73: The submit flow in CreateCreatorPackPage assigns coverUrl =
await uploadCoverImage(coverFile) but proceeds even if uploadCoverImage returns
null; update the submit handler in CreateCreatorPackPage to mirror
EditCreatorPackPage by checking the result of uploadCoverImage (use the
coverFile, coverUrl, uploadCoverImage symbols) and abort the submit (return or
throw/stop execution) when coverUrl === null, surface an appropriate user-facing
error message or set form error so the user knows the upload failed instead of
creating a pack with cover_image_url: null.

In `@src/pages/EditCreatorPackPage.tsx`:
- Around line 50-72: The loadPack effect should verify ownership after calling
fetchPackBySlug: after receiving data in loadPack (function name loadPack,
variable fetchPackBySlug) compare data.user_id (or data.owner_id) against the
current logged-in user id from your auth/context (e.g., currentUser.id or
user.id from useAuth/useCurrentUser) and if they don't match call
navigate('/creator-packs/manage') (or show a not-authorized state) and return
early; otherwise proceed to call setPackId, setTitle, setSmallDescription,
setDescription, setExternalLink, setSelectedTags, setExistingCoverUrl, setStatus
and setReviewReason as currently implemented. Ensure the auth/currentUser value
is imported or available in the component and handle the loading flag
consistently when redirecting.

---

Duplicate comments:
In `@src/components/resources/FavoritesTab.tsx`:
- Around line 203-205: The ext normalization is too permissive; update the
rawExt->ext handling so only short alphanumeric extensions are allowed and
stored lowercase (e.g., test /^[a-z0-9]{1,8}$/i or truncate to 8 chars) and
otherwise fall back to 'file'; specifically change the ext assignment around
rawExt/ext to validate length and set ext = rawExt.toLowerCase() (or ext =
rawExt.slice(0,8).toLowerCase() if you prefer truncation) and keep the fallback
'file'; also ensure the filename normalization (resource.title -> filename)
remains trimmed to a safe length (e.g., limit to 255 chars) after replacing
invalid chars to avoid overly long ZIP entry names.

---

Nitpick comments:
In `@src/components/resources/FavoritesTab.tsx`:
- Around line 59-60: FavoritesTab reads loading from useHeartedResources but
data/actions from useUserFavorites, which can create stale/drifting state and
duplicate work; pick a single source (either enhance useUserFavorites to return
isLoading or switch to useHeartedResources for data/mutations) and update
FavoritesTab to import and use that one hook for both favoritesData,
moveFavorite and the loading flag (replace
favoritesLoading/favoritesData/moveFavorite usages accordingly) so loading and
data come from the same hook.
- Around line 88-108: favoriteResources and displayedResources currently call
favoritesData.some/find repeatedly causing O(n×m) work; build an index (e.g., a
Set of resource_url for membership checks and a Map from resource_url to
favorite record for lookups) once from favoritesData and reuse it in the filters
instead of calling favoritesData.some/find inside the predicates — create the
index above favoriteResources (using favoritesData.map/reduce) and replace uses
in favoriteResources, displayedResources and the other filter at lines
referenced by getResourceUrl and selectedFolderId to perform O(1) lookups.

In `@src/hooks/useCreatorPacks.ts`:
- Around line 249-272: The uploadCoverImage function currently uploads any file;
add client-side validation in uploadCoverImage to reject non-image MIME types
and files exceeding a max size before calling supabase.storage: check file.type
(e.g., allow image/png, image/jpeg, image/webp or
file.type.startsWith('image/')) against an allowed list and compare file.size to
a MAX_COVER_SIZE_BYTES constant, call toast.error and return null immediately on
invalid input, and only proceed to build fileName and call
supabase.storage.upload when validation passes (keep existing error handling
around the upload).

In `@src/pages/CreateCreatorPackPage.tsx`:
- Around line 29-41: The component lets unauthenticated users fill the form and
only discovers auth failure when calling createPack; add an early auth guard in
CreateCreatorPackPage by checking the authentication state (e.g.,
isAuthenticated or currentUser from your auth hook/context) on mount and before
rendering the form: if unauthenticated, either navigate to the sign-in page with
navigate('/signin') or render a signin prompt and disable the form inputs and
submit button; ensure this guard references the same useAuth/useUser boolean you
use elsewhere so createPack is only called when authenticated.

In `@src/pages/EditCreatorPackPage.tsx`:
- Around line 17-28: AVAILABLE_TAGS and isValidUrl are duplicated across
EditCreatorPackPage and CreateCreatorPackPage; extract them to shared modules
and import them instead. Create a constants export (e.g., export const
AVAILABLE_TAGS = [...] ) in a new src/constants/creatorPacks.ts and move
isValidUrl into a utility file (e.g., export const isValidUrl = (url: string) =>
{ ... }) in src/utils/validation.ts, then replace the local definitions in
EditCreatorPackPage (references: AVAILABLE_TAGS, isValidUrl) and
CreateCreatorPackPage with imports from those new modules and update any
tests/import paths accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65966cc and 6de1450.

📒 Files selected for processing (9)
  • src/components/admin/AdminCreatorPacksManager.tsx
  • src/components/resources/DownloadWarningDialog.tsx
  • src/components/resources/FavoritesSidebar.tsx
  • src/components/resources/FavoritesTab.tsx
  • src/components/resources/FolderDialog.tsx
  • src/hooks/useCreatorPacks.ts
  • src/integrations/supabase/types.ts
  • src/pages/CreateCreatorPackPage.tsx
  • src/pages/EditCreatorPackPage.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/resources/FavoritesSidebar.tsx
  • src/components/resources/FolderDialog.tsx
  • src/components/resources/DownloadWarningDialog.tsx
  • src/components/admin/AdminCreatorPacksManager.tsx

Comment on lines +247 to +317
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<IconHeart className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-vt323 mb-2">No favorites yet</h3>
<p className="text-muted-foreground">
Start exploring resources and add them to your favorites!
</p>
</motion.div>
);
}
<div className="flex flex-col md:flex-row gap-6 mx-auto w-full text-left">
<div className="w-full md:w-72 flex-shrink-0">
<div className="sticky top-28 h-[calc(100vh-8rem)] z-10">
<FavoritesSidebar
selectedFolderId={selectedFolderId}
onSelectFolder={setSelectedFolderId}
onCreateFolder={handleCreateFolderClick}
onEditFolder={handleEditFolderClick}
onDeleteFolder={handleDeleteFolderClick}
onDownloadFolder={handleDownloadFolder}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
</div>
</div>

return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{favoriteResources.map(resource => {
const resourceUrl = getResourceUrl(resource);
return (
<ResourceCard
key={`${resource.id}-${resourceUrl}`}
resource={resource}
onClick={onSelectResource}
/>
);
})}
</div>
<div className="flex-1 min-w-0 z-0">
{displayedResources.length === 0 ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-20 bg-muted/5 rounded-xl border border-border/50"
>
<IconHeart className="mx-auto h-12 w-12 text-muted-foreground mb-4 opacity-50" />
<h3 className="text-xl font-vt323 mb-2 font-medium">No resources found</h3>
<p className="text-muted-foreground">
{selectedFolderId
? "This folder is currently empty. Drop some resources here!"
: "Start exploring resources and add them to your favorites!"}
</p>
</motion.div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<AnimatePresence mode="popLayout">
{displayedResources.map(resource => (
<motion.div
key={`${resource.id}-${getResourceUrl(resource)}`}
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.2 }}
className="h-full"
>
<DraggableResourceCard
resource={resource}
onClick={onSelectResource}
/>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</div>
</div>

<DragOverlay>
{activeDragResource ? (
<div className="w-64 opacity-80 rotate-2 scale-105 pointer-events-none shadow-xl">
<ResourceCard resource={activeDragResource} onClick={() => { }} />
</div>
) : null}
</DragOverlay>
</DndContext>

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a keyboard-accessible fallback for moving resources between folders.

In this view, folder reassignment is drag-only. Keyboard-only users currently have no equivalent way to complete the move action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/resources/FavoritesTab.tsx` around lines 247 - 317, The UI
only supports dragging to move resources (handled by DndContext with
handleDragStart/handleDragEnd and DraggableResourceCard), leaving keyboard-only
users unable to reassign folders; add a keyboard-accessible fallback by exposing
the same move logic used in handleDragEnd as a callable function (e.g.,
moveResource(resourceId, targetFolderId)) and wire a "Move" action on
DraggableResourceCard (and/or the ResourceCard overlay) that opens a focusable
folder picker or modal from FavoritesSidebar to select the destination folder;
implement onKeyDown handlers (Enter/Space) on DraggableResourceCard to open the
picker, ensure the picker is fully keyboard-navigable and calls moveResource,
and reuse existing state/updaters (selectedFolderId, displayedResources,
getResourceUrl) so the UI updates identical to a drag operation.

Comment on lines +50 to +72
useEffect(() => {
const loadPack = async () => {
if (!slug) return;
setIsLoading(true);
const data = await fetchPackBySlug(slug);
if (data) {
setPackId(data.id);
setTitle(data.title);
setSmallDescription(data.small_description || '');
setDescription(data.description || '');
setExternalLink(data.external_link);
setSelectedTags(data.tags || []);
setExistingCoverUrl(data.cover_image_url);
setStatus(data.status);
setReviewReason(data.review_reason || null);
} else {
// Not found or not owned
navigate('/creator-packs/manage');
}
setIsLoading(false);
};
loadPack();
}, [slug, fetchPackBySlug, navigate]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing ownership verification in loadPack.

fetchPackBySlug doesn't verify the current user owns the pack being edited. A user could potentially access the edit page for another user's pack via direct URL navigation. Consider adding an ownership check after fetching.

🛡️ Proposed ownership check
         const loadPack = async () => {
             if (!slug) return;
             setIsLoading(true);
             const data = await fetchPackBySlug(slug);
-            if (data) {
+            if (data && data.user_id === user?.id) {
                 setPackId(data.id);
                 setTitle(data.title);
                 // ... rest of setters
             } else {
-                // Not found or not owned
+                // Not found or not owned by current user
                 navigate('/creator-packs/manage');
             }
             setIsLoading(false);
         };

Note: The updatePack call in the hook does include .eq('user_id', user.id), so the backend will reject unauthorized updates. However, adding a frontend check improves UX by preventing users from filling out the form only to have it fail on submit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/EditCreatorPackPage.tsx` around lines 50 - 72, The loadPack effect
should verify ownership after calling fetchPackBySlug: after receiving data in
loadPack (function name loadPack, variable fetchPackBySlug) compare data.user_id
(or data.owner_id) against the current logged-in user id from your auth/context
(e.g., currentUser.id or user.id from useAuth/useCurrentUser) and if they don't
match call navigate('/creator-packs/manage') (or show a not-authorized state)
and return early; otherwise proceed to call setPackId, setTitle,
setSmallDescription, setDescription, setExternalLink, setSelectedTags,
setExistingCoverUrl, setStatus and setReviewReason as currently implemented.
Ensure the auth/currentUser value is imported or available in the component and
handle the loading flag consistently when redirecting.

On Vercel serverless, ffmpeg-static's postinstall scripts don't run, causing the download API to fail. Now always uses system ffmpeg binary instead.
Copy link

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/download.js`:
- Around line 125-126: The current console.log in api/download.js prints the raw
user-provided url variable and can leak sensitive query tokens; replace that
logging with a sanitized form (e.g., parse the url via the URL API and log only
origin+pathname or a redacted query string, or log a hash/fingerprint of the
full URL) so no raw query parameters are emitted; update the call that
references url (the console.log('[ytdl-download] Processing download request
for:', url) statement) to use the sanitizedUrl/hashed value instead and ensure
any future logs follow the same redaction approach.
- Around line 130-131: The retry logging in getInfoWithRetry is misleading
because it hardcodes the max attempts instead of using the passed-in tries
parameter; update the helper (getInfoWithRetry) to reference the tries argument
for max-attempt messages and compute the current attempt from its
loop/index/counter variable (e.g., attempt or i) so logs show "attempt X of N"
consistent with the tries argument passed from callers like the call site that
uses 4; also ensure any error messages or final failure log use tries rather
than the previous hardcoded value.
- Around line 5-7: Replace the hard-coded system ffmpeg path in the download
handler by restoring and using the ffmpeg-static bundled binary instead of
ffmpeg.setFfmpegPath('ffmpeg'); add ffmpeg-static back as a dependency and call
ffmpeg.setFfmpegPath with the ffmpeg-static provided path (or disable merged
download on Vercel if you intentionally removed ffmpeg-static), and ensure the
bundled binary is included in Vercel function tracing (configure
outputFileTracingIncludes) so the merged video+audio flow in api/download.js can
reliably find ffmpeg at runtime.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6de1450 and fd0ad7a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • api/download.js
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment on lines +5 to +7
// On Vercel serverless, ffmpeg-static postinstall scripts don't run,
// so we always use the system ffmpeg binary.
ffmpeg.setFfmpegPath('ffmpeg');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Vercel Node.js serverless ffmpeg availability PATH 2026

💡 Result:

On Vercel Node.js Serverless / Vercel Functions (Fluid compute) in 2026, FFmpeg is not something you can assume is preinstalled or on PATH (so calling spawn("ffmpeg", ...) is not reliable). The supported approach is to bundle an FFmpeg binary with your function and execute it via an explicit path. [1]

Working pattern (recommended):

  • Add an FFmpeg binary dependency like ffmpeg-static.
  • Ensure the binary is included in the function bundle (Next.js file tracing does not always include native binaries unless you declare them). Vercel’s reference example uses outputFileTracingIncludes to include ./node_modules/ffmpeg-static/ffmpeg. [2]
  • Spawn FFmpeg by pointing to the bundled binary (example spawns ./node_modules/ffmpeg-static/ffmpeg, though you can also use the path exported by ffmpeg-static). [3]

Operational constraints to keep in mind:

  • Vercel Functions have a read-only filesystem, with only /tmp writable (up to 500 MB). [4]
  • Installing/bundling FFmpeg can be large; Vercel staff caution it can push you over function size limits (the community thread specifically mentions the 50 MB limit concern). [5]

So, for “PATH availability”: treat it as not available; always ship the binary and invoke it by explicit path. [1][3]

Sources: [1] [2] [3] [4] [5]


🏁 Script executed:

# Search for ffmpeg references in the codebase
rg -i "ffmpeg" --type js -B 2 -A 2

# Check for environment variable usage related to ffmpeg
rg "FFMPEG" --type js -B 2 -A 2

# Look for error handling around downloads
rg -i "download|merged|ffmpeg" api/download.js -B 2 -A 2

# Check if there's package.json with ffmpeg-static dependency
cat package.json | grep -i ffmpeg

Repository: creatorcluster/renderdragon.org

Length of output: 5083


🏁 Script executed:

# Check git history to understand the context of this change
git log --oneline -n 20 -- api/download.js

Repository: creatorcluster/renderdragon.org

Length of output: 127


Revert to bundled ffmpeg-static or ffmpeg will not be available on Vercel.

The code hard-codes ffmpeg.setFfmpegPath('ffmpeg') assuming system-level ffmpeg availability, but the comment's premise is flawed: ffmpeg-static dependency was removed and Vercel Node.js Serverless does not include ffmpeg in PATH by default. When users attempt merged downloads (video + audio), the ffmpeg command will fail at runtime with a generic error.

Solution: Restore ffmpeg-static as a dependency and use the bundled binary path. Ensure the binary is included in function tracing (via outputFileTracingIncludes in Vercel config if needed). Alternatively, if ffmpeg-static was intentionally removed, the merged download path must be disabled on Vercel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 5 - 7, Replace the hard-coded system ffmpeg
path in the download handler by restoring and using the ffmpeg-static bundled
binary instead of ffmpeg.setFfmpegPath('ffmpeg'); add ffmpeg-static back as a
dependency and call ffmpeg.setFfmpegPath with the ffmpeg-static provided path
(or disable merged download on Vercel if you intentionally removed
ffmpeg-static), and ensure the bundled binary is included in Vercel function
tracing (configure outputFileTracingIncludes) so the merged video+audio flow in
api/download.js can reliably find ffmpeg at runtime.

Comment on lines 125 to +126
console.log('[ytdl-download] Processing download request for:', url);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging raw user-provided URLs.

This logs full query strings, which can leak user-specific tokens/identifiers into logs.

Proposed fix
-    console.log('[ytdl-download] Processing download request for:', url);
+    let safeUrlForLog = url;
+    try {
+      const parsed = new URL(url);
+      parsed.search = '';
+      safeUrlForLog = parsed.toString();
+    } catch {}
+    console.log('[ytdl-download] Processing download request for:', safeUrlForLog);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 125 - 126, The current console.log in
api/download.js prints the raw user-provided url variable and can leak sensitive
query tokens; replace that logging with a sanitized form (e.g., parse the url
via the URL API and log only origin+pathname or a redacted query string, or log
a hash/fingerprint of the full URL) so no raw query parameters are emitted;
update the call that references url (the console.log('[ytdl-download] Processing
download request for:', url) statement) to use the sanitizedUrl/hashed value
instead and ensure any future logs follow the same redaction approach.

Comment on lines 130 to +131
const info = await getInfoWithRetry(url, 4, 2000);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Retry attempt logging is now inconsistent with 4 attempts.

getInfoWithRetry is called with 4 tries, but the attempt counter inside the helper is hardcoded around 4 and can emit misleading attempt numbers.

Proposed fix
-async function getInfoWithRetry(url, tries = 3, delayMs = 2000) {
+async function getInfoWithRetry(url, tries = 3, delayMs = 2000, maxTries = tries) {
@@
-    console.log(`[ytdl-download] Attempt ${4 - tries} failed:`, err.message);
+    console.log(`[ytdl-download] Attempt ${maxTries - tries + 1} failed:`, err.message);
@@
-      return getInfoWithRetry(url, tries - 1, delayMs * 1.5);
+      return getInfoWithRetry(url, tries - 1, delayMs * 1.5, maxTries);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/download.js` around lines 130 - 131, The retry logging in
getInfoWithRetry is misleading because it hardcodes the max attempts instead of
using the passed-in tries parameter; update the helper (getInfoWithRetry) to
reference the tries argument for max-attempt messages and compute the current
attempt from its loop/index/counter variable (e.g., attempt or i) so logs show
"attempt X of N" consistent with the tries argument passed from callers like the
call site that uses 4; also ensure any error messages or final failure log use
tries rather than the previous hardcoded value.

@creatorcluster creatorcluster merged commit 0d4a30f into creatorcluster:main Mar 1, 2026
1 of 2 checks passed
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