feat(auth): add Google OAuth provider and expand login options#44
feat(auth): add Google OAuth provider and expand login options#44creatorcluster merged 5 commits intocreatorcluster:mainfrom
Conversation
…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
|
@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. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds 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
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ❌ 2❌ Failed checks (2 warnings)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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:moveFavoriteshould mirror the same guard paths astoggleFavorite.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
toggleFavoriteconst 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'suseNavigateinstead ofwindow.location.href.Using
window.location.hrefcauses a full page reload, losing client-side state and the SPA navigation benefits. Consider usinguseNavigatefromreact-router-domfor 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?.usernameis 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
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
package.jsonsrc/App.tsxsrc/components/Hero.tsxsrc/components/Navbar.tsxsrc/components/admin/AdminCreatorPacksManager.tsxsrc/components/auth/OAuthProviders.tsxsrc/components/resources/CreatorPackCard.tsxsrc/components/resources/CreatorPacksTab.tsxsrc/components/resources/DownloadWarningDialog.tsxsrc/components/resources/FavoritesSidebar.tsxsrc/components/resources/FavoritesTab.tsxsrc/components/resources/FolderDialog.tsxsrc/components/resources/ResourceCard.tsxsrc/components/resources/ResourceFilters.tsxsrc/components/resources/ResourcesList.tsxsrc/hooks/useCreatorPacks.tssrc/hooks/useFavoriteFolders.tssrc/hooks/useResources.tssrc/hooks/useUserFavorites.tssrc/pages/Admin.tsxsrc/pages/Community.tsxsrc/pages/CreateCreatorPackPage.tsxsrc/pages/CreatorPackPage.tsxsrc/pages/EditCreatorPackPage.tsxsrc/pages/ManageCreatorPacksPage.tsxsrc/pages/ResourcesHub.tsxsrc/providers/AuthContext.tsxsrc/providers/AuthProvider.tsx
| <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> |
There was a problem hiding this comment.
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.
| <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.
| 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'; | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| }; |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -A2Repository: 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.
| 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'); | ||
| }, []); |
There was a problem hiding this comment.
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 SummaryThis PR introduces three major feature additions to the RenderDragon platform: Google OAuth Integration
Creator Packs System
Favorites Folder Organization
Additional Improvements
Breaking Change Confidence Score: 4/5
|
| 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
Last reviewed commit: 65966cc
| } | ||
|
|
||
| setIsZipping(true); | ||
| toast.info(`Preparing zip map for ${folder.name}...`); |
There was a problem hiding this comment.
Typo: "zip map" should be "zip file"
| 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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/components/resources/FavoritesTab.tsx (1)
203-205:⚠️ Potential issue | 🟡 MinorFurther 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
createPackwithout checking if the user is authenticated. WhilecreatePackin 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_TAGSandisValidUrlare duplicated betweenEditCreatorPackPageandCreateCreatorPackPage. Consider extracting these to a shared location (e.g.,src/constants/creatorPacks.tsandsrc/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
uploadCoverImagefunction accepts any file matchingimage/*. 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 fromuseUserFavorites. 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-indexfavoritesDatato avoid repeated O(n×m) scans.Lines 88-108 and Lines 177-180 call
.some/.findinside filters repeatedly. Build aSet/Maponce 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
📒 Files selected for processing (9)
src/components/admin/AdminCreatorPacksManager.tsxsrc/components/resources/DownloadWarningDialog.tsxsrc/components/resources/FavoritesSidebar.tsxsrc/components/resources/FavoritesTab.tsxsrc/components/resources/FolderDialog.tsxsrc/hooks/useCreatorPacks.tssrc/integrations/supabase/types.tssrc/pages/CreateCreatorPackPage.tsxsrc/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
| <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> | ||
|
|
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
api/download.jspackage.json
🚧 Files skipped from review as they are similar to previous changes (1)
- package.json
| // On Vercel serverless, ffmpeg-static postinstall scripts don't run, | ||
| // so we always use the system ffmpeg binary. | ||
| ffmpeg.setFfmpegPath('ffmpeg'); |
There was a problem hiding this comment.
🧩 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
outputFileTracingIncludesto 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 byffmpeg-static). [3]
Operational constraints to keep in mind:
- Vercel Functions have a read-only filesystem, with only
/tmpwritable (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 ffmpegRepository: 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.jsRepository: 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.
| console.log('[ytdl-download] Processing download request for:', url); | ||
|
|
There was a problem hiding this comment.
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.
| const info = await getInfoWithRetry(url, 4, 2000); | ||
|
|
There was a problem hiding this comment.
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.
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
Style