Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
600d69c
Add external program marketplace on dub.co and app.dub.co.
pepeladeira Jun 18, 2026
6babeb7
server-render external marketplace list + consolidate program count l…
pepeladeira Jun 18, 2026
01ba251
external marketplace ssr improvements
pepeladeira Jun 18, 2026
611af52
srr improvements
pepeladeira Jun 18, 2026
1b44926
ssr improvements
pepeladeira Jun 18, 2026
24bfca3
fix build error
pepeladeira Jun 18, 2026
493d088
update get-public-network-programs
pepeladeira Jun 18, 2026
73c969e
missed a few spots
steven-tey Jun 18, 2026
469af77
fix more missed issues
steven-tey Jun 19, 2026
ce5007e
fix merge conflicts
steven-tey Jun 24, 2026
c3768ff
skip auth redirect
steven-tey Jun 24, 2026
05b439a
Update middleware.ts
steven-tey Jun 24, 2026
4f6a220
small improvements
steven-tey Jun 24, 2026
6e4440e
redirect to marketplace page
steven-tey Jun 24, 2026
cc5d872
default exports: generateStaticParams, revalidate
steven-tey Jun 26, 2026
8e9c962
fix getMarketplaceProgramsSummary
steven-tey Jun 26, 2026
bd9cf5c
simplify marketplace url utils
steven-tey Jun 26, 2026
9d10da3
Merge branch 'main' into external-program-marketplace
steven-tey Jun 26, 2026
75d28fa
remove console log
steven-tey Jun 26, 2026
21fa6ac
Merge branch 'main' into external-program-marketplace
pepeladeira Jun 26, 2026
1f00a68
Merge branch 'main' into external-program-marketplace
steven-tey Jun 28, 2026
c70e311
Merge branch 'main' into external-program-marketplace
steven-tey Jun 29, 2026
f7b5caa
Merge branch 'main' into external-program-marketplace
steven-tey Aug 18, 2026
531c652
fix app redirects
steven-tey Aug 18, 2026
ed6bcbe
Merge branch 'main' into external-program-marketplace
steven-tey Aug 18, 2026
9decc83
Merge branch 'main' into external-program-marketplace
steven-tey Aug 18, 2026
5c172c7
fix nav
steven-tey Aug 18, 2026
2a68dcc
Merge branch 'main' into external-program-marketplace
steven-tey Sep 3, 2026
56c1b1a
Merge branch 'main' into external-program-marketplace
steven-tey Sep 3, 2026
de4c244
finalize copy
steven-tey Sep 3, 2026
243873c
sitemap
pepeladeira Sep 3, 2026
0c3bf9b
update OG images
steven-tey Sep 4, 2026
4c80195
Merge branch 'main' into external-program-marketplace
steven-tey Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/web/app/(ee)/api/marketplace/programs/counts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getNetworkProgramCounts } from "@/lib/fetchers/get-network-program-counts";
import { parsePublicMarketplaceQuery } from "@/lib/marketplace/parse-public-marketplace-query";
import { NextResponse } from "next/server";

// cache filtered/searched count responses at the edge (public, not partner-scoped)
export const revalidate = 3600;

// GET /api/marketplace/programs/counts - public marketplace total + filter counts
export async function GET(req: Request) {
const searchParams = Object.fromEntries(new URL(req.url).searchParams);
const { category, rewardType, search } =
parsePublicMarketplaceQuery(searchParams);

const counts = await getNetworkProgramCounts({
category,
rewardType,
search,
});

return NextResponse.json(counts);
}
16 changes: 16 additions & 0 deletions apps/web/app/(ee)/api/marketplace/programs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getPublicNetworkPrograms } from "@/lib/fetchers/get-public-network-programs";
import { parsePublicMarketplaceQuery } from "@/lib/marketplace/parse-public-marketplace-query";
import { NextResponse } from "next/server";

// cache filtered/searched list responses at the edge (public, not partner-scoped)
export const revalidate = 3600;

// GET /api/marketplace/programs - public marketplace program list
export async function GET(req: Request) {
const searchParams = Object.fromEntries(new URL(req.url).searchParams);
const params = parsePublicMarketplaceQuery(searchParams);

const programs = await getPublicNetworkPrograms(params);

return NextResponse.json(programs);
}
59 changes: 10 additions & 49 deletions apps/web/app/(ee)/api/network/programs/count/route.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,23 @@
import { withPartnerProfile } from "@/lib/auth/partner";
import { buildNetworkProgramCountWhereSql } from "@/lib/fetchers/get-network-program-counts";
import { prisma } from "@/lib/prisma";
import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups";
import { getNetworkProgramsCountQuerySchema } from "@/lib/zod/schemas/program-network";
import { Prisma } from "@prisma/client";
import { NextResponse } from "next/server";

const rewardTypeMap = {
sale: Prisma.sql`pg.saleRewardId IS NOT NULL`,
lead: Prisma.sql`pg.leadRewardId IS NOT NULL`,
click: Prisma.sql`pg.clickRewardId IS NOT NULL`,
discount: Prisma.sql`pg.discountId IS NOT NULL`,
};

// GET /api/network/programs/count - get the number of available programs in the network
export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
const { groupBy, category, rewardType, status, featured, search } =
getNetworkProgramsCountQuerySchema.parse(searchParams);

const searchSql = search ? Prisma.sql`CONCAT('%', ${search}, '%')` : null;
const commonWhereSql = Prisma.sql`
p.addedToMarketplaceAt IS NOT NULL
AND EXISTS (
SELECT 1 FROM PartnerGroup pg
WHERE
pg.programId = p.id
AND pg.slug = ${DEFAULT_PARTNER_GROUP.slug}
AND pg.applicationFormPublishedAt IS NOT NULL
${
rewardType && groupBy !== "rewardType"
? Prisma.sql`
AND ${rewardTypeMap[rewardType]}`
: Prisma.sql``
}
)
${
category && groupBy !== "category"
? Prisma.sql`
AND EXISTS (
SELECT 1 FROM ProgramCategory pc
WHERE pc.programId = p.id AND pc.category = ${category}
)`
: Prisma.sql``
}
${
status !== undefined && groupBy !== "status"
? Prisma.sql`
AND ${status === null ? Prisma.sql`NOT` : Prisma.sql``} EXISTS (
SELECT 1 FROM ProgramEnrollment pe
WHERE
pe.programId = p.id
AND pe.partnerId = ${partner.id}
${status === null ? Prisma.sql`` : Prisma.sql`AND pe.status = ${status}`}
)`
: Prisma.sql``
}
${featured !== undefined ? Prisma.sql`AND p.featuredOnMarketplaceAt IS ${featured ? Prisma.sql`NOT` : Prisma.sql``} NULL` : Prisma.sql``}
${searchSql ? Prisma.sql`AND (p.name LIKE ${searchSql} OR p.slug LIKE ${searchSql} OR p.domain LIKE ${searchSql})` : Prisma.sql``}
`;
const commonWhereSql = buildNetworkProgramCountWhereSql({
category: groupBy === "category" ? undefined : category,
rewardType: groupBy === "rewardType" ? undefined : rewardType,
status: groupBy === "status" ? undefined : status,
partnerId: partner.id,
featured,
search,
});

if (groupBy === "category") {
const categories = (await prisma.$queryRaw`
Expand Down Expand Up @@ -97,7 +58,7 @@ export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
SELECT pe.status, COUNT(p.id) AS _count
FROM Program p
LEFT JOIN ProgramEnrollment pe ON p.id = pe.programId AND pe.partnerId = ${partner.id}
WHERE p.addedToMarketplaceAt IS NOT NULL AND ${commonWhereSql}
WHERE ${commonWhereSql}
GROUP BY pe.status
ORDER BY _count DESC
`) as { status: string | null; _count: bigint }[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ export default async function ApplicationPage(props: {
!program.group.applicationFormData ||
!program.group.applicationFormPublishedAt
) {
// throw 404 if it's the default group, else redirect to the default group page
// for the default group:
// - if the program is found, redirect to the marketplace program page
// - otherwise, throw 404
// for other groups: redirect to the default group variant
if (partnerGroupSlug === DEFAULT_PARTNER_GROUP.slug) {
if (program) {
redirect(`/marketplace/${programSlug}`);
}
Comment thread
steven-tey marked this conversation as resolved.
notFound();
} else {
redirect(`/${programSlug}/apply`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ export default async function SuccessPage(props: {
!program.group.applicationFormData ||
!program.group.applicationFormPublishedAt
) {
// throw 404 if it's the default group, else redirect to the default group page
// for the default group:
// - if the program is found, redirect to the marketplace program page
// - otherwise, throw 404
// for other groups: redirect to the default group variant
if (partnerGroupSlug === DEFAULT_PARTNER_GROUP.slug) {
if (program) {
redirect(`/marketplace/${programSlug}`);
}
notFound();
} else {
redirect(`/${programSlug}/apply`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ export default async function ApplyPage(props: {
!program.group.landerData ||
!program.group.landerPublishedAt
) {
// throw 404 if it's the default group, else redirect to the default group page
// for the default group:
// - if the program is found, redirect to the marketplace program page
// - otherwise, throw 404
// for other groups: redirect to the default group variant
if (partnerGroupSlug === DEFAULT_PARTNER_GROUP.slug) {
if (program) {
redirect(`/marketplace/${programSlug}`);
}
notFound();
} else {
redirect(`/${programSlug}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { MarketplaceRouter } from "@/ui/program-marketplace/marketplace-router";
import {
generateMarketplaceProgramStaticParams,
revalidate,
} from "@/ui/program-marketplace/pages/marketplace-program-page";

export { revalidate };

export async function generateStaticParams() {
return generateMarketplaceProgramStaticParams();
}
export {
generateStaticParams,
revalidate,
} from "@/ui/program-marketplace/utils/default-exports";

export default async function MarketplacePage(props: {
params: Promise<{ segments?: string[] }>;
Expand Down
142 changes: 142 additions & 0 deletions apps/web/app/api/og/program/categories/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { PROGRAM_CATEGORIES } from "@/lib/network/program-categories";
import { DUB_WORDMARK } from "@dub/utils";
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
import { loadGoogleFont } from "../../load-google-font";

const DARK_CELLS = [
[2, 3],
[5, 3],
[56, 7],
[53, 1],
];

// GET /api/og/program/categories?categorySlug=ai
export async function GET(req: NextRequest) {
const categorySlug = req.nextUrl.searchParams.get("categorySlug");

if (!categorySlug) {
return new Response("Missing 'categorySlug' parameter", {
status: 400,
});
}

const category = PROGRAM_CATEGORIES.find(
({ id }) => id.toLowerCase() === categorySlug.toLowerCase(),
);

if (!category) {
return new Response("Category not found", {
status: 404,
});
}

const interSemibold = await loadGoogleFont("Inter:wght@600");

return new ImageResponse(
(
<div
tw="flex flex-col bg-white w-full h-full"
style={{ fontFamily: "Inter" }}
>
{/* @ts-ignore */}
<svg tw="absolute inset-0 text-black/10" width="1200" height="630">
<defs>
<pattern
id="grid"
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<path
d={`M 20 0 L 0 0 0 20`}
fill="transparent"
stroke="currentColor"
strokeWidth={1}
/>
</pattern>
<pattern
id="grid-large"
width={160}
height={160}
patternUnits="userSpaceOnUse"
>
<path
d={`M 160 0 L 0 0 0 160`}
fill="transparent"
stroke="currentColor"
strokeOpacity={0.5}
strokeWidth={1}
/>
</pattern>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#fff" stopOpacity={0} />
<stop offset="100%" stopColor="#fff" stopOpacity={1} />
</linearGradient>
</defs>
{DARK_CELLS.map(([x, y]) => (
<rect
key={`${x}-${y}`}
x={x * 20 + 1}
y={y * 20 + 1}
width={19}
height={19}
fill="black"
fillOpacity={0.02}
/>
))}
<rect fill="url(#grid)" width="1200" height="630" />
<rect fill="url(#grid-large)" width="1200" height="630" />
<rect fill="url(#gradient)" width="1200" height="630" />
</svg>

<div tw="relative flex flex-col mx-auto h-full bg-white w-[920px] px-16 py-20 overflow-hidden">
<div tw="flex items-center">
<img src={DUB_WORDMARK} height={56} />
<div tw="ml-4 h-10 w-px bg-neutral-300" />
<div tw="ml-4 text-left uppercase text-2xl text-neutral-500">
Program Marketplace
</div>
</div>
<div
tw="mt-32 text-5xl font-semibold text-neutral-800"
style={{
display: "block",
lineClamp: 2,
textOverflow: "ellipsis",
fontFamily: "Inter",
}}
>
{`${category.label} Affiliate Programs`}
</div>
<div tw="mt-10 flex">
<div
tw="w-full flex items-center rounded-md bg-neutral-100 border border-neutral-200 p-10 text-3xl"
style={{
display: "block",
lineClamp: 3,
textOverflow: "ellipsis",
}}
>
{category.listPageDescription}
</div>
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: interSemibold
? [
{
name: "Inter",
data: interSemibold,
style: "normal",
weight: 600,
},
]
: [],
},
);
}
5 changes: 3 additions & 2 deletions apps/web/app/app.dub.co/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NewsRSC } from "@/ui/layout/sidebar/news-rsc";
import { ReferButton } from "@/ui/layout/sidebar/refer-button";
import Toolbar from "@/ui/layout/toolbar/toolbar";
import { UpgradeBanner } from "@/ui/layout/upgrade-banner";
import { ModalProvider } from "@/ui/modals/modal-provider";
import { constructMetadata } from "@dub/utils";
import { ReactNode } from "react";

Expand All @@ -13,7 +14,7 @@ export const metadata = constructMetadata();

export default async function Layout({ children }: { children: ReactNode }) {
return (
<>
<ModalProvider>
<div className="min-h-screen w-full bg-white">
<UpgradeBanner />
<MainNav
Expand All @@ -30,6 +31,6 @@ export default async function Layout({ children }: { children: ReactNode }) {
</MainNav>
</div>
<Toolbar show={["onboarding"]} />
</>
</ModalProvider>
);
}
5 changes: 2 additions & 3 deletions apps/web/app/app.dub.co/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client";

import { ModalProvider } from "@/ui/modals/modal-provider";
import { Analytics as DubAnalytics } from "@dub/analytics/react";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
import { ReactNode, Suspense } from "react";

export default function AppLayout({ children }: { children: ReactNode }) {
return (
Expand All @@ -17,7 +16,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
refer: "refer.dub.co",
}}
/>
<ModalProvider>{children}</ModalProvider>
<Suspense>{children}</Suspense>
</SessionProvider>
);
}
Loading
Loading