Here’s the plan and the exact changes to ship “My Marketplace” (creator-only), move browsing into Admin, and fix the current state/URL funkiness.

What we’ll change

Sidebar & routing

Replace the two items (“Browse Marketplace”, “Search Assets”) with a single My Marketplace entry that only renders for users who are creators.

Public browsing stays available (route still exists), but it’s no longer in the left app sidebar; we’ll surface marketplace browsing from Admin → Marketplace for staff.

Kill the fragile ?search=true behavior and stop mixing local state with URL for active highlighting. We’ll make highlighting route-based only.

New creator page

/creator/my-marketplace combines:

My Assets (active/approved + pending/rejected)

Cancelled Assets (at the bottom)

Upload new CTA

Payouts & Performance (topline stats + quick link to Earnings)

Actions: Cancel/remove asset, re-list later, view sales/downloads.

Backend

Add creator endpoints:

GET /api/creator/me/marketplace → assets grouped by status, plus stats.

PATCH /api/creator/assets/:id/cancel → mark listing as cancelled (and hide from public).

GET /api/creator/me/payouts/summary → totals and recent payouts.

Update marketplace listing queries to exclude cancelled.

Light schema tweak to support a “cancelled” state on creator assets.

Your current marketplace page, sidebar items, and routes show the exact places to modify: Sidebar marketplace items and state (active highlight), URL param handling in the marketplace page, and the public /marketplace routes.

Frontend changes (concise diffs)
1) Sidebar: replace Marketplace section with “My Marketplace” (creator-only)

client/src/components/Sidebar.tsx

Remove the marketplaceItems array and the two buttons.

After fetching backendUser, gate the new menu item with a isCreator check (e.g., backendUser?.creatorId or backendUser?.roles?.includes('creator')—use whichever you expose in /api/auth/me).

Add one item:

// inside the nav, near Creator section:
{backendUser?.creatorId && (
  <div className="space-y-1">
    <button
      onClick={() => setLocation('/creator/my-marketplace')}
      className={`w-full flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
        location.startsWith('/creator/my-marketplace')
          ? 'bg-primary text-white font-bold'
          : 'text-muted-foreground hover:bg-lightgray hover:text-foreground'
      }`}
      data-testid="nav-my-marketplace"
    >
      <ShoppingBag className="mr-3 h-4 w-4" />
      My Marketplace
    </button>
  </div>
)}


This removes the local activeMarketplaceItem + ?search=true dance and makes highlighting purely route based. (Current sidebar shows the two legacy items and the local active state logic we’re removing.)

2) Routes: add the new creator page and move browsing to Admin

client/src/App.tsx

Keep the public browsing routes (so external landing still works), but we’ll not render them in the left sidebar; instead, add an Admin entry like /admin/marketplace that shows the browsing UI with staff filters if you want.

Add:

// Creator bundle page
<Route path="/creator/my-marketplace">
  <ProtectedRoute>
    <DashboardTemplatePage title="My Marketplace">
      <CreatorMyMarketplace /> {/* new page */}
    </DashboardTemplatePage>
  </ProtectedRoute>
</Route>


(The file already defines marketplace browsing routes under /marketplace; we’re leaving those intact but decoupling navigation.)

3) New page wiring

Create client/src/pages/Creator/CreatorMyMarketplace.tsx with simple tabs:

Overview: earnings to date, last payout, total sales.

My Assets: table/cards of non-cancelled assets by status (Approved, Pending, Rejected) with a Cancel listing action.

Cancelled Assets: simple list at bottom with Re-list action.

Upload: button linking to /creator/upload.

View full earnings: link to /creator/earnings.

The list pulls from GET /api/creator/me/marketplace (new) and triggers PATCH /api/creator/assets/:id/cancel.

4) MarketplacePage cleanup (optional but recommended)

If you’d like to keep “open filters by default” behavior, replace the brittle ?search=true with a stable param like panel=filters and simply leave it in the URL (no replaceState). Or drop it entirely now that this page isn’t in the sidebar. The current code removes search=true, which is what caused the race with the sidebar’s local state.

Backend changes

You already have public endpoints that expose approved assets only (ID detail, assets by creator, etc.). We’ll extend with creator-scoped endpoints and exclude cancelled from public queries.

1) Schema tweak

Add a cancelled state (or a nullable cancelledAt). Since creator_assets uses an approval status enum, the cleanest is adding 'cancelled' to the enum plus a timestamp.

shared/schema.ts

Extend creatorApprovalStatusEnum to include 'cancelled'.

Add:

cancelledAt: timestamp("cancelled_at"),


right next to approvedAt. (Current table shows approvalStatus, approvedAt, rejectionReason, etc.)

Why enum (vs boolean)? Because “cancelled” is mutually exclusive from “approved/pending/rejected” and should be respected by marketplace queries and analytics consistently.

2) Public queries must exclude cancelled

Where you build the public asset lists, filter out approvalStatus !== 'approved' and approvalStatus !== 'cancelled' (i.e., only approved). Your existing code already gates on approved; once creators can cancel, that’s covered, but make sure any listing aggregations don’t unintentionally include cancelled records. (Your current GETs intentionally return approved only. Keep that logic.)

3) New creator endpoints (Express/TS)

In server/routes.ts:

// GET /api/creator/me/marketplace
app.get('/api/creator/me/marketplace', requireAuth, async (req, res) => {
  try {
    const userId = req.user.id;
    const creator = await storage.getCreatorByUserId(userId);
    if (!creator) return res.status(403).json({ error: 'Not a creator' });

    const records = await storage.getCreatorAssetsByCreatorId(creator.id);
    const group = (s: string) => records.filter(r => r.approvalStatus === s);

    // Pull asset meta for cards
    const withAsset = async (r: any) => {
      const a = await storage.getAsset(r.assetId);
      return a ? { ...r, asset: a } : null;
    };

    const [approved, pending, rejected, cancelled] = await Promise.all([
      Promise.all(group('approved').map(withAsset)),
      Promise.all(group('pending').map(withAsset)),
      Promise.all(group('rejected').map(withAsset)),
      Promise.all(group('cancelled').map(withAsset)),
    ]);

    const clean = (arr: any[]) => arr.filter(Boolean);

    // Simple topline stats
    const totalSales = records.reduce((n, r) => n + (r.salesCount || 0), 0);
    const totalRevenueCents = records.reduce((n, r) => n + (r.totalRevenue || 0), 0);

    res.json({
      assets: {
        approved: clean(approved),
        pending: clean(pending),
        rejected: clean(rejected),
        cancelled: clean(cancelled),
      },
      stats: { totalSales, totalRevenueCents }
    });
  } catch (e) {
    handleError(res, e, 'Failed to load creator marketplace');
  }
});

// PATCH /api/creator/assets/:id/cancel
app.patch('/api/creator/assets/:id/cancel', requireAuth, async (req, res) => {
  try {
    const userId = req.user.id;
    const creator = await storage.getCreatorByUserId(userId);
    if (!creator) return res.status(403).json({ error: 'Not a creator' });

    const rec = await storage.getCreatorAsset(req.params.id);
    if (!rec || rec.creatorId !== creator.id) return res.status(404).json({ error: 'Asset not found' });

    // mark as cancelled
    const updated = await storage.updateCreatorAsset(rec.id, {
      approvalStatus: 'cancelled',
      cancelledAt: new Date()
    });

    res.json({ success: true, asset: updated });
  } catch (e) {
    handleError(res, e, 'Failed to cancel listing');
  }
});

// GET /api/creator/me/payouts/summary
app.get('/api/creator/me/payouts/summary', requireAuth, async (req, res) => {
  try {
    const userId = req.user.id;
    const creator = await storage.getCreatorByUserId(userId);
    if (!creator) return res.status(403).json({ error: 'Not a creator' });

    // Your storage layer likely tracks payouts connected to Stripe Connect
    const summary = await storage.getCreatorPayoutSummary(creator.id);
    res.json(summary);
  } catch (e) {
    handleError(res, e, 'Failed to load payouts summary');
  }
});


This complements your existing /api/marketplace/* public endpoints that currently enforce approved visibility.

UI copy & micro-prompts

My Marketplace (page title)
“Manage your listings, track performance, and get paid.”

Empty state (no assets)
“You haven’t listed anything yet. Upload your first asset to start selling.”

Cancel listing (confirm)
“Cancel this listing? It will no longer be visible in the marketplace. You can re-list later.”

Cancelled section header
“Cancelled Assets (not publicly visible)”

Re-list (tooltip)
“Move this asset back to review. It won’t be public until approved.”

QA checklist

Sidebar shows My Marketplace only when user is a creator; otherwise it doesn’t render. (Uses /api/auth/me you already call in the sidebar to determine identity.)

Creator can cancel a listing; it disappears from public browse immediately and appears under “Cancelled Assets”.

Public marketplace and creator profile pages continue to serve approved assets only.

Route-based highlighting works; no more race with URL param removal in MarketplacePage.

Admin can still access browse/review via Admin Dashboard (you already have Admin routes; add a link or a small wrapper page if desired).

Hand-off summary for SuperNova

Remove Sidebar marketplace items; add creator-gated “My Marketplace”. (Sidebar.tsx)

Add /creator/my-marketplace page with tabs (Overview, My Assets, Cancelled Assets, Upload, Payouts). (App.tsx)

Add creator endpoints and extend schema with approvalStatus='cancelled' + cancelledAt. (routes.ts & schema.ts)

Keep public marketplace routes but don’t surface them in the left sidebar anymore. (App.tsx)

Remove the ?search=true param logic (or replace with stable panel=filters if you still want a deep-link). (MarketplacePage.tsx)