Skip to content

Commit 238c2f8

Browse files
authored
Merge pull request #2168 from dubinc/program-lander-og
Program lander OG image
2 parents 41575ee + 60db279 commit 238c2f8

File tree

10 files changed

+428
-204
lines changed

10 files changed

+428
-204
lines changed

apps/web/app/api/og/program/route.tsx

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { constructRewardAmount } from "@/lib/api/sales/construct-reward-amount";
2+
import { getProgramViaEdge } from "@/lib/planetscale/get-program-via-edge";
3+
import { ImageResponse } from "next/og";
4+
import { NextRequest } from "next/server";
5+
import { SVGProps } from "react";
6+
7+
export const runtime = "edge";
8+
9+
const DARK_CELLS = [
10+
[2, 3],
11+
[5, 3],
12+
[56, 7],
13+
[53, 1],
14+
];
15+
16+
export async function GET(req: NextRequest) {
17+
const [interMedium, interSemibold] = await Promise.all([
18+
fetch(new URL("@/styles/Inter-Medium.ttf", import.meta.url)).then((res) =>
19+
res.arrayBuffer(),
20+
),
21+
fetch(new URL("@/styles/Inter-Semibold.ttf", import.meta.url)).then((res) =>
22+
res.arrayBuffer(),
23+
),
24+
]);
25+
26+
const slug = req.nextUrl.searchParams.get("slug");
27+
28+
if (!slug)
29+
return new Response("Missing 'slug' parameter", {
30+
status: 400,
31+
});
32+
33+
const program = await getProgramViaEdge({ slug });
34+
35+
if (!program || !program.landerData)
36+
return new Response(`Program not found`, {
37+
status: 404,
38+
});
39+
40+
const logo = program.wordmark || program.logo;
41+
const brandColor = program.brandColor || "#000000";
42+
43+
return new ImageResponse(
44+
(
45+
<div
46+
tw="flex flex-col bg-white w-full h-full"
47+
style={{ fontFamily: "Inter Medium" }}
48+
>
49+
{/* @ts-ignore */}
50+
<svg tw="absolute inset-0 text-black/10" width="1200" height="630">
51+
<defs>
52+
<pattern
53+
id="grid"
54+
width={20}
55+
height={20}
56+
patternUnits="userSpaceOnUse"
57+
>
58+
<path
59+
d={`M 20 0 L 0 0 0 20`}
60+
fill="transparent"
61+
stroke="currentColor"
62+
strokeWidth={1}
63+
/>
64+
</pattern>
65+
<pattern
66+
id="grid-large"
67+
width={160}
68+
height={160}
69+
patternUnits="userSpaceOnUse"
70+
>
71+
<path
72+
d={`M 160 0 L 0 0 0 160`}
73+
fill="transparent"
74+
stroke="currentColor"
75+
strokeOpacity={0.5}
76+
strokeWidth={1}
77+
/>
78+
</pattern>
79+
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
80+
<stop offset="0%" stopColor="#fff" stopOpacity={0} />
81+
<stop offset="100%" stopColor="#fff" stopOpacity={1} />
82+
</linearGradient>
83+
</defs>
84+
{DARK_CELLS.map(([x, y]) => (
85+
<rect
86+
key={`${x}-${y}`}
87+
x={x * 20 + 1}
88+
y={y * 20 + 1}
89+
width={19}
90+
height={19}
91+
fill="black"
92+
fillOpacity={0.02}
93+
/>
94+
))}
95+
<rect fill="url(#grid)" width="1200" height="630" />
96+
<rect fill="url(#grid-large)" width="1200" height="630" />
97+
<rect fill="url(#gradient)" width="1200" height="630" />
98+
</svg>
99+
100+
<div tw="relative flex flex-col mx-auto h-full bg-white w-[879px] px-16 py-20 overflow-hidden">
101+
{logo && <img src={logo} height={48} />}
102+
<div
103+
tw="mt-16 text-left uppercase text-lg"
104+
style={{ color: brandColor }}
105+
>
106+
Partner Program
107+
</div>
108+
<div
109+
tw="mt-4 text-4xl font-semibold text-neutral-800"
110+
style={{
111+
display: "block",
112+
lineClamp: 2,
113+
textOverflow: "ellipsis",
114+
fontFamily: "Inter Semibold",
115+
}}
116+
>
117+
{`Join the ${program.name} affiliate program`}
118+
</div>
119+
<div tw="mt-10 flex">
120+
{program.rewardAmount && program.rewardType && (
121+
<div tw="w-full flex items-center rounded-md bg-neutral-100 border border-neutral-200 p-8 text-2xl">
122+
{/* @ts-ignore */}
123+
<InvoiceDollar tw="w-8 h-8 mr-4" />
124+
<strong
125+
tw="font-semibold mr-1"
126+
style={{ fontFamily: "Inter Semibold" }}
127+
>
128+
{constructRewardAmount({
129+
amount: program.rewardAmount,
130+
type: program.rewardType as any,
131+
})}{" "}
132+
per {program.rewardEvent}
133+
</strong>
134+
{program.rewardMaxDuration === null ? (
135+
"for the customer's lifetime"
136+
) : program.rewardMaxDuration &&
137+
program.rewardMaxDuration > 1 ? (
138+
<>
139+
, and again every month for {program.rewardMaxDuration}{" "}
140+
months
141+
</>
142+
) : null}
143+
</div>
144+
)}
145+
</div>
146+
<div
147+
tw="mt-10 text-white px-4 h-16 flex items-center text-2xl justify-center rounded-lg border-2 border-white/30 shadow-xl"
148+
style={{
149+
fontFamily: "Inter Semibold",
150+
backgroundColor: brandColor,
151+
}}
152+
>
153+
Apply today
154+
</div>
155+
</div>
156+
</div>
157+
),
158+
{
159+
width: 1200,
160+
height: 630,
161+
fonts: [
162+
{
163+
name: "Inter Medium",
164+
data: interMedium,
165+
},
166+
{
167+
name: "Inter Semibold",
168+
data: interSemibold,
169+
},
170+
],
171+
},
172+
);
173+
}
174+
175+
function InvoiceDollar({
176+
strokeWidth = 1.5,
177+
...props
178+
}: SVGProps<SVGSVGElement>) {
179+
return (
180+
<svg
181+
height="18"
182+
width="18"
183+
viewBox="0 0 18 18"
184+
xmlns="http://www.w3.org/2000/svg"
185+
{...props}
186+
>
187+
<g fill="currentColor">
188+
<path
189+
d="M14.75,3.75v12.5l-2.75-1.5-3,1.5-3-1.5-2.75,1.5V3.75c0-1.105,.895-2,2-2h7.5c1.105,0,2,.895,2,2Z"
190+
fill="none"
191+
stroke="currentColor"
192+
strokeLinecap="round"
193+
strokeLinejoin="round"
194+
strokeWidth={strokeWidth}
195+
/>
196+
<path
197+
d="M10.724,6.556c-.374-.885-1.122-1.086-1.688-1.086-.526,0-1.907,.28-1.779,1.606,.09,.931,.967,1.277,1.734,1.414s1.88,.429,1.907,1.551c.023,.949-.83,1.597-1.861,1.597-.985,0-1.67-.383-1.934-1.25"
198+
fill="none"
199+
stroke="currentColor"
200+
strokeLinecap="round"
201+
strokeLinejoin="round"
202+
strokeWidth={strokeWidth}
203+
/>
204+
<line
205+
fill="none"
206+
stroke="currentColor"
207+
strokeLinecap="round"
208+
strokeLinejoin="round"
209+
strokeWidth={strokeWidth}
210+
x1="9"
211+
x2="9"
212+
y1="4.75"
213+
y2="5.47"
214+
/>
215+
<line
216+
fill="none"
217+
stroke="currentColor"
218+
strokeLinecap="round"
219+
strokeLinejoin="round"
220+
strokeWidth={strokeWidth}
221+
x1="9"
222+
x2="9"
223+
y1="11.638"
224+
y2="12.25"
225+
/>
226+
</g>
227+
</svg>
228+
);
229+
}

apps/web/app/partners.dub.co/(apply)/[programSlug]/layout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { formatRewardDescription } from "@/ui/partners/program-reward-descriptio
44
import { prisma } from "@dub/prisma";
55
import { Prisma } from "@dub/prisma/client";
66
import { Wordmark } from "@dub/ui";
7+
import { APP_DOMAIN } from "@dub/utils";
78
import { constructMetadata } from "@dub/utils/src/functions";
89
import { notFound } from "next/navigation";
910
import { PropsWithChildren } from "react";
@@ -26,6 +27,7 @@ export async function generateMetadata({
2627
description: `Join the ${program.name} affiliate program and earn ${formatRewardDescription(
2728
{ reward },
2829
)} by referring ${program.name} to your friends and followers.`,
30+
image: `${APP_DOMAIN}/api/og/program?slug=${program.slug}`,
2931
});
3032
}
3133

apps/web/app/partners.dub.co/(auth)/(default)/waitlist/page-client.tsx

-73
This file was deleted.

apps/web/app/partners.dub.co/(auth)/(default)/waitlist/page.tsx

-10
This file was deleted.

apps/web/lib/middleware/partners.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const AUTHENTICATED_PATHS = [
77
"/programs",
88
"/marketplace",
99
"/onboarding",
10-
"/waitlist",
1110
"/settings",
1211
"/account",
1312
];
@@ -21,6 +20,10 @@ export default async function PartnersMiddleware(req: NextRequest) {
2120
(p) => path === "/" || path.startsWith(p),
2221
);
2322

23+
const isLoginPath = ["/login", "/register"].some(
24+
(p) => path.startsWith(p) || path.endsWith(p),
25+
);
26+
2427
if (!user && isAuthenticatedPath) {
2528
if (path.startsWith(`/programs/`)) {
2629
const programSlug = path.split("/")[2];
@@ -33,18 +36,20 @@ export default async function PartnersMiddleware(req: NextRequest) {
3336
req.url,
3437
),
3538
);
36-
} else if (user) {
39+
} else if (user && (isAuthenticatedPath || isLoginPath)) {
3740
const defaultPartnerId = await getDefaultPartnerId(user);
3841

3942
if (!defaultPartnerId && !path.startsWith("/onboarding")) {
4043
return NextResponse.redirect(new URL("/onboarding", req.url));
4144
} else if (path === "/" || path.startsWith("/pn_")) {
4245
return NextResponse.redirect(new URL("/programs", req.url));
43-
} else if (
44-
["/login", "/register"].some(
45-
(p) => path.startsWith(p) || path.endsWith(p),
46-
)
47-
) {
46+
} else if (isLoginPath) {
47+
// if is custom program login or register path, redirect to /programs/:programSlug
48+
const programSlugRegex = /^\/([^\/]+)\/(login|register)$/;
49+
const match = path.match(programSlugRegex);
50+
if (match) {
51+
return NextResponse.redirect(new URL(`/programs/${match[1]}`, req.url));
52+
}
4853
return NextResponse.redirect(new URL("/", req.url)); // Redirect authenticated users to dashboard
4954
}
5055
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { conn } from "./connection";
2+
import { EdgeProgramProps } from "./types";
3+
4+
export const getProgramViaEdge = async ({ slug }: { slug: string }) => {
5+
const { rows } =
6+
(await conn.execute(
7+
`SELECT Program.*,
8+
Reward.event AS rewardEvent, Reward.type AS rewardType, Reward.amount AS rewardAmount, Reward.maxDuration as rewardMaxDuration
9+
FROM Program
10+
LEFT JOIN Reward ON Reward.id = Program.defaultRewardId
11+
WHERE slug = ?`,
12+
[slug],
13+
)) || {};
14+
15+
const program =
16+
rows && Array.isArray(rows) && rows.length > 0
17+
? (rows[0] as EdgeProgramProps)
18+
: null;
19+
20+
return program;
21+
};

0 commit comments

Comments
 (0)