Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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}`);
}
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
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>
);
}
77 changes: 77 additions & 0 deletions apps/web/app/app.dub.co/marketplace/[[...segments]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { getNetworkProgram } from "@/lib/fetchers/get-network-program";
import { PROGRAM_CATEGORIES_MAP } from "@/lib/network/program-categories";
import { MarketplaceExternalRouter } from "@/ui/program-marketplace/external/marketplace-external-router";
import {
getMarketplaceCanonicalUrl,
getMarketplacePathFromSegments,
} from "@/ui/program-marketplace/utils/urls";
import { constructMetadata } from "@dub/utils";
import { Category } from "@prisma/client";
import { Metadata } from "next";

export {
generateStaticParams,
revalidate,
} from "@/ui/program-marketplace/utils/default-exports";

export async function generateMetadata(props: {
params: Promise<{ segments?: string[] }>;
}): Promise<Metadata> {
const { segments = [] } = await props.params;
const pathname = getMarketplacePathFromSegments(segments);

const year = new Date().getFullYear();

let title = `Best SaaS affiliate programs in ${year}`;
let description = `Browse and apply to the best SaaS affiliate programs on Dub's Partner Network.`;
let image: string | undefined;

if (segments.length === 1 && segments[0] === "all") {
title = "All Programs";
description = "Browse all partner programs on Dub.";
} else if (
segments.length === 1 &&
segments[0] !== "all" &&
segments[0] !== "popular"
) {
const program = await getNetworkProgram({ slug: segments[0] });

if (program) {
title = program.name;
description =
program.description ||
`Join the ${program.name} affiliate program on Dub's Partner Network.`;
image = program.marketplaceHeaderImage || program.logo || undefined;
} else {
title = "Program Details";
}
} else if (segments.length === 2 && segments[0] === "c") {
const category = Object.values(Category).find(
(value) => value.toLowerCase() === segments[1],
);

if (category) {
const categoryMeta = PROGRAM_CATEGORIES_MAP[category];
const label = categoryMeta?.label ?? category.replaceAll("_", " ");
title = `${label} Programs`;
description =
categoryMeta?.listPageDescription ??
`Partner programs in ${label.toLowerCase()}.`;
}
}

return constructMetadata({
title,
description,
image,
canonicalUrl: getMarketplaceCanonicalUrl(pathname),
});
}

export default async function MarketplaceExternalPage(props: {
params: Promise<{ segments?: string[] }>;
}) {
const { segments = [] } = await props.params;

return <MarketplaceExternalRouter segments={segments} />;
}
33 changes: 33 additions & 0 deletions apps/web/app/app.dub.co/marketplace/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MarketplaceExternalHeader } from "@/ui/program-marketplace/external/marketplace-external-header";
import { Footer } from "@dub/ui";
import { PropsWithChildren } from "react";

export default function MarketplaceExternalLayout({
children,
}: PropsWithChildren) {
return (
<div className="flex min-h-screen flex-col bg-white">
<MarketplaceExternalHeader />
<div className="relative flex flex-1 flex-col">
<MarketplaceExternalGridLines />
<main className="flex-1">{children}</main>
<div className="relative z-10 h-px w-full bg-neutral-200" />
<Footer className="border-t-0 md:rounded-t-none" />
</div>
</div>
);
}

function MarketplaceExternalGridLines() {
return (
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 flex justify-center"
>
<div className="relative h-full w-full max-w-screen-xl">
<div className="absolute inset-y-0 left-0 w-px bg-neutral-200 [mask-image:linear-gradient(to_bottom,transparent,#000_96px)]" />
<div className="absolute inset-y-0 right-0 w-px bg-neutral-200 [mask-image:linear-gradient(to_bottom,transparent,#000_96px)]" />
</div>
</div>
);
}
Loading
Loading