Testingbranch - CodeRabbit Review - #1
Conversation
…nt deletion API, and new UI components and hooks.
WalkthroughThis pull request introduces a comprehensive user profile system with customizable theming, a blog publishing platform with admin management, account deletion functionality, Minecraft icon resources with pagination, and substantial refactoring of the authentication system and UI component architecture. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Browser as Browser
participant ProfileEditor as ProfileEditor<br/>(Component)
participant Supabase as Supabase DB
participant LocalStorage as LocalStorage
User->>Browser: Opens /account/profile
Browser->>ProfileEditor: Mount component
ProfileEditor->>Supabase: Fetch user profile data
Supabase-->>ProfileEditor: Return profile + theme config
ProfileEditor->>LocalStorage: Load draft (if exists)
LocalStorage-->>ProfileEditor: Return saved draft
ProfileEditor->>Browser: Render edit form (Content/Appearance tabs)
User->>ProfileEditor: Edit profile fields (name, bio, links)
ProfileEditor->>LocalStorage: Auto-save draft
User->>ProfileEditor: Toggle Preview mode
ProfileEditor->>ProfileEditor: Render live preview<br/>with ProfileThemeEngine
User->>ProfileEditor: Click Publish
ProfileEditor->>Supabase: Update profile (INSERT/UPDATE)
Supabase-->>ProfileEditor: Confirm success
rect rgb(200, 220, 240)
ProfileEditor->>LocalStorage: Clear draft
ProfileEditor->>Browser: Show success toast
end
User->>Browser: Navigate away
Browser->>Browser: Display updated profile
sequenceDiagram
participant User as User
participant Browser as Browser
participant BlogEditor as BlogEditor<br/>(Component)
participant Supabase as Supabase DB
participant Router as React Router
User->>Browser: Creates new blog post
Browser->>BlogEditor: Navigate to /admin/blogs/new
BlogEditor->>Browser: Render empty form
User->>BlogEditor: Enter title, content (Markdown)
BlogEditor->>BlogEditor: Auto-generate slug from title
User->>BlogEditor: Toggle Preview mode
BlogEditor->>BlogEditor: Render Markdown preview<br/>via ReactMarkdown
User->>BlogEditor: Click Save
rect rgb(240, 200, 200)
BlogEditor->>BlogEditor: Validate required fields
end
BlogEditor->>Supabase: INSERT new blog post<br/>(title, slug, content, author_id)
Supabase-->>BlogEditor: Return created blog (id, etc)
BlogEditor->>Browser: Show success toast
BlogEditor->>Router: Navigate to /admin
sequenceDiagram
participant User as User
participant Browser as Browser
participant DeleteFlow as Delete Account<br/>Flow
participant API as /api/deleteAccount
participant Supabase as Supabase<br/>(Admin)
participant Auth as Supabase Auth
User->>Browser: Clicks "Delete Account"
Browser->>DeleteFlow: Confirmation dialog shown
User->>DeleteFlow: Confirms deletion
DeleteFlow->>Browser: Extract session token
rect rgb(240, 180, 180)
DeleteFlow->>API: POST with Bearer token<br/>(Authorization header)
end
API->>Supabase: createClient(token)<br/>Call getUser() to verify token
Supabase-->>API: Return user ID or error
API->>API: Fetch admin secret key<br/>from environment
API->>Supabase: createAdminClient(secret)<br/>Call deleteUser(user.id)
Supabase-->>Auth: Delete user from Auth
API-->>DeleteFlow: Return 200 success
rect rgb(200, 240, 200)
DeleteFlow->>Browser: Show success message
DeleteFlow->>Browser: Sign out locally
DeleteFlow->>Browser: Redirect to home
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/hooks/useDownloadCounts.ts (1)
36-48: Non-atomic increment causes lost updates under concurrent access.The current implementation reads the count from local state, increments client-side, then writes back. If two users (or tabs) increment simultaneously, both read the same value and write
count + 1, losing one increment. This is a classic read-modify-write race condition.Consider using a database-level atomic increment via an RPC function or a raw SQL increment:
🔎 Suggested approach using Supabase RPC
Create a database function:
CREATE OR REPLACE FUNCTION increment_download_count(rid INTEGER) RETURNS void AS $$ BEGIN INSERT INTO downloads (resource_id, count) VALUES (rid, 1) ON CONFLICT (resource_id) DO UPDATE SET count = downloads.count + 1; END; $$ LANGUAGE plpgsql;Then call it atomically:
const incrementDownload = useCallback(async (resourceId: number) => { try { - const currentCount = downloadCounts[resourceId] || 0; - const newCount = currentCount + 1; - - const { error } = await supabase - .from('downloads') - .upsert({ - resource_id: resourceId, - count: newCount - }, { - onConflict: 'resource_id' - }); + const { error } = await supabase.rpc('increment_download_count', { + rid: resourceId + }); if (error) { console.error('Error incrementing download count:', error); return; } setDownloadCounts(prev => ({ ...prev, - [resourceId]: newCount, + [resourceId]: (prev[resourceId] || 0) + 1, }));src/components/ui/form.tsx (1)
101-120: Critical bug: Form message content is not rendered.The
<p>element is self-closing on line 118, so thebodyvariable (containing the error message or children) is never rendered. Users will see an empty error message container instead of the actual validation message.🔎 Proposed fix
return ( <p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props} - /> + > + {body} + </p> )src/components/Navbar.tsx (1)
450-461: Sign Out button does not perform sign out.The Sign Out button in the mobile drawer closes the drawer but doesn't actually sign out the user. The comment acknowledges this but it should be implemented.
🔎 Proposed fix
+import { supabase } from '@/integrations/supabase/client'; // In the component: <Button onClick={() => { - // Assuming sign out logic is handled elsewhere, e.g., via useAuth context - // You might need to add a sign-out function here if useAuth doesn't provide one - // Example: signOut(); + supabase.auth.signOut(); setIsDrawerOpen(false); }} variant="outline" className="w-full pixel-corners font-vt323" > Sign Out </Button>Alternatively, if
useAuthprovides asignOutmethod:-const { user, loading } = useAuth(); +const { user, loading, signOut } = useAuth(); // Then in the button: onClick={() => { + signOut?.(); setIsDrawerOpen(false); }}
🧹 Nitpick comments (33)
src/lib/showcase.ts (2)
80-89: Strengthen error type checking before accessing properties.The type assertion
e as { code?: string }doesn't validate the error structure. If Supabase throws an error without acodeproperty, accessingerror?.codewill silently fail the comparison, which is the desired fallback behavior, but relying on optional chaining for control flow is fragile.🔎 Recommended refinement with explicit type guard
} catch (e: unknown) { // Unique constraint violation on slug, try next candidate - const error = e as { code?: string }; - if (error?.code === '23505') { + if (typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === '23505') { suffix += 1 candidate = `${base}-${suffix}` continue } throw e }
143-157: Strengthen error type checking before accessing properties.Similar to the
createShowcasePagefunction, the type assertion lacks runtime validation. The fallback logic for missing columns suggests schema migration compatibility, which is a reasonable pattern, but the error check should be more defensive.🔎 Recommended refinement with explicit type guard
} catch (e: unknown) { // If the position column doesn't exist yet, fall back to created_at ordering - const error = e as { code?: string }; - if (error?.code === '42703') { + if (typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === '42703') { const { data, error } = await sb .from('showcase_media') .select('*') .eq('page_id', pageId) .order('created_at', { ascending: true }) if (error) throw error return (data || []) as ShowcaseMedia[] } throw e }src/components/ui/use-form-field.ts (1)
11-21: Consider adding a similar validation forFormItemContext.The
itemContext.idis used on line 34 and to derive IDs on lines 39-41. IfuseFormFieldis called outside of aFormItem,idwill be undefined, producing malformed IDs likeundefined-form-item.You may want to add a validation check for
itemContext.idas well for defensive coding.🔎 Optional enhancement
if (!fieldContext.name) { throw new Error("useFormField should be used within <FormField>") } + + if (!itemContext.id) { + throw new Error("useFormField should be used within <FormItem>") + }src/components/ui/sidebar-context.tsx (1)
10-20: Consider renaming to avoid type/value name collision.While TypeScript permits both a type and a const with the same name, having both
type SidebarContext(line 10) andconst SidebarContext(line 20) may cause confusion when reading or maintaining the code.🔎 Optional refactor to improve clarity
Option 1: Rename the type
-export type SidebarContext = { +export type SidebarContextType = { state: "expanded" | "collapsed" open: boolean setOpen: (open: boolean) => void openMobile: boolean setOpenMobile: (open: boolean) => void isMobile: boolean toggleSidebar: () => void } -export const SidebarContext = React.createContext<SidebarContext | null>(null) +export const SidebarContext = React.createContext<SidebarContextType | null>(null)Option 2: Rename the const
export type SidebarContext = { state: "expanded" | "collapsed" open: boolean setOpen: (open: boolean) => void openMobile: boolean setOpenMobile: (open: boolean) => void isMobile: boolean toggleSidebar: () => void } -export const SidebarContext = React.createContext<SidebarContext | null>(null) +export const SidebarContextInstance = React.createContext<SidebarContext | null>(null)Then update the import in
src/components/ui/sidebar.tsxaccordingly.src/components/ui/navigation-menu.tsx (1)
53-54: Remove the unnecessary empty string literal.The empty string
{""}serves no purpose and should be removed for cleaner code.🔎 Proposed fix
- {children} - {""} + {children} <IconChevronDownsrc/components/VideoPlayer.tsx (2)
64-73: Cleanup effect has unnecessary dependency.The dependency
[playerRef]is a ref object that never changes identity, so this effectively behaves like[]. While it works, explicitly using an empty dependency array makes the intent clearer (cleanup only on unmount).Suggested fix
// Dispose the player on unmount useEffect(() => { const player = playerRef.current; return () => { if (player && !player.isDisposed()) { player.dispose(); playerRef.current = null; } }; - }, [playerRef]); + }, []);
21-22: Consider using Video.js Player type instead ofany.For better type safety, you can import and use Video.js's Player type:
Suggested improvement
+import type Player from 'video.js/dist/types/player'; + const VideoPlayer: React.FC<VideoPlayerProps> = ({ // ... }) => { const videoRef = useRef<HTMLDivElement>(null); - const playerRef = useRef<any>(null); + const playerRef = useRef<Player | null>(null);src/providers/AuthProvider.tsx (2)
11-30: Potential race condition in auth initialization.The auth state listener is set up before checking for an existing session, but both can call
setLoading(false). IfgetSession()resolves beforeonAuthStateChangefires for an existing session, the loading state will be set correctly. However, if they resolve in the opposite order, there's a brief window where state could be inconsistent.Consider using a ref or checking if already initialized:
🔎 Suggested improvement
+ const [initialized, setInitialized] = useState(false); + useEffect(() => { // Set up auth state listener FIRST const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { console.log("Auth state changed:", event, session?.user?.email); setSession(session); setUser(session?.user ?? null); - setLoading(false); + if (!initialized) { + setInitialized(true); + setLoading(false); + } }); // THEN check for existing session supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); setUser(session?.user ?? null); - setLoading(false); + if (!initialized) { + setInitialized(true); + setLoading(false); + } }); return () => subscription.unsubscribe(); - }, []); + }, [initialized]);
168-172: Unused helper function.
getUsernameFromEmailis defined but not included in theAuthContext.Providervalue (lines 215-225), making it inaccessible to consumers. Either add it to the context value or remove it if not needed.🔎 Option 1: Add to context value
value={{ user, session, loading, signUp, signIn, signOut, signInWithGitHub, signInWithDiscord, refreshUser, + getUsernameFromEmail, }}Note: You'll also need to add it to
AuthContextTypeinsrc/providers/AuthContext.tsx.🔎 Option 2: Remove if unused
- // Helper function to extract username from email - const getUsernameFromEmail = (email: string | null | undefined): string => { - if (!email) return "User"; - return email.split("@")[0] || "User"; - }; -src/pages/Admin.tsx (1)
65-69: LGTM! Consider using a reusable Separator component.The AdminBlogsManager is correctly integrated into the admin UI with proper spacing.
💡 Optional: Extract divider to a reusable Separator component
If this divider pattern is used elsewhere, consider extracting it:
// src/components/ui/separator.tsx (if not already exists) const Separator = ({ className = "" }) => ( <div className={cn("h-px bg-border/50", className)} /> );Then use:
- <div className="h-px bg-border/50" /> + <Separator />supabase/migrations/20251214152600_add_profile_customization.sql (1)
1-6: Consider adding indexes for JSONB column queries.If you plan to query the JSONB columns (
theme_config,links,social_links) frequently, consider adding GIN indexes to improve query performance. Additionally, you may want to add a CHECK constraint or RLS policy to restrict who can set theverifiedflag.💡 Optional enhancements for performance and security
-- Add indexes for JSONB columns if queried frequently CREATE INDEX IF NOT EXISTS idx_profiles_theme_config ON profiles USING GIN (theme_config); CREATE INDEX IF NOT EXISTS idx_profiles_links ON profiles USING GIN (links); CREATE INDEX IF NOT EXISTS idx_profiles_social_links ON profiles USING GIN (social_links); -- Consider adding a CHECK constraint or trigger to validate JSONB structure -- Example: Ensure theme_config has required keys -- ALTER TABLE profiles ADD CONSTRAINT valid_theme_config -- CHECK (theme_config ? 'primaryColor' OR theme_config = '{}'::jsonb);scripts/export_resources.ts (1)
18-48: Add error handling for file operations and consider pagination for large datasets.The script fetches all resources without pagination, which could cause memory issues or timeouts with large datasets. Additionally,
fs.writeFileSynccan throw errors that aren't currently handled.💡 Suggested improvements
async function exportResources() { console.log('Fetching resources...'); - const { data, error } = await supabase - .from('resources') - .select('*'); + let allData = []; + let from = 0; + const PAGE_SIZE = 1000; + + while (true) { + const { data, error } = await supabase + .from('resources') + .select('*') + .range(from, from + PAGE_SIZE - 1); + + if (error) { + console.error('Error fetching data:', error); + return; + } + + if (!data || data.length === 0) break; + + allData = allData.concat(data); + if (data.length < PAGE_SIZE) break; + from += PAGE_SIZE; + } - if (error) { - console.error('Error fetching data:', error); - return; - } // Group by category for the structure the app likes - const grouped = data.reduce((acc, resource) => { + const grouped = allData.reduce((acc, resource) => { const cat = resource.category || 'uncategorized'; if (!acc.categories[cat]) { acc.categories[cat] = []; } acc.categories[cat].push({ id: resource.id, title: resource.title, ext: resource.filetype, url: resource.download_url, credit: resource.credit, - // Add any others if needed }); return acc; }, { categories: {} }); - fs.writeFileSync('resources_export.json', JSON.stringify(grouped, null, 2)); - console.log('Exported to resources_export.json'); + try { + fs.writeFileSync('resources_export.json', JSON.stringify(grouped, null, 2)); + console.log('Exported to resources_export.json'); + } catch (error) { + console.error('Error writing file:', error); + process.exit(1); + } }src/components/admin/AdminResourcesManager.tsx (1)
78-93: Consider refactoring to avoid exhaustive-deps suppressions.The
eslint-disablecomments forexhaustive-depssuggest potential issues with the dependency array. While the current implementation may work, consider these alternatives to avoid suppressions:
- Remove
pagefromuseCallbackdependencies and pass it as a parameter- Use a ref to access the current page value
- Split into separate stable callbacks for initial load vs. pagination
💡 Alternative pattern without suppressions
const pageRef = useRef(page); pageRef.current = page; const fetchResources = useCallback(async (isNewSearch = false) => { try { setLoading(true); const currentPage = isNewSearch ? 0 : pageRef.current; const from = currentPage * RESOURCES_PER_PAGE; // ... rest of the logic } catch (error) { // ... } }, [searchTerm, selectedCategory]); useEffect(() => { fetchResources(true); }, [searchTerm, selectedCategory, fetchResources]); useEffect(() => { if (page > 0) { fetchResources(); } }, [page, fetchResources]);src/lib/mciApi.ts (1)
13-54: Consider adding caching and request timeout.The API call lacks caching and timeout handling, which could lead to unnecessary network requests and potential hangs. Consider implementing caching for the MCI resources data.
💡 Suggested improvements
let cachedResources: Resource[] | null = null; let cacheTimestamp: number | null = null; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes export const fetchMciResources = async (): Promise<Resource[]> => { // Check cache first if (cachedResources && cacheTimestamp && Date.now() - cacheTimestamp < CACHE_DURATION) { return cachedResources; } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch(MCI_API_URL, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Failed to fetch MCI resources: ${response.status}`); } const data: MciItem[] = await response.json(); // ... mapping logic ... const resources = data.map((item, index) => { // ... existing mapping logic ... }); // Update cache cachedResources = resources; cacheTimestamp = Date.now(); return resources; } catch (error) { console.error("Error fetching MCI resources:", error); // Return cached data if available, otherwise empty array return cachedResources || []; } };src/types/resources.ts (1)
7-7: Consider the type safety trade-off for subcategory.Relaxing
subcategoryfrom a strict union('davinci' | 'adobe')to a genericstringincreases flexibility but loses compile-time type checking. This could allow typos or invalid values to slip through.If subcategories are expected to grow dynamically, consider documenting the expected values or implementing runtime validation to maintain data integrity.
src/components/profile/ProfileThemeEngine.tsx (1)
42-50: Consider adding URL validation for background images.While React's style handling prevents XSS, adding validation to ensure
backgroundImageis a valid URL (e.g., using URL constructor or regex) would improve robustness and provide better error handling for invalid input.💡 Optional validation example
const getBackgroundStyle = () => { if (config.backgroundType === 'image' && config.backgroundImage) { + try { + new URL(config.backgroundImage); + } catch { + console.warn('Invalid background image URL:', config.backgroundImage); + return { backgroundColor: config.backgroundColor }; + } return { backgroundImage: `url(${config.backgroundImage})`, backgroundSize: 'cover', backgroundPosition: 'center', backgroundAttachment: 'fixed' }; }src/components/resources/ResourcesList.tsx (1)
212-241: Pagination rendering logic works correctly.The ellipsis logic displays page numbers for: first page, last page, or within ±1 of the current page. Ellipsis appears at ±2 positions. This provides a good balance between showing context and managing space.
💡 Optional: Consider extracting pagination logic to a helper function
For improved readability and testability, the page number rendering logic could be extracted into a utility function that returns the array of pages/ellipsis to display.
+const getPaginationItems = (currentPage: number, totalPages: number) => { + return Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => { + if ( + page === 1 || + page === totalPages || + (page >= currentPage - 1 && page <= currentPage + 1) + ) { + return { type: 'page', value: page }; + } else if (page === currentPage - 2 || page === currentPage + 2) { + return { type: 'ellipsis', key: `ellipsis-${page}` }; + } + return null; + }).filter(Boolean); +};src/pages/Blogs.tsx (1)
76-89: Consider adding error handling for profile fetch failures.While the profiles fetch has error handling at the blogs level (line 69), the nested profile fetch (lines 77-88) silently fails. Consider logging or handling profile fetch errors to aid debugging.
💡 Add error handling
if (authorIds.length > 0) { - const { data: profilesData } = await supabase + const { data: profilesData, error: profileError } = await supabase .from("profiles") .select("id, display_name, avatar_url, username") .in("id", authorIds); + if (profileError) { + console.error("Error loading profiles:", profileError); + } + if (profilesData) {src/pages/BlogView.tsx (1)
133-144: Comprehensive prose styling for markdown content.The extensive Tailwind utility classes provide fine-grained control over markdown rendering. This works well for a one-off styling need.
💡 Optional: Extract prose styling to a CSS class
For better maintainability and reusability, consider extracting these prose styles to a dedicated CSS class in
index.css:@layer components { .blog-prose { @apply prose prose-invert max-w-none font-geist leading-loose; @apply [&>p]:mb-6 [&>p]:leading-7; @apply [&>h1]:hidden; @apply [&>h2]:mt-10 [&>h2]:mb-4 [&>h2]:text-2xl [&>h2]:font-semibold; /* ... rest of styles */ } }Then use:
<div className="blog-prose">src/components/admin/BlogEditor.tsx (1)
44-48: Auto-slug generation is well-implemented.The auto-generation only applies to new posts (when
idis absent) and correctly uses the slugify utility. This provides good UX while allowing manual override.💡 Optional: Debounce auto-slug generation
For improved performance during fast typing, consider debouncing the auto-slug generation:
import { useEffect, useRef } from 'react'; useEffect(() => { if (!id && title) { const timer = setTimeout(() => { setSlug(slugify(title)); }, 300); return () => clearTimeout(timer); } }, [title, id]);src/hooks/useProfile.ts (1)
43-43: Consider using a type guard instead of inline type assertion.The type cast
(data as { avatar_url?: string | null }).avatar_urlsuggests the Supabase schema types may not includeavatar_url. This could hide schema drift.🔎 Alternative: Define expected schema type
// At the top of the file or in a types file interface ProfileRow { id: string; email: string; display_name?: string | null; first_name?: string | null; last_name?: string | null; avatar_url?: string | null; created_at: string; updated_at: string; } // Then in the query: const { data, error } = await supabase .from("profiles") .select("*") .eq("id", user.id) .single<ProfileRow>();src/components/resources/ResourceFilters.tsx (1)
213-243: Extract duplicated subcategory options into a shared constant.The minecraft-icons subcategory options are duplicated between MobileFilters and DesktopFilters. Extract to a constant for maintainability.
🔎 Proposed refactor
// Add at the top of the file, after imports const MINECRAFT_ICON_SUBCATEGORIES = [ { value: "all", label: "All Icons" }, { value: "1. Swords", label: "Swords" }, { value: "2. Pickaxes", label: "Pickaxes" }, { value: "3. Axes", label: "Axes" }, { value: "4. Shovels", label: "Shovels" }, { value: "5. Hoes", label: "Hoes" }, { value: "10. Items", label: "Items" }, { value: "10. Food", label: "Food" }, { value: "11. Materials", label: "Materials" }, { value: "14. Potions", label: "Potions" }, { value: "15. Projectiles", label: "Projectiles" }, { value: "16. Dyes", label: "Dyes" }, { value: "21. Decoration", label: "Decoration" }, { value: "22. Coral", label: "Coral" }, { value: "23. Flowers", label: "Flowers" }, { value: "26. Redstone", label: "Redstone" }, { value: "30. Legacy spawn eggs", label: "Spawn Eggs" }, ] as const; // Then use in both components: <SelectContent className="max-h-[300px]"> {MINECRAFT_ICON_SUBCATEGORIES.map(({ value, label }) => ( <SelectItem key={value} value={value}>{label}</SelectItem> ))} </SelectContent>Also applies to: 349-377
src/components/AudioPlayer.tsx (1)
49-53: Consider adding user feedback for load failures.Currently, load errors are silently logged to console. Users may not understand why the waveform isn't loading.
🔎 Proposed enhancement
+ const [loadError, setLoadError] = useState(false); ws.load(src).catch((err) => { if (err.name === 'AbortError') return; console.error('WaveSurfer load error:', err); + if (isMounted) { + setLoadError(true); + setIsLoading(false); + } }); // In the JSX, replace the loading overlay condition: - {isLoading && ( + {(isLoading || loadError) && ( <div className="absolute inset-0 z-10 flex items-center justify-center bg-card/60 backdrop-blur-[1px]"> - <IconLoader2 className="h-6 w-6 animate-spin text-cow-purple" /> - <span className="ml-2 text-xs font-vt323 tracking-wider text-muted-foreground">LOADING WAVEFORM...</span> + {loadError ? ( + <span className="text-xs font-vt323 text-destructive">Failed to load audio</span> + ) : ( + <> + <IconLoader2 className="h-6 w-6 animate-spin text-cow-purple" /> + <span className="ml-2 text-xs font-vt323 tracking-wider text-muted-foreground">LOADING WAVEFORM...</span> + </> + )} </div> )}src/components/profile/ProfileEditor.tsx (2)
136-141: Consider replacing nativeconfirm()with a custom dialog.The native
confirm()dialog doesn't match the app's styled UI and can be jarring. Consider using a confirmation dialog component.
98-99: Type safety bypassed withas anyfor JSON columns.The casts
(data.links as any)and(themeConfig as any)bypass type checking. Consider defining proper types for the Supabase JSON columns.🔎 Suggested approach
// Define a type that matches what Supabase expects import { Json } from '@/integrations/supabase/types'; // When reading: setLinks((data.links as ProfileLink[]) || []); setThemeConfig((data.theme_config as ProfileThemeConfig) || defaultThemeConfig); // When writing, if needed: links: links as unknown as Json, theme_config: themeConfig as unknown as Json,Also applies to: 117-118
src/components/resources/ResourceCard.tsx (1)
172-190: Consider lazy-loading video source for animations.The video element loads immediately when
isInViewbecomes true. For better performance, consider usingpreload="none"or lazy-loading the source.🔎 Proposed enhancement
{isInView ? ( <video src={previewUrl} autoPlay loop muted playsInline + preload="metadata" className="w-full h-full object-cover" />src/hooks/useUserFavorites.ts (1)
20-24: Toast may fire multiple times on query retries.When the query fails and throws, React Query will retry by default. Each retry that fails will trigger the toast again, potentially spamming the user with error messages.
🔎 Proposed fix: Move toast to meta or disable retries
const { data: favorites = [], isLoading } = useQuery({ queryKey: ['userFavorites', user?.id], queryFn: async () => { if (!user?.id) return []; const { data, error } = await supabase .from('user_favorites') .select('resource_id') .eq('user_id', user.id); if (error) { console.error('Error fetching favorites:', error); - toast.error('Failed to load favorites'); throw error; } return data?.map(fav => fav.resource_id.toString()) || []; }, enabled: !!user?.id, staleTime: 1000 * 60 * 5, // Cache for 5 minutes + retry: false, // Disable retries to prevent duplicate error handling });src/pages/Showcase.tsx (1)
383-388: Minor: Redundant type casts.The explicit
as NewAsset | nullcasts are unnecessary sincenullis already a valid return type that gets filtered out on line 388.🔎 Simplified version
- if (!(isImage || isVideo || isAudio)) return null as NewAsset | null; + if (!(isImage || isVideo || isAudio)) return null; const kind = isImage ? ("image" as const) : isVideo ? ("video" as const) : ("audio" as const); const url = (f.ufsUrl && typeof f.ufsUrl === 'string') ? f.ufsUrl : (f.url ?? ""); - if (!url) return null as NewAsset | null; + if (!url) return null;src/hooks/useResources.ts (2)
30-37: Consider extracting API URL to configuration.The hardcoded API URL on line 33 should be moved to an environment variable or configuration file for easier environment management and to avoid committing potentially sensitive URLs.
+const RESOURCES_API_URL = import.meta.env.VITE_RESOURCES_API_URL || 'https://hamburger-api.powernplant101-c6b.workers.dev/all'; + const fetchResources = useCallback(async () => { try { setIsLoading(true); - const response = await fetch('https://hamburger-api.powernplant101-c6b.workers.dev/all'); + const response = await fetch(RESOURCES_API_URL);
207-227: Consider adding timeout for download fetches.The fetch call on line 209 has no timeout, which could cause the download to hang indefinitely on slow or unresponsive servers. Consider using
AbortControllerwith a timeout.🔎 Proposed fix with timeout
try { if (shouldForceDownload) { - const res = await fetch(fileUrl); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout + + const res = await fetch(fileUrl, { signal: controller.signal }); + clearTimeout(timeoutId); + if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`); const blob = await res.blob();src/pages/Profile.tsx (2)
53-65: Improve type safety: avoidas anycast.The
as anycast on line 61 bypasses TypeScript's type checking. Consider parsing the JSON fields explicitly or using a proper type guard.🔎 Proposed improvement
if (error) throw error; if (active) { - setProfile(data as any); // Cast because of JSON types + if (data) { + setProfile({ + ...data, + links: data.links as ProfileLink[] | null, + theme_config: data.theme_config as ProfileThemeConfig | null, + social_links: data.social_links as SocialLinks | null, + }); + } else { + setProfile(null); + } }
138-141: Consider restricting Markdown elements for user bio.User-controlled bio content is rendered with ReactMarkdown. While ReactMarkdown sanitizes by default, consider explicitly restricting allowed elements to prevent potential abuse (e.g., excessive headings, images, or links).
{profile.bio && ( <div className={`mt-4 prose prose-invert prose-lg max-w-none ${theme.avatarPosition === 'left' ? 'text-left' : theme.avatarPosition === 'right' ? 'text-right' : 'text-center'}`}> - <ReactMarkdown>{profile.bio}</ReactMarkdown> + <ReactMarkdown + allowedElements={['p', 'strong', 'em', 'a', 'br', 'ul', 'ol', 'li']} + unwrapDisallowed + > + {profile.bio} + </ReactMarkdown> </div> )}src/types/profile.ts (1)
73-82: LGTM! Consider addressing the TODO comment.The type definitions are well-structured. Note the comment on line 78 suggests using a pixel font for the
pixeltheme, but'geist'is currently assigned. If a pixel-style font is desired, consider adding it to thefontFamilyunion type.Would you like me to help identify pixel-style web fonts that could be added to the theme system?
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ 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 (79)
AI_RULES.mdapi/deleteAccount.jschangelog.mdindex.htmlpackage.jsonscripts/export_resources.tsserver.jssrc/App.tsxsrc/components/AudioPlayer.tsxsrc/components/DonateButton.tsxsrc/components/Footer.tsxsrc/components/Hero.tsxsrc/components/Navbar.tsxsrc/components/SupportersList.tsxsrc/components/ThemeToggle.tsxsrc/components/VideoPlayer.tsxsrc/components/admin/AdminBlogsManager.tsxsrc/components/admin/AdminResourcesManager.tsxsrc/components/admin/BlogEditor.tsxsrc/components/auth/UserMenu.tsxsrc/components/profile/ProfileEditor.tsxsrc/components/profile/ProfileThemeEngine.tsxsrc/components/profile/SvglPicker.tsxsrc/components/resources/ResourceCard.tsxsrc/components/resources/ResourceDetailDialog.tsxsrc/components/resources/ResourceFilters.tsxsrc/components/resources/ResourcePreview.tsxsrc/components/resources/ResourcesList.tsxsrc/components/ui/alert-dialog.tsxsrc/components/ui/badge-variants.tssrc/components/ui/badge.tsxsrc/components/ui/button-variants.tssrc/components/ui/button.tsxsrc/components/ui/calendar.tsxsrc/components/ui/command.tsxsrc/components/ui/form.tsxsrc/components/ui/navigation-menu-variants.tssrc/components/ui/navigation-menu.tsxsrc/components/ui/pagination.tsxsrc/components/ui/sidebar-context.tsxsrc/components/ui/sidebar.tsxsrc/components/ui/textarea.tsxsrc/components/ui/toggle-variants.tssrc/components/ui/toggle.tsxsrc/components/ui/use-form-field.tssrc/data/supporters.tssrc/hooks/useAuth.tsxsrc/hooks/useDownloadCounts.tssrc/hooks/useProfile.tssrc/hooks/useResources.tssrc/hooks/useUserFavorites.tssrc/index.csssrc/integrations/supabase/types.tssrc/lib/mciApi.tssrc/lib/showcase.tssrc/lib/showcases.tssrc/lib/utils.tssrc/main.tsxsrc/pages/Admin.tsxsrc/pages/AiTitleHelper.tsxsrc/pages/BlogView.tsxsrc/pages/Blogs.tsxsrc/pages/GuideView.tsxsrc/pages/Index.tsxsrc/pages/NotFound.tsxsrc/pages/Profile.tsxsrc/pages/ResourcesHub.tsxsrc/pages/Showcase.tsxsrc/pages/YouTubeDownloader.tsxsrc/providers/AuthContext.tsxsrc/providers/AuthProvider.tsxsrc/types/profile.tssrc/types/resources.tssupabase/migrations/20251214152600_add_profile_customization.sqltailwind.config.tstest.mdvercel.jsonvite.config.tsvite.config.ts.timestamp-1765978667252-2d9e972de6409.mjs
💤 Files with no reviewable changes (2)
- AI_RULES.md
- changelog.md
🧰 Additional context used
🧬 Code graph analysis (29)
src/components/admin/BlogEditor.tsx (7)
api/generateTitles.js (1)
text(89-89)src/hooks/useAuth.tsx (1)
useAuth(4-10)src/components/ui/button.tsx (1)
Button(28-28)src/components/ui/label.tsx (1)
Label(24-24)src/components/ui/input.tsx (1)
Input(22-22)src/components/ui/switch.tsx (1)
Switch(27-27)src/components/ui/textarea.tsx (1)
Textarea(23-23)
src/components/auth/UserMenu.tsx (1)
src/components/ui/dropdown-menu.tsx (1)
DropdownMenuItem(237-237)
src/lib/mciApi.ts (1)
src/types/resources.ts (1)
Resource(3-18)
src/pages/BlogView.tsx (1)
src/components/ui/avatar.tsx (3)
Avatar(48-48)AvatarImage(48-48)AvatarFallback(48-48)
src/components/profile/ProfileEditor.tsx (4)
src/hooks/useAuth.tsx (1)
useAuth(4-10)src/types/profile.ts (3)
ProfileThemeConfig(1-17)defaultThemeConfig(39-48)predefinedThemes(50-83)src/lib/utils.ts (1)
getSmartIconUrl(8-52)src/components/profile/SvglPicker.tsx (1)
SvglPicker(33-178)
scripts/export_resources.ts (1)
api/deleteAccount.js (1)
supabaseUrl(35-35)
api/deleteAccount.js (1)
src/utils/api.ts (1)
Response(4-8)
src/components/ui/toggle.tsx (1)
src/components/ui/toggle-variants.ts (1)
toggleVariants(3-23)
src/pages/ResourcesHub.tsx (1)
src/components/ui/button.tsx (1)
Button(28-28)
src/components/resources/ResourceCard.tsx (4)
src/types/resources.ts (1)
Resource(3-18)src/hooks/useAuth.tsx (1)
useAuth(4-10)src/hooks/useUserFavorites.ts (1)
useUserFavorites(6-80)src/lib/utils.ts (1)
cn(4-6)
src/pages/Blogs.tsx (2)
src/components/ui/card.tsx (4)
Card(79-79)CardHeader(79-79)CardTitle(79-79)CardContent(79-79)src/components/ui/avatar.tsx (3)
Avatar(48-48)AvatarImage(48-48)AvatarFallback(48-48)
src/App.tsx (3)
src/components/admin/BlogEditor.tsx (1)
BlogEditor(24-171)src/pages/Blogs.tsx (1)
Blogs(47-163)src/pages/BlogView.tsx (1)
BlogView(26-155)
src/pages/Profile.tsx (2)
src/types/profile.ts (3)
ProfileLink(19-27)ProfileThemeConfig(1-17)defaultThemeConfig(39-48)src/lib/utils.ts (1)
getSmartIconUrl(8-52)
src/components/resources/ResourceDetailDialog.tsx (1)
src/components/ui/dialog.tsx (1)
DialogDescription(119-119)
src/components/DonateButton.tsx (1)
src/data/supporters.ts (1)
supporters(6-12)
src/components/ui/button.tsx (1)
src/components/ui/button-variants.ts (1)
buttonVariants(3-28)
src/hooks/useResources.ts (3)
src/types/resources.ts (1)
Resource(3-18)src/hooks/useDownloadCounts.ts (1)
useDownloadCounts(4-65)src/lib/mciApi.ts (1)
fetchMciResources(13-55)
server.js (1)
api/deleteAccount.js (1)
handler(9-97)
src/components/AudioPlayer.tsx (2)
src/lib/utils.ts (1)
cn(4-6)src/components/ui/button.tsx (1)
Button(28-28)
src/components/Navbar.tsx (7)
src/hooks/useAuth.tsx (1)
useAuth(4-10)src/hooks/useProfile.ts (1)
useProfile(18-148)src/components/Logo.tsx (1)
Logo(3-20)src/components/ui/button.tsx (1)
Button(28-28)src/components/ui/dropdown-menu.tsx (1)
DropdownMenuContent(236-236)src/components/ui/collapsible.tsx (2)
Collapsible(9-9)CollapsibleTrigger(9-9)src/components/ui/toggle.tsx (1)
Toggle(22-22)
src/pages/YouTubeDownloader.tsx (2)
api/downloadThumbnail.js (2)
message(54-54)response(24-24)api/download.js (1)
response(164-225)
src/providers/AuthProvider.tsx (1)
src/providers/AuthContext.tsx (2)
AuthResult(5-8)AuthContext(33-33)
src/pages/Admin.tsx (1)
src/components/admin/AdminBlogsManager.tsx (1)
AdminBlogsManager(23-148)
src/components/Footer.tsx (1)
src/components/Logo.tsx (1)
Logo(3-20)
src/components/ui/badge.tsx (2)
src/components/ui/badge-variants.ts (1)
badgeVariants(3-21)src/lib/utils.ts (1)
cn(4-6)
src/components/resources/ResourceFilters.tsx (3)
src/components/ui/button.tsx (1)
Button(28-28)src/components/ui/select.tsx (5)
Select(169-169)SelectTrigger(172-172)SelectValue(171-171)SelectContent(173-173)SelectItem(175-175)src/components/ui/sheet.tsx (2)
SheetContent(129-129)Sheet(128-128)
src/components/SupportersList.tsx (1)
src/data/supporters.ts (1)
supporters(6-12)
src/pages/Showcase.tsx (1)
src/lib/showcases.ts (1)
ShowcaseTag(7-7)
src/hooks/useUserFavorites.ts (2)
src/hooks/useAuth.tsx (1)
useAuth(4-10)src/lib/supabaseClient.ts (1)
supabase(1-1)
🪛 Biome (2.1.2)
src/pages/Profile.tsx
[error] 220-220: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".
Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.
(lint/security/noBlankTarget)
| const corsHeaders = { | ||
| 'Access-Control-Allow-Origin': '*', | ||
| 'Access-Control-Allow-Methods': 'POST, OPTIONS', | ||
| 'Access-Control-Allow-Headers': 'Content-Type, Authorization', | ||
| }; |
There was a problem hiding this comment.
Consider restricting CORS origin for this sensitive endpoint.
Using Access-Control-Allow-Origin: '*' allows any website to make requests to this account deletion endpoint. While the Bearer token provides authentication, a more restrictive CORS policy would add defense-in-depth.
🔎 Suggested fix
+const allowedOrigins = [
+ process.env.ALLOWED_ORIGIN || 'https://renderdragon.org',
+ 'http://localhost:8080', // dev
+];
+
+const getCorsHeaders = (origin) => {
+ const isAllowed = allowedOrigins.includes(origin);
+ return {
+ 'Access-Control-Allow-Origin': isAllowed ? origin : allowedOrigins[0],
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+ };
+};
-const corsHeaders = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
-};Then use getCorsHeaders(req.headers.origin || req.headers.get?.('origin')) when constructing responses.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In api/deleteAccount.js around lines 3 to 7, the current CORS header uses
Access-Control-Allow-Origin: '*' which is too permissive for an account deletion
endpoint; replace the static wildcard with a helper that returns a restricted
origin (e.g., allowlist check) and construct response headers using the incoming
request origin (use req.headers.origin || req.headers.get?.('origin') to read
it), returning that origin only if it passes validation; keep other CORS headers
but ensure the helper falls back to a safe default (deny or empty) when origin
is not allowed.
| const supabaseUrl = process.env.VITE_SUPABASE_URL; | ||
| const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY; | ||
|
|
||
| if (!supabaseUrl || !supabaseKey) { | ||
| console.error('Supabase credentials missing in .env'); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Use non-VITE prefixed environment variables for Node.js scripts.
The VITE_ prefix is for client-side environment variables injected at build time by Vite. In Node.js scripts, these variables won't be available. Use non-prefixed environment variables or add a fallback pattern.
🔧 Suggested fix
-const supabaseUrl = process.env.VITE_SUPABASE_URL;
-const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY;
+const supabaseUrl = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL;
+const supabaseKey = process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;📝 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 supabaseUrl = process.env.VITE_SUPABASE_URL; | |
| const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY; | |
| if (!supabaseUrl || !supabaseKey) { | |
| console.error('Supabase credentials missing in .env'); | |
| process.exit(1); | |
| } | |
| const supabaseUrl = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL; | |
| const supabaseKey = process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY; | |
| if (!supabaseUrl || !supabaseKey) { | |
| console.error('Supabase credentials missing in .env'); | |
| process.exit(1); | |
| } |
🤖 Prompt for AI Agents
In scripts/export_resources.ts around lines 8 to 14, the script reads VITE_
prefixed env vars which are only available to Vite-built clients; update reads
to use non-prefixed Node env vars (e.g. process.env.SUPABASE_URL and
process.env.SUPABASE_ANON_KEY) and add a fallback pattern (check VITE_* only if
non-prefixed are missing) so the script works both locally and in build
contexts; keep the existing error log and process.exit(1) when neither the
primary nor fallback variables are present.
| useState(() => { | ||
| const theme = localStorage.getItem('theme') as 'light' | 'dark' || | ||
| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | ||
| if (theme === 'dark') { | ||
| document.documentElement.classList.add('dark'); | ||
| } else { | ||
| document.documentElement.classList.remove('dark'); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use useEffect instead of useState initializer for theme setup.
Using useState initializer for side effects (DOM manipulation) is semantically incorrect. The initializer should only compute initial state. Use useEffect for DOM side effects to ensure correct behavior in React's strict mode and future versions.
🔧 Recommended fix
- // Global Theme Initialization
- useState(() => {
- const theme = localStorage.getItem('theme') as 'light' | 'dark' ||
- (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
- if (theme === 'dark') {
- document.documentElement.classList.add('dark');
- } else {
- document.documentElement.classList.remove('dark');
- }
- });
+ // Global Theme Initialization
+ useEffect(() => {
+ const theme = localStorage.getItem('theme') as 'light' | 'dark' ||
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+ if (theme === 'dark') {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ }, []);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/App.tsx around lines 57 to 65, the theme setup is doing DOM side-effects
inside a useState initializer; replace this with a proper pattern: initialize
state with the theme value (computed synchronously from localStorage and
prefers-color-scheme) using useState only for value computation, then move the
document.documentElement.classList.add/remove logic into a useEffect that runs
when the theme state changes and also persist the theme to localStorage when it
updates. Ensure the effect has theme in its dependency array and do not perform
DOM mutations inside the state initializer.
| const handleSave = async () => { | ||
| if (!title || !slug || !user) { | ||
| toast.error("Title and slug are required"); | ||
| return; | ||
| } | ||
| setSaving(true); | ||
|
|
||
| const payload = { | ||
| title, | ||
| slug, | ||
| content, | ||
| published, | ||
| author_id: user.id, | ||
| updated_at: new Date().toISOString(), | ||
| }; | ||
|
|
||
| try { | ||
| if (id) { | ||
| // Update | ||
| const { error } = await supabase | ||
| .from("blogs") | ||
| .update(payload) | ||
| .eq("id", id); | ||
| if (error) throw error; | ||
| toast.success("Blog updated saved"); | ||
| } else { | ||
| // Create | ||
| const { error } = await supabase | ||
| .from("blogs") | ||
| .insert([payload]); | ||
| if (error) throw error; | ||
| toast.success("Blog created successfully"); | ||
| navigate("/admin"); // Redirect or clear form | ||
| } | ||
| } catch (e: any) { | ||
| console.error("Error saving blog:", e); | ||
| toast.error(`Error saving: ${e.message}`); | ||
| } finally { | ||
| setSaving(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find database-related files (migrations, schema definitions)
find . -type f \( -name "*migration*" -o -name "*schema*" -o -name "*supabase*" \) 2>/dev/null | head -20Repository: smb060606/renderdragon.org
Length of output: 97
🏁 Script executed:
# Search for slug validation or check functions in the codebase
rg "checkSlug|validateSlug|slug.*unique|unique.*slug" --type ts --type tsx --type js 2>/dev/null | head -20Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check the BlogEditor.tsx file to see full context around state management
rg -A 30 "const \[.*slug" src/components/admin/BlogEditor.tsx 2>/dev/nullRepository: smb060606/renderdragon.org
Length of output: 919
🏁 Script executed:
# Search for any database schema or supabase configuration
rg "blogs.*table|unique.*slug|CREATE TABLE.*blogs" --type sql --type ts --type js 2>/dev/null | head -20Repository: smb060606/renderdragon.org
Length of output: 242
🏁 Script executed:
# Read the supabaseClient setup
cat src/lib/supabaseClient.tsRepository: smb060606/renderdragon.org
Length of output: 127
🏁 Script executed:
# Look at the showcase.ts slug pattern
rg -B 5 -A 15 "ensure unique slug" src/lib/showcase.tsRepository: smb060606/renderdragon.org
Length of output: 642
🏁 Script executed:
# Check if there are any SQL files or DDL statements
find . -type f -name "*.sql" 2>/dev/nullRepository: smb060606/renderdragon.org
Length of output: 374
🏁 Script executed:
# Search for blogs table references and any constraint definitions
rg "blogs" --type ts --type tsx src/ | grep -i "unique\|constraint\|schema" | head -10Repository: smb060606/renderdragon.org
Length of output: 97
🏁 Script executed:
# Read the migration files to check for blogs table schema
cat ./supabase/migrations/20250702100614-84c46566-7fc5-4a4f-86ad-c3e3603c3acc.sqlRepository: smb060606/renderdragon.org
Length of output: 2416
🏁 Script executed:
# Check for other migration references to blogs table
rg "blogs" ./supabase/migrations/ --type sqlRepository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check the supabase client integration for any schema hints
cat src/integrations/supabase/client.ts 2>/dev/null || ls -la src/integrations/Repository: smb060606/renderdragon.org
Length of output: 573
🏁 Script executed:
# Look at all migration files for blogs table definition
for file in ./supabase/migrations/*.sql; do echo "=== $file ==="; grep -A 20 "CREATE TABLE.*blogs\|ALTER TABLE.*blogs" "$file" 2>/dev/null; doneRepository: smb060606/renderdragon.org
Length of output: 406
🏁 Script executed:
# Check the auto-generated Supabase types for blogs table schema
cat src/integrations/supabase/types.ts 2>/dev/null | grep -A 30 "blogs"Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Search for any reference to blogs in migrations
rg "blog" ./supabase/migrations/Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check if blogs table exists in the types.ts file
wc -l src/integrations/supabase/types.ts && echo "---" && head -100 src/integrations/supabase/types.ts | tail -50Repository: smb060606/renderdragon.org
Length of output: 1557
🏁 Script executed:
# Look for any reference to "blog" anywhere in the codebase
rg "from.*blog|\.blogs\|" src/ --type ts -iRepository: smb060606/renderdragon.org
Length of output: 690
🏁 Script executed:
# Check if there's a separate blog-related migration or schema
find ./supabase -type f -name "*.sql" -exec grep -l "blog" {} \;Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Search for all table creation statements in migrations
rg "CREATE TABLE" ./supabase/migrations/Repository: smb060606/renderdragon.org
Length of output: 179
🏁 Script executed:
# Check the complete types.ts file for any blogs table definition
grep -n "blogs" src/integrations/supabase/types.tsRepository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Look at the full types.ts to see all table definitions
tail -200 src/integrations/supabase/types.ts | head -100Repository: smb060606/renderdragon.org
Length of output: 2894
🏁 Script executed:
# Check if blogs table is used but not yet created
rg "blogs" src/integrations/supabase/types.ts -C 5Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check the entire BlogEditor file to understand what's happening
head -150 src/components/admin/BlogEditor.tsxRepository: smb060606/renderdragon.org
Length of output: 5544
🏁 Script executed:
# Check if blogs table is defined at all in types.ts
cat src/integrations/supabase/types.ts | grep -i "blogs"Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check the beginning of types.ts to see all tables defined
head -200 src/integrations/supabase/types.tsRepository: smb060606/renderdragon.org
Length of output: 5516
🏁 Script executed:
# Search all migrations for any blog-related SQL
grep -i "blog" ./supabase/migrations/*.sqlRepository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check if there's any documentation or README about the database setup
ls -la ./supabase/ && cat ./supabase/README.md 2>/dev/null || echo "No README found"Repository: smb060606/renderdragon.org
Length of output: 371
🏁 Script executed:
# List all migration files with their dates
ls -lh ./supabase/migrations/Repository: smb060606/renderdragon.org
Length of output: 492
🏁 Script executed:
# Read the most recent migration file to see what's in it
tail -c 1000 ./supabase/migrations/20251214152600_add_profile_customization.sqlRepository: smb060606/renderdragon.org
Length of output: 382
🏁 Script executed:
# Check all migrations for any blog mentions (case-insensitive)
for f in ./supabase/migrations/*.sql; do
if grep -qi "blog\|\.from.*blog" "$f"; then
echo "=== $(basename $f) ==="
cat "$f"
fi
doneRepository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check if there's a schema.sql or any other SQL file in supabase root
find ./supabase -name "*.sql" -type fRepository: smb060606/renderdragon.org
Length of output: 374
🏁 Script executed:
# Let's verify by checking what tables are actually available in the database schema
cat src/integrations/supabase/types.ts | grep -E "^\s+[a-z_]+:\s+\{" | head -20Repository: smb060606/renderdragon.org
Length of output: 173
🏁 Script executed:
# Check if there are any other SQL files or schema definitions
find . -name "*.sql" -o -name "schema.*" | grep -v node_modules | grep -v migrationsRepository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Search for any CREATE TABLE statement across the entire repo
rg "CREATE TABLE" --type sql --type ts --type tsxRepository: smb060606/renderdragon.org
Length of output: 97
🏁 Script executed:
# Search for blogs table references in TypeScript files (without tsx suffix)
rg "blogs" src/ | grep -v "node_modules"Repository: smb060606/renderdragon.org
Length of output: 3266
🏁 Script executed:
# Check the full list of available tables in the types file one more time
rg "^\s+[a-z_]+:\s+\{" src/integrations/supabase/types.tsRepository: smb060606/renderdragon.org
Length of output: 173
🏁 Script executed:
# Check if blogs table is created via a Supabase function or edge function
find ./supabase/functions -type f -name "*.ts" | xargs grep -l "blogs" 2>/dev/nullRepository: smb060606/renderdragon.org
Length of output: 54
🏁 Script executed:
# Check if there's any mention of blogs table creation anywhere
rg "CREATE.*TABLE.*blogs\|blogs.*table\|blog_" ./supabase/Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Look for any .env or config files that might reference schema
find . -name ".env*" -o -name "supabase.json" -o -name "config.*" | grep -v node_modulesRepository: smb060606/renderdragon.org
Length of output: 92
🏁 Script executed:
# Check the Supabase config file
cat ./supabase/config.tomlRepository: smb060606/renderdragon.org
Length of output: 105
🏁 Script executed:
# Look for BlogPost type definition in the codebase
rg "interface BlogPost\|type BlogPost" src/Repository: smb060606/renderdragon.org
Length of output: 52
🏁 Script executed:
# Check if there are any hardcoded SQL queries or schema elsewhere
rg "\.from\(.*blogs" src/ -A 3 | head -40Repository: smb060606/renderdragon.org
Length of output: 2405
The blogs table is missing from the Supabase database schema.
The code references .from("blogs") throughout the application (BlogEditor.tsx, Blogs.tsx, BlogView.tsx, AdminBlogsManager.tsx), but this table is not defined in the auto-generated Supabase types file and no migrations create it. The available tables are: downloads, profiles, resources, and user_favorites.
Create the blogs table in a migration with appropriate columns and constraints. Once the table exists, implement slug uniqueness validation as suggested in the original comment to prevent constraint violations.
| .update(payload) | ||
| .eq("id", id); | ||
| if (error) throw error; | ||
| toast.success("Blog updated saved"); |
There was a problem hiding this comment.
Fix typo in success message.
Line 95: "Blog updated saved" should be "Blog updated successfully" or "Blog saved successfully" for grammatical correctness.
🔎 Suggested fix
- toast.success("Blog updated saved");
+ toast.success("Blog updated successfully");📝 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.
| toast.success("Blog updated saved"); | |
| toast.success("Blog updated successfully"); |
🤖 Prompt for AI Agents
In src/components/admin/BlogEditor.tsx around line 95, the toast success message
reads "Blog updated saved" which is grammatically incorrect; update the string
to a clear message such as "Blog updated successfully" (or "Blog saved
successfully") by replacing the existing text passed to toast.success so the
notification reads correctly.
| </CardHeader> | ||
| <CardContent> | ||
| <p className="text-sm text-muted-foreground line-clamp-3 font-geist-mono"> | ||
| {removeMarkdown(blog.content || "").slice(0, 150)}... |
There was a problem hiding this comment.
Minor: Ellipsis always appended regardless of content length.
Line 148 always appends "..." even when the content is shorter than 150 characters, which could result in unnecessary ellipsis for short posts.
🔎 Suggested fix
- {removeMarkdown(blog.content || "").slice(0, 150)}...
+ {(() => {
+ const text = removeMarkdown(blog.content || "");
+ return text.length > 150 ? text.slice(0, 150) + "..." : text;
+ })()}Or more concisely:
- {removeMarkdown(blog.content || "").slice(0, 150)}...
+ {removeMarkdown(blog.content || "").slice(0, 150)}{removeMarkdown(blog.content || "").length > 150 ? "..." : ""}📝 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.
| {removeMarkdown(blog.content || "").slice(0, 150)}... | |
| {(() => { | |
| const text = removeMarkdown(blog.content || ""); | |
| return text.length > 150 ? text.slice(0, 150) + "..." : text; | |
| })()} |
🤖 Prompt for AI Agents
In src/pages/Blogs.tsx around line 148, the code always appends "..." after the
sliced preview which causes an unnecessary ellipsis for content shorter than 150
chars; update the rendering to compute the preview text first (use
removeMarkdown(blog.content || "").slice(0,150)), then conditionally append
"..." only if the original plain text length exceeds 150 characters (e.g., if
(plainText.length > 150) append ellipsis), so short posts display without a
trailing ellipsis.
| import { Link } from "react-router-dom"; | ||
| import { motion } from "framer-motion"; | ||
| import { IconHome, IconCompass, IconTool } from "@tabler/icons-react"; | ||
| import { IconHome, IconCompass, IconTool, IconPick } from "@tabler/icons-react"; |
There was a problem hiding this comment.
IconPick does not exist in the @tabler/icons-react library and will cause an import error.
The import statement at line 6 and usage at line 136 reference IconPick, which does not appear to be a valid icon in the @tabler/icons-react library. This will result in a failed module import and runtime error. Verify the correct icon name from the Tabler Icons library (e.g., IconPickaxe, IconTool, or another available tool icon) and update both the import statement and component usage accordingly.
🤖 Prompt for AI Agents
In src/pages/NotFound.tsx around line 6 (and usage at line 136), the import and
usage of IconPick is invalid; replace IconPick with the correct Tabler icon
(e.g., IconPickaxe or IconTool) by updating the import list to import the valid
icon name and then update the component usage at line 136 to match that valid
icon identifier so the module import succeeds and runtime errors are avoided.
| <a | ||
| href="/" | ||
| target="_blank" | ||
| className="fixed bottom-4 right-4 z-50 bg-white text-black font-geist-mono py-2.5 px-5 shadow-xl hover:scale-105 transition-transform border border-white/20 flex flex-row items-center gap-2.5 rounded-lg no-underline" | ||
| > | ||
| <img src="/renderdragon.png" alt="RD" className="w-5 h-5 object-contain" /> | ||
| <span className="text-sm font-bold">Made in RenderDragon.</span> | ||
| </a> |
There was a problem hiding this comment.
Security: Missing rel="noopener" on external link.
As flagged by static analysis, the branding link opens in a new tab without rel="noopener" or rel="noreferrer", which is a security risk (the opened page can access window.opener).
🔎 Proposed fix
<a
href="/"
target="_blank"
+ rel="noopener noreferrer"
className="fixed bottom-4 right-4 z-50 bg-white text-black font-geist-mono py-2.5 px-5 shadow-xl hover:scale-105 transition-transform border border-white/20 flex flex-row items-center gap-2.5 rounded-lg no-underline"
>📝 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="/" | |
| target="_blank" | |
| className="fixed bottom-4 right-4 z-50 bg-white text-black font-geist-mono py-2.5 px-5 shadow-xl hover:scale-105 transition-transform border border-white/20 flex flex-row items-center gap-2.5 rounded-lg no-underline" | |
| > | |
| <img src="/renderdragon.png" alt="RD" className="w-5 h-5 object-contain" /> | |
| <span className="text-sm font-bold">Made in RenderDragon.</span> | |
| </a> | |
| <a | |
| href="/" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="fixed bottom-4 right-4 z-50 bg-white text-black font-geist-mono py-2.5 px-5 shadow-xl hover:scale-105 transition-transform border border-white/20 flex flex-row items-center gap-2.5 rounded-lg no-underline" | |
| > | |
| <img src="/renderdragon.png" alt="RD" className="w-5 h-5 object-contain" /> | |
| <span className="text-sm font-bold">Made in RenderDragon.</span> | |
| </a> |
🧰 Tools
🪛 Biome (2.1.2)
[error] 220-220: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".
Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.
(lint/security/noBlankTarget)
🤖 Prompt for AI Agents
In src/pages/Profile.tsx around lines 218 to 225, the anchor opens a new tab
with target="_blank" but lacks rel="noopener" (and ideally "noreferrer"), which
allows the opened page access to window.opener; update the anchor to include
rel="noopener noreferrer" (or at minimum rel="noopener") to prevent the opened
page from gaining a reference to the opener and mitigate the security risk.
| const authToken = import.meta.env.VITE_SUBMIT_AUTH_TOKEN; | ||
|
|
||
| for (const file of files) { | ||
| const form = new FormData(); | ||
| form.append('file', file); | ||
| if (description) form.append('description', description); | ||
| if (credit) form.append('credit', credit); | ||
|
|
||
| const res = await fetch('https://submit-renderdragon.vercel.app/api/public-upload', { | ||
| const res = await fetch('https://debian.tail5bdcac.ts.net/', { | ||
| method: 'POST', | ||
| headers: description ? { 'x-description': description } : undefined, | ||
| headers: { | ||
| 'x-api-key': authToken | ||
| }, |
There was a problem hiding this comment.
Security concern: API key exposed in client-side bundle.
The VITE_SUBMIT_AUTH_TOKEN environment variable is embedded in the client-side JavaScript bundle and visible to anyone inspecting the network requests or source code. This effectively makes the "authentication" meaningless since any user can extract and reuse this token.
Consider:
- Removing the x-api-key header if the endpoint is meant to be publicly accessible
- Implementing proper user authentication where the backend validates the user's session
- Using a server-side proxy to add the API key
🔎 Suggested approach using user authentication
- const authToken = import.meta.env.VITE_SUBMIT_AUTH_TOKEN;
+ // Use user's session token if authenticated
+ const { data: { session } } = await supabase.auth.getSession();
const res = await fetch('https://debian.tail5bdcac.ts.net/', {
method: 'POST',
headers: {
- 'x-api-key': authToken
+ ...(session?.access_token && { 'Authorization': `Bearer ${session.access_token}` })
},
body: form,
});Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/pages/ResourcesHub.tsx around lines 316 to 328, the code sends the
VITE_SUBMIT_AUTH_TOKEN from the client bundle in the x-api-key header which
exposes the secret; remove use of the client-side env value and stop sending the
x-api-key from the browser, and instead implement one of two fixes: (A) create a
server-side endpoint (or proxy) that accepts the file/metadata from the client,
injects the API key server-side from a non-public env var, forwards the request
to https://debian.tail5bdcac.ts.net/, and returns the response to the client,
ensuring the server validates the user session before forwarding; or (B) if the
upstream endpoint is meant to be public, remove the header entirely and update
the backend/endpoint to not require a secret; also move VITE_SUBMIT_AUTH_TOKEN
to server-only env configuration and update any docs/tests accordingly.
| // vite.config.ts | ||
| import { defineConfig, loadEnv } from "file:///F:/reddragon/renderdragon.org/node_modules/vite/dist/node/index.js"; | ||
| import sitemap from "file:///F:/reddragon/renderdragon.org/node_modules/vite-plugin-sitemap/dist/index.js"; | ||
| import react from "file:///F:/reddragon/renderdragon.org/node_modules/@vitejs/plugin-react-swc/index.js"; | ||
| import path from "path"; | ||
| var __vite_injected_original_dirname = "F:\\reddragon\\renderdragon.org"; | ||
| var vite_config_default = defineConfig(({ mode }) => { | ||
| const env = loadEnv(mode, process.cwd(), ""); | ||
| return { | ||
| server: { | ||
| host: "::", | ||
| port: 8080, | ||
| proxy: { | ||
| "/api": { | ||
| target: "http://localhost:3000", | ||
| changeOrigin: true, | ||
| secure: false | ||
| } | ||
| } | ||
| }, | ||
| plugins: [ | ||
| sitemap({ | ||
| hostname: "https://renderdragon.org" | ||
| }), | ||
| react() | ||
| ].filter(Boolean), | ||
| resolve: { | ||
| alias: { | ||
| "@": path.resolve(__vite_injected_original_dirname, "./src") | ||
| } | ||
| }, | ||
| optimizeDeps: { | ||
| include: [ | ||
| "html2canvas", | ||
| "@radix-ui/react-primitive", | ||
| "@radix-ui/react-use-callback-ref", | ||
| "@radix-ui/react-use-controllable-state", | ||
| "@radix-ui/react-use-layout-effect", | ||
| "@radix-ui/react-use-previous", | ||
| "@radix-ui/react-visually-hidden", | ||
| "aria-hidden", | ||
| "react-remove-scroll", | ||
| "@radix-ui/react-context", | ||
| "@radix-ui/react-compose-refs" | ||
| ] | ||
| }, | ||
| build: { | ||
| commonjsOptions: { | ||
| include: [/node_modules/], | ||
| transformMixedEsModules: true | ||
| } | ||
| }, | ||
| // Vite env configuration | ||
| define: { | ||
| "process.env": env | ||
| } | ||
| }; | ||
| }); | ||
| export { | ||
| vite_config_default as default | ||
| }; | ||
| //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJGOlxcXFxyZWRkcmFnb25cXFxccmVuZGVyZHJhZ29uLm9yZ1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiRjpcXFxccmVkZHJhZ29uXFxcXHJlbmRlcmRyYWdvbi5vcmdcXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0Y6L3JlZGRyYWdvbi9yZW5kZXJkcmFnb24ub3JnL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnLCBsb2FkRW52IH0gZnJvbSBcInZpdGVcIjtcclxuaW1wb3J0IHNpdGVtYXAgZnJvbSAndml0ZS1wbHVnaW4tc2l0ZW1hcCc7XHJcbmltcG9ydCByZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djXCI7XHJcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XHJcblxyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoKHsgbW9kZSB9KSA9PiB7XHJcbiAgLy8gTG9hZCBlbnYgZmlsZSBiYXNlZCBvbiBgbW9kZWAgaW4gdGhlIGN1cnJlbnQgd29ya2luZyBkaXJlY3RvcnkuXHJcbiAgLy8gU2V0IHRoZSB0aGlyZCBwYXJhbWV0ZXIgdG8gJycgdG8gbG9hZCBhbGwgZW52IHJlZ2FyZGxlc3Mgb2YgdGhlIGBWSVRFX2AgcHJlZml4LlxyXG4gIGNvbnN0IGVudiA9IGxvYWRFbnYobW9kZSwgcHJvY2Vzcy5jd2QoKSwgJycpO1xyXG5cclxuICByZXR1cm4ge1xyXG4gICAgc2VydmVyOiB7XHJcbiAgICAgIGhvc3Q6IFwiOjpcIixcclxuICAgICAgcG9ydDogODA4MCxcclxuICAgICAgcHJveHk6IHtcclxuICAgICAgICAnL2FwaSc6IHtcclxuICAgICAgICAgIHRhcmdldDogJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcsXHJcbiAgICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXHJcbiAgICAgICAgICBzZWN1cmU6IGZhbHNlLFxyXG4gICAgICAgIH0sXHJcbiAgICAgIH0sXHJcbiAgICB9LFxyXG4gICAgcGx1Z2luczogW1xyXG4gICAgICBzaXRlbWFwKHtcclxuICAgICAgICBob3N0bmFtZTogJ2h0dHBzOi8vcmVuZGVyZHJhZ29uLm9yZycsXHJcbiAgICAgIH0pLFxyXG4gICAgICByZWFjdCgpLFxyXG4gICAgXS5maWx0ZXIoQm9vbGVhbiksXHJcbiAgICByZXNvbHZlOiB7XHJcbiAgICAgIGFsaWFzOiB7XHJcbiAgICAgICAgXCJAXCI6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsIFwiLi9zcmNcIiksXHJcbiAgICAgIH0sXHJcbiAgICB9LFxyXG4gICAgb3B0aW1pemVEZXBzOiB7XHJcbiAgICAgIGluY2x1ZGU6IFtcclxuICAgICAgICAnaHRtbDJjYW52YXMnLFxyXG4gICAgICAgICdAcmFkaXgtdWkvcmVhY3QtcHJpbWl0aXZlJyxcclxuICAgICAgICAnQHJhZGl4LXVpL3JlYWN0LXVzZS1jYWxsYmFjay1yZWYnLFxyXG4gICAgICAgICdAcmFkaXgtdWkvcmVhY3QtdXNlLWNvbnRyb2xsYWJsZS1zdGF0ZScsXHJcbiAgICAgICAgJ0ByYWRpeC11aS9yZWFjdC11c2UtbGF5b3V0LWVmZmVjdCcsXHJcbiAgICAgICAgJ0ByYWRpeC11aS9yZWFjdC11c2UtcHJldmlvdXMnLFxyXG4gICAgICAgICdAcmFkaXgtdWkvcmVhY3QtdmlzdWFsbHktaGlkZGVuJyxcclxuICAgICAgICAnYXJpYS1oaWRkZW4nLFxyXG4gICAgICAgICdyZWFjdC1yZW1vdmUtc2Nyb2xsJyxcclxuICAgICAgICAnQHJhZGl4LXVpL3JlYWN0LWNvbnRleHQnLFxyXG4gICAgICAgICdAcmFkaXgtdWkvcmVhY3QtY29tcG9zZS1yZWZzJ1xyXG4gICAgICBdXHJcbiAgICB9LFxyXG4gICAgYnVpbGQ6IHtcclxuICAgICAgY29tbW9uanNPcHRpb25zOiB7XHJcbiAgICAgICAgaW5jbHVkZTogWy9ub2RlX21vZHVsZXMvXSxcclxuICAgICAgICB0cmFuc2Zvcm1NaXhlZEVzTW9kdWxlczogdHJ1ZVxyXG4gICAgICB9XHJcbiAgICB9LFxyXG4gICAgLy8gVml0ZSBlbnYgY29uZmlndXJhdGlvblxyXG4gICAgZGVmaW5lOiB7XHJcbiAgICAgICdwcm9jZXNzLmVudic6IGVudlxyXG4gICAgfVxyXG4gIH07XHJcbn0pOyJdLAogICJtYXBwaW5ncyI6ICI7QUFBK1EsU0FBUyxjQUFjLGVBQWU7QUFDclQsT0FBTyxhQUFhO0FBQ3BCLE9BQU8sV0FBVztBQUNsQixPQUFPLFVBQVU7QUFIakIsSUFBTSxtQ0FBbUM7QUFNekMsSUFBTyxzQkFBUSxhQUFhLENBQUMsRUFBRSxLQUFLLE1BQU07QUFHeEMsUUFBTSxNQUFNLFFBQVEsTUFBTSxRQUFRLElBQUksR0FBRyxFQUFFO0FBRTNDLFNBQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxNQUNOLE1BQU07QUFBQSxNQUNOLE1BQU07QUFBQSxNQUNOLE9BQU87QUFBQSxRQUNMLFFBQVE7QUFBQSxVQUNOLFFBQVE7QUFBQSxVQUNSLGNBQWM7QUFBQSxVQUNkLFFBQVE7QUFBQSxRQUNWO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxJQUNBLFNBQVM7QUFBQSxNQUNQLFFBQVE7QUFBQSxRQUNOLFVBQVU7QUFBQSxNQUNaLENBQUM7QUFBQSxNQUNELE1BQU07QUFBQSxJQUNSLEVBQUUsT0FBTyxPQUFPO0FBQUEsSUFDaEIsU0FBUztBQUFBLE1BQ1AsT0FBTztBQUFBLFFBQ0wsS0FBSyxLQUFLLFFBQVEsa0NBQVcsT0FBTztBQUFBLE1BQ3RDO0FBQUEsSUFDRjtBQUFBLElBQ0EsY0FBYztBQUFBLE1BQ1osU0FBUztBQUFBLFFBQ1A7QUFBQSxRQUNBO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsUUFDQTtBQUFBLFFBQ0E7QUFBQSxRQUNBO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxJQUNBLE9BQU87QUFBQSxNQUNMLGlCQUFpQjtBQUFBLFFBQ2YsU0FBUyxDQUFDLGNBQWM7QUFBQSxRQUN4Qix5QkFBeUI7QUFBQSxNQUMzQjtBQUFBLElBQ0Y7QUFBQTtBQUFBLElBRUEsUUFBUTtBQUFBLE1BQ04sZUFBZTtBQUFBLElBQ2pCO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== |
There was a problem hiding this comment.
This build artifact should not be committed.
This file is a Vite-generated cache/compiled configuration with:
- Absolute local paths (
file:///F:/reddragon/...) that won't work on other machines - Machine-specific
__vite_injected_original_dirname - Embedded base64 sourcemap with original source
This file should be added to .gitignore and removed from the repository.
🔎 Add to .gitignore
# Add to .gitignore
+vite.config.ts.timestamp-*
+*.timestamp-*.mjsThen remove the file:
git rm --cached "vite.config.ts.timestamp-1765978667252-2d9e972de6409.mjs"Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In vite.config.ts.timestamp-1765978667252-2d9e972de6409.mjs lines 1-62, this is
a Vite-generated build/cache artifact containing absolute local paths,
machine-specific injected dirname and embedded sourcemap and must not be
committed; add an appropriate pattern (e.g., generated vite/*.mjs or this exact
filename) to .gitignore, remove the file from the repo index with git rm
--cached "vite.config.ts.timestamp-1765978667252-2d9e972de6409.mjs", commit the
.gitignore change and the removal, and ensure future builds are excluded from
commits.
|
@coderabbitai - in this file src/pages/YouTubeDownloader.tsx, line 101-102 (link) confirm this logic: Is the URL being constructed with user input but without validation and could that enable SSRF attacks? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 17682 @smb060606: No, there is no SSRF vulnerability at lines 101-102. Here's why the code is safe: Lines 101-102: const base = 'https://mediapye.vercel.app';
const url = `${base}/api/youtube?input=${encodeURIComponent(youtubeUrl.trim())}`;Security analysis:
const looksLikeId = /^[a-zA-Z0-9_-]{11}$/.test(youtubeUrl.trim());
if (!looksLikeId && !isValidYoutubeUrl(youtubeUrl)) {
toast.error('Please enter a valid YouTube URL or 11-character video ID');
setUrlError(true);
return;
}Note: While the The URL construction at lines 101-102 follows secure practices for client-side API calls. |
|
@coderabbitai - in this file src/hooks/useResources.ts, line 1 (link) confirm this logic: will disabling eslint for entire file reduces code quality checks? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 10162 @smb060606: Yes, disabling eslint for the entire file significantly reduces code quality checks. The Problem: Line 1 contains: /* eslint-disable @typescript-eslint/no-explicit-any */This disables TypeScript's Where Looking at the code, Object.entries(data.categories).forEach(([category, files]: [string, any[]]) => {Why This Is Bad:
Recommended Fix: Define a proper interface for the API response: // Remove the eslint-disable comment on line 1
// Add type definitions
interface FileObject {
id: number;
title: string;
credit?: string;
ext: string;
url: string;
}
interface ApiResponse {
categories: Record<string, FileObject[]>;
}
// Then on line 43, use proper typing:
Object.entries(data.categories).forEach(([category, files]: [string, FileObject[]]) => {
files.forEach(file => {
// Now `file` is properly typed with autocomplete and type checking
// ...This provides full type safety without needing any eslint disables. Alternative (if you can't define types immediately): Use a targeted disable comment on just line 43: // eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.entries(data.categories).forEach(([category, files]: [string, any[]]) => {This limits the impact to just that one line rather than the entire file. Would you like me to generate the complete type-safe refactoring? |
Testing CodeRabbit code review
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.