Skip to content

Testingbranch - CodeRabbit Review - #1

Open
smb060606 wants to merge 4 commits into
mainfrom
testingbranch-coderabbit
Open

Testingbranch - CodeRabbit Review#1
smb060606 wants to merge 4 commits into
mainfrom
testingbranch-coderabbit

Conversation

@smb060606

@smb060606 smb060606 commented Dec 23, 2025

Copy link
Copy Markdown
Collaborator

Testing CodeRabbit code review

Summary by CodeRabbit

  • New Features

    • Added blog system with editor and public blog pages
    • Added user profile customization with themes, bio, and social links
    • Added Minecraft Icons resource category
    • Enhanced audio player with waveform visualization
    • Improved donate button with interactive animations
  • Improvements

    • Added account deletion capability
    • Enhanced video playback support
    • Improved resource loading with lazy-loading
    • Updated navigation with new Blogs section

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

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown

Walkthrough

This 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

Cohort / File(s) Summary
Profile System
src/components/profile/*, src/pages/Profile.tsx, src/types/profile.ts
New ProfileEditor component with localStorage autosave, two-tab UI (Content/Appearance), theme configuration, social links, and live preview via ProfileThemeEngine. Profile page redesigned with dynamic theming, rich bio support, and icon-based link rendering. New types for theme configuration and profile customization.
Blog System
src/components/admin/BlogEditor.tsx, src/components/admin/AdminBlogsManager.tsx, src/pages/Blog*.tsx
New BlogEditor component for create/edit with Markdown preview. AdminBlogsManager displays blog table with status, date, and actions. New Blogs list page and BlogView detail page with author profile integration and Markdown rendering.
Account & Auth Refactoring
api/deleteAccount.js, src/providers/Auth*, src/hooks/useAuth.tsx, server.js
New API endpoint for secure account deletion with CORS and token validation. AuthProvider and AuthContext separated into dedicated modules. useAuth simplified to consume external context with runtime validation.
Resource Enhancements
src/components/resources/*, src/hooks/useResources.ts, src/lib/mciApi.ts, src/types/resources.ts
Added minecraft-icons category with subcategory filtering. Introduced pagination (12 items per page). New AudioPlayer with waveform display and visibility-aware playback. New VideoPlayer component. LazyLoading for resource previews with IntersectionObserver. ResourceDetailDialog navigation between filtered results.
UI Component Refactoring
src/components/ui/button*.ts, src/components/ui/badge*.ts, src/components/ui/toggle*.ts, src/components/ui/*-variants.ts
Extracted variant definitions into dedicated modules for button, badge, and toggle components. Improves modularity and reusability across UI library.
Form & Sidebar Refactoring
src/components/ui/form.tsx, src/components/ui/use-form-field.ts, src/components/ui/sidebar*.tsx
Moved form field contexts and hooks to external module. Extracted sidebar context and constants into dedicated sidebar-context.tsx.
Navigation & Layout
src/components/Navbar.tsx, src/components/Footer.tsx, src/components/Hero.tsx, src/components/DonateButton.tsx
Navbar enhanced with Blogs link, blog banner, theme toggle, and authentication UI. Footer updated with Blogs and Community Assets links. Hero animation refinements (removed pulse effects). DonateButton now expands on hover with count display.
Data & Utilities
src/data/supporters.ts, src/lib/utils.ts, src/lib/showcase.ts, scripts/export_resources.ts
New supporters data module. Added getSmartIconUrl utility for domain-based icon resolution. Improved type safety in showcase functions. New export_resources.ts script for Supabase resource extraction.
Database & Types
supabase/migrations/*.sql, src/integrations/supabase/types.ts
Added profile customization columns (theme_config, links, bio, social_links, verified). Updated generated types to reflect new profile fields.
Package & Config Updates
package.json, vite.config.ts, vercel.json, tailwind.config.ts
Added dependencies: @fontsource fonts, video.js, file-saver, jszip. Updated Vite proxy and Vercel rewrites for MCI API. Extended Tailwind fontFamily with geist-mono.
Documentation & Metadata
AI_RULES.md, changelog.md, index.html, src/main.tsx
Removed AI_RULES.md content. Cleared changelog. Updated page titles and metadata. Added Geist font imports.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


A rabbit hops through profiles so fine,
With themes and colors all align,
Blogs published, accounts erased clean,
Icons and waveforms on the screen,
Refactored components, modular and bright,
This pull request shines with new light! 🐰✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Testingbranch - CodeRabbit Review' is vague and generic, using non-descriptive terms that do not convey the actual changes in the pull request. Replace the title with a clear, specific summary of the main change. Examples: 'Add blog, profile editor, and account deletion features' or 'Implement user profile customization with blog management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch testingbranch-coderabbit

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

@smb060606

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 the body variable (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 useAuth provides a signOut method:

-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 a code property, accessing error?.code will 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 createShowcasePage function, 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 for FormItemContext.

The itemContext.id is used on line 34 and to derive IDs on lines 39-41. If useFormField is called outside of a FormItem, id will be undefined, producing malformed IDs like undefined-form-item.

You may want to add a validation check for itemContext.id as 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) and const 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.tsx accordingly.

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}
     <IconChevronDown
src/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 of any.

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). If getSession() resolves before onAuthStateChange fires 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.

getUsernameFromEmail is defined but not included in the AuthContext.Provider value (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 AuthContextType in src/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 the verified flag.

💡 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.writeFileSync can 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-disable comments for exhaustive-deps suggest potential issues with the dependency array. While the current implementation may work, consider these alternatives to avoid suppressions:

  1. Remove page from useCallback dependencies and pass it as a parameter
  2. Use a ref to access the current page value
  3. 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 subcategory from a strict union ('davinci' | 'adobe') to a generic string increases 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 backgroundImage is 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 id is 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_url suggests the Supabase schema types may not include avatar_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 native confirm() 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 with as any for 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 isInView becomes true. For better performance, consider using preload="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 | null casts are unnecessary since null is 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 AbortController with 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: avoid as any cast.

The as any cast 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 pixel theme, but 'geist' is currently assigned. If a pixel-style font is desired, consider adding it to the fontFamily union 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 07ba79d and 6a411ad.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (79)
  • AI_RULES.md
  • api/deleteAccount.js
  • changelog.md
  • index.html
  • package.json
  • scripts/export_resources.ts
  • server.js
  • src/App.tsx
  • src/components/AudioPlayer.tsx
  • src/components/DonateButton.tsx
  • src/components/Footer.tsx
  • src/components/Hero.tsx
  • src/components/Navbar.tsx
  • src/components/SupportersList.tsx
  • src/components/ThemeToggle.tsx
  • src/components/VideoPlayer.tsx
  • src/components/admin/AdminBlogsManager.tsx
  • src/components/admin/AdminResourcesManager.tsx
  • src/components/admin/BlogEditor.tsx
  • src/components/auth/UserMenu.tsx
  • src/components/profile/ProfileEditor.tsx
  • src/components/profile/ProfileThemeEngine.tsx
  • src/components/profile/SvglPicker.tsx
  • src/components/resources/ResourceCard.tsx
  • src/components/resources/ResourceDetailDialog.tsx
  • src/components/resources/ResourceFilters.tsx
  • src/components/resources/ResourcePreview.tsx
  • src/components/resources/ResourcesList.tsx
  • src/components/ui/alert-dialog.tsx
  • src/components/ui/badge-variants.ts
  • src/components/ui/badge.tsx
  • src/components/ui/button-variants.ts
  • src/components/ui/button.tsx
  • src/components/ui/calendar.tsx
  • src/components/ui/command.tsx
  • src/components/ui/form.tsx
  • src/components/ui/navigation-menu-variants.ts
  • src/components/ui/navigation-menu.tsx
  • src/components/ui/pagination.tsx
  • src/components/ui/sidebar-context.tsx
  • src/components/ui/sidebar.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/toggle-variants.ts
  • src/components/ui/toggle.tsx
  • src/components/ui/use-form-field.ts
  • src/data/supporters.ts
  • src/hooks/useAuth.tsx
  • src/hooks/useDownloadCounts.ts
  • src/hooks/useProfile.ts
  • src/hooks/useResources.ts
  • src/hooks/useUserFavorites.ts
  • src/index.css
  • src/integrations/supabase/types.ts
  • src/lib/mciApi.ts
  • src/lib/showcase.ts
  • src/lib/showcases.ts
  • src/lib/utils.ts
  • src/main.tsx
  • src/pages/Admin.tsx
  • src/pages/AiTitleHelper.tsx
  • src/pages/BlogView.tsx
  • src/pages/Blogs.tsx
  • src/pages/GuideView.tsx
  • src/pages/Index.tsx
  • src/pages/NotFound.tsx
  • src/pages/Profile.tsx
  • src/pages/ResourcesHub.tsx
  • src/pages/Showcase.tsx
  • src/pages/YouTubeDownloader.tsx
  • src/providers/AuthContext.tsx
  • src/providers/AuthProvider.tsx
  • src/types/profile.ts
  • src/types/resources.ts
  • supabase/migrations/20251214152600_add_profile_customization.sql
  • tailwind.config.ts
  • test.md
  • vercel.json
  • vite.config.ts
  • vite.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)

Comment thread api/deleteAccount.js
Comment on lines +3 to +7
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +8 to +14
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment thread src/App.tsx
Comment on lines +57 to +65
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');
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +71 to +111
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);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find database-related files (migrations, schema definitions)
find . -type f \( -name "*migration*" -o -name "*schema*" -o -name "*supabase*" \) 2>/dev/null | head -20

Repository: 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 -20

Repository: 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/null

Repository: 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 -20

Repository: smb060606/renderdragon.org

Length of output: 242


🏁 Script executed:

# Read the supabaseClient setup
cat src/lib/supabaseClient.ts

Repository: 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.ts

Repository: 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/null

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

Repository: 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.sql

Repository: smb060606/renderdragon.org

Length of output: 2416


🏁 Script executed:

# Check for other migration references to blogs table
rg "blogs" ./supabase/migrations/ --type sql

Repository: 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; done

Repository: 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 -50

Repository: 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 -i

Repository: 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.ts

Repository: 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 -100

Repository: 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 5

Repository: 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.tsx

Repository: 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.ts

Repository: smb060606/renderdragon.org

Length of output: 5516


🏁 Script executed:

# Search all migrations for any blog-related SQL
grep -i "blog" ./supabase/migrations/*.sql

Repository: 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.sql

Repository: 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
done

Repository: 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 f

Repository: 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 -20

Repository: 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 migrations

Repository: 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 tsx

Repository: 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.ts

Repository: 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/null

Repository: 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_modules

Repository: smb060606/renderdragon.org

Length of output: 92


🏁 Script executed:

# Check the Supabase config file
cat ./supabase/config.toml

Repository: 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 -40

Repository: 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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment thread src/pages/Blogs.tsx
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-3 font-geist-mono">
{removeMarkdown(blog.content || "").slice(0, 150)}...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{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.

Comment thread src/pages/NotFound.tsx
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment thread src/pages/Profile.tsx
Comment on lines +218 to +225
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment on lines +316 to +328
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
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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:

  1. Removing the x-api-key header if the endpoint is meant to be publicly accessible
  2. Implementing proper user authentication where the backend validates the user's session
  3. 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.

Comment on lines +1 to +62
// 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==

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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-*.mjs

Then 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.

@smb060606

Copy link
Copy Markdown
Collaborator Author

@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?

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the YouTubeDownloader.tsx file to examine lines 101-102 and surrounding context
cat src/pages/YouTubeDownloader.tsx

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:

  1. Hardcoded destination: The base URL is hardcoded as 'https://mediapye.vercel.app'. The user input (youtubeUrl) is only used as a query parameter value, not in constructing the destination URL itself.

  2. Proper encoding: The user input is properly encoded using encodeURIComponent(), which ensures special characters cannot break out of the query parameter context.

  3. Client-side context: This is a client-side fetch in the browser. Even if manipulated, it only affects the user's own browser request, not causing a server to make arbitrary requests (which is what SSRF is).

  4. Validation exists: Lines 84-91 do validate the input before the fetch:

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 isValidYoutubeUrl regex could be stricter (the .+ is permissive), this doesn't create SSRF risk since the destination host is hardcoded. The validation primarily prevents invalid inputs from being sent to the third-party API.

The URL construction at lines 101-102 follows secure practices for client-side API calls.

@smb060606

Copy link
Copy Markdown
Collaborator Author

@coderabbitai - in this file src/hooks/useResources.ts, line 1 (link)

confirm this logic: will disabling eslint for entire file reduces code quality checks?

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the useResources.ts file to examine line 1 and understand the eslint configuration
cat src/hooks/useResources.ts

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 any type checking for the entire file, which is overly broad. The @typescript-eslint/no-explicit-any rule exists to enforce type safety—one of TypeScript's core benefits.

Where any is Actually Used:

Looking at the code, any appears on line 43:

Object.entries(data.categories).forEach(([category, files]: [string, any[]]) => {

Why This Is Bad:

  1. Defeats type safety: Allows unsafe type usage throughout the file
  2. Increases runtime error risk: No compile-time checks on file properties (lines 44-58)
  3. Reduces maintainability: Future developers won't get type hints
  4. Too broad: Disables the rule everywhere when only one line needs it

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?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants