Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
432c3e4
Partner rewind sidebar card
TWilson023 Dec 9, 2025
e2baf80
Merge branch 'program-marketplace-promo' into partner-rewind
TWilson023 Dec 10, 2025
f560f28
Add banner
TWilson023 Dec 10, 2025
74cecee
Back-end stuff
TWilson023 Dec 10, 2025
f3b7a68
Page setup
TWilson023 Dec 10, 2025
2658cd7
Intro
TWilson023 Dec 10, 2025
1a8714c
Update page-client.tsx
TWilson023 Dec 10, 2025
3d241c3
Merge branch 'main' into partner-rewind
TWilson023 Dec 11, 2025
93f41fc
Main rewind state logic + animations
TWilson023 Dec 11, 2025
a3484a7
Hook up rewind data
TWilson023 Dec 11, 2025
256cdf9
WIP review slides
TWilson023 Dec 11, 2025
3f590d8
WIP rewind slides
TWilson023 Dec 11, 2025
b4ee416
Merge branch 'main' into partner-rewind
TWilson023 Dec 11, 2025
c192f0c
Merge branch 'main' into partner-rewind
TWilson023 Dec 12, 2025
1b8ee01
Add conclusion
TWilson023 Dec 12, 2025
fdec78f
WIP images
TWilson023 Dec 12, 2025
3e73dcf
Shareable images
TWilson023 Dec 12, 2025
819da0f
Update route.tsx
TWilson023 Dec 12, 2025
1dceb23
Create dub-partner-rewind.tsx
TWilson023 Dec 12, 2025
ee63e5f
Mobile rewind card improvements
TWilson023 Dec 12, 2025
838ee11
Merge branch 'main' into partner-rewind
steven-tey Dec 15, 2025
c78f321
Merge branch 'main' into partner-rewind
TWilson023 Dec 15, 2025
9009162
Script + tweak
TWilson023 Dec 15, 2025
2e1543f
Update conclusion.tsx
TWilson023 Dec 15, 2025
b762b31
Update partner-rewind-card.tsx
TWilson023 Dec 15, 2025
0b4327b
Add $10 earnings minimum
TWilson023 Dec 15, 2025
b59942b
Merge branch 'main' into partner-rewind
TWilson023 Dec 16, 2025
bdac89f
Typo fixes
TWilson023 Dec 16, 2025
4ef0399
Delete card-stack.tsx
TWilson023 Dec 16, 2025
3204e06
Update rewind.tsx
TWilson023 Dec 16, 2025
7217463
Update share-rewind-modal.tsx
TWilson023 Dec 16, 2025
ae4fcb3
Update share-rewind-modal.tsx
TWilson023 Dec 16, 2025
1749618
Merge branch 'main' into partner-rewind
steven-tey Dec 17, 2025
87421c7
update script, REWIND_EARNINGS_MINIMUM
steven-tey Dec 17, 2025
942d69e
rename route
steven-tey Dec 17, 2025
aa8f531
add DubPartnerRewind to EMAIL_TEMPLATES_MAP
steven-tey Dec 17, 2025
cf579f8
show ProgramMarketplaceBanner when partner rewind is dismissed
steven-tey Dec 17, 2025
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
19 changes: 19 additions & 0 deletions apps/web/app/(ee)/api/partner-profile/rewind/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DubApiError } from "@/lib/api/errors";
import { getPartnerRewind } from "@/lib/api/partners/get-partner-rewind";
import { withPartnerProfile } from "@/lib/auth/partner";
import { NextResponse } from "next/server";

// GET /api/partner-profile/rewind - get a partner rewind
export const GET = withPartnerProfile(async ({ partner }) => {
const partnerRewind = await getPartnerRewind({
partnerId: partner.id,
});

if (!partnerRewind)
throw new DubApiError({
code: "not_found",
message: "Partner rewind not found",
});

return NextResponse.json(partnerRewind);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import useProgramEnrollments from "@/lib/swr/use-program-enrollments";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";
import { ProgramCard, ProgramCardSkeleton } from "@/ui/partners/program-card";
import { ProgramMarketplaceBanner } from "@/ui/partners/program-marketplace/program-marketplace-banner";
import { PartnerRewindBanner } from "@/ui/partners/rewind/partner-rewind-banner";
import { SimpleEmptyState } from "@/ui/shared/simple-empty-state";
import { HexadecagonStar } from "@dub/ui/icons";
import { useId } from "react";
Expand All @@ -20,7 +20,8 @@ export function PartnersDashboardPageClient() {

return (
<PageWidthWrapper className="pb-10">
<ProgramMarketplaceBanner />
{/* <ProgramMarketplaceBanner /> // TODO: Add back after rewind is over */}
<PartnerRewindBanner />

{programEnrollments?.length == 0 ? (
<SimpleEmptyState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { usePartnerRewindStatus } from "@/ui/partners/rewind/use-partner-rewind-status";
import { Button, ChevronLeft, Wordmark } from "@dub/ui";
import { cn } from "@dub/utils";
import { useEffect } from "react";
import { navButtonClassName } from "./rewind";

export function Conclusion({
onRestart,
onClose,
}: {
onRestart: () => void;
onClose: () => void;
}) {
const { status, setStatus } = usePartnerRewindStatus();

// Set status to card (shows in sidebar) after the user has finished the rewind
useEffect(() => {
if (status !== "card") setStatus("card");
}, [status]);

return (
<div className="flex flex-col items-center gap-6 text-center">
<div className={cn("flex flex-col items-center gap-2")}>
<Wordmark
className={cn(
"h-12",
"animate-slide-up-fade [--offset:10px] [animation-duration:1.5s]",
)}
/>
<h2
className={cn(
"text-content-emphasis text-2xl font-bold",
"animate-slide-up-fade [--offset:10px] [animation-delay:0.2s] [animation-duration:1.5s] [animation-fill-mode:both]",
)}
>
Partner Rewind &rsquo;25
</h2>
</div>
<p
className={cn(
"text-content-default max-w-[480px] text-pretty text-xl font-medium",
"animate-slide-up-fade [--offset:10px] [animation-delay:0.3s] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
Thank you for all your hard work being a Dub Partner. We can&rsquo;t
wait to see what you&rsquo;ll do in 2026!
</p>
<div className="animate-slide-up-fade flex items-center gap-2 [--offset:10px] [animation-delay:0.4s] [animation-duration:1s] [animation-fill-mode:both]">
<button
type="button"
onClick={onRestart}
className={navButtonClassName}
>
<ChevronLeft className="size-3 [&_*]:stroke-2" />
</button>
<Button
text="Close"
className="h-8 w-fit rounded-lg px-4"
onClick={onClose}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Button, Wordmark } from "@dub/ui";
import { cn } from "@dub/utils";

export function Intro({ onStart }: { onStart: () => void }) {
return (
<div className="flex flex-col items-center gap-6 text-center">
<div
className={cn(
"flex flex-col items-center gap-2",
"animate-partner-rewind-intro",
)}
>
<Wordmark
className={cn(
"h-12",
"animate-slide-up-fade [--offset:10px] [animation-duration:1.5s]",
)}
/>
<h2
className={cn(
"text-content-emphasis text-2xl font-bold",
"animate-slide-up-fade [--offset:10px] [animation-delay:0.2s] [animation-duration:1.5s] [animation-fill-mode:both]",
)}
>
Partner Rewind &rsquo;25
</h2>
</div>
<p
className={cn(
"text-content-default max-w-[420px] text-pretty text-xl font-medium",
"animate-slide-up-fade [--offset:10px] [animation-delay:1.9s] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
This was a huge year for partners. Let&rsquo;s rewind to have a look at
your 2025 impact.
</p>
<div className="animate-slide-up-fade [--offset:10px] [animation-delay:2s] [animation-duration:1s] [animation-fill-mode:both]">
<Button
text="Rewind the year"
className="h-9 w-fit rounded-lg px-4"
onClick={onStart}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { PartnerRewindProps } from "@/lib/types";
import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Conclusion } from "./conclusion";
import { Intro } from "./intro";
import { Rewind } from "./rewind";

export function PartnerRewind2025PageClient({
partnerRewind,
}: {
partnerRewind: PartnerRewindProps;
}) {
const router = useRouter();

const [state, setState] = useState<"intro" | "rewind" | "conclusion">(
"intro",
);

return (
<AnimatePresence initial={false} mode="wait">
<motion.div
key={state}
initial={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
transition={{ duration: 0.5 }}
className="w-full"
>
{state === "intro" && <Intro onStart={() => setState("rewind")} />}
{state === "rewind" && (
<Rewind
partnerRewind={partnerRewind}
onComplete={() => setState("conclusion")}
/>
)}
{state === "conclusion" && (
<Conclusion
onRestart={() => setState("intro")}
onClose={() => router.push("/")}
/>
)}
</motion.div>
</AnimatePresence>
);
}
78 changes: 78 additions & 0 deletions apps/web/app/(ee)/partners.dub.co/(dashboard)/rewind/2025/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { getPartnerRewind } from "@/lib/api/partners/get-partner-rewind";
import { getSession } from "@/lib/auth";
import { PageContent } from "@/ui/layout/page-content";
import {
REWIND_ASSETS_PATH,
REWIND_STEPS,
} from "@/ui/partners/rewind/constants";
import { prisma } from "@dub/prisma";
import { Grid } from "@dub/ui";
import { cn } from "@dub/utils";
import { redirect } from "next/navigation";
import { preload } from "react-dom";
import { PartnerRewind2025PageClient } from "./page-client";

export default async function PartnerRewind2025Page() {
const { user } = await getSession();

if (!user.defaultPartnerId) redirect("/");

const partnerUser = await prisma.partnerUser.findUnique({
select: { partnerId: true },
where: {
userId_partnerId: {
userId: user.id,
partnerId: user.defaultPartnerId,
},
},
});

if (!partnerUser) redirect("/");

const partnerRewind = await getPartnerRewind({
partnerId: partnerUser.partnerId,
});

if (!partnerRewind) redirect("/");

preload(`${REWIND_ASSETS_PATH}/top-medallion.png`, { as: "image" });
REWIND_STEPS.forEach((step) => {
preload(`${REWIND_ASSETS_PATH}/${step.video}`, { as: "video" });
});

return (
<PageContent
title="Partner rewind"
className="flex h-full flex-col"
contentWrapperClassName="grow pt-0 lg:pt-0"
>
<div className="bg-bg-muted relative size-full">
<div className="animate-fade-in absolute inset-0 overflow-hidden [mask-image:radial-gradient(transparent,black)]">
<Grid
cellSize={56}
patternOffset={[-4, -28]}
className="text-border-default/80"
/>
<Gradient className="absolute bottom-0 left-0 h-[720px] w-96 -translate-x-1/2 translate-y-1/2 -rotate-[55deg] opacity-30" />
<Gradient className="absolute right-0 top-0 h-[720px] w-96 -translate-y-1/2 translate-x-1/2 -rotate-[55deg] opacity-20" />
</div>

<div className="scrollbar-hide flex size-full items-center justify-center overflow-y-auto p-6">
<PartnerRewind2025PageClient partnerRewind={partnerRewind} />
</div>
</div>
</PageContent>
);
}

function Gradient({ className }: { className?: string }) {
return (
<div
className={cn(
"[background-image:radial-gradient(140%_146%_at_93%_14%,#72FE7D,rgba(114,254,125,0)_50%),radial-gradient(126%_82%_at_56%_100%,#FD3A4E,rgba(253,58,78,0)_50%),radial-gradient(131%_124%_at_11%_35%,#855AFC,rgba(133,90,252,0)_50%),radial-gradient(117%_77%_at_100%_100%,#E4C795,rgba(228,199,149,0)_50%),radial-gradient(86%_74%_at_40%_59%,#3A8BFD,rgba(58,139,253,0)_50%),radial-gradient(115%_96%_at_42%_69%,#EEA5BA,rgba(238,165,186,0)_50%)]",
"blur-[60px]",
className,
)}
/>
);
}
Loading