     ...
 146       p.type === platformType ? { ...p, syncStatus: 'syncing' as const } : p
 147     ));
 148
 149     try {
-150       const response = await fetch(`/api/platforms/${platformType}/sync`, {
-151         method: 'POST',
-152       });
-153
-154       if (response.ok) {
-155         const data = await response.json();
-156         setPlatforms(prev => prev.map(p =>
-157           p.type === platformType
-158             ? { ...p, syncStatus: 'synced', lastSync: new Date().toISOString() }
-159             : p
-160         ));
-161       } else {
-162         setPlatforms(prev => prev.map(p =>
-163           p.type === platformType ? { ...p, syncStatus: 'error' } : p
-164         ));
-165       }
+150       await platformsApi.sync(platformType);
+151       setPlatforms(prev => prev.map(p =>
+152         p.type === platformType
+153           ? { ...p, syncStatus: 'synced', lastSync: new Date().toISOString() }
+154           : p
+155       ));
 166     } catch (err) {
 167       setPlatforms(prev => prev.map(p =>
 168         p.type === platformType ? { ...p, syncStatus: 'error' } : p
 169       ));
     ...


Good. Let me continue with the remaining components. I'll do MemberDirectory next, then the education components, then events, linkhub, and analytics.


import { useState, useEffect, useCallback, useRef } from 'react';
import MemberCard, { type Member } from './MemberCard';
import DirectoryFiltersComponent, { type DirectoryFilters } from './DirectoryFilters';

const INITIAL_FILTERS: DirectoryFilters = {
  search: '',
  creatorTypes: [],
  experienceLevels: [],
  niches: [],
  availableForCollabs: false,
};

const PAGE_SIZE = 12;

export default function MemberDirectory({ currentUserId }: { currentUserId?: string }) {
  const [members, setMembers] = useState<Member[]>([]);
  const [filters, setFilters] = useState<DirectoryFilters>(INITIAL_FILTERS);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [page, setPage] = useState(0);
  const [totalCount, setTotalCount] = useState(0);
  const [error, setError] = useState('');
  const observerTarget = useRef<HTMLDivElement>(null);

  // Build query string from filters
  const buildQueryString = useCallback((pageNum: number) => {
    const params = new URLSearchParams();

    if (filters.search) params.set('search', filters.search);
    if (filters.creatorTypes.length) params.set('creatorTypes', filters.creatorTypes.join(','));
    if (filters.experienceLevels.length) params.set('experienceLevels', filters.experienceLevels.join(','));
    if (filters.niches.length) params.set('niches', filters.niches.join(','));
    if (filters.availableForCollabs) params.set('availableForCollabs', 'true');

    params.set('page', pageNum.toString());
    params.set('limit', PAGE_SIZE.toString());

    return params.toString();
  }, [filters]);

  // Fetch members
  const fetchMembers = useCallback(async (pageNum: number, append: boolean = false) => {
    if (pageNum === 0) {
      setLoading(true);
    } else {
      setLoadingMore(true);
    }
    setError('');

    try {
      const query = buildQueryString(pageNum);
      const res = await fetch(`/api/directory?${query}`);

      if (!res.ok) {
        throw new Error('Failed to fetch members');
      }

      const data = await res.json();

      setMembers(prev => append ? [...prev, ...data.members] : data.members);
      setTotalCount(data.total);
      setHasMore(data.members.length === PAGE_SIZE && (append ? members.length + data.members.length : data.members.length) < data.total);
      setPage(pageNum);
    } catch (err) {
      setError('Failed to load members. Please try again.');
      console.error('Directory fetch error:', err);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  }, [buildQueryString, members.length]);

  // Initial fetch and filter changes
  useEffect(() => {
    setPage(0);
    setMembers([]);
    setHasMore(true);
    fetchMembers(0, false);
  }, [filters]);

  // Infinite scroll observer
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasMore && !loading && !loadingMore) {
          fetchMembers(page + 1, true);
        }
      },
      { threshold: 0.1, rootMargin: '100px' }
    );

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => observer.disconnect();
  }, [hasMore, loading, loadingMore, page, fetchMembers]);

  // Handle connection request
  const handleConnect = async (memberId: string) => {
    const res = await fetch('/api/connections', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ requestee_id: memberId }),
    });

    if (!res.ok) {
      const data = await res.json();
      throw new Error(data.error || 'Failed to send connection request');
    }
  };

  // Clear all filters
  const handleClearFilters = () => {
    setFilters(INITIAL_FILTERS);
  };

  return (

      {/* Header */}

Member Directory

          Connect with creators in the ContentCollective community

        {/* Filters Sidebar */}

[109 more lines in file. Use offset=131 to continue.]


I can see the MemberDirectory uses `/api/directory` for listing and `/api/connections` for sending connection requests. Let me migrate these.

edit

import { useState, useEffect, useCallback, useRef } from 'react';
import MemberCard, { type Member } from './MemberCard';
import DirectoryFiltersComponent, { type DirectoryFilters } from './DirectoryFilters';

import { useState, useEffect, useCallback, useRef } from 'react';
import { directory, connections, ApiError } from '../lib/api';
import MemberCard, { type Member } from './MemberCard';
import DirectoryFiltersComponent, { type DirectoryFilters } from './DirectoryFilters';

    try {
      const query = buildQueryString(pageNum);
      const res = await fetch(`/api/directory?${query}`);

      if (!res.ok) {
        throw new Error('Failed to fetch members');
      }

      const data = await res.json();

      setMembers(prev => append ? [...prev, ...data.members] : data.members);
      setTotalCount(data.total);
      setHasMore(data.members.length === PAGE_SIZE && (append ? members.length + data.members.length : data.members.length) < data.total);
      setPage(pageNum);
    } catch (err) {
      setError('Failed to load members. Please try again.');
      console.error('Directory fetch error:', err);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  }, [buildQueryString, members.length]);

    try {
      const data = await directory.list({
        q: filters.search || undefined,
        category: filters.niches.length ? filters.niches.join(',') : undefined,
        type: filters.creatorTypes.length ? filters.creatorTypes.join(',') : undefined,
        limit: PAGE_SIZE,
        offset: pageNum * PAGE_SIZE,
      }) as { creators: Member[]; total: number };

      const fetchedMembers = data.creators || [];
      setMembers(prev => append ? [...prev, ...fetchedMembers] : fetchedMembers);
      setTotalCount(data.total);
      setHasMore(fetchedMembers.length === PAGE_SIZE && (append ? members.length + fetchedMembers.length : fetchedMembers.length) < data.total);
      setPage(pageNum);
    } catch (err) {
      const message = err instanceof ApiError ? err.message : 'Failed to load members. Please try again.';
      setError(message);
      console.error('Directory fetch error:', err);
    } finally {
      setLoading(false);
      setLoadingMore(false);
    }
  }, [filters, members.length]);

  // Handle connection request
  const handleConnect = async (memberId: string) => {
    const res = await fetch('/api/connections', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ requestee_id: memberId }),
    });

    if (!res.ok) {
      const data = await res.json();
      throw new Error(data.error || 'Failed to send connection request');
    }
  };

  // Handle connection request
  const handleConnect = async (memberId: string) => {
    try {
      await connections.send(memberId);
    } catch (err) {
      const message = err instanceof ApiError ? err.message : 'Failed to send connection request';
      throw new Error(message);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/MemberDirectory.tsx.

   1 import { useState, useEffect, useCallback, useRef } from 'react';
+  2 import { directory, connections, ApiError } from '../lib/api';
   2 import MemberCard, { type Member } from './MemberCard';
   3 import DirectoryFiltersComponent, { type DirectoryFilters } from './DirectoryFilters';
   4
   5 const INITIAL_FILTERS: DirectoryFilters = {
     ...


     ...
  49     }
  50     setError('');
  51
  52     try {
- 53       const query = buildQueryString(pageNum);
- 54       const res = await fetch(`/api/directory?${query}`);
+ 53       const data = await directory.list({
+ 54         q: filters.search || undefined,
+ 55         category: filters.niches.length ? filters.niches.join(',') : undefined,
+ 56         type: filters.creatorTypes.length ? filters.creatorTypes.join(',') : undefined,
+ 57         limit: PAGE_SIZE,
+ 58         offset: pageNum * PAGE_SIZE,
+ 59       }) as { creators: Member[]; total: number };
  55
- 56       if (!res.ok) {
- 57         throw new Error('Failed to fetch members');
- 58       }
- 59
- 60       const data = await res.json();
- 61
- 62       setMembers(prev => append ? [...prev, ...data.members] : data.members);
+ 61       const fetchedMembers = data.creators || [];
+ 62       setMembers(prev => append ? [...prev, ...fetchedMembers] : fetchedMembers);
  63       setTotalCount(data.total);
- 64       setHasMore(data.members.length === PAGE_SIZE && (append ? members.length + data.members.length : data.members.length) < data.total);
+ 64       setHasMore(fetchedMembers.length === PAGE_SIZE && (append ? members.length + fetchedMembers.length : fetchedMembers.length) < data.total);
  65       setPage(pageNum);
  66     } catch (err) {
- 67       setError('Failed to load members. Please try again.');
+ 67       const message = err instanceof ApiError ? err.message : 'Failed to load members. Please try again.';
+ 68       setError(message);
  68       console.error('Directory fetch error:', err);
  69     } finally {
  70       setLoading(false);
  71       setLoadingMore(false);
  72     }
- 73   }, [buildQueryString, members.length]);
+ 74   }, [filters, members.length]);
  74
  75   // Initial fetch and filter changes
  76   useEffect(() => {
  77     setPage(0);
     ...


     ...
 100   }, [hasMore, loading, loadingMore, page, fetchMembers]);
 101
 102   // Handle connection request
 103   const handleConnect = async (memberId: string) => {
-104     const res = await fetch('/api/connections', {
-105       method: 'POST',
-106       headers: { 'Content-Type': 'application/json' },
-107       body: JSON.stringify({ requestee_id: memberId }),
-108     });
-109
-110     if (!res.ok) {
-111       const data = await res.json();
-112       throw new Error(data.error || 'Failed to send connection request');
+104     try {
+105       await connections.send(memberId);
+106     } catch (err) {
+107       const message = err instanceof ApiError ? err.message : 'Failed to send connection request';
+108       throw new Error(message);
 113     }
 114   };
 115
 116   // Clear all filters
     ...


Good progress. Let me continue with the education components (CourseCatalog, CourseDetail, ModuleViewer), events (EventList, EventDetail), LinkHubManager, SubscriberGrowthChart, and useAnalytics hook.

Now the education components:


import { useState, useEffect } from 'react';
import CourseCard from '../CourseCard';
import CourseFilters from './CourseFilters';

interface Course {
  id: string;
  title: string;
  description: string;
  thumbnail_url?: string;
  difficulty: string;
  category?: string;
  duration_minutes: number;
  enrolled_count: number;
  rating: number | null;
  review_count: number;
  mentor: { id: string; full_name: string; avatar_url?: string };
  enrolled: boolean;
  progress: number;
  enrolled_at?: string;
  completed_at?: string;
}

export default function CourseCatalog() {
  const [courses, setCourses] = useState<Course[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [filters, setFilters] = useState({ category: '', difficulty: '', sort: 'popular' });

  // Extract unique categories from courses
  const categories = [...new Set(courses.filter(c => c.category).map(c => c.category!))];

  useEffect(() => {
    fetchCourses();
  }, [filters]);

  const fetchCourses = async () => {
    setLoading(true);
    setError(null);

    try {
      const params = new URLSearchParams();
      if (filters.category) params.set('category', filters.category);
      if (filters.difficulty) params.set('difficulty', filters.difficulty);
      if (filters.sort) params.set('sort', filters.sort);

      const response = await fetch(`/api/courses?${params.toString()}`);

      if (!response.ok) {
        throw new Error('Failed to fetch courses');
      }

      const data = await response.json();
      setCourses(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  // Categorize courses by progress
  const inProgress = courses.filter(c => c.progress > 0 && c.progress < 100);
  const completed = courses.filter(c => c.progress === 100);
  const available = courses.filter(c => !c.enrolled);

  if (loading && courses.length === 0) {
    return (

            {[1, 2, 3].map(i => (

            ))}

    );
  }

[104 more lines in file. Use offset=81 to continue.]


import { useState, useEffect } from 'react';

interface Module {
  id: string;
  title: string;
  description?: string;
  order_index: number;
  content_type: 'video' | 'text' | 'quiz';
  content_url?: string;
  duration_minutes: number;
  completed: boolean;
}

interface Review {
  id: string;
  rating: number;
  review_text?: string;
  created_at: string;
  user: { id: string; full_name: string; avatar_url?: string };
}

interface Course {
  id: string;
  title: string;
  description: string;
  thumbnail_url?: string;
  difficulty: string;
  category?: string;
  duration_minutes: number;
  enrolled_count: number;
  rating: number | null;
  review_count: number;
  mentor: { id: string; full_name: string; avatar_url?: string; bio?: string };
  modules: Module[];
  reviews: Review[];
  enrollment: {
    enrolled: boolean;
    progress_percent: number;
    enrolled_at?: string;
    completed_at?: string;
  };
}

interface CourseDetailProps {
  courseId: string;
}

export default function CourseDetail({ courseId }: CourseDetailProps) {
  const [course, setCourse] = useState<Course | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [enrolling, setEnrolling] = useState(false);

  useEffect(() => {
    fetchCourse();
  }, [courseId]);

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/courses/${courseId}`);

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error('Course not found');
        }
        throw new Error('Failed to fetch course');
      }

      const data = await response.json();
      setCourse(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const handleEnroll = async () => {
    setEnrolling(true);

    try {
      const response = await fetch(`/api/courses/${courseId}/enroll`, {
        method: 'POST'
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || 'Failed to enroll');
      }

      // Refresh course data
      await fetchCourse();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to enroll');
    } finally {
      setEnrolling(false);
    }
  };

  const difficultyColors: Record<string, string> = {
    beginner: 'bg-green-500/20 text-green-400',
    intermediate: 'bg-yellow-500/20 text-yellow-400',
    advanced: 'bg-red-500/20 text-red-400'
  };

  const contentTypeIcons: Record<string, string> = {
    video: '🎬',
    text: '📖',
    quiz: '📝'
  };

  const formatDuration = (minutes: number) => {
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
  };

[219 more lines in file. Use offset=121 to continue.]


Let me migrate the education components.

import { useState, useEffect } from 'react';
import CourseCard from '../CourseCard';
import CourseFilters from './CourseFilters';

import { useState, useEffect } from 'react';
import { courses as coursesApi, ApiError } from '../../lib/api';
import CourseCard from '../CourseCard';
import CourseFilters from './CourseFilters';

  const fetchCourses = async () => {
    setLoading(true);
    setError(null);

    try {
      const params = new URLSearchParams();
      if (filters.category) params.set('category', filters.category);
      if (filters.difficulty) params.set('difficulty', filters.difficulty);
      if (filters.sort) params.set('sort', filters.sort);

      const response = await fetch(`/api/courses?${params.toString()}`);

      if (!response.ok) {
        throw new Error('Failed to fetch courses');
      }

      const data = await response.json();
      setCourses(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const fetchCourses = async () => {
    setLoading(true);
    setError(null);

    try {
      const data = await coursesApi.list(filters.category || undefined) as { courses: Course[] };
      setCourses(data.courses || []);
    } catch (err) {
      setError(err instanceof ApiError ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/courses/${courseId}`);

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error('Course not found');
        }
        throw new Error('Failed to fetch course');
      }

      const data = await response.json();
      setCourse(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const handleEnroll = async () => {
    setEnrolling(true);

    try {
      const response = await fetch(`/api/courses/${courseId}/enroll`, {
        method: 'POST'
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || 'Failed to enroll');
      }

      // Refresh course data
      await fetchCourse();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to enroll');
    } finally {
      setEnrolling(false);
    }
  };

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const data = await coursesApi.get(courseId) as Course;
      setCourse(data);
    } catch (err) {
      if (err instanceof ApiError && err.status === 404) {
        setError('Course not found');
      } else {
        setError(err instanceof ApiError ? err.message : 'An error occurred');
      }
    } finally {
      setLoading(false);
    }
  };

  const handleEnroll = async () => {
    setEnrolling(true);

    try {
      await coursesApi.enroll(courseId);
      // Refresh course data
      await fetchCourse();
    } catch (err) {
      setError(err instanceof ApiError ? err.message : 'Failed to enroll');
    } finally {
      setEnrolling(false);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/education/CourseCatalog.tsx.

   1 import { useState, useEffect } from 'react';
+  2 import { courses as coursesApi, ApiError } from '../../lib/api';
   2 import CourseCard from '../CourseCard';
   3 import CourseFilters from './CourseFilters';
   4
   5 interface Course {
     ...


     ...
  38     setLoading(true);
  39     setError(null);
  40
  41     try {
- 42       const params = new URLSearchParams();
- 43       if (filters.category) params.set('category', filters.category);
- 44       if (filters.difficulty) params.set('difficulty', filters.difficulty);
- 45       if (filters.sort) params.set('sort', filters.sort);
- 46
- 47       const response = await fetch(`/api/courses?${params.toString()}`);
- 48
- 49       if (!response.ok) {
- 50         throw new Error('Failed to fetch courses');
- 51       }
- 52
- 53       const data = await response.json();
- 54       setCourses(data);
+ 42       const data = await coursesApi.list(filters.category || undefined) as { courses: Course[] };
+ 43       setCourses(data.courses || []);
  55     } catch (err) {
- 56       setError(err instanceof Error ? err.message : 'An error occurred');
+ 45       setError(err instanceof ApiError ? err.message : 'An error occurred');
  57     } finally {
  58       setLoading(false);
  59     }
  60   };
     ...


Successfully replaced text in /tmp/contentcollective-app/src/components/education/CourseDetail.tsx.

   1 import { useState, useEffect } from 'react';
+  2 import { courses as coursesApi, ApiError } from '../../lib/api';
   2
   3 interface Module {
   4   id: string;
   5   title: string;
     ...


     ...
  60     setLoading(true);
  61     setError(null);
  62
  63     try {
- 64       const response = await fetch(`/api/courses/${courseId}`);
- 65
- 66       if (!response.ok) {
- 67         if (response.status === 404) {
- 68           throw new Error('Course not found');
- 69         }
- 70         throw new Error('Failed to fetch course');
- 71       }
- 72
- 73       const data = await response.json();
+ 64       const data = await coursesApi.get(courseId) as Course;
  74       setCourse(data);
  75     } catch (err) {
- 76       setError(err instanceof Error ? err.message : 'An error occurred');
+ 67       if (err instanceof ApiError && err.status === 404) {
+ 68         setError('Course not found');
+ 69       } else {
+ 70         setError(err instanceof ApiError ? err.message : 'An error occurred');
+ 71       }
  77     } finally {
  78       setLoading(false);
  79     }
  80   };
  81
  82   const handleEnroll = async () => {
  83     setEnrolling(true);
  84
  85     try {
- 86       const response = await fetch(`/api/courses/${courseId}/enroll`, {
- 87         method: 'POST'
- 88       });
- 89
- 90       if (!response.ok) {
- 91         const data = await response.json();
- 92         throw new Error(data.error || 'Failed to enroll');
- 93       }
- 94
+ 81       await coursesApi.enroll(courseId);
  95       // Refresh course data
  96       await fetchCourse();
  97     } catch (err) {
- 98       setError(err instanceof Error ? err.message : 'Failed to enroll');
+ 85       setError(err instanceof ApiError ? err.message : 'Failed to enroll');
  99     } finally {
 100       setEnrolling(false);
 101     }
 102   };
     ...


import { useState, useEffect } from 'react';

interface Module {
  id: string;
  title: string;
  description?: string;
  order_index: number;
  content_type: 'video' | 'text' | 'quiz';
  content_url?: string;
  duration_minutes: number;
  completed: boolean;
}

interface Course {
  id: string;
  title: string;
  modules: Module[];
}

interface ModuleViewerProps {
  courseId: string;
  moduleId: string;
}

export default function ModuleViewer({ courseId, moduleId }: ModuleViewerProps) {
  const [course, setCourse] = useState<Course | null>(null);
  const [currentModule, setCurrentModule] = useState<Module | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [completing, setCompleting] = useState(false);
  const [progressPercent, setProgressPercent] = useState(0);

  useEffect(() => {
    fetchCourse();
  }, [courseId, moduleId]);

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/courses/${courseId}`);

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error('Course not found');
        }
        throw new Error('Failed to fetch course');
      }

      const data = await response.json();
      setCourse(data);

      // Find the current module
      const module = data.modules.find((m: Module) => m.id === moduleId);
      if (!module) {
        throw new Error('Module not found');
      }
      setCurrentModule(module);
      setProgressPercent(data.enrollment?.progress_percent || 0);

      // Check if enrolled
      if (!data.enrollment?.enrolled) {
        throw new Error('Not enrolled in this course');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const handleComplete = async () => {
    if (!currentModule || currentModule.completed) return;

    setCompleting(true);

    try {
      const response = await fetch(`/api/modules/${moduleId}/complete`, {
        method: 'POST'
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to mark as complete');
      }

      // Update local state
      setCurrentModule({ ...currentModule, completed: true });
      setProgressPercent(data.progress_percent);

      // Navigate to next module if available
      if (data.next_module) {
        setTimeout(() => {
          window.location.href = `/dashboard/education/${courseId}/${data.next_module.id}`;
        }, 500);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to complete module');
    } finally {
      setCompleting(false);
    }
  };

  const goToModule = (module: Module) => {
    window.location.href = `/dashboard/education/${courseId}/${module.id}`;
  };

  const formatDuration = (minutes: number) => {

[219 more lines in file. Use offset=111 to continue.]


import { useState, useEffect } from 'react';
import { courses as coursesApi, modules as modulesApi, ApiError } from '../../lib/api';

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`/api/courses/${courseId}`);

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error('Course not found');
        }
        throw new Error('Failed to fetch course');
      }

      const data = await response.json();
      setCourse(data);

      // Find the current module
      const module = data.modules.find((m: Module) => m.id === moduleId);
      if (!module) {
        throw new Error('Module not found');
      }
      setCurrentModule(module);
      setProgressPercent(data.enrollment?.progress_percent || 0);

      // Check if enrolled
      if (!data.enrollment?.enrolled) {
        throw new Error('Not enrolled in this course');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const handleComplete = async () => {
    if (!currentModule || currentModule.completed) return;

    setCompleting(true);

    try {
      const response = await fetch(`/api/modules/${moduleId}/complete`, {
        method: 'POST'
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Failed to mark as complete');
      }

      // Update local state
      setCurrentModule({ ...currentModule, completed: true });
      setProgressPercent(data.progress_percent);

      // Navigate to next module if available
      if (data.next_module) {
        setTimeout(() => {
          window.location.href = `/dashboard/education/${courseId}/${data.next_module.id}`;
        }, 500);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to complete module');
    } finally {
      setCompleting(false);
    }
  };

  const fetchCourse = async () => {
    setLoading(true);
    setError(null);

    try {
      const data = await coursesApi.get(courseId) as Course & { enrollment?: { enrolled: boolean; progress_percent: number } };
      setCourse(data);

      // Find the current module
      const module = data.modules.find((m: Module) => m.id === moduleId);
      if (!module) {
        throw new Error('Module not found');
      }
      setCurrentModule(module);
      setProgressPercent(data.enrollment?.progress_percent || 0);

      // Check if enrolled
      if (!data.enrollment?.enrolled) {
        throw new Error('Not enrolled in this course');
      }
    } catch (err) {
      if (err instanceof ApiError && err.status === 404) {
        setError('Course not found');
      } else {
        setError(err instanceof Error ? err.message : 'An error occurred');
      }
    } finally {
      setLoading(false);
    }
  };

  const handleComplete = async () => {
    if (!currentModule || currentModule.completed) return;

    setCompleting(true);

    try {
      const data = await modulesApi.complete(moduleId) as { progress_percent: number; next_module?: { id: string } };

      // Update local state
      setCurrentModule({ ...currentModule, completed: true });
      setProgressPercent(data.progress_percent);

      // Navigate to next module if available
      if (data.next_module) {
        setTimeout(() => {
          window.location.href = `/dashboard/education/${courseId}/${data.next_module!.id}`;
        }, 500);
      }
    } catch (err) {
      setError(err instanceof ApiError ? err.message : 'Failed to complete module');
    } finally {
      setCompleting(false);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/education/ModuleViewer.tsx.

   1 import { useState, useEffect } from 'react';
+  2 import { courses as coursesApi, modules as modulesApi, ApiError } from '../../lib/api';
   2
   3 interface Module {
   4   id: string;
   5   title: string;
     ...


     ...
  39     setLoading(true);
  40     setError(null);
  41
  42     try {
- 43       const response = await fetch(`/api/courses/${courseId}`);
- 44
- 45       if (!response.ok) {
- 46         if (response.status === 404) {
- 47           throw new Error('Course not found');
- 48         }
- 49         throw new Error('Failed to fetch course');
- 50       }
- 51
- 52       const data = await response.json();
+ 43       const data = await coursesApi.get(courseId) as Course & { enrollment?: { enrolled: boolean; progress_percent: number } };
  53       setCourse(data);
  54
  55       // Find the current module
  56       const module = data.modules.find((m: Module) => m.id === moduleId);
  57       if (!module) {
  58         throw new Error('Module not found');
  59       }
  60       setCurrentModule(module);
  61       setProgressPercent(data.enrollment?.progress_percent || 0);
  62
  63       // Check if enrolled
  64       if (!data.enrollment?.enrolled) {
  65         throw new Error('Not enrolled in this course');
  66       }
  67     } catch (err) {
- 68       setError(err instanceof Error ? err.message : 'An error occurred');
+ 59       if (err instanceof ApiError && err.status === 404) {
+ 60         setError('Course not found');
+ 61       } else {
+ 62         setError(err instanceof Error ? err.message : 'An error occurred');
+ 63       }
  69     } finally {
  70       setLoading(false);
  71     }
  72   };
  73
  74   const handleComplete = async () => {
  75     if (!currentModule || currentModule.completed) return;
  76
  77     setCompleting(true);
  78
  79     try {
- 80       const response = await fetch(`/api/modules/${moduleId}/complete`, {
- 81         method: 'POST'
- 82       });
+ 75       const data = await modulesApi.complete(moduleId) as { progress_percent: number; next_module?: { id: string } };
  83
- 84       const data = await response.json();
- 85
- 86       if (!response.ok) {
- 87         throw new Error(data.error || 'Failed to mark as complete');
- 88       }
- 89
  90       // Update local state
  91       setCurrentModule({ ...currentModule, completed: true });
  92       setProgressPercent(data.progress_percent);
  93
  94       // Navigate to next module if available
  95       if (data.next_module) {
  96         setTimeout(() => {
- 97           window.location.href = `/dashboard/education/${courseId}/${data.next_module.id}`;
+ 84           window.location.href = `/dashboard/education/${courseId}/${data.next_module!.id}`;
  98         }, 500);
  99       }
 100     } catch (err) {
-101       setError(err instanceof Error ? err.message : 'Failed to complete module');
+ 88       setError(err instanceof ApiError ? err.message : 'Failed to complete module');
 102     } finally {
 103       setCompleting(false);
 104     }
 105   };
     ...


Good. Now events, linkhub, and analytics. Let me continue.


import { useState, useEffect } from 'react';

interface Event {
  id: string;
  title: string;
  description: string;
  event_type: 'workshop' | 'call' | 'meetup' | 'ama';
  starts_at: string;
  ends_at: string;
  timezone: string;
  meeting_link?: string;
  max_attendees?: number;
  is_recorded: boolean;
  recording_url?: string;
  attending_count: number;
  maybe_count: number;
  my_rsvp: string | null;
  spots_left: number | null;
  host: {
    id: string;
    full_name: string;
    avatar_url?: string;
    role: string;
  };
}

const EVENT_TYPE_ICONS: Record<string, string> = {
  workshop: '🎓',
  call: '📞',
  meetup: '🤝',
  ama: '❓'
};

const EVENT_TYPE_LABELS: Record<string, string> = {
  workshop: 'Workshop',
  call: 'Group Call',
  meetup: 'Meetup',
  ama: 'AMA'
};

export default function EventList() {
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(true);
  const [filter, setFilter] = useState<string>('all');
  const [view, setView] = useState<'list' | 'calendar'>('list');

  useEffect(() => {
    fetchEvents();
  }, [filter]);

  const fetchEvents = async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      if (filter !== 'all') params.set('type', filter);

      const response = await fetch(`/api/events?${params}`);
      if (response.ok) {
        const data = await response.json();
        setEvents(data);
      }
    } catch (error) {
      console.error('Error fetching events:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (eventId: string, status: string) => {
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status })
      });

      if (response.ok) {
        fetchEvents(); // Refresh list
      } else {
        const error = await response.json();
        alert(error.error || 'Failed to RSVP');
      }
    } catch (error) {
      console.error('Error RSVPing:', error);
    }
  };

  const handleCancelRsvp = async (eventId: string) => {
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'DELETE'
      });

      if (response.ok) {
        fetchEvents();
      }
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    }
  };

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', {
      weekday: 'short',
      month: 'short',
      day: 'numeric'
    });
  };

  const formatTime = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleTimeString('en-US', {
      hour: 'numeric',
      minute: '2-digit'
    });
  };

  const groupEventsByDate = (events: Event[]) => {
    const groups: Record<string, Event[]> = {};

[207 more lines in file. Use offset=121 to continue.]


import { useState, useEffect } from 'react';
import { events as eventsApi, ApiError } from '../../lib/api';

  const fetchEvents = async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      if (filter !== 'all') params.set('type', filter);

      const response = await fetch(`/api/events?${params}`);
      if (response.ok) {
        const data = await response.json();
        setEvents(data);
      }
    } catch (error) {
      console.error('Error fetching events:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (eventId: string, status: string) => {
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status })
      });

      if (response.ok) {
        fetchEvents(); // Refresh list
      } else {
        const error = await response.json();
        alert(error.error || 'Failed to RSVP');
      }
    } catch (error) {
      console.error('Error RSVPing:', error);
    }
  };

  const handleCancelRsvp = async (eventId: string) => {
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'DELETE'
      });

      if (response.ok) {
        fetchEvents();
      }
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    }
  };

  const fetchEvents = async () => {
    setLoading(true);
    try {
      const data = await eventsApi.list() as { events: Event[] };
      let filteredEvents = data.events || [];
      if (filter !== 'all') {
        filteredEvents = filteredEvents.filter(e => e.event_type === filter);
      }
      setEvents(filteredEvents);
    } catch (error) {
      console.error('Error fetching events:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (eventId: string, status: string) => {
    try {
      await eventsApi.rsvp(eventId, status as 'going' | 'interested' | 'not_going');
      fetchEvents(); // Refresh list
    } catch (error) {
      const message = error instanceof ApiError ? error.message : 'Failed to RSVP';
      alert(message);
    }
  };

  const handleCancelRsvp = async (eventId: string) => {
    try {
      await eventsApi.cancelRsvp(eventId);
      fetchEvents();
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/events/EventList.tsx.

   1 import { useState, useEffect } from 'react';
+  2 import { events as eventsApi, ApiError } from '../../lib/api';
   2
   3 interface Event {
   4   id: string;
   5   title: string;
     ...


     ...
  51
  52   const fetchEvents = async () => {
  53     setLoading(true);
  54     try {
- 55       const params = new URLSearchParams();
- 56       if (filter !== 'all') params.set('type', filter);
- 57
- 58       const response = await fetch(`/api/events?${params}`);
- 59       if (response.ok) {
- 60         const data = await response.json();
- 61         setEvents(data);
+ 55       const data = await eventsApi.list() as { events: Event[] };
+ 56       let filteredEvents = data.events || [];
+ 57       if (filter !== 'all') {
+ 58         filteredEvents = filteredEvents.filter(e => e.event_type === filter);
  62       }
+ 60       setEvents(filteredEvents);
  63     } catch (error) {
  64       console.error('Error fetching events:', error);
  65     } finally {
  66       setLoading(false);
  67     }
  68   };
  69
  70   const handleRsvp = async (eventId: string, status: string) => {
  71     try {
- 72       const response = await fetch(`/api/events/${eventId}/rsvp`, {
- 73         method: 'POST',
- 74         headers: { 'Content-Type': 'application/json' },
- 75         body: JSON.stringify({ status })
- 76       });
- 77
- 78       if (response.ok) {
- 79         fetchEvents(); // Refresh list
- 80       } else {
- 81         const error = await response.json();
- 82         alert(error.error || 'Failed to RSVP');
- 83       }
+ 70       await eventsApi.rsvp(eventId, status as 'going' | 'interested' | 'not_going');
+ 71       fetchEvents(); // Refresh list
  84     } catch (error) {
- 85       console.error('Error RSVPing:', error);
+ 73       const message = error instanceof ApiError ? error.message : 'Failed to RSVP';
+ 74       alert(message);
  86     }
  87   };
  88
  89   const handleCancelRsvp = async (eventId: string) => {
  90     try {
- 91       const response = await fetch(`/api/events/${eventId}/rsvp`, {
- 92         method: 'DELETE'
- 93       });
- 94
- 95       if (response.ok) {
- 96         fetchEvents();
- 97       }
+ 80       await eventsApi.cancelRsvp(eventId);
+ 81       fetchEvents();
  98     } catch (error) {
  99       console.error('Error canceling RSVP:', error);
 100     }
 101   };
     ...


import { useState, useEffect } from 'react';

interface Attendee {
  id: string;
  full_name: string;
  avatar_url?: string;
}

interface EventDetailData {
  id: string;
  title: string;
  description: string;
  event_type: 'workshop' | 'call' | 'meetup' | 'ama';
  starts_at: string;
  ends_at: string;
  timezone: string;
  meeting_link?: string;
  max_attendees?: number;
  is_recorded: boolean;
  recording_url?: string;
  attending_count: number;
  maybe_count: number;
  attendees: Attendee[];
  maybe_attendees: Attendee[];
  my_rsvp: string | null;
  is_host: boolean;
  is_admin: boolean;
  spots_left: number | null;
  is_full: boolean;
  host: {
    id: string;
    full_name: string;
    avatar_url?: string;
    role: string;
    bio?: string;
  };
}

interface Props {
  eventId: string;
}

const EVENT_TYPE_ICONS: Record<string, string> = {
  workshop: '🎓',
  call: '📞',
  meetup: '🤝',
  ama: '❓'
};

const EVENT_TYPE_LABELS: Record<string, string> = {
  workshop: 'Workshop',
  call: 'Group Call',
  meetup: 'Meetup',
  ama: 'AMA Session'
};

export default function EventDetail({ eventId }: Props) {
  const [event, setEvent] = useState<EventDetailData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [rsvpLoading, setRsvpLoading] = useState(false);

  useEffect(() => {
    fetchEvent();
  }, [eventId]);

  const fetchEvent = async () => {
    try {
      const response = await fetch(`/api/events/${eventId}`);
      if (response.ok) {
        const data = await response.json();
        setEvent(data);
      } else {
        setError('Event not found');
      }
    } catch (err) {
      setError('Failed to load event');
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (status: string) => {
    setRsvpLoading(true);
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status })
      });

      if (response.ok) {
        fetchEvent();
      } else {
        const error = await response.json();
        alert(error.error || 'Failed to RSVP');
      }
    } catch (error) {
      console.error('Error RSVPing:', error);
    } finally {
      setRsvpLoading(false);
    }
  };

  const handleCancelRsvp = async () => {
    setRsvpLoading(true);
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'DELETE'
      });

      if (response.ok) {
        fetchEvent();
      }
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    } finally {
      setRsvpLoading(false);
    }
  };

  const formatDateTime = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', {
      weekday: 'long',
      month: 'long',
      day: 'numeric',
      year: 'numeric',
      hour: 'numeric',
      minute: '2-digit'
    });
  };

  const getDuration = (start: string, end: string) => {
    const startDate = new Date(start);
    const endDate = new Date(end);
    const diffMs = endDate.getTime() - startDate.getTime();
    const diffMins = Math.round(diffMs / 60000);

    if (diffMins < 60) return `${diffMins} minutes`;

[239 more lines in file. Use offset=141 to continue.]


  const fetchEvent = async () => {
    try {
      const response = await fetch(`/api/events/${eventId}`);
      if (response.ok) {
        const data = await response.json();
        setEvent(data);
      } else {
        setError('Event not found');
      }
    } catch (err) {
      setError('Failed to load event');
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (status: string) => {
    setRsvpLoading(true);
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ status })
      });

      if (response.ok) {
        fetchEvent();
      } else {
        const error = await response.json();
        alert(error.error || 'Failed to RSVP');
      }
    } catch (error) {
      console.error('Error RSVPing:', error);
    } finally {
      setRsvpLoading(false);
    }
  };

  const handleCancelRsvp = async () => {
    setRsvpLoading(true);
    try {
      const response = await fetch(`/api/events/${eventId}/rsvp`, {
        method: 'DELETE'
      });

      if (response.ok) {
        fetchEvent();
      }
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    } finally {
      setRsvpLoading(false);
    }
  };

  const fetchEvent = async () => {
    try {
      const data = await eventsApi.get(eventId) as EventDetailData;
      setEvent(data);
    } catch (err) {
      if (err instanceof ApiError && err.status === 404) {
        setError('Event not found');
      } else {
        setError('Failed to load event');
      }
    } finally {
      setLoading(false);
    }
  };

  const handleRsvp = async (status: string) => {
    setRsvpLoading(true);
    try {
      await eventsApi.rsvp(eventId, status as 'going' | 'interested' | 'not_going');
      fetchEvent();
    } catch (error) {
      const message = error instanceof ApiError ? error.message : 'Failed to RSVP';
      alert(message);
    } finally {
      setRsvpLoading(false);
    }
  };

  const handleCancelRsvp = async () => {
    setRsvpLoading(true);
    try {
      await eventsApi.cancelRsvp(eventId);
      fetchEvent();
    } catch (error) {
      console.error('Error canceling RSVP:', error);
    } finally {
      setRsvpLoading(false);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/events/EventDetail.tsx.

   1 import { useState, useEffect } from 'react';
+  2 import { events as eventsApi, ApiError } from '../../lib/api';
   2
   3 interface Attendee {
   4   id: string;
   5   full_name: string;
     ...


     ...
  66   }, [eventId]);
  67
  68   const fetchEvent = async () => {
  69     try {
- 70       const response = await fetch(`/api/events/${eventId}`);
- 71       if (response.ok) {
- 72         const data = await response.json();
- 73         setEvent(data);
- 74       } else {
+ 70       const data = await eventsApi.get(eventId) as EventDetailData;
+ 71       setEvent(data);
+ 72     } catch (err) {
+ 73       if (err instanceof ApiError && err.status === 404) {
  75         setError('Event not found');
+ 75       } else {
+ 76         setError('Failed to load event');
  76       }
- 77     } catch (err) {
- 78       setError('Failed to load event');
  79     } finally {
  80       setLoading(false);
  81     }
  82   };
  83
  84   const handleRsvp = async (status: string) => {
  85     setRsvpLoading(true);
  86     try {
- 87       const response = await fetch(`/api/events/${eventId}/rsvp`, {
- 88         method: 'POST',
- 89         headers: { 'Content-Type': 'application/json' },
- 90         body: JSON.stringify({ status })
- 91       });
- 92
- 93       if (response.ok) {
- 94         fetchEvent();
- 95       } else {
- 96         const error = await response.json();
- 97         alert(error.error || 'Failed to RSVP');
- 98       }
+ 86       await eventsApi.rsvp(eventId, status as 'going' | 'interested' | 'not_going');
+ 87       fetchEvent();
  99     } catch (error) {
-100       console.error('Error RSVPing:', error);
+ 89       const message = error instanceof ApiError ? error.message : 'Failed to RSVP';
+ 90       alert(message);
 101     } finally {
 102       setRsvpLoading(false);
 103     }
 104   };
 105
 106   const handleCancelRsvp = async () => {
 107     setRsvpLoading(true);
 108     try {
-109       const response = await fetch(`/api/events/${eventId}/rsvp`, {
-110         method: 'DELETE'
-111       });
-112
-113       if (response.ok) {
-114         fetchEvent();
-115       }
+ 99       await eventsApi.cancelRsvp(eventId);
+100       fetchEvent();
 116     } catch (error) {
 117       console.error('Error canceling RSVP:', error);
 118     } finally {
 119       setRsvpLoading(false);
     ...


import { useState, useEffect, useCallback } from 'react';
import type { LinkHub, LinkHubLink, LinkHubTheme, LinkPlatformType } from '../../lib/linkhub';
import { PLATFORM_ICONS, THEME_STYLES, isValidSlug, isValidUrl, detectPlatformFromUrl } from '../../lib/linkhub';

interface LinkHubManagerProps {
  initialHub: LinkHub | null;
  initialLinks: LinkHubLink[];
}

type Tab = 'settings' | 'links' | 'analytics';

const THEMES: { value: LinkHubTheme; label: string }[] = [
  { value: 'default', label: 'Default (Rose Gold)' },
  { value: 'dark', label: 'Dark' },
  { value: 'light', label: 'Light' },
  { value: 'pink', label: 'Pink' },
  { value: 'purple', label: 'Purple' },
  { value: 'gradient_sunset', label: 'Sunset Gradient' },
  { value: 'gradient_ocean', label: 'Ocean Gradient' },
  { value: 'gradient_forest', label: 'Forest Gradient' },
  { value: 'neon', label: 'Neon' },
  { value: 'minimal', label: 'Minimal' },
];

const PLATFORMS: { value: LinkPlatformType; label: string }[] = [
  { value: 'custom', label: 'Custom' },
  { value: 'onlyfans', label: 'OnlyFans' },
  { value: 'fansly', label: 'Fansly' },
  { value: 'patreon', label: 'Patreon' },
  { value: 'twitter', label: 'Twitter/X' },
  { value: 'instagram', label: 'Instagram' },
  { value: 'tiktok', label: 'TikTok' },
  { value: 'youtube', label: 'YouTube' },
  { value: 'twitch', label: 'Twitch' },
  { value: 'snapchat', label: 'Snapchat' },
  { value: 'reddit', label: 'Reddit' },
  { value: 'discord', label: 'Discord' },
  { value: 'telegram', label: 'Telegram' },
  { value: 'cashapp', label: 'Cash App' },
  { value: 'venmo', label: 'Venmo' },
  { value: 'paypal', label: 'PayPal' },
  { value: 'amazon_wishlist', label: 'Amazon Wishlist' },
  { value: 'throne', label: 'Throne' },
];

export default function LinkHubManager({ initialHub, initialLinks }: LinkHubManagerProps) {
  const [activeTab, setActiveTab] = useState<Tab>('settings');
  const [hub, setHub] = useState<LinkHub | null>(initialHub);
  const [links, setLinks] = useState<LinkHubLink[]>(initialLinks);
  const [saving, setSaving] = useState(false);
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
  const [analytics, setAnalytics] = useState<any>(null);
  const [loadingAnalytics, setLoadingAnalytics] = useState(false);

  // Settings form state
  const [formData, setFormData] = useState({
    custom_slug: hub?.custom_slug || '',
    display_name: hub?.display_name || '',
    bio: hub?.bio || '',
    avatar_url: hub?.avatar_url || '',
    theme: hub?.theme || 'default',
    background_url: hub?.background_url || '',
    age_gate_enabled: hub?.age_gate_enabled || false,
    age_gate_message: hub?.age_gate_message || 'You must be 18+ to view this content',
    is_published: hub?.is_published || false,
    meta_title: hub?.meta_title || '',
    meta_description: hub?.meta_description || ''
  });

  // Link editing state
  const [editingLink, setEditingLink] = useState<LinkHubLink | null>(null);
  const [showAddLink, setShowAddLink] = useState(false);
  const [newLink, setNewLink] = useState({
    title: '',
    url: '',
    icon: '',
    platform_type: 'custom' as LinkPlatformType,
    is_visible: true
  });

  // Load analytics when tab changes
  useEffect(() => {
    if (activeTab === 'analytics' && hub && !analytics) {
      loadAnalytics();
    }
  }, [activeTab, hub]);

  const loadAnalytics = async () => {
    setLoadingAnalytics(true);
    try {
      const res = await fetch('/api/linkhub/analytics?days=30');
      const data = await res.json();
      if (data.analytics) {
        setAnalytics(data.analytics);
      }
    } catch (e) {
      console.error('Error loading analytics:', e);
    }
    setLoadingAnalytics(false);
  };

  const showMessage = (type: 'success' | 'error', text: string) => {
    setMessage({ type, text });
    setTimeout(() => setMessage(null), 3000);
  };

  // Save hub settings
  const saveSettings = async () => {
    if (!formData.display_name.trim()) {
      showMessage('error', 'Display name is required');
      return;
    }

    if (!formData.custom_slug.trim() || !isValidSlug(formData.custom_slug)) {
      showMessage('error', 'Invalid URL slug. Use lowercase letters, numbers, hyphens, and underscores.');
      return;
    }

    setSaving(true);
    try {
      const res = await fetch('/api/linkhub', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to save settings');
        return;
      }

      setHub(data.hub);
      showMessage('success', 'Settings saved!');
    } catch (e) {
      showMessage('error', 'Failed to save settings');
    }
    setSaving(false);
  };

[670 more lines in file. Use offset=141 to continue.]


LinkHubManager is large. Let me read a bit more to understand the fetch calls better.


  // Add link
  const addLink = async () => {
    if (!newLink.title.trim() || !newLink.url.trim()) {
      showMessage('error', 'Title and URL are required');
      return;
    }

    if (!isValidUrl(newLink.url)) {
      showMessage('error', 'Please enter a valid URL');
      return;
    }

    setSaving(true);
    try {
      const res = await fetch('/api/linkhub/links', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newLink)
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to add link');
        return;
      }

      setLinks([...links, data.link]);
      setNewLink({ title: '', url: '', icon: '', platform_type: 'custom', is_visible: true });
      setShowAddLink(false);
      showMessage('success', 'Link added!');
    } catch (e) {
      showMessage('error', 'Failed to add link');
    }
    setSaving(false);
  };

  // Update link
  const updateLink = async (link: LinkHubLink) => {
    setSaving(true);
    try {
      const res = await fetch(`/api/linkhub/links/${link.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: link.title,
          url: link.url,
          icon: link.icon,
          platform_type: link.platform_type,
          is_visible: link.is_visible
        })
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to update link');
        return;
      }

      setLinks(links.map(l => l.id === link.id ? data.link : l));
      setEditingLink(null);
      showMessage('success', 'Link updated!');
    } catch (e) {
      showMessage('error', 'Failed to update link');
    }
    setSaving(false);
  };

  // Delete link
  const deleteLink = async (linkId: string) => {
    if (!confirm('Are you sure you want to delete this link?')) return;

    setSaving(true);
    try {
      const res = await fetch(`/api/linkhub/links/${linkId}`, {
        method: 'DELETE'
      });

      if (!res.ok) {
        const data = await res.json();
        showMessage('error', data.error || 'Failed to delete link');
        return;
      }

      setLinks(links.filter(l => l.id !== linkId));
      showMessage('success', 'Link deleted!');
    } catch (e) {
      showMessage('error', 'Failed to delete link');
    }
    setSaving(false);
  };

  // Move link up/down
  const moveLink = async (index: number, direction: 'up' | 'down') => {
    const newIndex = direction === 'up' ? index - 1 : index + 1;
    if (newIndex < 0 || newIndex >= links.length) return;

    const newLinks = [...links];
    [newLinks[index], newLinks[newIndex]] = [newLinks[newIndex], newLinks[index]];

    try {
      const res = await fetch('/api/linkhub/links/reorder', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ link_ids: newLinks.map(l => l.id) })
      });

      if (res.ok) {
        const data = await res.json();
        setLinks(data.links);
      }
    } catch (e) {
      console.error('Reorder failed:', e);
    }
  };

  // Auto-detect platform when URL changes
  const handleUrlChange = (url: string, setter: (data: any) => void, current: any) => {

[550 more lines in file. Use offset=261 to continue.]


Now I have enough context to migrate LinkHubManager. Let me do the edits.

import { useState, useEffect, useCallback } from 'react';
import type { LinkHub, LinkHubLink, LinkHubTheme, LinkPlatformType } from '../../lib/linkhub';
import { PLATFORM_ICONS, THEME_STYLES, isValidSlug, isValidUrl, detectPlatformFromUrl } from '../../lib/linkhub';

import { useState, useEffect, useCallback } from 'react';
import { linkhub as linkhubApi, ApiError } from '../../lib/api';
import type { LinkHub, LinkHubLink, LinkHubTheme, LinkPlatformType } from '../../lib/linkhub';
import { PLATFORM_ICONS, THEME_STYLES, isValidSlug, isValidUrl, detectPlatformFromUrl } from '../../lib/linkhub';

  const loadAnalytics = async () => {
    setLoadingAnalytics(true);
    try {
      const res = await fetch('/api/linkhub/analytics?days=30');
      const data = await res.json();
      if (data.analytics) {
        setAnalytics(data.analytics);
      }
    } catch (e) {
      console.error('Error loading analytics:', e);
    }
    setLoadingAnalytics(false);
  };

  const loadAnalytics = async () => {
    setLoadingAnalytics(true);
    try {
      const data = await linkhubApi.getAnalytics('30d') as { analytics?: unknown };
      if (data.analytics) {
        setAnalytics(data.analytics);
      }
    } catch (e) {
      console.error('Error loading analytics:', e);
    }
    setLoadingAnalytics(false);
  };

    setSaving(true);
    try {
      const res = await fetch('/api/linkhub', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to save settings');
        return;
      }

      setHub(data.hub);
      showMessage('success', 'Settings saved!');
    } catch (e) {
      showMessage('error', 'Failed to save settings');
    }
    setSaving(false);
  };

    setSaving(true);
    try {
      // Note: linkhub.get() is for fetching, we need a custom fetch for PUT
      const url = (import.meta.env.PUBLIC_API_URL || '') + '/api/linkhub';
      const res = await fetch(url, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(formData)
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to save settings');
        return;
      }

      setHub(data.hub);
      showMessage('success', 'Settings saved!');
    } catch (e) {
      showMessage('error', 'Failed to save settings');
    }
    setSaving(false);
  };

  // Add link
  const addLink = async () => {
    if (!newLink.title.trim() || !newLink.url.trim()) {
      showMessage('error', 'Title and URL are required');
      return;
    }

    if (!isValidUrl(newLink.url)) {
      showMessage('error', 'Please enter a valid URL');
      return;
    }

    setSaving(true);
    try {
      const res = await fetch('/api/linkhub/links', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newLink)
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to add link');
        return;
      }

      setLinks([...links, data.link]);
      setNewLink({ title: '', url: '', icon: '', platform_type: 'custom', is_visible: true });
      setShowAddLink(false);
      showMessage('success', 'Link added!');
    } catch (e) {
      showMessage('error', 'Failed to add link');
    }
    setSaving(false);
  };

  // Update link
  const updateLink = async (link: LinkHubLink) => {
    setSaving(true);
    try {
      const res = await fetch(`/api/linkhub/links/${link.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: link.title,
          url: link.url,
          icon: link.icon,
          platform_type: link.platform_type,
          is_visible: link.is_visible
        })
      });

      const data = await res.json();

      if (!res.ok) {
        showMessage('error', data.error || 'Failed to update link');
        return;
      }

      setLinks(links.map(l => l.id === link.id ? data.link : l));
      setEditingLink(null);
      showMessage('success', 'Link updated!');
    } catch (e) {
      showMessage('error', 'Failed to update link');
    }
    setSaving(false);
  };

  // Delete link
  const deleteLink = async (linkId: string) => {
    if (!confirm('Are you sure you want to delete this link?')) return;

    setSaving(true);
    try {
      const res = await fetch(`/api/linkhub/links/${linkId}`, {
        method: 'DELETE'
      });

      if (!res.ok) {
        const data = await res.json();
        showMessage('error', data.error || 'Failed to delete link');
        return;
      }

      setLinks(links.filter(l => l.id !== linkId));
      showMessage('success', 'Link deleted!');
    } catch (e) {
      showMessage('error', 'Failed to delete link');
    }
    setSaving(false);
  };

  // Move link up/down
  const moveLink = async (index: number, direction: 'up' | 'down') => {
    const newIndex = direction === 'up' ? index - 1 : index + 1;
    if (newIndex < 0 || newIndex >= links.length) return;

    const newLinks = [...links];
    [newLinks[index], newLinks[newIndex]] = [newLinks[newIndex], newLinks[index]];

    try {
      const res = await fetch('/api/linkhub/links/reorder', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ link_ids: newLinks.map(l => l.id) })
      });

      if (res.ok) {
        const data = await res.json();
        setLinks(data.links);
      }
    } catch (e) {
      console.error('Reorder failed:', e);
    }
  };

  // Add link
  const addLink = async () => {
    if (!newLink.title.trim() || !newLink.url.trim()) {
      showMessage('error', 'Title and URL are required');
      return;
    }

    if (!isValidUrl(newLink.url)) {
      showMessage('error', 'Please enter a valid URL');
      return;
    }

    setSaving(true);
    try {
      const data = await linkhubApi.createLink(newLink.title, newLink.url, newLink.icon) as { link: LinkHubLink };
      setLinks([...links, data.link]);
      setNewLink({ title: '', url: '', icon: '', platform_type: 'custom', is_visible: true });
      setShowAddLink(false);
      showMessage('success', 'Link added!');
    } catch (e) {
      const message = e instanceof ApiError ? e.message : 'Failed to add link';
      showMessage('error', message);
    }
    setSaving(false);
  };

  // Update link
  const updateLink = async (link: LinkHubLink) => {
    setSaving(true);
    try {
      const data = await linkhubApi.updateLink(link.id, {
        title: link.title,
        url: link.url,
        icon: link.icon,
        platform_type: link.platform_type,
        is_visible: link.is_visible
      }) as { link: LinkHubLink };

      setLinks(links.map(l => l.id === link.id ? data.link : l));
      setEditingLink(null);
      showMessage('success', 'Link updated!');
    } catch (e) {
      const message = e instanceof ApiError ? e.message : 'Failed to update link';
      showMessage('error', message);
    }
    setSaving(false);
  };

  // Delete link
  const deleteLink = async (linkId: string) => {
    if (!confirm('Are you sure you want to delete this link?')) return;

    setSaving(true);
    try {
      await linkhubApi.deleteLink(linkId);
      setLinks(links.filter(l => l.id !== linkId));
      showMessage('success', 'Link deleted!');
    } catch (e) {
      const message = e instanceof ApiError ? e.message : 'Failed to delete link';
      showMessage('error', message);
    }
    setSaving(false);
  };

  // Move link up/down
  const moveLink = async (index: number, direction: 'up' | 'down') => {
    const newIndex = direction === 'up' ? index - 1 : index + 1;
    if (newIndex < 0 || newIndex >= links.length) return;

    const newLinks = [...links];
    [newLinks[index], newLinks[newIndex]] = [newLinks[newIndex], newLinks[index]];

    try {
      const order = newLinks.map((l, i) => ({ id: l.id, order_index: i }));
      const data = await linkhubApi.reorderLinks(order) as { links: LinkHubLink[] };
      setLinks(data.links);
    } catch (e) {
      console.error('Reorder failed:', e);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/linkhub/LinkHubManager.tsx.

   1 import { useState, useEffect, useCallback } from 'react';
+  2 import { linkhub as linkhubApi, ApiError } from '../../lib/api';
   2 import type { LinkHub, LinkHubLink, LinkHubTheme, LinkPlatformType } from '../../lib/linkhub';
   3 import { PLATFORM_ICONS, THEME_STYLES, isValidSlug, isValidUrl, detectPlatformFromUrl } from '../../lib/linkhub';
   4
   5 interface LinkHubManagerProps {
     ...


     ...
  88
  89   const loadAnalytics = async () => {
  90     setLoadingAnalytics(true);
  91     try {
- 92       const res = await fetch('/api/linkhub/analytics?days=30');
- 93       const data = await res.json();
+ 92       const data = await linkhubApi.getAnalytics('30d') as { analytics?: unknown };
  94       if (data.analytics) {
  95         setAnalytics(data.analytics);
  96       }
  97     } catch (e) {
     ...


     ...
 117     }
 118
 119     setSaving(true);
 120     try {
-121       const res = await fetch('/api/linkhub', {
+121       // Note: linkhub.get() is for fetching, we need a custom fetch for PUT
+122       const url = (import.meta.env.PUBLIC_API_URL || '') + '/api/linkhub';
+123       const res = await fetch(url, {
 122         method: 'PUT',
 123         headers: { 'Content-Type': 'application/json' },
+126         credentials: 'include',
 124         body: JSON.stringify(formData)
 125       });
 126
 127       const data = await res.json();
     ...


     ...
 155     }
 156
 157     setSaving(true);
 158     try {
-159       const res = await fetch('/api/linkhub/links', {
-160         method: 'POST',
-161         headers: { 'Content-Type': 'application/json' },
-162         body: JSON.stringify(newLink)
-163       });
-164
-165       const data = await res.json();
-166
-167       if (!res.ok) {
-168         showMessage('error', data.error || 'Failed to add link');
-169         return;
-170       }
-171
+159       const data = await linkhubApi.createLink(newLink.title, newLink.url, newLink.icon) as { link: LinkHubLink };
 172       setLinks([...links, data.link]);
 173       setNewLink({ title: '', url: '', icon: '', platform_type: 'custom', is_visible: true });
 174       setShowAddLink(false);
 175       showMessage('success', 'Link added!');
 176     } catch (e) {
-177       showMessage('error', 'Failed to add link');
+165       const message = e instanceof ApiError ? e.message : 'Failed to add link';
+166       showMessage('error', message);
 178     }
 179     setSaving(false);
 180   };
 181
 182   // Update link
 183   const updateLink = async (link: LinkHubLink) => {
 184     setSaving(true);
 185     try {
-186       const res = await fetch(`/api/linkhub/links/${link.id}`, {
-187         method: 'PUT',
-188         headers: { 'Content-Type': 'application/json' },
-189         body: JSON.stringify({
-190           title: link.title,
-191           url: link.url,
-192           icon: link.icon,
-193           platform_type: link.platform_type,
-194           is_visible: link.is_visible
-195         })
-196       });
+175       const data = await linkhubApi.updateLink(link.id, {
+176         title: link.title,
+177         url: link.url,
+178         icon: link.icon,
+179         platform_type: link.platform_type,
+180         is_visible: link.is_visible
+181       }) as { link: LinkHubLink };
 197
-198       const data = await res.json();
-199
-200       if (!res.ok) {
-201         showMessage('error', data.error || 'Failed to update link');
-202         return;
-203       }
-204
 205       setLinks(links.map(l => l.id === link.id ? data.link : l));
 206       setEditingLink(null);
 207       showMessage('success', 'Link updated!');
 208     } catch (e) {
-209       showMessage('error', 'Failed to update link');
+187       const message = e instanceof ApiError ? e.message : 'Failed to update link';
+188       showMessage('error', message);
 210     }
 211     setSaving(false);
 212   };
 213
 214   // Delete link
 215   const deleteLink = async (linkId: string) => {
 216     if (!confirm('Are you sure you want to delete this link?')) return;
 217
 218     setSaving(true);
 219     try {
-220       const res = await fetch(`/api/linkhub/links/${linkId}`, {
-221         method: 'DELETE'
-222       });
-223
-224       if (!res.ok) {
-225         const data = await res.json();
-226         showMessage('error', data.error || 'Failed to delete link');
-227         return;
-228       }
-229
+199       await linkhubApi.deleteLink(linkId);
 230       setLinks(links.filter(l => l.id !== linkId));
 231       showMessage('success', 'Link deleted!');
 232     } catch (e) {
-233       showMessage('error', 'Failed to delete link');
+203       const message = e instanceof ApiError ? e.message : 'Failed to delete link';
+204       showMessage('error', message);
 234     }
 235     setSaving(false);
 236   };
 237
 238   // Move link up/down
 239   const moveLink = async (index: number, direction: 'up' | 'down') => {
 240     const newIndex = direction === 'up' ? index - 1 : index + 1;
 241     if (newIndex < 0 || newIndex >= links.length) return;
 242
 243     const newLinks = [...links];
 244     [newLinks[index], newLinks[newIndex]] = [newLinks[newIndex], newLinks[index]];
 245
 246     try {
-247       const res = await fetch('/api/linkhub/links/reorder', {
-248         method: 'POST',
-249         headers: { 'Content-Type': 'application/json' },
-250         body: JSON.stringify({ link_ids: newLinks.map(l => l.id) })
-251       });
-252
-253       if (res.ok) {
-254         const data = await res.json();
-255         setLinks(data.links);
-256       }
+218       const order = newLinks.map((l, i) => ({ id: l.id, order_index: i }));
+219       const data = await linkhubApi.reorderLinks(order) as { links: LinkHubLink[] };
+220       setLinks(data.links);
 257     } catch (e) {
 258       console.error('Reorder failed:', e);
 259     }
 260   };
     ...


Good. Now SubscriberGrowthChart and useAnalytics hook.


import { useState, useEffect, useCallback } from 'react';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  Legend
} from 'recharts';

// Platform colors matching the design system
const PLATFORM_COLORS: Record<string, string> = {
  'OnlyFans': '#E8B4B8',     // Rose gold (primary)
  'Fansly': '#93C5FD',       // Light blue
  'Patreon': '#C4B5FD',      // Light purple
  'Gumroad': '#86EFAC',      // Light green
  'Ko-fi': '#FCD34D',        // Yellow
  'All Platforms': '#E8B4B8' // Rose gold for combined
};

type ViewMode = 'daily' | 'weekly' | 'monthly';
type DateRange = '7d' | '30d' | '90d';

interface PlatformData {
  name: string;
  subscribers: number;
  revenue: number;
  engagement: number;
}

interface DataPoint {
  date: string;
  subscribers: number;
  [key: string]: number | string; // Platform-specific subscriber counts
}

interface SubscriberGrowthChartProps {
  range?: DateRange;
  onRangeChange?: (range: DateRange) => void;
}

// Custom tooltip for better detail display
const CustomTooltip = ({ active, payload, label }: any) => {
  if (!active || !payload?.length) return null;

  return (

{label}

      {payload.map((entry: any, index: number) => (

          {entry.name}:

            {entry.value.toLocaleString()}

      ))}

  );
};

export default function SubscriberGrowthChart({
  range: initialRange = '30d',
  onRangeChange
}: SubscriberGrowthChartProps) {
  const [range, setRange] = useState<DateRange>(initialRange);
  const [viewMode, setViewMode] = useState<ViewMode>('daily');
  const [showPerPlatform, setShowPerPlatform] = useState(false);
  const [selectedPlatforms, setSelectedPlatforms] = useState<Set<string>>(new Set(['all']));
  const [data, setData] = useState<DataPoint[]>([]);
  const [platforms, setPlatforms] = useState<PlatformData[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [metrics, setMetrics] = useState({
    totalSubscribers: 0,
    newThisPeriod: 0,
    growthPercentage: 0,
    bestPlatform: ''
  });

  // Listen for date range changes from parent page
  useEffect(() => {
    const handleRangeChange = (event: CustomEvent<{ range: DateRange }>) => {
      setRange(event.detail.range);
    };

    window.addEventListener('dateRangeChange', handleRangeChange as EventListener);
    return () => {
      window.removeEventListener('dateRangeChange', handleRangeChange as EventListener);
    };
  }, []);

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(`/api/analytics?range=${range}`);
      const result = await response.json();

      if (result.subscriberHistory) {
        // Transform to include platform breakdown if available
        const transformedData = result.subscriberHistory.map((item: any) => {
          const point: DataPoint = {
            date: item.date,
            subscribers: item.subscribers
          };

          // Add per-platform data if available
          if (item.platforms) {
            Object.keys(item.platforms).forEach(platform => {
              point[platform] = item.platforms[platform];
            });
          }

          return point;
        });

        setData(transformedData);
      }

      if (result.platformBreakdown) {
        setPlatforms(result.platformBreakdown);

        // Calculate best performing platform
        const bestPlatform = result.platformBreakdown.reduce(
          (best: PlatformData | null, current: PlatformData) => {

[280 more lines in file. Use offset=131 to continue.]


Let me migrate SubscriberGrowthChart and then useAnalytics.

import { useState, useEffect, useCallback } from 'react';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  Legend
} from 'recharts';

import { useState, useEffect, useCallback } from 'react';
import { analytics as analyticsApi } from '../lib/api';
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  Legend
} from 'recharts';

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(`/api/analytics?range=${range}`);
      const result = await response.json();

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const result = await analyticsApi.get(range) as { subscriberHistory?: any[]; platformBreakdown?: PlatformData[]; metrics?: any };


Successfully replaced text in /tmp/contentcollective-app/src/components/SubscriberGrowthChart.tsx.

   1 import { useState, useEffect, useCallback } from 'react';
+  2 import { analytics as analyticsApi } from '../lib/api';
   2 import {
   3   LineChart,
   4   Line,
   5   XAxis,
     ...


     ...
  98
  99   const fetchData = useCallback(async () => {
 100     setLoading(true);
 101     try {
-102       const response = await fetch(`/api/analytics?range=${range}`);
-103       const result = await response.json();
+102       const result = await analyticsApi.get(range) as { subscriberHistory?: any[]; platformBreakdown?: PlatformData[]; metrics?: any };
 104
 105       if (result.subscriberHistory) {
 106         // Transform to include platform breakdown if available
 107         const transformedData = result.subscriberHistory.map((item: any) => {
     ...


Now let me check and migrate the useAnalytics hook.


/**
 * React hooks for cross-platform analytics
 * Use these hooks to fetch aggregated analytics data from the new APIs
 */

import { useState, useEffect, useCallback } from 'react';

// Types matching API responses
export interface OverviewData {
  totals: {
    subscribers: number;
    revenue: number;
    engagementRate: number;
    platforms: number;
  };
  changes: {
    subscribers: { value: number; percent: number };
    revenue: { value: number; percent: number };
    engagementRate: { value: number; percent: number };
  };
  timeSeries: {
    revenue: Array<{ date: string; value: number }>;
    subscribers: Array<{ date: string; value: number }>;
  };
  breakdown: {
    byPlatform: Array<{
      platformId: string;
      platformName: string;
      platformType: string;
      subscribers: number;
      revenue: number;
      engagementRate: number;
      revenueShare: number;
    }>;
    bySource: {
      subscriptions: number;
      tips: number;
      messages: number;
      referrals: number;
    };
  };
  meta: {
    range: string;
    currentPeriod: { start: string; end: string };
    previousPeriod: { start: string; end: string };
    lastSyncAt: string | null;
  };
}

export interface CompareData {
  platforms: Array<{
    platformId: string;
    platformName: string;
    platformType: string;
    current: {
      subscribers: number;
      revenue: number;
      engagementRate: number;
      tips: number;
    };
    previous: {
      subscribers: number;
      revenue: number;
      engagementRate: number;
      tips: number;
    };
    growth: {
      subscribers: { value: number; percent: number };
      revenue: { value: number; percent: number };
      engagementRate: { value: number; percent: number };
    };
    share: {
      subscribers: number;
      revenue: number;
    };
  }>;
  comparison: {
    bestPerformer: {
      platformId: string;
      platformName: string;
      platformType: string;
      metric: string;
      value: number;
      share?: number;
      growthRate?: number;
    } | null;
    fastestGrowing: {
      platformId: string;
      platformName: string;
      platformType: string;
      metric: string;
      value: number;
      growthRate?: number;
    } | null;
    highestEngagement: {
      platformId: string;
      platformName: string;
      platformType: string;
      metric: string;
      value: number;

[258 more lines in file. Use offset=101 to continue.]


Let me read more of the useAnalytics hook to see the fetch calls.


/**
 * Hook to fetch aggregated analytics overview
 * @param options Configuration options
 * @returns Overview data, loading state, error, and refresh function
 */
export function useAnalyticsOverview(options: UseAnalyticsOptions = {}): AnalyticsState<OverviewData> {
  const {
    range: initialRange = '30d',
    autoRefresh = false,
    refreshInterval = 300000 // 5 minutes
  } = options;

  const [range, setRange] = useState<Range>(initialRange);
  const [data, setData] = useState<OverviewData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/analytics/overview?range=${range}`);

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.error || `HTTP ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to fetch analytics';
      setError(message);
      console.error('[useAnalyticsOverview]', message);
    } finally {
      setLoading(false);
    }
  }, [range]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  useEffect(() => {
    if (autoRefresh && refreshInterval > 0) {
      const interval = setInterval(fetchData, refreshInterval);
      return () => clearInterval(interval);
    }
  }, [autoRefresh, refreshInterval, fetchData]);

  return {
    data,
    loading,
    error,
    refresh: fetchData,
    setRange
  };
}

/**
 * Hook to fetch platform comparison data
 * @param options Configuration options
 * @returns Comparison data, loading state, error, and refresh function
 */
export function useAnalyticsCompare(options: UseCompareOptions = {}): AnalyticsState<CompareData> & { setMetric: (metric: Metric) => void } {
  const {
    range: initialRange = '30d',
    metric: initialMetric = 'revenue',
    autoRefresh = false,
    refreshInterval = 300000
  } = options;

  const [range, setRange] = useState<Range>(initialRange);
  const [metric, setMetric] = useState<Metric>(initialMetric);
  const [data, setData] = useState<CompareData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/analytics/compare?range=${range}&metric=${metric}`);

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.error || `HTTP ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to fetch comparison';
      setError(message);
      console.error('[useAnalyticsCompare]', message);
    } finally {
      setLoading(false);
    }
  }, [range, metric]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  useEffect(() => {
    if (autoRefresh && refreshInterval > 0) {
      const interval = setInterval(fetchData, refreshInterval);
      return () => clearInterval(interval);
    }
  }, [autoRefresh, refreshInterval, fetchData]);

  return {
    data,
    loading,
    error,
    refresh: fetchData,
    setRange,

[59 more lines in file. Use offset=300 to continue.]


I can see useAnalyticsOverview and useAnalyticsCompare use raw fetch. Let me migrate them.

/**
 * React hooks for cross-platform analytics
 * Use these hooks to fetch aggregated analytics data from the new APIs
 */

import { useState, useEffect, useCallback } from 'react';
import { analytics as analyticsApi, ApiError } from '../api';

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/analytics/overview?range=${range}`);

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.error || `HTTP ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to fetch analytics';
      setError(message);
      console.error('[useAnalyticsOverview]', message);
    } finally {
      setLoading(false);
    }
  }, [range]);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const result = await analyticsApi.getOverview(range) as OverviewData;
      setData(result);
    } catch (err) {
      const message = err instanceof ApiError ? err.message : 'Failed to fetch analytics';
      setError(message);
      console.error('[useAnalyticsOverview]', message);
    } finally {
      setLoading(false);
    }
  }, [range]);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/analytics/compare?range=${range}&metric=${metric}`);

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.error || `HTTP ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to fetch comparison';
      setError(message);
      console.error('[useAnalyticsCompare]', message);
    } finally {
      setLoading(false);
    }
  }, [range, metric]);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const result = await analyticsApi.compare(range, metric) as CompareData;
      setData(result);
    } catch (err) {
      const message = err instanceof ApiError ? err.message : 'Failed to fetch comparison';
      setError(message);
      console.error('[useAnalyticsCompare]', message);
    } finally {
      setLoading(false);
    }
  }, [range, metric]);


Successfully replaced text in /tmp/contentcollective-app/src/lib/hooks/useAnalytics.ts.

     ...
   3  * Use these hooks to fetch aggregated analytics data from the new APIs
   4  */
   5
   6 import { useState, useEffect, useCallback } from 'react';
+  7 import { analytics as analyticsApi, ApiError } from '../api';
   7
   8 // Types matching API responses
   9 export interface OverviewData {
  10   totals: {
     ...


     ...
 200     try {
 201       setLoading(true);
 202       setError(null);
 203
-204       const response = await fetch(`/api/analytics/overview?range=${range}`);
-205
-206       if (!response.ok) {
-207         const errorData = await response.json().catch(() => ({}));
-208         throw new Error(errorData.error || `HTTP ${response.status}`);
-209       }
-210
-211       const result = await response.json();
+204       const result = await analyticsApi.getOverview(range) as OverviewData;
 212       setData(result);
 213     } catch (err) {
-214       const message = err instanceof Error ? err.message : 'Failed to fetch analytics';
+207       const message = err instanceof ApiError ? err.message : 'Failed to fetch analytics';
 215       setError(message);
 216       console.error('[useAnalyticsOverview]', message);
 217     } finally {
 218       setLoading(false);
     ...


     ...
 255     try {
 256       setLoading(true);
 257       setError(null);
 258
-259       const response = await fetch(`/api/analytics/compare?range=${range}&metric=${metric}`);
-260
-261       if (!response.ok) {
-262         const errorData = await response.json().catch(() => ({}));
-263         throw new Error(errorData.error || `HTTP ${response.status}`);
-264       }
-265
-266       const result = await response.json();
+259       const result = await analyticsApi.compare(range, metric) as CompareData;
 267       setData(result);
 268     } catch (err) {
-269       const message = err instanceof Error ? err.message : 'Failed to fetch comparison';
+262       const message = err instanceof ApiError ? err.message : 'Failed to fetch comparison';
 270       setError(message);
 271       console.error('[useAnalyticsCompare]', message);
 272     } finally {
 273       setLoading(false);
     ...


All the major components have been migrated. Let me verify by checking for any remaining raw fetch calls to /api/ endpoints.

Let me verify all migrations and check for any remaining raw fetch calls:

exec

cd /tmp/contentcollective-app && grep -rn "fetch.*['\"/]api/" src/components src/lib/hooks --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v "lib/api.ts" | grep -v "// Note:" | wc -l


src/components/ProfileForm.tsx:124:      const res = await fetch('/api/avatar', {


Only one remaining - the avatar upload in ProfileForm which uses FormData and needs to stay as raw fetch for now (file upload). Let me update the API base URL prefix for it though.

Only the avatar upload remains (intentionally — FormData file upload). Let me fix its URL:


    }

    setUploading(true);
    setMessage(null);

    try {
      const formData = new FormData();
      formData.append('file', file);

      const res = await fetch('/api/avatar', {
        method: 'POST',
        body: formData,
      });

      if (res.ok) {
        const { avatar_url } = await res.json();
        setProfile(prev => prev ? { ...prev, avatar_url } : null);
        setMessage({ type: 'success', text: 'Avatar updated!' });
      } else {
        const error = await res.json();
        setMessage({ type: 'error', text: error.error || 'Failed to upload avatar' });
      }
    } catch (error) {
      setMessage({ type: 'error', text: 'Failed to upload avatar' });
    } finally {
      setUploading(false);
    }
  };

  const handleSocialLinkChange = (platform: keyof SocialLinks, value: string) => {

[238 more lines in file. Use offset=145 to continue.]


Let me update the avatar upload to use the API base URL so it works in production.

edit

    setUploading(true);
    setMessage(null);

    try {
      const formData = new FormData();
      formData.append('file', file);

      const res = await fetch('/api/avatar', {
        method: 'POST',
        body: formData,
      });

      if (res.ok) {
        const { avatar_url } = await res.json();
        setProfile(prev => prev ? { ...prev, avatar_url } : null);
        setMessage({ type: 'success', text: 'Avatar updated!' });
      } else {
        const error = await res.json();
        setMessage({ type: 'error', text: error.error || 'Failed to upload avatar' });
      }
    } catch (error) {
      setMessage({ type: 'error', text: 'Failed to upload avatar' });
    } finally {
      setUploading(false);
    }
  };

    setUploading(true);
    setMessage(null);

    try {
      const formData = new FormData();
      formData.append('file', file);

      const apiUrl = import.meta.env.PUBLIC_API_URL || '';
      const res = await fetch(`${apiUrl}/api/avatar`, {
        method: 'POST',
        credentials: 'include',
        body: formData,
      });

      if (res.ok) {
        const { avatar_url } = await res.json();
        setProfile(prev => prev ? { ...prev, avatar_url } : null);
        setMessage({ type: 'success', text: 'Avatar updated!' });
      } else {
        const error = await res.json();
        setMessage({ type: 'error', text: error.error || 'Failed to upload avatar' });
      }
    } catch (error) {
      setMessage({ type: 'error', text: 'Failed to upload avatar' });
    } finally {
      setUploading(false);
    }
  };


Successfully replaced text in /tmp/contentcollective-app/src/components/ProfileForm.tsx.

     ...
 120     try {
 121       const formData = new FormData();
 122       formData.append('file', file);
 123
-124       const res = await fetch('/api/avatar', {
+124       const apiUrl = import.meta.env.PUBLIC_API_URL || '';
+125       const res = await fetch(`${apiUrl}/api/avatar`, {
 125         method: 'POST',
+127         credentials: 'include',
 126         body: formData,
 127       });
 128
 129       if (res.ok) {
     ...


Now let me commit all these changes and push to GitHub.

Now let me commit and push all the migrations:

exec

cd /tmp/contentcollective-app && git add -A && git status


On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   src/components/MemberDirectory.tsx
	modified:   src/components/PlatformConnect.tsx
	modified:   src/components/ProfileForm.tsx
	modified:   src/components/SubscriberGrowthChart.tsx
	modified:   src/components/education/CourseCatalog.tsx
	modified:   src/components/education/CourseDetail.tsx
	modified:   src/components/education/ModuleViewer.tsx
	modified:   src/components/events/EventDetail.tsx
	modified:   src/components/events/EventList.tsx
	modified:   src/components/linkhub/LinkHubManager.tsx
	modified:   src/components/messages/ConversationsList.tsx
	modified:   src/components/messages/MessageRequests.tsx
	modified:   src/components/messages/MessagesContainer.tsx
	modified:   src/components/messages/NewMessageModal.tsx
	modified:   src/lib/hooks/useAnalytics.ts


cd /tmp/contentcollective-app && git commit -m "Migrate all components to centralized API client

Components migrated:
- MemberDirectory: directory.list(), connections.send()
- PlatformConnect: platforms.list/disconnect/sync()
- ProfileForm: profile.get/update()
- SubscriberGrowthChart: analytics.get()
- CourseCatalog: courses.list()
- CourseDetail: courses.get/enroll()
- ModuleViewer: courses.get(), modules.complete()
- EventList/EventDetail: events.list/get/rsvp/cancelRsvp()
- LinkHubManager: linkhub.* methods
- ConversationsList: messages.listConversations()
- MessageRequests: messages.getRequests/acceptRequest/declineRequest()
- MessagesContainer: messages.getMessages/getRequests()
- NewMessageModal: connections.list(), messages.startConversation()
- useAnalytics hook: analytics.getOverview/compare()

All components now use the centralized API client with proper
error handling via ApiError class. Avatar upload kept as raw
fetch due to FormData requirements." && git push origin main


[main 0a02072] Migrate all components to centralized API client
 Committer: Brandon Wilson <brandon@Brandons-Mac-mini.local>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 15 files changed, 165 insertions(+), 327 deletions(-)
To https://github.com/bcwilsondotcom/contentcollective-app.git
   fe5babb..0a02072  main -> main


Excellent! All component migrations committed and pushed. Let me update the progress file and give Brandon a summary.

edit

# ContentCollective Progress

## Current Status

# ContentCollective Progress

## Last Updated: 2026-02-04 01:00 MST

## Current Status

exec

cat >> /tmp/contentcollective-work/PROGRESS.md << 'EOF'

---

## Session 2026-02-04 (continued)

### Infrastructure Setup
- ✅ Installed Supabase CLI and Vercel CLI via Homebrew
- ✅ Linked Supabase project (ref: fbvorjoougfitkvpljve)
- ✅ Reset database and applied migrations (28 tables created)
- ✅ RLS policies and seed data applied
- ✅ Vercel env vars configured:
  - API: SUPABASE_SERVICE_ROLE_KEY, CRON_SECRET
  - App: PUBLIC_API_URL

### API Verification
- ✅ API live at api.contentcollective.net
- ✅ Serving real data from Supabase (7 community spaces returned)

### Component Migrations (15 files, -162 lines)
All components now use centralized API client:
- MemberDirectory.tsx
- PlatformConnect.tsx
- ProfileForm.tsx
- SubscriberGrowthChart.tsx
- CourseCatalog.tsx
- CourseDetail.tsx
- ModuleViewer.tsx
- EventList.tsx
- EventDetail.tsx
- LinkHubManager.tsx
- ConversationsList.tsx
- MessageRequests.tsx
- MessagesContainer.tsx
- NewMessageModal.tsx
- useAnalytics.ts hook

### Commits
- API: `3bbfb4b` - Add Supabase migrations and seed data
- App: `0a02072` - Migrate all components to centralized API client

### Remaining Work
1. Remove internal API routes from app repo (#86, #87)
2. Remove mock data fallbacks (#88)
3. Test auth flow end-to-end
4. Deploy to staging and verify
EOF


Could not find the exact text in /tmp/contentcollective-work/PROGRESS.md. The old text must match exactly including all whitespace and newlines.


(no output)


The progress file was updated. Let me give Brandon a summary of what was done.

**Done this session:**

**Infrastructure:**
- ✅ Supabase + Vercel CLIs installed
- ✅ Database reset with full schema (28 tables)
- ✅ Migrations + RLS + seed data applied
- ✅ Vercel env vars set
- ✅ API live and serving from Supabase

**Component Migrations (15 files):**
- Messages: ConversationsList, MessageRequests, MessagesContainer, NewMessageModal
- Education: CourseCatalog, CourseDetail, ModuleViewer
- Events: EventList, EventDetail
- Other: MemberDirectory, PlatformConnect, ProfileForm, LinkHubManager, SubscriberGrowthChart
- Hook: useAnalytics

**Commits pushed:**
- API: `3bbfb4b` — migrations
- App: `0a02072` — component migrations

**Remaining:**
1. Remove internal API routes from app (#86, #87)
2. Remove mock data fallbacks (#88)
3. Test auth flow
4. Deploy to staging

Want me to continue with removing the internal routes, or stop here for the night?


[Signal Brandon id:+12817509483 +4m 2026-02-04 01:03 MST] ensure all changes are merged into main branches of repos, via PRs, old branches are deleted as PRs get merged, and that code and functionality works after it's been deployed into prod (there is no stg right now, only prod)
[message_id: 1770192225177]


Brandon wants me to:
1. Ensure all changes are in main branches via PRs
2. Delete old branches after PRs merge
3. Verify code works in production after deployment

Let me check the current state of all repos - are there any open PRs or branches that need to be merged?

cd /tmp/contentcollective-api && git branch -a && echo "---" && gh pr list --state all --limit 10

cd /tmp/contentcollective-app && git branch -a && echo "---" && gh pr list --state all --limit 10


* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
---
6	feat: Admin endpoints for invites, waitlist, users	feature/3-admin-endpoints	MERGED	2026-02-02T04:22:17Z