Here’s the clean, role-split setup you asked for, plus drop-in code so SuperNova can wire it up fast.

What we’re doing (summary)

Creators: get a new My Marketplace page (tabbed) under /creator/my-marketplace to manage listings, cancellations, uploads, and payouts.

Admins/Owners: get a Marketplace Management plaque (tile) in Admin Tools linking to /admin/marketplace for curation/oversight.

Public browsing: /marketplace stays public, but we don’t show it in the left app sidebar anymore (keeps users focused). This also removes the brittle ?search=true pattern that fought your sidebar state.

1) Sidebar cleanup (remove “Browse/Search”, add My Marketplace only for creators)

In client/src/components/Sidebar.tsx, delete the old marketplaceItems sublist (the two buttons) and the state that tried to track the active item. Replace it with a single creator-gated button:

{/* Creator Marketplace */ }
<div className="space-y-1">
  <button
    onClick={() => setIsMarketplaceOpen(!isMarketplaceOpen)}
    className="w-full flex items-center px-3 py-2 rounded-lg text-sm font-medium text-muted-foreground hover:bg-lightgray hover:text-foreground transition-colors"
    data-testid="nav-marketplace"
  >
    <ShoppingBag className="mr-3 h-4 w-4" />
    Creator Marketplace
    {isMarketplaceOpen ? <ChevronDown className="ml-auto h-4 w-4" /> : <ChevronRight className="ml-auto h-4 w-4" />}
  </button>

  {isMarketplaceOpen && (
    <div className="ml-6 space-y-1">
      {/* Always hide “Browse/Search” in sidebar now */}
      {/* Show My Marketplace ONLY if the user is a creator/admin */}
      {backendUser?.roles?.includes('creator') && (
        <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"
        >
          <User className="mr-3 h-4 w-4" />
          My Marketplace
        </button>
      )}
    </div>
  )}
</div>


(We’re removing the old local “active item” logic; URL becomes the source of truth. Your current file uses that local activeMarketplaceItem and the ?search=true race—this eliminates both. )

2) Creator page (tabbed) — client/src/pages/Creator/MyMarketplacePage.tsx

This is a tabbed version (Overview / Listings / Cancelled / Uploads / Payouts). It calls creator-scoped endpoints and matches your data model for creator assets & earnings.

import { useEffect, useState } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "wouter";
import { toast } from "sonner";
import { BarChart3, DollarSign, ShoppingBag, Upload, Eye, X, Archive } from "lucide-react";

type CreatorAsset = {
  id: string;
  title: string;
  description?: string;
  price: number;
  salesCount: number;
  createdAt: string;
  previewUrl?: string;
  approvalStatus: "approved" | "pending" | "rejected" | "cancelled";
};

type PayoutSummary = {
  lifetimeEarningsCents: number;
  pendingPayoutCents: number;
  lastPayoutAt?: string;
};

const fmt = (cents: number = 0) => `$${(cents / 100).toFixed(2)}`;

export default function MyMarketplacePage() {
  const qc = useQueryClient();

  const { data, isLoading } = useQuery({
    queryKey: ["creator-me-marketplace"],
    queryFn: async () => {
      const res = await fetch("/api/creator/me/marketplace", { credentials: "include" });
      if (!res.ok) throw new Error("Failed to load");
      return res.json() as Promise<{
        assets: { approved: CreatorAsset[]; pending: CreatorAsset[]; rejected: CreatorAsset[]; cancelled: CreatorAsset[]; };
        stats: { totalSales: number; totalRevenueCents: number; };
      }>;
    }
  });

  const { data: payouts } = useQuery({
    queryKey: ["creator-payouts-summary"],
    queryFn: async () => {
      const res = await fetch("/api/creator/me/payouts/summary", { credentials: "include" });
      if (!res.ok) throw new Error("Failed to load payouts");
      return res.json() as Promise<PayoutSummary>;
    }
  });

  const cancelMutation = useMutation({
    mutationFn: async (id: string) => {
      const res = await fetch(`/api/creator/assets/${id}/cancel`, { method: "PATCH", credentials: "include" });
      if (!res.ok) throw new Error("Failed to cancel");
      return res.json();
    },
    onSuccess: () => {
      toast.success("Listing cancelled.");
      qc.invalidateQueries({ queryKey: ["creator-me-marketplace"] });
    },
    onError: () => toast.error("Could not cancel listing.")
  });

  const loadingCards = (
    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6">
      {[...Array(6)].map((_, i) => (
        <Card key={i}><Skeleton className="aspect-video w-full" /><CardContent className="p-4"><Skeleton className="h-4 w-3/4 mb-2" /><Skeleton className="h-3 w-1/2" /></CardContent></Card>
      ))}
    </div>
  );

  const AssetGrid = ({ items, cancellable = false }: { items: CreatorAsset[]; cancellable?: boolean }) => (
    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6">
      {items.map(a => (
        <Card key={a.id} className="overflow-hidden">
          <div className="aspect-video bg-gray-100 dark:bg-gray-800">
            {a.previewUrl && <img src={a.previewUrl} alt={a.title} className="w-full h-full object-cover" />}
          </div>
          <CardContent className="p-4 space-y-2">
            <div className="flex items-start justify-between">
              <div>
                <div className="font-semibold">{a.title}</div>
                <div className="text-xs text-muted-foreground">Created {new Date(a.createdAt).toLocaleDateString()}</div>
              </div>
              <Badge variant="secondary">{fmt(a.price)}</Badge>
            </div>
            <div className="text-xs text-muted-foreground">Sales: {a.salesCount}</div>
            <div className="flex gap-2">
              <Link href={`/marketplace/asset/${a.id}`}><Button variant="outline" size="sm"><Eye className="h-4 w-4 mr-2" />View</Button></Link>
              {cancellable && <Button variant="destructive" size="sm" onClick={() => cancelMutation.mutate(a.id)}><X className="h-4 w-4 mr-2" />Cancel</Button>}
            </div>
          </CardContent>
        </Card>
      ))}
    </div>
  );

  const approved = data?.assets.approved ?? [];
  const pending = data?.assets.pending ?? [];
  const rejected = data?.assets.rejected ?? [];
  const cancelled = data?.assets.cancelled ?? [];

  return (
    <div className="space-y-8">
      {/* Overview stats */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        <Card><CardContent className="p-4 flex items-center"><DollarSign className="h-8 w-8 text-primary mr-3" /><div><div className="text-2xl font-bold">{fmt(payouts?.lifetimeEarningsCents)}</div><div className="text-sm text-muted-foreground">Lifetime Earnings</div></div></CardContent></Card>
        <Card><CardContent className="p-4 flex items-center"><ShoppingBag className="h-8 w-8 text-primary mr-3" /><div><div className="text-2xl font-bold">{approved.reduce((n, a) => n + (a.salesCount || 0), 0).toLocaleString()}</div><div className="text-sm text-muted-foreground">Sales (Active)</div></div></CardContent></Card>
        <Card><CardContent className="p-4 flex items-center"><BarChart3 className="h-8 w-8 text-primary mr-3" /><div><div className="text-2xl font-bold">{fmt(payouts?.pendingPayoutCents)}</div><div className="text-sm text-muted-foreground">Pending Payout</div></div></CardContent></Card>
      </div>

      {/* Tabs */}
      <Tabs defaultValue="listings">
        <TabsList>
          <TabsTrigger value="listings">Listings</TabsTrigger>
          <TabsTrigger value="pending">Pending</TabsTrigger>
          <TabsTrigger value="rejected">Rejected</TabsTrigger>
          <TabsTrigger value="cancelled">Cancelled</TabsTrigger>
          <TabsTrigger value="upload">Upload</TabsTrigger>
          <TabsTrigger value="payouts">Payouts</TabsTrigger>
        </TabsList>

        <TabsContent value="listings" className="mt-6">
          {isLoading ? loadingCards : (approved.length ? <AssetGrid items={approved} cancellable /> : <Card><CardContent className="p-6 text-muted-foreground">No active listings. Click Upload.</CardContent></Card>)}
        </TabsContent>

        <TabsContent value="pending" className="mt-6">
          {isLoading ? loadingCards : (pending.length ? <AssetGrid items={pending} /> : <Card><CardContent className="p-6 text-muted-foreground">Nothing pending review.</CardContent></Card>)}
        </TabsContent>

        <TabsContent value="rejected" className="mt-6">
          {isLoading ? loadingCards : (rejected.length ? <AssetGrid items={rejected} /> : <Card><CardContent className="p-6 text-muted-foreground">No rejected listings.</CardContent></Card>)}
        </TabsContent>

        <TabsContent value="cancelled" className="mt-6">
          {isLoading ? loadingCards : (cancelled.length ? <AssetGrid items={cancelled} /> : <Card><CardContent className="p-6 text-muted-foreground">No cancelled assets.</CardContent></Card>)}
        </TabsContent>

        <TabsContent value="upload" className="mt-6">
          <Card><CardContent className="p-6"><Link href="/creator/upload"><Button><Upload className="h-4 w-4 mr-2" />Go to Upload</Button></Link></CardContent></Card>
        </TabsContent>

        <TabsContent value="payouts" className="mt-6">
          <Card><CardContent className="p-6 space-y-2">
            <div>Lifetime: <strong>{fmt(payouts?.lifetimeEarningsCents)}</strong></div>
            <div>Pending: <strong>{fmt(payouts?.pendingPayoutCents)}</strong></div>
            <div className="text-sm text-muted-foreground">Last payout: {payouts?.lastPayoutAt ? new Date(payouts.lastPayoutAt).toLocaleDateString() : '—'}</div>
            <Link href="/creator/earnings"><Button variant="outline" className="mt-2">Open Earnings</Button></Link>
          </CardContent></Card>
        </TabsContent>
      </Tabs>
    </div>
  );
}

3) Admin plaque + page
3a) Add a Marketplace Management plaque in Admin Dashboard

Inside your Admin Dashboard “Admin Tools” grid, add a tile linking to /admin/marketplace:

<Link href="/admin/marketplace" className="block">
  <Button variant="secondary" className="w-full justify-start">
    🛍️ Marketplace Management
  </Button>
</Link>


(That dashboard already shows the Admin Tools button row—this is one more tile there.)

3b) New page — client/src/pages/Admin/AdminMarketplace.tsx

A compact review/search hub for staff (pending approvals, quick search, and creator filter):

import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";

type Row = {
  id: string; title: string; creatorName: string; price: number; status: string; createdAt: string;
  previewUrl?: string; salesCount?: number;
};

export default function AdminMarketplace() {
  const [search, setSearch] = useState("");
  const qc = useQueryClient();

  const { data, isLoading, refetch } = useQuery({
    queryKey: ["admin-marketplace", search],
    queryFn: async () => {
      const qs = new URLSearchParams({ search }).toString();
      const res = await fetch(`/api/admin/marketplace/overview?${qs}`, { credentials: "include" });
      if (!res.ok) throw new Error("Failed to load");
      return res.json() as Promise<{ pending: Row[]; approved: Row[]; rejected: Row[] }>;
    }
  });

  const approve = useMutation({
    mutationFn: async (id: string) => {
      const r = await fetch(`/api/admin/marketplace/assets/${id}/approve`, { method: "PATCH", credentials: "include" });
      if (!r.ok) throw new Error("fail");
      return r.json();
    },
    onSuccess: () => { toast.success("Approved"); qc.invalidateQueries({ queryKey: ["admin-marketplace"] }); }
  });

  const reject = useMutation({
    mutationFn: async (id: string) => {
      const r = await fetch(`/api/admin/marketplace/assets/${id}/reject`, { method: "PATCH", credentials: "include" });
      if (!r.ok) throw new Error("fail");
      return r.json();
    },
    onSuccess: () => { toast.success("Rejected"); qc.invalidateQueries({ queryKey: ["admin-marketplace"] }); }
  });

  const Section = ({ title, items, actions }: { title: string; items?: Row[]; actions?: boolean }) => (
    <Card>
      <CardHeader><CardTitle>{title} ({items?.length ?? 0})</CardTitle></CardHeader>
      <CardContent className="space-y-4">
        {(items ?? []).map(a => (
          <div key={a.id} className="flex items-center justify-between border rounded-md p-3">
            <div className="min-w-0">
              <div className="font-medium truncate">{a.title}</div>
              <div className="text-xs text-muted-foreground">{a.creatorName} • ${ (a.price/100).toFixed(2) } • {new Date(a.createdAt).toLocaleDateString()}</div>
            </div>
            <div className="flex items-center gap-2">
              <Badge variant="outline">{a.status}</Badge>
              {actions && (
                <>
                  <Button size="sm" onClick={() => approve.mutate(a.id)}>Approve</Button>
                  <Button size="sm" variant="destructive" onClick={() => reject.mutate(a.id)}>Reject</Button>
                </>
              )}
            </div>
          </div>
        ))}
      </CardContent>
    </Card>
  );

  return (
    <div className="space-y-6">
      <Card>
        <CardContent className="p-4 flex gap-2">
          <Input placeholder="Search title or creator…" value={search} onChange={(e) => setSearch(e.target.value)} />
          <Button onClick={() => refetch()}>Search</Button>
        </CardContent>
      </Card>
      <Section title="Pending Review" items={data?.pending} actions />
      <Section title="Approved" items={data?.approved} />
      <Section title="Rejected" items={data?.rejected} />
    </div>
  );
}

3c) Route

Add to client/src/App.tsx:

import AdminMarketplace from "@/pages/Admin/AdminMarketplace";

<Route path="/admin/marketplace"><AdminMarketplace /></Route>


(Your App router already centralizes marketplace public routes and admin pages; this adds the admin view cleanly. )

4) Backend stubs (creator + admin)

You already have solid marketplace endpoints (public browsing returns approved only; creators list their assets; delete/soft-delete exists). We’ll add the minimum to support cancel, creator overview, and admin overview/approve/reject.

Add to server/routes.ts:

// CREATOR – overview (grouped by status)
app.get('/api/creator/me/marketplace', authenticateToken, async (req, res) => {
  const user = await storage.getUserByFirebaseUid(req.user!.uid);
  const creator = user && await storage.getCreatorByUserId(user.id);
  if (!creator) return res.status(403).json({ error: 'Not a creator' });

  const rows = await storage.getCreatorAssetsByCreatorId(creator.id);
  const group = (s: string) => rows.filter(r => r.approvalStatus === s);
  res.json({
    assets: {
      approved: group('approved'),
      pending: group('pending'),
      rejected: group('rejected'),
      cancelled: group('cancelled'),
    },
    stats: {
      totalSales: rows.reduce((n, r) => n + (r.salesCount || 0), 0),
      totalRevenueCents: rows.reduce((n, r) => n + (r.totalRevenue || 0), 0),
    }
  });
});

// CREATOR – cancel listing
app.patch('/api/creator/assets/:id/cancel', authenticateToken, async (req, res) => {
  const user = await storage.getUserByFirebaseUid(req.user!.uid);
  const creator = user && await storage.getCreatorByUserId(user.id);
  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' });

  const updated = await storage.updateCreatorAsset(rec.id, { approvalStatus: 'cancelled', cancelledAt: new Date() });
  return res.json({ success: true, asset: updated });
});

// CREATOR – payouts summary
app.get('/api/creator/me/payouts/summary', authenticateToken, async (req, res) => {
  const user = await storage.getUserByFirebaseUid(req.user!.uid);
  const creator = user && await storage.getCreatorByUserId(user.id);
  if (!creator) return res.status(403).json({ error: 'Not a creator' });
  const summary = await storage.getCreatorPayoutSummary(creator.id);
  res.json(summary);
});

// ADMIN – overview
app.get('/api/admin/marketplace/overview', authenticateAdmin, async (req, res) => {
  const q = (req.query.search as string || '').toLowerCase();
  const rows = await storage.getCreatorAssets(); // all statuses
  const map = (r: any) => ({
    id: r.id, title: r.title, creatorName: r.creatorName, price: r.price,
    status: r.approvalStatus, createdAt: r.createdAt, previewUrl: r.previewUrl, salesCount: r.salesCount,
  });
  const filtered = rows.filter((r: any) => !q || r.title.toLowerCase().includes(q) || (r.creatorName||'').toLowerCase().includes(q)).map(map);
  res.json({
    pending: filtered.filter(r => r.status === 'pending'),
    approved: filtered.filter(r => r.status === 'approved'),
    rejected: filtered.filter(r => r.status === 'rejected'),
  });
});

// ADMIN – approve/reject
app.patch('/api/admin/marketplace/assets/:id/approve', authenticateAdmin, async (req, res) => {
  const updated = await storage.updateCreatorAsset(req.params.id, { approvalStatus: 'approved', approvedAt: new Date(), approvedBy: req.admin!.id });
  res.json({ success: true, asset: updated });
});
app.patch('/api/admin/marketplace/assets/:id/reject', authenticateAdmin, async (req, res) => {
  const updated = await storage.updateCreatorAsset(req.params.id, { approvalStatus: 'rejected' });
  res.json({ success: true, asset: updated });
});


Your public marketplace endpoints already enforce “approved only,” so cancelled items won’t appear publicly as long as you keep that constraint (which you do).

5) Schema note

If you don’t already have it, add cancelled to your approval enum and an optional cancelledAt timestamp on creator_assets. The table already supports statuses and timestamps—this is just one more explicit state that we’ll filter out from public browse.

6) Why this works

No more state/URL fights: the marketplace page no longer relies on ?search=true toggling (you currently remove it on mount, causing the sidebar highlight race). We keep URL purely for real filters, and highlighting is path-based.

Role-clean UX: creators manage in one place; admins curate centrally; public still browses cleanly.

Backwards-compatible: public routes (/marketplace, /marketplace/asset/:id) remain unchanged for SEO and sharability.