diff --git a/apps/web/app/(ee)/api/track/application/route.ts b/apps/web/app/(ee)/api/track/application/route.ts
new file mode 100644
index 00000000000..4a1f9a00a77
--- /dev/null
+++ b/apps/web/app/(ee)/api/track/application/route.ts
@@ -0,0 +1,119 @@
+import { COMMON_CORS_HEADERS } from "@/lib/api/cors";
+import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
+import { parseRequestBody } from "@/lib/api/utils";
+import { getIP } from "@/lib/api/utils/get-ip";
+import { APPLICATION_ID_COOKIE } from "@/lib/application-tracker/constants";
+import { recordApplicationEvent } from "@/lib/application-tracker/record-application-event";
+import { trackApplicationInputSchema } from "@/lib/application-tracker/schema";
+import { withAxiom } from "@/lib/axiom/server";
+import { detectBot } from "@/lib/middleware/utils/detect-bot";
+import { ratelimit, redis } from "@/lib/upstash";
+import { prisma } from "@dub/prisma";
+import { nanoid } from "@dub/utils";
+import { addMonths } from "date-fns";
+import { cookies } from "next/headers";
+import { NextResponse } from "next/server";
+
+function extractProgramFromPathname(pathname: string) {
+ return pathname.split("/").filter(Boolean)[0]?.toLowerCase() ?? null;
+}
+
+// POST /api/track/application – Track an application event
+export const POST = withAxiom(async (req) => {
+ try {
+ if (detectBot(req)) {
+ return NextResponse.json(
+ { applicationId: null },
+ { status: 202, headers: COMMON_CORS_HEADERS },
+ );
+ }
+
+ const ip = await getIP();
+ const { success } = await ratelimit(10, "10 s").limit(
+ `track-application:${ip}`,
+ );
+
+ if (!success) {
+ throw new DubApiError({
+ code: "rate_limit_exceeded",
+ message: "Too many requests. Please try again later.",
+ });
+ }
+
+ let { eventName, pathname, applicationId, referrerUsername } =
+ trackApplicationInputSchema.parse(await parseRequestBody(req));
+
+ // Extract the program slug from pathname
+ const programSlug = extractProgramFromPathname(pathname);
+
+ if (!programSlug) {
+ throw new DubApiError({
+ code: "bad_request",
+ message: "Couldn't extract program slug from pathname.",
+ });
+ }
+
+ // Find the program
+ const program = await prisma.program.findUnique({
+ where: {
+ slug: programSlug,
+ },
+ select: {
+ id: true,
+ slug: true,
+ },
+ });
+
+ if (!program) {
+ throw new DubApiError({
+ code: "bad_request",
+ message: "Program not found.",
+ });
+ }
+
+ // Set or create cookie
+ if (!applicationId) {
+ applicationId = nanoid(16);
+
+ const cookieStore = await cookies();
+
+ cookieStore.set(APPLICATION_ID_COOKIE, applicationId, {
+ expires: addMonths(new Date(), 1), // 30 days
+ path: `/${program.slug}`,
+ });
+ }
+
+ // Dedupe the events
+ const redisKey = `applicationEvent:${applicationId}:${eventName}`;
+ const isFirstTime = await redis.set(redisKey, "1", {
+ nx: true,
+ ex: 30 * 24 * 60 * 60, // 30 days
+ });
+
+ console.log({ isFirstTime, redisKey });
+
+ if (isFirstTime) {
+ await recordApplicationEvent({
+ applicationId,
+ programId: program.id,
+ partnerId: "",
+ eventName,
+ req,
+ });
+ }
+
+ return NextResponse.json(
+ { applicationId },
+ { status: 202, headers: COMMON_CORS_HEADERS },
+ );
+ } catch (error) {
+ return handleAndReturnErrorResponse(error, COMMON_CORS_HEADERS);
+ }
+});
+
+export const OPTIONS = () => {
+ return new Response(null, {
+ status: 204,
+ headers: COMMON_CORS_HEADERS,
+ });
+};
diff --git a/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx b/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx
index d92a9a7c787..0100aaac64b 100644
--- a/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx
@@ -1,6 +1,7 @@
import { getProgram } from "@/lib/fetchers/get-program";
import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups";
import { programApplicationFormSchema } from "@/lib/zod/schemas/program-application-form";
+import { ApplicationTracker } from "@/ui/application-tracker/application-tracker";
import { ApplicationFormHero } from "@/ui/partners/groups/design/application-form/application-hero-preview";
import { ProgramApplicationForm } from "@/ui/partners/groups/design/application-form/program-application-form";
import { LanderRewards } from "@/ui/partners/lander/lander-rewards";
@@ -51,8 +52,8 @@ export default async function ApplicationPage(props: {
}
>
+
- {/* Hero section */}
- {/* Application form */}
diff --git a/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx b/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx
index dfc21b821e6..279e27360a6 100644
--- a/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx
@@ -1,6 +1,8 @@
import { getProgram } from "@/lib/fetchers/get-program";
import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups";
import { programLanderSchema } from "@/lib/zod/schemas/program-lander";
+
+import { ApplicationTracker } from "@/ui/application-tracker/application-tracker";
import { BLOCK_COMPONENTS } from "@/ui/partners/lander/blocks";
import { LanderHero } from "@/ui/partners/lander/lander-hero";
import { LanderRewards } from "@/ui/partners/lander/lander-rewards";
@@ -47,6 +49,7 @@ export default async function ApplyPage(props: {
} as CSSProperties
}
>
+
diff --git a/apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/register/page-client.tsx b/apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/register/page-client.tsx
index 80d711355ee..5da92c00728 100644
--- a/apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/register/page-client.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/register/page-client.tsx
@@ -14,7 +14,7 @@ import { truncate } from "@dub/utils";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
-type PartialProgram = Pick
;
+type PartialProgram = Pick;
export default function RegisterPageClient({
program,
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx
index 6d61a6fefb3..0ef3c115c42 100644
--- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx
@@ -20,6 +20,7 @@ import {
buttonVariants,
} from "@dub/ui";
import { OG_AVATAR_URL, cn } from "@dub/utils";
+import slugify from "@sindresorhus/slugify";
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import { useAction } from "next-safe-action/hooks";
import { RefObject, useEffect, useRef } from "react";
@@ -35,6 +36,7 @@ import { SettingsRow } from "./settings-row";
type BasicInfoFormData = {
name: string;
email: string;
+ username: string | null;
image: string | null;
country: string;
profileType: PartnerProfileType;
@@ -52,6 +54,7 @@ export function ProfileDetailsForm({ partner }: { partner?: PartnerProps }) {
defaultValues: {
name: partner?.name,
email: partner?.email ?? "",
+ username: partner?.username ?? null,
image: partner?.image,
country: partner?.country ?? "",
profileType: partner?.profileType ?? "individual",
@@ -291,6 +294,40 @@ function BasicInfoForm({
})}
/>
+