89	OPEN	[Architecture] Configure API base URL environment variable		2026-02-02T03:52:58Z
88	OPEN	[Architecture] Remove all fake/mock data - Real data from API only		2026-02-02T03:52:57Z
87	OPEN	[Architecture] Remove internal API routes - All calls to api.contentcollective.net		2026-02-02T03:52:56Z
86	OPEN	Remove API routes after migration to standalone API		2026-02-02T01:35:43Z
79	OPEN	[Events] Event reminders	enhancement, priority: should, type: fullstack	2026-02-02T00:40:29Z
78	CLOSED	[Events] RSVP functionality	enhancement, priority: must, type: fullstack	2026-02-02T01:02:06Z
77	CLOSED	[Events] Event detail page	enhancement, priority: must, type: frontend	2026-02-02T01:02:04Z
76	CLOSED	[Events] Events calendar	enhancement, priority: must, type: frontend	2026-02-02T01:02:03Z
75	CLOSED	[Education] Course enrollment	enhancement, priority: must, type: fullstack	2026-02-02T01:02:01Z
74	CLOSED	[Education] Progress tracking UI	enhancement, priority: should, type: fullstack	2026-02-02T01:02:00Z
73	CLOSED	[Education] Module viewer (video/text)	enhancement, priority: must, type: frontend	2026-02-02T01:01:59Z
72	CLOSED	[Education] Course detail page	enhancement, priority: must, type: frontend	2026-02-02T01:01:57Z
71	CLOSED	[Education] Course catalog page	enhancement, priority: must, type: frontend	2026-02-02T01:01:56Z
70	OPEN	[DM System] Block and report functionality	enhancement, priority: must, type: fullstack	2026-02-02T00:39:59Z
69	CLOSED	[DM System] Message requests inbox	enhancement, priority: should, type: fullstack	2026-02-02T01:01:54Z
68	CLOSED	[DM System] Message thread view	enhancement, priority: must, type: fullstack	2026-02-02T01:01:53Z
67	CLOSED	[DM System] Conversation list	enhancement, priority: must, type: frontend	2026-02-02T01:01:52Z
66	CLOSED	[Member Directory] Connection request button	enhancement, priority: should, type: fullstack	2026-02-02T01:01:50Z
65	CLOSED	[Member Directory] Profile cards	enhancement, priority: must, type: frontend	2026-02-02T01:01:49Z
64	CLOSED	[Member Directory] Search and filter members	enhancement, priority: must, type: fullstack	2026-02-02T01:01:47Z
63	CLOSED	[Link Hub] Click analytics	enhancement, priority: should, type: fullstack	2026-02-02T01:01:46Z
62	CLOSED	[Link Hub] Theme customization	enhancement, priority: should, type: frontend	2026-02-02T01:01:45Z
61	CLOSED	[Link Hub] Age gate toggle	enhancement, priority: could, type: fullstack	2026-02-02T01:01:43Z
60	CLOSED	[Link Hub] Platform links editor	enhancement, priority: must, type: frontend	2026-02-02T01:01:42Z
59	CLOSED	[Link Hub] Custom URL (contentcollective.net/c/username)	enhancement, priority: must, type: fullstack	2026-02-02T01:01:40Z
58	CLOSED	[Onboarding] Join first space prompt	enhancement, priority: should, type: fullstack	2026-02-02T01:01:39Z
57	CLOSED	[Onboarding] Feature tour	enhancement, priority: should, type: frontend	2026-02-02T01:01:37Z
56	CLOSED	[Onboarding] Community guidelines acknowledgment	enhancement, priority: must, type: fullstack	2026-02-02T01:01:36Z
55	CLOSED	[Onboarding] Goal setting	enhancement, priority: should, type: fullstack	2026-02-02T01:01:34Z
54	CLOSED	[Onboarding] Profile completion wizard	enhancement, priority: must, type: fullstack	2026-02-02T01:01:33Z


5	OPEN	[Architecture] API documentation - Document all endpoints		2026-02-02T03:53:28Z
4	OPEN	[Architecture] Verify all app endpoints work - Test against Supabase		2026-02-02T03:53:27Z
3	CLOSED	[Architecture] Admin endpoints - Waitlist, user management, invite CRUD		2026-02-02T04:43:18Z
2	OPEN	Migrate API from app repo to standalone API service		2026-02-02T01:35:42Z
1	OPEN	Build waitlist signup endpoint		2026-02-01T19:35:03Z


Excellent! Now I have a very clear picture. Let me also check for more open issues to see all the remaining work.

exec

gh issue list --repo bcwilsondotcom/contentcollective-app --state open --limit 100 2>/dev/null | cat


89	OPEN	[Architecture] Configure API base URL environment variable		2026-02-02T03:52:58Z
88	OPEN	[Architecture] Remove all fake/mock data - Real data from API only		2026-02-02T03:52:57Z
87	OPEN	[Architecture] Remove internal API routes - All calls to api.contentcollective.net		2026-02-02T03:52:56Z
86	OPEN	Remove API routes after migration to standalone API		2026-02-02T01:35:43Z
79	OPEN	[Events] Event reminders	enhancement, priority: should, type: fullstack	2026-02-02T00:40:29Z
70	OPEN	[DM System] Block and report functionality	enhancement, priority: must, type: fullstack	2026-02-02T00:39:59Z
34	OPEN	🧑🏫 Mentors: Mentor Dashboard	priority: should, type: frontend	2026-02-01T23:53:26Z
33	OPEN	🧑🏫 Mentors: Session Scheduling	priority: should, type: fullstack	2026-02-01T23:53:25Z
31	OPEN	💬 Community: Comments and Reactions	priority: should, type: fullstack	2026-02-01T23:53:07Z
30	OPEN	💬 Community: Post Creation	priority: should, type: frontend	2026-02-01T23:53:06Z
28	OPEN	📚 Education: Course Completion Certificates	priority: should, type: fullstack	2026-02-01T23:52:50Z
27	OPEN	📚 Education: Progress Tracking	priority: should, type: fullstack	2026-02-01T23:52:49Z
26	OPEN	📚 Education: Video/Content Hosting	priority: should, type: backend	2026-02-01T23:52:48Z
10	OPEN	Create profiles table with role support		2026-02-01T09:20:10Z
9	OPEN	QA: Full app testing and bug fixes		2026-02-01T08:24:22Z
4	OPEN	Build platform connection UI and OAuth flows		2026-02-01T08:36:40Z
2	OPEN	Implement working auth flow with Supabase SSR		2026-02-01T08:23:52Z


Now I have a complete picture. Let me compile the audit report. Here's what I've found:

**REPOS:**
1. contentcollective-api - Standalone Hono API (4,042 lines of routes code)
2. contentcollective-app - Astro + React frontend (8,000+ lines of components)
3. contentcollective-marketing - Marketing/landing pages
4. contentcollective-admin - Admin portal
5. contentcollective-design - Design system/component library

**WHAT'S BUILT (appears complete):**
- API routes for: auth, profile, directory, connections, messages, community, events, courses, modules, analytics, activity, linkhub, platforms, admin, cron
- Frontend pages for: dashboard, directory, messages, community, events, education, analytics, linkhub, settings, mentor dashboard
- Admin portal: waitlist management, invite management, login/auth
- Design system with Tailwind preset and components

**WHAT'S NOT ACTUALLY WORKING:**
1. Mock data fallbacks everywhere - no real Supabase schema exists
2. Platform integrations (OnlyFans, Fansly) are stubs - require browser extension that doesn't exist
3. OAuth for Twitter/Instagram/TikTok - endpoints exist but no real OAuth configured
4. Email sending - TODOs for "send email via email service"
5. File uploads (avatars, media) - likely just placeholders

**OPEN ISSUES (17 total):**
Architecture:
- #89: Configure API base URL env variable
- #88: Remove all fake/mock data
- #87: Remove internal API routes (migrate to api.contentcollective.net)
- #86: Remove API routes after migration
- #2: Migrate API from app repo
- #4: API documentation
- #1: Waitlist signup endpoint

Features:
- #79: Event reminders
- #70: Block and report functionality (DMs)

- #34: Mentor Dashboard
- #33: Session Scheduling
- #31: Community comments and reactions
- #30: Post creation
- #28: Course completion certificates
- #27: Progress tracking
- #26: Video/content hosting
- #10: Create profiles table with role support
- #9: QA - Full app testing
- #4: Platform connection UI and OAuth
- #2: Working auth flow with Supabase SSR

**THE BIG GAPS:**
1. **No Supabase schema** - The app has SQL examples in the README but no actual migrations/schema file
2. **No real auth** - Still needs Supabase SSR implementation to work properly
3. **Mock data everywhere** - Real API calls won't work without the database
4. **Platform integrations are stubs** - Need a browser extension for OF/Fansly, OAuth config for social platforms
5. **No content hosting** - Courses reference video modules but no actual video storage
6. **Missing email service** - Admin notifications, password reset, etc.

This is a solid prototype with the UI layer complete, but the backend and data infrastructure still need significant work before it's production-ready. I should share this assessment with Brandon.

Alright, here's the full audit:

---

## ContentCollective Status Report

### What Exists (5 Repos)

| Repo | Purpose | LOC |
|------|---------|-----|
| `contentcollective-api` | Hono + Bun backend | 4,042 lines (routes) |
| `contentcollective-app` | Astro + React frontend | 8,000+ lines (components) |
| `contentcollective-marketing` | Landing/marketing site | Complete |
| `contentcollective-admin` | Admin portal | Functional |
| `contentcollective-design` | Tailwind preset + components | Usable |

---

### What's Built (UI exists, routes exist)

**✅ Core Features:**
- Auth (signin/signup/signout/invite codes)
- Profile management
- Member directory with search/filter
- Connection requests
- Direct messages with threads
- Community spaces + posts
- Events with RSVP
- Courses + modules (education)
- Analytics dashboard
- LinkHub (link-in-bio)
- Platform connections (UI exists)
- Admin: waitlist, invites, user management

---

### What's NOT Working

**🔴 Critical Gaps:**

1. **No Supabase schema deployed** — SQL examples in README, but no migrations. The database doesn't exist.

2. **Mock data fallbacks everywhere** — Every route has `if (!isSupabaseConfigured()) return mockData`. Without the DB, it's all fake.

3. **Auth isn't wired up** — Issue #2: "Implement working auth flow with Supabase SSR" is still open.

4. **Platform integrations are stubs:**
   - OnlyFans/Fansly adapters say "requires browser extension" — extension doesn't exist
   - Twitter/Instagram/TikTok OAuth URLs are placeholders

5. **No email service** — Admin routes have `// TODO: Send email via email service`

6. **No content hosting** — Course modules reference videos but no storage configured

---

### Open Issues (17)

**Architecture:**
- #89: Configure API base URL env var
- #88: Remove all mock data
- #87: Remove internal API routes (use api.contentcollective.net)
- #86: Clean up after API migration
- #5: API documentation

**Must-Have Features:**
- #70: Block/report functionality (DMs)
- #10: Create profiles table with roles
- #2: Working auth with Supabase SSR
- #4: Platform OAuth flows

**Should-Have:**
- #79: Event reminders
- #34: Mentor dashboard
- #33: Session scheduling
- #31: Community comments/reactions
- #28: Course completion certificates
- #27: Progress tracking
- #26: Video/content hosting

---

### Bottom Line

**This is a well-structured prototype with comprehensive UI — but the backend/data layer is incomplete.**

The code is clean, the architecture is good, but to actually launch:

1. Deploy Supabase schema (tables, RLS policies, functions)
2. Wire up real auth
3. Remove all mock data fallbacks
4. Configure env vars for production
5. Set up email service (Resend, Postmark, etc.)
6. Decide what to do about platform integrations (browser extension vs OAuth vs manual import)

---

Want me to start on any of these? I'd suggest beginning with the Supabase schema — nothing else works without it.


[Signal Brandon id:+12817509483 +1m 2026-02-04 00:14 MST] do it.
[message_id: 1770189292923]


Brandon wants me to start working on the ContentCollective project. Based on my audit, the most critical thing is the Supabase schema - nothing else works without it.

Let me spawn a sub-agent to work on this while I keep Brandon updated. This is a good use case for sessions_spawn since it's a discrete, well-scoped task.

Actually, let me think about the approach:

1. First, I need to create the full Supabase schema based on what the API routes expect
2. Then wire up auth
3. Then remove mock data
4. etc.

Since Brandon said "do it" and wants me to work through the night, I should:
1. Start with the schema work myself (or spawn a sub-agent)
2. Set up a state file to track progress
3. Maybe set up a cron to continue work periodically

Let me start by creating a comprehensive Supabase schema. I'll work on this directly first, then spawn sub-agents for parallel tasks once the foundation is laid.

Let me:
1. Analyze all the API routes to understand the exact table/column requirements
2. Create a comprehensive SQL migration file
3. Include RLS policies for security
4. Include indexes for performance

This is going to be substantial work. Let me get started.

On it. Starting with the Supabase schema — I'll work through this systematically.

mkdir -p /tmp/contentcollective-work && cd /tmp/contentcollective-work && echo "# ContentCollective Build Progress\n\nStarted: $(date)\n\n## Current Task: Supabase Schema\n\nStatus: In Progress" > PROGRESS.md

cd /tmp/contentcollective-api && grep -h "from\|\.from(" src/routes/*.ts | grep -oE "from\(['\"][^'\"]+['\"]" | sort -u


(no output)


from("activity_feed"
from("analytics_daily"
from("blocks"
from("community_posts"
from("community_spaces"
from("connections"
from("course_enrollments"
from("course_modules"
from("course_reviews"
from("courses"
from("event_rsvps"
from("events"
from("invites"
from("linkhub_clicks"
from("linkhub_links"
from("linkhub_views"
from("message_requests"
from("message_threads"
from("messages"
from("module_progress"
from("notifications"
from("platform_connections"
from("post_comments"
from("post_likes"
from("profiles"
from("revenue_transactions"
from("space_members"
from("waitlist"


Good - I have a list of all the tables needed. Now let me analyze each API route file to understand the column structure for each table.

cd /tmp/contentcollective-api && cat src/routes/profile.ts

cd /tmp/contentcollective-api && cat src/routes/auth.ts

cd /tmp/contentcollective-api && cat src/routes/admin.ts


import { Hono } from "hono";
import { json } from "../lib/response";
import {
  createSupabase,
  getAuthUser,
  isSupabaseConfigured,
} from "../lib/supabase";

export const profileRoutes = new Hono();

// Mock profile for development
const mockProfile = {
  id: "mock-user-id",
  email: "demo@example.com",
  full_name: "Demo Creator",
  role: "creator",
  avatar_url: null,
  bio: "Content creator passionate about connecting with fans.",
  category: "lifestyle",
  social_links: {
    twitter: "https://twitter.com/democreator",
    instagram: "https://instagram.com/democreator",
    linktree: "https://linktr.ee/democreator",
  },
  onboarding_completed: true,
};

// GET /api/profile
profileRoutes.get("/", async (c) => {
  const auth = await getAuthUser(c);

  if (!auth) {
    if (!isSupabaseConfigured()) {
      return json.success(c, mockProfile);
    }
    return json.unauthorized(c);
  }

  const { data: profile, error } = await auth.supabase
    .from("profiles")
    .select("*")
    .eq("id", auth.user.id)
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, profile);
});

// PATCH /api/profile
profileRoutes.patch("/", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const updates = await c.req.json();

  // Validate allowed fields
  const allowedFields = [
    "full_name",
    "username",
    "bio",
    "avatar_url",
    "category",
    "creator_type",
    "social_links",
    "onboarding_completed",
    "is_public",
    "open_to_messages",
    "notification_preferences",
  ];

  const filteredUpdates: Record<string, unknown> = {};
  for (const key of Object.keys(updates)) {
    if (allowedFields.includes(key)) {
      filteredUpdates[key] = updates[key];
    }
  }

  if (Object.keys(filteredUpdates).length === 0) {
    return json.error(c, "No valid fields to update");
  }

  // Check username uniqueness if updating
  if (filteredUpdates.username) {
    const { data: existing } = await auth.supabase
      .from("profiles")
      .select("id")
      .eq("username", filteredUpdates.username)
      .neq("id", auth.user.id)
      .single();

    if (existing) {
      return json.conflict(c, "Username is already taken");
    }
  }

  filteredUpdates.updated_at = new Date().toISOString();

  const { data, error } = await auth.supabase
    .from("profiles")
    .update(filteredUpdates)
    .eq("id", auth.user.id)
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, data);
});

// POST /api/avatar (file upload would need different handling)
profileRoutes.post("/avatar", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  // For now, accept a URL directly
  const body = await c.req.json();
  const { avatar_url } = body;

  if (!avatar_url) {
    return json.error(c, "Avatar URL is required");
  }

  const { data, error } = await auth.supabase
    .from("profiles")
    .update({ avatar_url, updated_at: new Date().toISOString() })
    .eq("id", auth.user.id)
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { avatar_url: data.avatar_url });
});


import { Hono } from "hono";
import { deleteCookie, setCookie } from "hono/cookie";
import { json } from "../lib/response";
import { createSupabase, isSupabaseConfigured } from "../lib/supabase";

export const authRoutes = new Hono();

// POST /api/auth/signin
authRoutes.post("/signin", async (c) => {
  const body = await c.req.json();
  const { email, password } = body;

  if (!email || !password) {
    return json.error(c, "Email and password are required");
  }

  const supabase = createSupabase();
  if (!supabase) {
    return json.serverError(c, "Server configuration error");
  }

  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    let errorMessage = "Invalid email or password";
    if (error.message.includes("Email not confirmed")) {
      errorMessage = "Please check your email to confirm your account";
    } else if (error.message.includes("Too many requests")) {
      errorMessage = "Too many attempts. Please try again later.";
    }
    return c.json({ error: errorMessage }, 401);
  }

  // Set auth cookies
  if (data.session) {
    const isProduction = process.env.NODE_ENV === "production";
    const cookieOptions = {
      path: "/",
      httpOnly: true,
      secure: isProduction,
      sameSite: "lax" as const,
      maxAge: 60 * 60 * 24 * 7, // 1 week
    };

    setCookie(c, "sb-access-token", data.session.access_token, cookieOptions);
    setCookie(c, "sb-refresh-token", data.session.refresh_token, cookieOptions);
  }

  return json.success(c, {
    user: {
      id: data.user.id,
      email: data.user.email,
      role: data.user.user_metadata?.role || "creator",
    },
  });
});

// POST /api/auth/signup
authRoutes.post("/signup", async (c) => {
  const body = await c.req.json();
  const { email, password, invite_code } = body;

  if (!email || !password) {
    return json.error(c, "Email and password are required");
  }

  const supabase = createSupabase();
  if (!supabase) {
    return json.serverError(c, "Server configuration error");
  }

  // Validate invite code if provided
  if (invite_code) {
    const { data: invite } = await supabase
      .from("invites")
      .select("*")
      .eq("code", invite_code)
      .eq("used", false)
      .single();

    if (!invite) {
      return json.error(c, "Invalid or expired invite code");
    }
  }

  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        invite_code: invite_code || null,
      },
    },
  });

  if (error) {
    let errorMessage = "Failed to create account";
    if (error.message.includes("already registered")) {
      errorMessage = "An account with this email already exists";
    }
    return json.error(c, errorMessage);
  }

  // Mark invite as used
  if (invite_code && data.user) {
    await supabase
      .from("invites")
      .update({
        used: true,
        used_by: data.user.id,
        used_at: new Date().toISOString(),
      })
      .eq("code", invite_code);
  }

  return json.created(c, {
    message: "Account created. Please check your email to confirm.",
    user: data.user ? { id: data.user.id, email: data.user.email } : null,
  });
});

// POST /api/auth/signout
authRoutes.post("/signout", async (c) => {
  const supabase = createSupabase();
  if (supabase) {
    await supabase.auth.signOut();
  }

  deleteCookie(c, "sb-access-token", { path: "/" });
  deleteCookie(c, "sb-refresh-token", { path: "/" });

  return json.success(c, { message: "Signed out successfully" });
});

// GET /api/auth/session
authRoutes.get("/session", async (c) => {
  const { getCookie } = await import("hono/cookie");
  const accessToken = getCookie(c, "sb-access-token");

  if (!accessToken) {
    return json.success(c, { user: null });
  }

  const supabase = createSupabase();
  if (!supabase) {
    return json.success(c, { user: null });
  }

  const {
    data: { user },
    error,
  } = await supabase.auth.getUser(accessToken);

  if (error || !user) {
    return json.success(c, { user: null });
  }

  return json.success(c, {
    user: {
      id: user.id,
      email: user.email,
      role: user.user_metadata?.role || "creator",
    },
  });
});

// POST /api/auth/validate-invite
authRoutes.post("/validate-invite", async (c) => {
  const body = await c.req.json();
  const { code } = body;

  if (!code) {
    return json.error(c, "Invite code is required");
  }

  const supabase = createSupabase();
  if (!supabase) {
    // Mock mode - accept any code
    return json.success(c, { valid: true });
  }

  const { data: invite } = await supabase
    .from("invites")
    .select("id, code, expires_at")
    .eq("code", code)
    .eq("used", false)
    .single();

  if (!invite) {
    return json.error(c, "Invalid or expired invite code");
  }

  if (invite.expires_at && new Date(invite.expires_at) < new Date()) {
    return json.error(c, "Invite code has expired");
  }

  return json.success(c, { valid: true });
});


import type { Context } from "hono";
import { Hono } from "hono";
import { json } from "../lib/response";
import { createAdminSupabase, getAuthUser } from "../lib/supabase";

export const adminRoutes = new Hono();

// Helper to check admin role and return admin supabase client
async function requireAdmin(c: Context) {
  const auth = await getAuthUser(c);
  if (!auth) {
    return null;
  }

  // Check if user has admin role
  const { data: profile } = await auth.supabase
    .from("profiles")
    .select("role")
    .eq("id", auth.user.id)
    .single();

  if (profile?.role !== "admin") {
    return null;
  }

  // Return admin client for privileged operations
  const adminSupabase = createAdminSupabase();
  if (!adminSupabase) {
    return null;
  }

  return { ...auth, adminSupabase };
}

// ============================================
// INVITE MANAGEMENT
// ============================================

// GET /api/admin/invites - List all invites
adminRoutes.get("/invites", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const url = new URL(c.req.url);
  const limit = Number.parseInt(url.searchParams.get("limit") || "50");
  const offset = Number.parseInt(url.searchParams.get("offset") || "0");
  const status = url.searchParams.get("status"); // used, unused, expired

  let query = auth.adminSupabase
    .from("invites")
    .select(
      "*, created_by:profiles!invites_created_by_fkey(id, full_name, email)",
      { count: "exact" },
    )
    .order("created_at", { ascending: false })
    .range(offset, offset + limit - 1);

  if (status === "used") {
    query = query.eq("used", true);
  } else if (status === "unused") {
    query = query.eq("used", false);
  } else if (status === "expired") {
    query = query.eq("used", false).lt("expires_at", new Date().toISOString());
  }

  const { data: invites, error, count } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { invites, total: count });
});

// POST /api/admin/invites - Create new invite(s)
adminRoutes.post("/invites", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const body = await c.req.json();
  const { count = 1, expires_in_days = 7, note } = body;

  if (count < 1 || count > 100) {
    return json.error(c, "Count must be between 1 and 100");
  }

  const invites = [];
  const expiresAt = new Date();
  expiresAt.setDate(expiresAt.getDate() + expires_in_days);

  for (let i = 0; i < count; i++) {
    const code = generateInviteCode();
    invites.push({
      code,
      created_by: auth.user.id,
      expires_at: expiresAt.toISOString(),
      note: note || null,
      used: false,
    });
  }

  const { data, error } = await auth.adminSupabase
    .from("invites")
    .insert(invites)
    .select();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.created(c, { invites: data });
});

// GET /api/admin/invites/:id - Get single invite
adminRoutes.get("/invites/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  const { data: invite, error } = await auth.adminSupabase
    .from("invites")
    .select(
      "*, created_by:profiles!invites_created_by_fkey(id, full_name, email), used_by:profiles!invites_used_by_fkey(id, full_name, email)",
    )
    .eq("id", id)
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "Invite not found");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { invite });
});

// PUT /api/admin/invites/:id - Update invite
adminRoutes.put("/invites/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");
  const body = await c.req.json();

  // Only allow updating note and expires_at on unused invites
  const updates: Record<string, unknown> = {};
  if (body.note !== undefined) updates.note = body.note;
  if (body.expires_at !== undefined) updates.expires_at = body.expires_at;
  if (body.expires_in_days !== undefined) {
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + body.expires_in_days);
    updates.expires_at = expiresAt.toISOString();
  }

  if (Object.keys(updates).length === 0) {
    return json.error(c, "No valid fields to update");
  }

  const { data: invite, error } = await auth.adminSupabase
    .from("invites")
    .update(updates)
    .eq("id", id)
    .eq("used", false) // Can only update unused invites
    .select()
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "Invite not found or already used");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { invite });
});

// DELETE /api/admin/invites/:id - Delete invite
adminRoutes.delete("/invites/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  const { error } = await auth.adminSupabase
    .from("invites")
    .delete()
    .eq("id", id)
    .eq("used", false); // Can only delete unused invites

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { message: "Invite deleted" });
});

// POST /api/admin/invites/:id/resend - Resend invite email
adminRoutes.post("/invites/:id/resend", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  // Get the invite with associated waitlist entry
  const { data: invite, error: fetchError } = await auth.adminSupabase
    .from("invites")
    .select("*, waitlist:waitlist_id(*)")
    .eq("id", id)
    .single();

  if (fetchError) {
    if (fetchError.code === "PGRST116") {
      return json.notFound(c, "Invite not found");
    }
    return json.serverError(c, fetchError.message);
  }

  if (invite.used) {
    return json.error(c, "Cannot resend a used invite");
  }

  // Check if expired and extend if needed
  const isExpired = new Date(invite.expires_at) < new Date();
  let updatedExpiry = invite.expires_at;

  if (isExpired) {
    const newExpiry = new Date();
    newExpiry.setDate(newExpiry.getDate() + 7);
    updatedExpiry = newExpiry.toISOString();

    const { error: updateError } = await auth.adminSupabase
      .from("invites")
      .update({ expires_at: updatedExpiry })
      .eq("id", id);

    if (updateError) {
      return json.serverError(c, updateError.message);
    }
  }

  // TODO: Actually send email via email service
  // For now, return success with invite details for manual follow-up

  return json.success(c, {
    message: isExpired
      ? "Invite expiry extended and ready to resend"
      : "Invite ready to resend",
    invite: {
      id: invite.id,
      code: invite.code,
      expires_at: updatedExpiry,
      email: invite.waitlist?.email || null,
    },
    extended: isExpired,
  });
});

// ============================================
// WAITLIST MANAGEMENT
// ============================================

// GET /api/admin/waitlist - List waitlist entries
adminRoutes.get("/waitlist", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const url = new URL(c.req.url);
  const limit = Number.parseInt(url.searchParams.get("limit") || "50");
  const offset = Number.parseInt(url.searchParams.get("offset") || "0");
  const status = url.searchParams.get("status"); // pending, invited, joined
  const search = url.searchParams.get("search");

  let query = auth.adminSupabase
    .from("waitlist")
    .select("*", { count: "exact" })
    .order("created_at", { ascending: false })
    .range(offset, offset + limit - 1);

  if (status) {
    query = query.eq("status", status);
  }

  if (search) {
    query = query.or(`email.ilike.%${search}%,name.ilike.%${search}%`);
  }

  const { data: entries, error, count } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { waitlist: entries, total: count });
});

// GET /api/admin/waitlist/:id - Get single waitlist entry
adminRoutes.get("/waitlist/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  const { data: entry, error } = await auth.adminSupabase
    .from("waitlist")
    .select("*")
    .eq("id", id)
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "Waitlist entry not found");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { entry });
});

// PUT /api/admin/waitlist/:id - Update waitlist entry status
adminRoutes.put("/waitlist/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");
  const body = await c.req.json();

  const updates: Record<string, unknown> = {};
  if (body.status !== undefined) updates.status = body.status;
  if (body.notes !== undefined) updates.notes = body.notes;
  if (body.priority !== undefined) updates.priority = body.priority;

  if (Object.keys(updates).length === 0) {
    return json.error(c, "No valid fields to update");
  }

  updates.updated_at = new Date().toISOString();

  const { data: entry, error } = await auth.adminSupabase
    .from("waitlist")
    .update(updates)
    .eq("id", id)
    .select()
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "Waitlist entry not found");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { entry });
});

// DELETE /api/admin/waitlist/:id - Delete waitlist entry
adminRoutes.delete("/waitlist/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  const { error } = await auth.adminSupabase
    .from("waitlist")
    .delete()
    .eq("id", id);

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { message: "Waitlist entry deleted" });
});

// POST /api/admin/waitlist/:id/approve - Approve and send invite to waitlist entry
adminRoutes.post("/waitlist/:id/approve", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  // Get the waitlist entry
  const { data: entry, error: fetchError } = await auth.adminSupabase
    .from("waitlist")
    .select("*")
    .eq("id", id)
    .single();

  if (fetchError) {
    if (fetchError.code === "PGRST116") {
      return json.notFound(c, "Waitlist entry not found");
    }
    return json.serverError(c, fetchError.message);
  }

  if (entry.status === "invited") {
    return json.error(c, "Entry already approved and invited");
  }

  // Create an invite for this person
  const code = generateInviteCode();
  const expiresAt = new Date();
  expiresAt.setDate(expiresAt.getDate() + 7);

  const { data: invite, error: inviteError } = await auth.adminSupabase
    .from("invites")
    .insert({
      code,
      created_by: auth.user.id,
      expires_at: expiresAt.toISOString(),
      note: `Auto-generated for waitlist entry: ${entry.email}`,
      used: false,
      waitlist_id: id,
    })
    .select()
    .single();

  if (inviteError) {
    return json.serverError(c, inviteError.message);
  }

  // Update waitlist status
  const { data: updatedEntry, error: updateError } = await auth.adminSupabase
    .from("waitlist")
    .update({
      status: "invited",
      invite_code: code,
      invited_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
    })
    .eq("id", id)
    .select()
    .single();

  if (updateError) {
    return json.serverError(c, updateError.message);
  }

  // TODO: Send email with invite code via email service
  // For now, we just return success and the admin can manually notify

  return json.success(c, {
    entry: updatedEntry,
    invite,
    message: "Waitlist entry approved and invite created",
  });
});

// POST /api/admin/waitlist/bulk-approve - Bulk approve multiple waitlist entries
adminRoutes.post("/waitlist/bulk-approve", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const body = await c.req.json();
  const { ids } = body;

  if (!Array.isArray(ids) || ids.length === 0) {
    return json.error(c, "ids array is required");
  }

  if (ids.length > 100) {
    return json.error(c, "Cannot approve more than 100 entries at once");
  }

  // Get all pending entries
  const { data: entries, error: fetchError } = await auth.adminSupabase
    .from("waitlist")
    .select("*")
    .in("id", ids)
    .neq("status", "invited");

  if (fetchError) {
    return json.serverError(c, fetchError.message);
  }

  if (!entries || entries.length === 0) {
    return json.error(c, "No eligible entries found to approve");
  }

  const results = {
    approved: [] as string[],
    failed: [] as { id: string; reason: string }[],
    invites: [] as { id: string; code: string; email: string }[],
  };

  // Process each entry
  for (const entry of entries) {
    try {
      const code = generateInviteCode();
      const expiresAt = new Date();
      expiresAt.setDate(expiresAt.getDate() + 7);

      // Create invite
      const { error: inviteError } = await auth.adminSupabase
        .from("invites")
        .insert({
          code,
          created_by: auth.user.id,
          expires_at: expiresAt.toISOString(),
          note: `Bulk approval for: ${entry.email}`,
          used: false,
          waitlist_id: entry.id,
        });

      if (inviteError) {
        results.failed.push({ id: entry.id, reason: inviteError.message });
        continue;
      }

      // Update waitlist entry
      const { error: updateError } = await auth.adminSupabase
        .from("waitlist")
        .update({
          status: "invited",
          invite_code: code,
          invited_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        })
        .eq("id", entry.id);

      if (updateError) {
        results.failed.push({ id: entry.id, reason: updateError.message });
        continue;
      }

      results.approved.push(entry.id);
      results.invites.push({ id: entry.id, code, email: entry.email });
    } catch (err) {
      results.failed.push({
        id: entry.id,
        reason: err instanceof Error ? err.message : "Unknown error",
      });
    }
  }

  return json.success(c, {
    message: `Approved ${results.approved.length} of ${ids.length} entries`,
    ...results,
  });
});

// POST /api/admin/waitlist/:id/reject - Reject a single waitlist entry
adminRoutes.post("/waitlist/:id/reject", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");
  const body = await c.req.json().catch(() => ({}));
  const { reason, sendEmail = false } = body;

  // Get the waitlist entry
  const { data: entry, error: fetchError } = await auth.adminSupabase
    .from("waitlist")
    .select("*")
    .eq("id", id)
    .single();

  if (fetchError) {
    if (fetchError.code === "PGRST116") {
      return json.notFound(c, "Waitlist entry not found");
    }
    return json.serverError(c, fetchError.message);
  }

  if (entry.status === "rejected") {
    return json.error(c, "Entry already rejected");
  }

  // Update waitlist status to rejected
  const updates: Record<string, unknown> = {
    status: "rejected",
    updated_at: new Date().toISOString(),
  };

  // Append rejection reason to notes if provided
  if (reason) {
    const existingNotes = entry.notes || "";
    updates.notes = existingNotes
      ? `${existingNotes}\n[Rejected] ${reason}`
      : `[Rejected] ${reason}`;
  }

  const { data: updatedEntry, error: updateError } = await auth.adminSupabase
    .from("waitlist")
    .update(updates)
    .eq("id", id)
    .select()
    .single();

  if (updateError) {
    return json.serverError(c, updateError.message);
  }

  // TODO: Send rejection email if sendEmail is true
  // For now, just log it
  if (sendEmail) {
    console.log(`[TODO] Send rejection email to: ${entry.email}`);
  }

  return json.success(c, {
    entry: updatedEntry,
    message: "Waitlist entry rejected",
  });
});

// POST /api/admin/waitlist/bulk-reject - Bulk reject multiple waitlist entries
adminRoutes.post("/waitlist/bulk-reject", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const body = await c.req.json();
  const { ids, reason } = body;

  if (!Array.isArray(ids) || ids.length === 0) {
    return json.error(c, "ids array is required");
  }

  if (ids.length > 100) {
    return json.error(c, "Cannot reject more than 100 entries at once");
  }

  // Get all non-rejected entries
  const { data: entries, error: fetchError } = await auth.adminSupabase
    .from("waitlist")
    .select("*")
    .in("id", ids)
    .neq("status", "rejected");

  if (fetchError) {
    return json.serverError(c, fetchError.message);
  }

  if (!entries || entries.length === 0) {
    return json.error(c, "No eligible entries found to reject");
  }

  const results = {
    rejected: [] as string[],
    failed: [] as { id: string; reason: string }[],
  };

  // Process each entry
  for (const entry of entries) {
    try {
      const updates: Record<string, unknown> = {
        status: "rejected",
        updated_at: new Date().toISOString(),
      };

      // Append rejection reason to notes if provided
      if (reason) {
        const existingNotes = entry.notes || "";
        updates.notes = existingNotes
          ? `${existingNotes}\n[Bulk Rejected] ${reason}`
          : `[Bulk Rejected] ${reason}`;
      }

      const { error: updateError } = await auth.adminSupabase
        .from("waitlist")
        .update(updates)
        .eq("id", entry.id);

      if (updateError) {
        results.failed.push({ id: entry.id, reason: updateError.message });
        continue;
      }

      results.rejected.push(entry.id);
    } catch (err) {
      results.failed.push({
        id: entry.id,
        reason: err instanceof Error ? err.message : "Unknown error",
      });
    }
  }

  return json.success(c, {
    message: `Rejected ${results.rejected.length} of ${ids.length} entries`,
    ...results,
  });
});

// ============================================
// USER MANAGEMENT
// ============================================

// GET /api/admin/users - List users
adminRoutes.get("/users", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const url = new URL(c.req.url);
  const limit = Number.parseInt(url.searchParams.get("limit") || "50");
  const offset = Number.parseInt(url.searchParams.get("offset") || "0");
  const role = url.searchParams.get("role");
  const search = url.searchParams.get("search");
  const status = url.searchParams.get("status"); // active, suspended

  let query = auth.adminSupabase
    .from("profiles")
    .select("*", { count: "exact" })
    .order("created_at", { ascending: false })
    .range(offset, offset + limit - 1);

  if (role) {
    query = query.eq("role", role);
  }

  if (status) {
    query = query.eq("status", status);
  }

  if (search) {
    query = query.or(
      `full_name.ilike.%${search}%,email.ilike.%${search}%,username.ilike.%${search}%`,
    );
  }

  const { data: users, error, count } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { users, total: count });
});

// GET /api/admin/users/:id - Get single user
adminRoutes.get("/users/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");

  const { data: user, error } = await auth.adminSupabase
    .from("profiles")
    .select("*")
    .eq("id", id)
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "User not found");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { user });
});

// PUT /api/admin/users/:id - Update user
adminRoutes.put("/users/:id", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const id = c.req.param("id");
  const body = await c.req.json();

  // Only allow updating specific fields
  const updates: Record<string, unknown> = {};
  if (body.role !== undefined) updates.role = body.role;
  if (body.status !== undefined) updates.status = body.status;
  if (body.notes !== undefined) updates.admin_notes = body.notes;

  if (Object.keys(updates).length === 0) {
    return json.error(c, "No valid fields to update");
  }

  updates.updated_at = new Date().toISOString();

  const { data: user, error } = await auth.adminSupabase
    .from("profiles")
    .update(updates)
    .eq("id", id)
    .select()
    .single();

  if (error) {
    if (error.code === "PGRST116") {
      return json.notFound(c, "User not found");
    }
    return json.serverError(c, error.message);
  }

  return json.success(c, { user });
});

// ============================================
// STATS / DASHBOARD
// ============================================

// ============================================
// EXPORT
// ============================================

// POST /api/admin/export/waitlist - Export waitlist as CSV or JSON
adminRoutes.post("/export/waitlist", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  const body = await c.req.json().catch(() => ({}));
  const { format = "csv", status, dateFrom, dateTo, fields } = body;

  let query = auth.adminSupabase
    .from("waitlist")
    .select("*")
    .order("created_at", { ascending: false });

  if (status) {
    query = query.eq("status", status);
  }

  if (dateFrom) {
    query = query.gte("created_at", dateFrom);
  }

  if (dateTo) {
    query = query.lte("created_at", dateTo);
  }

  const { data: entries, error } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  if (!entries || entries.length === 0) {
    return json.success(c, { message: "No entries to export", data: "" });
  }

  if (format === "json") {
    return json.success(c, { entries });
  }

  // Build CSV
  const defaultHeaders = [
    "id",
    "email",
    "name",
    "status",
    "invite_code",
    "created_at",
    "invited_at",
    "source",
    "notes",
  ];
  const headers =
    Array.isArray(fields) && fields.length > 0 ? fields : defaultHeaders;
  const csvRows = [headers.join(",")];

  for (const entry of entries) {
    const row = headers.map((h: string) => {
      const val = entry[h as keyof typeof entry];
      if (val === null || val === undefined) return "";
      // Escape quotes and wrap in quotes if contains comma
      const str = String(val);
      if (str.includes(",") || str.includes('"') || str.includes("\n")) {
        return `"${str.replace(/"/g, '""')}"`;
      }
      return str;
    });
    csvRows.push(row.join(","));
  }

  const csv = csvRows.join("\n");

  // Return as downloadable CSV
  return new Response(csv, {
    status: 200,
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": `attachment; filename="waitlist-export-${new Date().toISOString().split("T")[0]}.csv"`,
    },
  });
});

// ============================================
// STATS / DASHBOARD
// ============================================

// GET /api/admin/stats - Get admin dashboard stats
adminRoutes.get("/stats", async (c) => {
  const auth = await requireAdmin(c);
  if (!auth) {
    return json.forbidden(c, "Admin access required");
  }

  // Get counts in parallel
  const [usersResult, waitlistResult, invitesResult] = await Promise.all([
    auth.adminSupabase
      .from("profiles")
      .select("id", { count: "exact", head: true }),
    auth.adminSupabase
      .from("waitlist")
      .select("id", { count: "exact", head: true }),
    auth.adminSupabase
      .from("invites")
      .select("id, used", { count: "exact", head: true })
      .eq("used", false),
  ]);

  const stats = {
    total_users: usersResult.count || 0,
    total_waitlist: waitlistResult.count || 0,
    available_invites: invitesResult.count || 0,
  };

  return json.success(c, { stats });
});

// ============================================
// HELPERS
// ============================================

function generateInviteCode(): string {
  const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Removed confusing chars
  let code = "";
  for (let i = 0; i < 8; i++) {
    code += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return code;
}


Good, I have detailed info on profiles, auth, admin routes. Now let me look at the other routes to get a complete picture of all the table schemas needed.

cd /tmp/contentcollective-api && cat src/routes/connections.ts

cd /tmp/contentcollective-api && cat src/routes/messages.ts


import { Hono } from "hono";
import { json } from "../lib/response";
import { getAuthUser, isSupabaseConfigured } from "../lib/supabase";

export const connectionsRoutes = new Hono();

// Mock connections for development
const mockConnections = [
  {
    id: "conn-1",
    user: {
      id: "user-1",
      full_name: "Alex Rivera",
      avatar_url: null,
      creator_type: "influencer",
    },
    status: "accepted",
    direction: "sent",
    created_at: new Date().toISOString(),
  },
  {
    id: "conn-2",
    user: {
      id: "user-2",
      full_name: "Jordan Taylor",
      avatar_url: null,
      creator_type: "educator",
    },
    status: "pending",
    direction: "received",
    created_at: new Date().toISOString(),
  },
];

// GET /api/connections
connectionsRoutes.get("/", async (c) => {
  const auth = await getAuthUser(c);

  if (!auth) {
    if (!isSupabaseConfigured()) {
      return json.success(c, { connections: mockConnections, total: 2 });
    }
    return json.unauthorized(c);
  }

  const url = new URL(c.req.url);
  const status = url.searchParams.get("status") || "accepted";
  const direction = url.searchParams.get("direction");

  interface ConnectionItem {
    id: string;
    user: unknown;
    status: string;
    direction: string;
    note?: string;
    created_at: string;
    responded_at?: string;
  }

  const connections: ConnectionItem[] = [];

  // Get sent connections
  if (!direction || direction === "sent") {
    let sentQuery = auth.supabase
      .from("connections")
      .select(`
        id, status, note, created_at, responded_at,
        requestee:profiles!connections_requestee_id_fkey(id, username, full_name, avatar_url, creator_type)
      `)
      .eq("requester_id", auth.user.id);

    if (status !== "all") {
      sentQuery = sentQuery.eq("status", status);
    }

    const { data: sentConnections } = await sentQuery.order("created_at", {
      ascending: false,
    });

    if (sentConnections) {
      for (const conn of sentConnections) {
        connections.push({
          id: conn.id,
          user: conn.requestee,
          status: conn.status,
          direction: "sent",
          note: conn.note,
          created_at: conn.created_at,
          responded_at: conn.responded_at,
        });
      }
    }
  }

  // Get received connections
  if (!direction || direction === "received") {
    let receivedQuery = auth.supabase
      .from("connections")
      .select(`
        id, status, note, created_at, responded_at,
        requester:profiles!connections_requester_id_fkey(id, username, full_name, avatar_url, creator_type)
      `)
      .eq("requestee_id", auth.user.id);

    if (status !== "all") {
      receivedQuery = receivedQuery.eq("status", status);
    }

    const { data: receivedConnections } = await receivedQuery.order(
      "created_at",
      { ascending: false },
    );

    if (receivedConnections) {
      for (const conn of receivedConnections) {
        connections.push({
          id: conn.id,
          user: conn.requester,
          status: conn.status,
          direction: "received",
          note: conn.note,
          created_at: conn.created_at,
          responded_at: conn.responded_at,
        });
      }
    }
  }

  // Sort by created_at
  connections.sort(
    (a, b) =>
      new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
  );

  return json.success(c, { connections, total: connections.length });
});

// POST /api/connections
connectionsRoutes.post("/", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const body = await c.req.json();
  const { requestee_id, note } = body;

  if (!requestee_id) {
    return json.error(c, "requestee_id is required");
  }

  if (auth.user.id === requestee_id) {
    return json.error(c, "Cannot connect with yourself");
  }

  // Check if requestee exists
  const { data: requestee } = await auth.supabase
    .from("profiles")
    .select("id")
    .eq("id", requestee_id)
    .single();

  if (!requestee) {
    return json.notFound(c, "User not found");
  }

  // Check for existing connection
  const { data: existingConnection } = await auth.supabase
    .from("connections")
    .select("id, status")
    .or(
      `and(requester_id.eq.${auth.user.id},requestee_id.eq.${requestee_id}),and(requester_id.eq.${requestee_id},requestee_id.eq.${auth.user.id})`,
    )
    .single();

  if (existingConnection) {
    const statusMessages: Record<string, string> = {
      pending: "Connection request already pending",
      accepted: "You are already connected",
      declined: "Previous request was declined",
      blocked: "Cannot send connection request",
    };
    return json.conflict(
      c,
      statusMessages[existingConnection.status] || "Connection already exists",
    );
  }

  // Check for blocks
  const { data: blocked } = await auth.supabase
    .from("blocks")
    .select("id")
    .or(
      `and(blocker_id.eq.${requestee_id},blocked_id.eq.${auth.user.id}),and(blocker_id.eq.${auth.user.id},blocked_id.eq.${requestee_id})`,
    )
    .single();

  if (blocked) {
    return json.forbidden(c, "Cannot send connection request");
  }

  const { data: connection, error } = await auth.supabase
    .from("connections")
    .insert({
      requester_id: auth.user.id,
      requestee_id,
      status: "pending",
      note: note || null,
    })
    .select()
    .single();

  if (error) {
    return json.serverError(c, "Failed to send connection request");
  }

  return json.created(c, connection);
});

// PATCH /api/connections/:id
connectionsRoutes.patch("/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const connectionId = c.req.param("id");
  const body = await c.req.json();
  const { action } = body; // accept, decline, block

  if (!["accept", "decline", "block"].includes(action)) {
    return json.error(c, "Invalid action");
  }

  // Get connection and verify user is the requestee
  const { data: connection } = await auth.supabase
    .from("connections")
    .select("*")
    .eq("id", connectionId)
    .eq("requestee_id", auth.user.id)
    .eq("status", "pending")
    .single();

  if (!connection) {
    return json.notFound(c, "Connection request not found");
  }

  const statusMap: Record<string, string> = {
    accept: "accepted",
    decline: "declined",
    block: "blocked",
  };

  const { error } = await auth.supabase
    .from("connections")
    .update({
      status: statusMap[action],
      responded_at: new Date().toISOString(),
    })
    .eq("id", connectionId);

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { message: `Connection ${action}ed` });
});

// DELETE /api/connections/:id
connectionsRoutes.delete("/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const connectionId = c.req.param("id");

  // User can delete connections they're part of (either as requester or requestee)
  const { error } = await auth.supabase
    .from("connections")
    .delete()
    .eq("id", connectionId)
    .or(`requester_id.eq.${auth.user.id},requestee_id.eq.${auth.user.id}`);

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { message: "Connection removed" });
});


import { Hono } from "hono";
import { json } from "../lib/response";
import { getAuthUser, isSupabaseConfigured } from "../lib/supabase";

export const messagesRoutes = new Hono();

// GET /api/messages - List conversations
messagesRoutes.get("/", async (c) => {
  const auth = await getAuthUser(c);

  if (!auth) {
    if (!isSupabaseConfigured()) {
      return json.success(c, { conversations: [], total: 0 });
    }
    return json.unauthorized(c);
  }

  const { data: conversations, error } = await auth.supabase
    .from("message_threads")
    .select(`
      id,
      last_message_at,
      unread_count,
      other_user:profiles!message_threads_other_user_id_fkey(id, full_name, avatar_url),
      last_message:messages!message_threads_last_message_id_fkey(content, created_at)
    `)
    .eq("user_id", auth.user.id)
    .order("last_message_at", { ascending: false });

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, {
    conversations: conversations || [],
    total: conversations?.length || 0,
  });
});

// GET /api/messages/:id - Get conversation messages
messagesRoutes.get("/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const threadId = c.req.param("id");
  const url = new URL(c.req.url);
  const limit = Number.parseInt(url.searchParams.get("limit") || "50");
  const before = url.searchParams.get("before");

  // Verify user is part of thread
  const { data: thread } = await auth.supabase
    .from("message_threads")
    .select("id, other_user_id")
    .eq("id", threadId)
    .eq("user_id", auth.user.id)
    .single();

  if (!thread) {
    return json.notFound(c, "Conversation not found");
  }

  let query = auth.supabase
    .from("messages")
    .select("id, content, sender_id, created_at, read_at")
    .eq("thread_id", threadId)
    .order("created_at", { ascending: false })
    .limit(limit);

  if (before) {
    query = query.lt("created_at", before);
  }

  const { data: messages, error } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, {
    messages: (messages || []).reverse(),
    hasMore: messages?.length === limit,
  });
});

// POST /api/messages/:id - Send message
messagesRoutes.post("/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const threadId = c.req.param("id");
  const body = await c.req.json();
  const { content } = body;

  if (!content || content.trim().length === 0) {
    return json.error(c, "Message content is required");
  }

  // Verify user is part of thread
  const { data: thread } = await auth.supabase
    .from("message_threads")
    .select("id, other_user_id")
    .eq("id", threadId)
    .eq("user_id", auth.user.id)
    .single();

  if (!thread) {
    return json.notFound(c, "Conversation not found");
  }

  const { data: message, error } = await auth.supabase
    .from("messages")
    .insert({
      thread_id: threadId,
      sender_id: auth.user.id,
      recipient_id: thread.other_user_id,
      content: content.trim(),
    })
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  // Update thread
  await auth.supabase
    .from("message_threads")
    .update({
      last_message_id: message.id,
      last_message_at: message.created_at,
    })
    .eq("id", threadId);

  // Increment unread for recipient's thread
  await auth.supabase.rpc("increment_unread_messages", {
    p_thread_id: threadId,
    p_user_id: thread.other_user_id,
  });

  return json.created(c, message);
});

// POST /api/messages/:id/read
messagesRoutes.post("/:id/read", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const threadId = c.req.param("id");

  // Mark all unread messages in thread as read
  await auth.supabase
    .from("messages")
    .update({ read_at: new Date().toISOString() })
    .eq("thread_id", threadId)
    .eq("recipient_id", auth.user.id)
    .is("read_at", null);

  // Reset unread count
  await auth.supabase
    .from("message_threads")
    .update({ unread_count: 0 })
    .eq("id", threadId)
    .eq("user_id", auth.user.id);

  return json.success(c, { message: "Marked as read" });
});

// POST /api/messages - Start new conversation
messagesRoutes.post("/", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const body = await c.req.json();
  const { recipient_id, content } = body;

  if (!recipient_id || !content) {
    return json.error(c, "Recipient and content are required");
  }

  // Check if conversation already exists
  const { data: existingThread } = await auth.supabase
    .from("message_threads")
    .select("id")
    .eq("user_id", auth.user.id)
    .eq("other_user_id", recipient_id)
    .single();

  if (existingThread) {
    // Use existing thread
    return c.redirect(`/api/messages/${existingThread.id}`, 303);
  }

  // Check if recipient accepts messages
  const { data: recipient } = await auth.supabase
    .from("profiles")
    .select("id, open_to_messages")
    .eq("id", recipient_id)
    .single();

  if (!recipient) {
    return json.notFound(c, "Recipient not found");
  }

  if (!recipient.open_to_messages) {
    // Create message request instead
    const { data: request, error } = await auth.supabase
      .from("message_requests")
      .insert({
        sender_id: auth.user.id,
        recipient_id,
        message: content.trim(),
        status: "pending",
      })
      .select()
      .single();

    if (error) {
      return json.serverError(c, error.message);
    }

    return json.created(c, { type: "request", request });
  }

  // Create threads for both users
  const { data: senderThread } = await auth.supabase
    .from("message_threads")
    .insert({ user_id: auth.user.id, other_user_id: recipient_id })
    .select()
    .single();

  await auth.supabase
    .from("message_threads")
    .insert({ user_id: recipient_id, other_user_id: auth.user.id });

  // Create first message
  const { data: message } = await auth.supabase
    .from("messages")
    .insert({
      thread_id: senderThread.id,
      sender_id: auth.user.id,
      recipient_id,
      content: content.trim(),
    })
    .select()
    .single();

  return json.created(c, {
    type: "thread",
    thread_id: senderThread.id,
    message,
  });
});

// GET /api/messages/requests
messagesRoutes.get("/requests", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const { data: requests } = await auth.supabase
    .from("message_requests")
    .select(`
      id, message, status, created_at,
      sender:profiles!message_requests_sender_id_fkey(id, full_name, avatar_url)
    `)
    .eq("recipient_id", auth.user.id)
    .eq("status", "pending")
    .order("created_at", { ascending: false });

  return json.success(c, { requests: requests || [] });
});

// POST /api/messages/requests/:id/accept
messagesRoutes.post("/requests/:id/accept", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const requestId = c.req.param("id");

  const { data: request } = await auth.supabase
    .from("message_requests")
    .select("*")
    .eq("id", requestId)
    .eq("recipient_id", auth.user.id)
    .eq("status", "pending")
    .single();

  if (!request) {
    return json.notFound(c, "Request not found");
  }

  // Update request status
  await auth.supabase
    .from("message_requests")
    .update({ status: "accepted" })
    .eq("id", requestId);

  // Create threads
  const { data: thread } = await auth.supabase
    .from("message_threads")
    .insert({ user_id: auth.user.id, other_user_id: request.sender_id })
    .select()
    .single();

  await auth.supabase
    .from("message_threads")
    .insert({ user_id: request.sender_id, other_user_id: auth.user.id });

  // Create the initial message
  await auth.supabase.from("messages").insert({
    thread_id: thread.id,
    sender_id: request.sender_id,
    recipient_id: auth.user.id,
    content: request.message,
    created_at: request.created_at,
  });

  return json.success(c, { thread_id: thread.id });
});

// POST /api/messages/requests/:id/decline
messagesRoutes.post("/requests/:id/decline", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const requestId = c.req.param("id");

  await auth.supabase
    .from("message_requests")
    .update({ status: "declined" })
    .eq("id", requestId)
    .eq("recipient_id", auth.user.id);

  return json.success(c, { message: "Request declined" });
});


Good, I'm getting a clear picture of the schema. Let me continue looking at the remaining routes to get the full picture.

cd /tmp/contentcollective-api && cat src/routes/community.ts

cd /tmp/contentcollective-api && cat src/routes/courses.ts


import { Hono } from "hono";
import { json } from "../lib/response";
import {
  createSupabase,
  getAuthUser,
  isSupabaseConfigured,
} from "../lib/supabase";

export const communityRoutes = new Hono();

// GET /api/community/spaces
communityRoutes.get("/spaces", async (c) => {
  const supabase = createSupabase();

  if (!supabase) {
    return json.success(c, {
      spaces: [
        { id: "1", name: "General", slug: "general", members_count: 150 },
        {
          id: "2",
          name: "Content Tips",
          slug: "content-tips",
          members_count: 89,
        },
      ],
    });
  }

  const { data: spaces, error } = await supabase
    .from("community_spaces")
    .select("id, name, slug, description, icon, members_count, is_public")
    .eq("is_public", true)
    .order("members_count", { ascending: false });

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { spaces: spaces || [] });
});

// POST /api/community/spaces/:slug/join
communityRoutes.post("/spaces/:slug/join", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const slug = c.req.param("slug");

  const { data: space } = await auth.supabase
    .from("community_spaces")
    .select("id")
    .eq("slug", slug)
    .single();

  if (!space) {
    return json.notFound(c, "Space not found");
  }

  // Check if already a member
  const { data: existing } = await auth.supabase
    .from("space_members")
    .select("id")
    .eq("space_id", space.id)
    .eq("user_id", auth.user.id)
    .single();

  if (existing) {
    return json.conflict(c, "Already a member of this space");
  }

  const { error } = await auth.supabase
    .from("space_members")
    .insert({ space_id: space.id, user_id: auth.user.id });

  if (error) {
    return json.serverError(c, error.message);
  }

  // Increment member count
  await auth.supabase.rpc("increment_space_members", { space_id: space.id });

  return json.created(c, { message: "Joined space successfully" });
});

// GET /api/community/posts
communityRoutes.get("/posts", async (c) => {
  const supabase = createSupabase();

  if (!supabase) {
    return json.success(c, { posts: [], total: 0 });
  }

  const url = new URL(c.req.url);
  const space_id = url.searchParams.get("space_id");
  const limit = Number.parseInt(url.searchParams.get("limit") || "20");
  const cursor = url.searchParams.get("cursor");

  let query = supabase
    .from("community_posts")
    .select(`
      id,
      content,
      created_at,
      likes_count,
      comments_count,
      author:profiles!community_posts_author_id_fkey(id, full_name, avatar_url)
    `)
    .order("created_at", { ascending: false })
    .limit(limit + 1);

  if (space_id) {
    query = query.eq("space_id", space_id);
  }

  if (cursor) {
    query = query.lt("created_at", cursor);
  }

  const { data: posts, error } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  const hasMore = (posts?.length || 0) > limit;
  const result = posts?.slice(0, limit) || [];

  return json.success(c, {
    posts: result,
    hasMore,
    nextCursor: hasMore ? result[result.length - 1]?.created_at : null,
  });
});

// POST /api/community/posts
communityRoutes.post("/posts", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const body = await c.req.json();
  const { content, space_id } = body;

  if (!content || content.trim().length === 0) {
    return json.error(c, "Content is required");
  }

  if (content.length > 5000) {
    return json.error(c, "Content too long (max 5000 characters)");
  }

  const { data: post, error } = await auth.supabase
    .from("community_posts")
    .insert({
      author_id: auth.user.id,
      content: content.trim(),
      space_id: space_id || null,
    })
    .select(`
      id,
      content,
      created_at,
      author:profiles!community_posts_author_id_fkey(id, full_name, avatar_url)
    `)
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.created(c, post);
});

// POST /api/community/posts/:id/like
communityRoutes.post("/posts/:id/like", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const postId = c.req.param("id");

  // Toggle like
  const { data: existing } = await auth.supabase
    .from("post_likes")
    .select("id")
    .eq("post_id", postId)
    .eq("user_id", auth.user.id)
    .single();

  if (existing) {
    // Unlike
    await auth.supabase
      .from("post_likes")
      .delete()
      .eq("post_id", postId)
      .eq("user_id", auth.user.id);

    return json.success(c, { liked: false });
  }

  // Like
  await auth.supabase
    .from("post_likes")
    .insert({ post_id: postId, user_id: auth.user.id });

  return json.success(c, { liked: true });
});

// GET /api/community/posts/:id/comments
communityRoutes.get("/posts/:id/comments", async (c) => {
  const supabase = createSupabase();
  if (!supabase) {
    return json.success(c, { comments: [] });
  }

  const postId = c.req.param("id");

  const { data: comments, error } = await supabase
    .from("post_comments")
    .select(`
      id,
      content,
      created_at,
      author:profiles!post_comments_author_id_fkey(id, full_name, avatar_url)
    `)
    .eq("post_id", postId)
    .order("created_at", { ascending: true });

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { comments: comments || [] });
});

// POST /api/community/posts/:id/comments
communityRoutes.post("/posts/:id/comments", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const postId = c.req.param("id");
  const body = await c.req.json();
  const { content } = body;

  if (!content || content.trim().length === 0) {
    return json.error(c, "Content is required");
  }

  const { data: comment, error } = await auth.supabase
    .from("post_comments")
    .insert({
      post_id: postId,
      author_id: auth.user.id,
      content: content.trim(),
    })
    .select(`
      id,
      content,
      created_at,
      author:profiles!post_comments_author_id_fkey(id, full_name, avatar_url)
    `)
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.created(c, comment);
});


import { Hono } from "hono";
import { json } from "../lib/response";
import {
  createSupabase,
  getAuthUser,
  isSupabaseConfigured,
} from "../lib/supabase";

export const coursesRoutes = new Hono();

// GET /api/courses
coursesRoutes.get("/", async (c) => {
  const supabase = createSupabase();

  if (!supabase) {
    return json.success(c, {
      courses: [
        {
          id: "1",
          title: "Content Creation Fundamentals",
          description: "Learn the basics of creating engaging content",
          mentor: { id: "m1", full_name: "Sarah Chen", avatar_url: null },
          price: 49.99,
          enrolled_count: 156,
        },
      ],
    });
  }

  const url = new URL(c.req.url);
  const category = url.searchParams.get("category");
  const limit = Number.parseInt(url.searchParams.get("limit") || "20");

  let query = supabase
    .from("courses")
    .select(`
      id, title, description, thumbnail_url, price, currency,
      enrolled_count, rating_avg, duration_hours, level,
      mentor:profiles!mentor_id(id, full_name, avatar_url)
    `)
    .eq("is_published", true)
    .order("enrolled_count", { ascending: false })
    .limit(limit);

  if (category) {
    query = query.eq("category", category);
  }

  const { data: courses, error } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { courses: courses || [] });
});

// GET /api/courses/:id
coursesRoutes.get("/:id", async (c) => {
  const courseId = c.req.param("id");
  const supabase = createSupabase();

  if (!supabase) {
    return json.notFound(c, "Supabase not configured");
  }

  const { data: course, error } = await supabase
    .from("courses")
    .select(`
      *,
      mentor:profiles!mentor_id(id, full_name, avatar_url, bio),
      modules:course_modules(id, title, description, order_index, content_type, content_url, duration_minutes)
    `)
    .eq("id", courseId)
    .eq("is_published", true)
    .single();

  if (error || !course) {
    return json.notFound(c, "Course not found");
  }

  // Sort modules
  course.modules =
    course.modules?.sort(
      (a: { order_index: number }, b: { order_index: number }) =>
        a.order_index - b.order_index,
    ) || [];

  // Fetch reviews
  const { data: reviews } = await supabase
    .from("course_reviews")
    .select(
      "id, rating, review_text, created_at, user:profiles!user_id(id, full_name, avatar_url)",
    )
    .eq("course_id", courseId)
    .order("created_at", { ascending: false })
    .limit(10);

  // Check enrollment if authenticated
  const auth = await getAuthUser(c);
  let enrollment = null;
  const moduleProgress: Record<string, boolean> = {};

  if (auth) {
    const { data: enrollmentData } = await auth.supabase
      .from("course_enrollments")
      .select("*")
      .eq("course_id", courseId)
      .eq("user_id", auth.user.id)
      .single();

    enrollment = enrollmentData;

    if (enrollment) {
      const { data: progressData } = await auth.supabase
        .from("module_progress")
        .select("module_id, completed_at")
        .eq("enrollment_id", enrollment.id);

      if (progressData) {
        for (const p of progressData) {
          moduleProgress[p.module_id] = !!p.completed_at;
        }
      }
    }
  }

  const modulesWithProgress = course.modules.map(
    (module: { id: string; order_index: number }) => ({
      ...module,
      completed: moduleProgress[module.id] || false,
    }),
  );

  return json.success(c, {
    ...course,
    modules: modulesWithProgress,
    reviews: reviews || [],
    enrollment: enrollment
      ? {
          enrolled: true,
          progress_percent: enrollment.progress_percent,
          enrolled_at: enrollment.enrolled_at,
          completed_at: enrollment.completed_at,
        }
      : {
          enrolled: false,
          progress_percent: 0,
          enrolled_at: null,
          completed_at: null,
        },
  });
});

// POST /api/courses/:id/enroll
coursesRoutes.post("/:id/enroll", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const courseId = c.req.param("id");

  // Check if already enrolled
  const { data: existing } = await auth.supabase
    .from("course_enrollments")
    .select("id")
    .eq("course_id", courseId)
    .eq("user_id", auth.user.id)
    .single();

  if (existing) {
    return json.conflict(c, "Already enrolled in this course");
  }

  // Get course to check price
  const { data: course } = await auth.supabase
    .from("courses")
    .select("id, price")
    .eq("id", courseId)
    .single();

  if (!course) {
    return json.notFound(c, "Course not found");
  }

  // For free courses, enroll directly
  // Paid courses would need payment integration
  if (course.price > 0) {
    return json.error(c, "Payment required for this course");
  }

  const { data: enrollment, error } = await auth.supabase
    .from("course_enrollments")
    .insert({
      course_id: courseId,
      user_id: auth.user.id,
      progress_percent: 0,
    })
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.created(c, enrollment);
});

// GET /api/courses/:id/progress
coursesRoutes.get("/:id/progress", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const courseId = c.req.param("id");

  const { data: enrollment } = await auth.supabase
    .from("course_enrollments")
    .select(
      "*, modules:module_progress(module_id, completed_at, time_spent_seconds)",
    )
    .eq("course_id", courseId)
    .eq("user_id", auth.user.id)
    .single();

  if (!enrollment) {
    return json.notFound(c, "Not enrolled in this course");
  }

  return json.success(c, enrollment);
});


Good, I'm building a comprehensive picture. Let me check the remaining routes quickly to complete my understanding.

cd /tmp/contentcollective-api && cat src/routes/events.ts

cd /tmp/contentcollective-api && cat src/routes/linkhub.ts


import { Hono } from "hono";
import { json } from "../lib/response";
import {
  createSupabase,
  getAuthUser,
  isSupabaseConfigured,
} from "../lib/supabase";

export const eventsRoutes = new Hono();

// GET /api/events
eventsRoutes.get("/", async (c) => {
  const supabase = createSupabase();

  if (!supabase) {
    return json.success(c, {
      events: [
        {
          id: "1",
          title: "Creator Meetup",
          description: "Monthly virtual meetup",
          starts_at: new Date(
            Date.now() + 7 * 24 * 60 * 60 * 1000,
          ).toISOString(),
          host: { id: "h1", full_name: "Content Collective", avatar_url: null },
          rsvp_count: 45,
        },
      ],
    });
  }

  const url = new URL(c.req.url);
  const upcoming = url.searchParams.get("upcoming") !== "false";
  const limit = Number.parseInt(url.searchParams.get("limit") || "20");

  let query = supabase
    .from("events")
    .select(`
      id, title, description, starts_at, ends_at, location, location_url,
      is_virtual, rsvp_count, max_attendees,
      host:profiles!events_host_id_fkey(id, full_name, avatar_url)
    `)
    .order("starts_at", { ascending: true })
    .limit(limit);

  if (upcoming) {
    query = query.gte("starts_at", new Date().toISOString());
  }

  const { data: events, error } = await query;

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, { events: events || [] });
});

// GET /api/events/:id
eventsRoutes.get("/:id", async (c) => {
  const eventId = c.req.param("id");
  const supabase = createSupabase();

  if (!supabase) {
    return json.notFound(c, "Event not found");
  }

  const { data: event, error } = await supabase
    .from("events")
    .select(`
      *,
      host:profiles!events_host_id_fkey(id, full_name, avatar_url, bio)
    `)
    .eq("id", eventId)
    .single();

  if (error || !event) {
    return json.notFound(c, "Event not found");
  }

  // Check RSVP status if authenticated
  const auth = await getAuthUser(c);
  let rsvp = null;

  if (auth) {
    const { data: rsvpData } = await auth.supabase
      .from("event_rsvps")
      .select("id, status, created_at")
      .eq("event_id", eventId)
      .eq("user_id", auth.user.id)
      .single();

    rsvp = rsvpData;
  }

  return json.success(c, { ...event, rsvp });
});

// POST /api/events/:id/rsvp
eventsRoutes.post("/:id/rsvp", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const eventId = c.req.param("id");
  const body = await c.req.json().catch(() => ({}));
  const { status = "going" } = body; // going, interested, not_going

  if (!["going", "interested", "not_going"].includes(status)) {
    return json.error(c, "Invalid RSVP status");
  }

  // Get event to check capacity
  const { data: event } = await auth.supabase
    .from("events")
    .select("id, max_attendees, rsvp_count")
    .eq("id", eventId)
    .single();

  if (!event) {
    return json.notFound(c, "Event not found");
  }

  // Check capacity for "going" RSVPs
  if (
    status === "going" &&
    event.max_attendees &&
    event.rsvp_count >= event.max_attendees
  ) {
    return json.error(c, "Event is at capacity");
  }

  // Check for existing RSVP
  const { data: existingRsvp } = await auth.supabase
    .from("event_rsvps")
    .select("id, status")
    .eq("event_id", eventId)
    .eq("user_id", auth.user.id)
    .single();

  if (existingRsvp) {
    // Update existing RSVP
    const { error } = await auth.supabase
      .from("event_rsvps")
      .update({ status, updated_at: new Date().toISOString() })
      .eq("id", existingRsvp.id);

    if (error) {
      return json.serverError(c, error.message);
    }

    // Update rsvp_count if status changed to/from going
    if (existingRsvp.status !== status) {
      if (status === "going") {
        await auth.supabase.rpc("increment_event_rsvp", { event_id: eventId });
      } else if (existingRsvp.status === "going") {
        await auth.supabase.rpc("decrement_event_rsvp", { event_id: eventId });
      }
    }

    return json.success(c, { status, updated: true });
  }

  // Create new RSVP
  const { error } = await auth.supabase.from("event_rsvps").insert({
    event_id: eventId,
    user_id: auth.user.id,
    status,
  });

  if (error) {
    return json.serverError(c, error.message);
  }

  // Increment rsvp_count if going
  if (status === "going") {
    await auth.supabase.rpc("increment_event_rsvp", { event_id: eventId });
  }

  return json.created(c, { status, created: true });
});

// DELETE /api/events/:id/rsvp
eventsRoutes.delete("/:id/rsvp", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const eventId = c.req.param("id");

  const { data: rsvp } = await auth.supabase
    .from("event_rsvps")
    .select("id, status")
    .eq("event_id", eventId)
    .eq("user_id", auth.user.id)
    .single();

  if (!rsvp) {
    return json.notFound(c, "RSVP not found");
  }

  await auth.supabase.from("event_rsvps").delete().eq("id", rsvp.id);

  // Decrement count if was going
  if (rsvp.status === "going") {
    await auth.supabase.rpc("decrement_event_rsvp", { event_id: eventId });
  }

  return json.success(c, { message: "RSVP cancelled" });
});


import { Hono } from "hono";
import { json } from "../lib/response";
import {
  createSupabase,
  getAuthUser,
  isSupabaseConfigured,
} from "../lib/supabase";

export const linkhubRoutes = new Hono();

// GET /api/linkhub - Get user's linkhub profile
linkhubRoutes.get("/", async (c) => {
  const auth = await getAuthUser(c);

  if (!auth) {
    if (!isSupabaseConfigured()) {
      return json.success(c, {
        username: "democreator",
        bio: "Content creator",
        links: [
          {
            id: "1",
            title: "My Website",
            url: "https://example.com",
            order_index: 0,
            clicks: 45,
          },
          {
            id: "2",
            title: "Twitter",
            url: "https://twitter.com/demo",
            order_index: 1,
            clicks: 32,
          },
        ],
      });
    }
    return json.unauthorized(c);
  }

  const { data: profile } = await auth.supabase
    .from("profiles")
    .select("id, username, full_name, bio, avatar_url, linkhub_theme")
    .eq("id", auth.user.id)
    .single();

  const { data: links } = await auth.supabase
    .from("linkhub_links")
    .select("*")
    .eq("user_id", auth.user.id)
    .order("order_index", { ascending: true });

  return json.success(c, {
    ...profile,
    links: links || [],
  });
});

// GET /api/linkhub/:username - Public profile view
linkhubRoutes.get("/:username", async (c) => {
  const username = c.req.param("username");
  const supabase = createSupabase();

  if (!supabase) {
    return json.notFound(c, "Profile not found");
  }

  const { data: profile } = await supabase
    .from("profiles")
    .select("id, username, full_name, bio, avatar_url, linkhub_theme")
    .eq("username", username)
    .eq("is_public", true)
    .single();

  if (!profile) {
    return json.notFound(c, "Profile not found");
  }

  const { data: links } = await supabase
    .from("linkhub_links")
    .select("id, title, url, icon, order_index")
    .eq("user_id", profile.id)
    .eq("is_active", true)
    .order("order_index", { ascending: true });

  return json.success(c, {
    ...profile,
    links: links || [],
  });
});

// POST /api/linkhub/view - Track page view
linkhubRoutes.post("/view", async (c) => {
  const body = await c.req.json();
  const { username } = body;

  const supabase = createSupabase();
  if (!supabase || !username) {
    return json.success(c, { tracked: false });
  }

  const { data: profile } = await supabase
    .from("profiles")
    .select("id")
    .eq("username", username)
    .single();

  if (profile) {
    await supabase.from("linkhub_views").insert({
      user_id: profile.id,
      referrer: c.req.header("Referer") || null,
      user_agent: c.req.header("User-Agent") || null,
    });
  }

  return json.success(c, { tracked: true });
});

// POST /api/linkhub/click - Track link click
linkhubRoutes.post("/click", async (c) => {
  const body = await c.req.json();
  const { link_id } = body;

  const supabase = createSupabase();
  if (!supabase || !link_id) {
    return json.success(c, { tracked: false });
  }

  await supabase.from("linkhub_clicks").insert({
    link_id,
    referrer: c.req.header("Referer") || null,
    user_agent: c.req.header("User-Agent") || null,
  });

  // Increment click count
  await supabase.rpc("increment_link_clicks", { p_link_id: link_id });

  return json.success(c, { tracked: true });
});

// GET /api/linkhub/analytics
linkhubRoutes.get("/analytics", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const url = new URL(c.req.url);
  const period = url.searchParams.get("period") || "7d";

  // Get view stats
  const { data: views } = await auth.supabase
    .from("linkhub_views")
    .select("created_at")
    .eq("user_id", auth.user.id);

  // Get link click stats
  const { data: links } = await auth.supabase
    .from("linkhub_links")
    .select("id, title, clicks")
    .eq("user_id", auth.user.id)
    .order("clicks", { ascending: false });

  return json.success(c, {
    total_views: views?.length || 0,
    total_clicks: links?.reduce((sum, l) => sum + (l.clicks || 0), 0) || 0,
    top_links: links?.slice(0, 5) || [],
  });
});

// GET /api/linkhub/links
linkhubRoutes.get("/links", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const { data: links } = await auth.supabase
    .from("linkhub_links")
    .select("*")
    .eq("user_id", auth.user.id)
    .order("order_index", { ascending: true });

  return json.success(c, { links: links || [] });
});

// POST /api/linkhub/links
linkhubRoutes.post("/links", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const body = await c.req.json();
  const { title, url, icon } = body;

  if (!title || !url) {
    return json.error(c, "Title and URL are required");
  }

  // Get highest order_index
  const { data: maxOrder } = await auth.supabase
    .from("linkhub_links")
    .select("order_index")
    .eq("user_id", auth.user.id)
    .order("order_index", { ascending: false })
    .limit(1)
    .single();

  const { data: link, error } = await auth.supabase
    .from("linkhub_links")
    .insert({
      user_id: auth.user.id,
      title,
      url,
      icon: icon || null,
      order_index: (maxOrder?.order_index || 0) + 1,
      is_active: true,
    })
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.created(c, link);
});

// PATCH /api/linkhub/links/:id
linkhubRoutes.patch("/links/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const linkId = c.req.param("id");
  const body = await c.req.json();

  const allowedFields = ["title", "url", "icon", "is_active"];
  const updates: Record<string, string | boolean> = {};
  for (const key of Object.keys(body)) {
    if (allowedFields.includes(key)) {
      updates[key] = body[key];
    }
  }

  const { data: link, error } = await auth.supabase
    .from("linkhub_links")
    .update(updates)
    .eq("id", linkId)
    .eq("user_id", auth.user.id)
    .select()
    .single();

  if (error) {
    return json.serverError(c, error.message);
  }

  return json.success(c, link);
});

// DELETE /api/linkhub/links/:id
linkhubRoutes.delete("/links/:id", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const linkId = c.req.param("id");

  await auth.supabase
    .from("linkhub_links")
    .delete()
    .eq("id", linkId)
    .eq("user_id", auth.user.id);

  return json.success(c, { message: "Link deleted" });
});

// POST /api/linkhub/links/reorder
linkhubRoutes.post("/links/reorder", async (c) => {
  const auth = await getAuthUser(c);
  if (!auth) {
    return json.unauthorized(c);
  }

  const body = await c.req.json();
  const { order } = body; // Array of { id, order_index }

  if (!Array.isArray(order)) {
    return json.error(c, "Order must be an array");
  }

  // Update each link's order
  for (const item of order) {
    await auth.supabase
      .from("linkhub_links")
      .update({ order_index: item.order_index })
      .eq("id", item.id)
      .eq("user_id", auth.user.id);
  }

  return json.success(c, { message: "Links reordered" });
});