Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 28 additions & 16 deletions apps/web/app/api/workspaces/[idOrSlug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ import { NextResponse } from "next/server";
// GET /api/workspaces/[idOrSlug] – get a specific workspace by id or slug
export const GET = withWorkspace(
async ({ workspace, headers }) => {
const domains = await prisma.domain.findMany({
where: {
projectId: workspace.id,
},
select: {
slug: true,
primary: true,
},
});
const [domains, yearInReviews] = await Promise.all([
prisma.domain.findMany({
where: {
projectId: workspace.id,
},
select: {
slug: true,
primary: true,
},
take: 100,
}),
prisma.yearInReview.findMany({
where: {
workspaceId: workspace.id,
year: 2024,
},
}),
]);

return NextResponse.json(
WorkspaceSchema.parse({
...workspace,
id: `ws_${workspace.id}`,
domains,
flags: await getFeatureFlags({
workspaceId: workspace.id,
{
...WorkspaceSchema.parse({
...workspace,
id: `ws_${workspace.id}`,
domains,
flags: await getFeatureFlags({
workspaceId: workspace.id,
}),
}),
}),
yearInReview: yearInReviews.length > 0 ? yearInReviews[0] : null,
},
{ headers },
);
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/app.dub.co/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MainNav } from "@/ui/layout/main-nav";
import { AppSidebarNav } from "@/ui/layout/sidebar/app-sidebar-nav";
import { HelpButtonRSC } from "@/ui/layout/sidebar/help-button-rsc";
import { NewsRSC } from "@/ui/layout/sidebar/news-rsc";
import { ReferButton } from "@/ui/layout/sidebar/refer-button";
import { YearEndReviewCard } from "@/ui/layout/sidebar/year-end-review-card";
import Toolbar from "@/ui/layout/toolbar/toolbar";
import { constructMetadata } from "@dub/utils";
import { ReactNode } from "react";
Expand All @@ -22,7 +22,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
<HelpButtonRSC />
</>
}
newsContent={<NewsRSC />}
newsContent={<YearEndReviewCard />}
>
{children}
</MainNav>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/app.dub.co/(onboarding)/[slug]/upgrade/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { PlanSelector } from "../../onboarding/(steps)/plan/plan-selector";
import { StepPage } from "../../onboarding/(steps)/step-page";
import ExitButton from "./exit-button";

export default function Plan() {
export default function UpgradePage({ params }: { params: { slug: string } }) {
return (
<div className="relative flex flex-col items-center">
<ExitButton />
<Link href="/">
<Link href={`/${params.slug}`}>
<Wordmark className="mt-6 h-8" />
</Link>
<div className="mt-8 flex w-full flex-col items-center px-3 pb-16 md:mt-20 lg:px-8">
Expand Down
206 changes: 206 additions & 0 deletions apps/web/app/app.dub.co/(onboarding)/[slug]/wrapped/[year]/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"use client";

import useWorkspace from "@/lib/swr/use-workspace";
import { BlurImage, ExpandingArrow } from "@dub/ui";
import {
cn,
DICEBEAR_AVATAR_URL,
smartTruncate,
STAGGER_CHILD_VARIANTS,
} from "@dub/utils";
import { COUNTRIES } from "@dub/utils/src/constants/countries";
import NumberFlow from "@number-flow/react";
import { motion } from "framer-motion";
import { redirect, useParams } from "next/navigation";

export default function WrappedPageClient() {
const { slug, year } = useParams();
const { name, logo, yearInReview, loading } = useWorkspace();

const { totalLinks, totalClicks, topLinks, topCountries } =
yearInReview || {};

const stats = {
"Total Links": totalLinks,
"Total Clicks": totalClicks,
};

const placeholderArray = Array.from({ length: 5 }, (_, index) => ({
item: "placeholder",
count: 0,
}));

if (!loading && !yearInReview) {
redirect(`/${slug}`);
}

return (
<div className="relative mx-auto my-10 max-w-lg px-4 sm:px-8">
<h1 className="animate-slide-up-fade font-display mx-0 mb-4 mt-8 p-0 text-center text-xl font-semibold text-black [animation-delay:150ms] [animation-duration:1s] [animation-fill-mode:both]">
Dub {year} Year in Review 🎊
</h1>
<p className="animate-slide-up-fade text-center text-sm leading-6 text-black [animation-delay:300ms] [animation-duration:1s] [animation-fill-mode:both]">
As we put a wrap on {year}, we wanted to say thank you for your support!
Here's a look back at your activity in {year}:
</p>

<div className="animate-slide-up-fade my-8 rounded-lg border border-neutral-200 bg-white p-2 shadow-md [animation-delay:450ms] [animation-duration:1s] [animation-fill-mode:both]">
<div
className="flex h-24 flex-col items-center justify-center rounded-lg"
style={{
backgroundImage: `url(https://assets.dub.co/misc/year-in-review-header.jpg)`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
{name ? (
<>
<BlurImage
src={logo || `${DICEBEAR_AVATAR_URL}${name}`}
alt={name || "Workspace Logo"}
className="h-8 rounded-full"
width={32}
height={32}
/>
<h2 className="mt-1 text-xl font-semibold">{name}</h2>
</>
) : (
<>
<div className="h-8 animate-pulse rounded-full bg-neutral-200" />
<div className="h-5 w-12 animate-pulse rounded-md bg-neutral-200" />
</>
)}
</div>
<div className="grid w-full grid-cols-2 gap-2 p-4">
{Object.entries(stats).map(([key, value]) => (
<StatCard key={key} title={key} value={value} />
))}
</div>
<div className="grid gap-2 p-4">
<StatTable
title="Top Links"
value={
topLinks
? (topLinks as { item: string; count: number }[])
: placeholderArray
}
/>
<StatTable
title="Top Countries"
value={
topCountries
? (topCountries as { item: string; count: number }[])
: placeholderArray
}
/>
</div>
</div>
</div>
);
}

const StatCard = ({
title,
value,
}: {
title: string;
value: number | undefined;
}) => {
return (
<div className="text-center">
<h3 className="font-medium text-neutral-500">{title}</h3>
<NumberFlow
value={value || 0}
className={cn(
"text-lg font-medium text-black",
value === undefined && "text-neutral-300",
)}
/>
</div>
);
};

const StatTable = ({
title,
value,
}: {
title: string;
value: { item: string; count: number }[];
}) => {
const { slug } = useParams();
return (
<div className="mb-2">
<h3 className="mb-2 font-medium text-neutral-500">{title}</h3>
<motion.div
variants={{
show: {
transition: {
delayChildren: 0.5,
staggerChildren: 0.08,
},
},
}}
initial="hidden"
animate="show"
className="grid divide-y divide-neutral-200 text-sm"
>
{value.map(({ item, count }, index) => {
const [domain, ...pathParts] = item.split("/");
const path = pathParts.join("/") || "_root";
return (
<motion.div
key={index}
variants={STAGGER_CHILD_VARIANTS}
className="text-sm text-gray-500"
>
<a
href={`/${slug}/analytics?${new URLSearchParams({
...(title === "Top Links"
? {
domain,
key: path,
}
: {
country: item,
}),
interval: "1y",
}).toString()}`}
key={index}
className="group flex justify-between py-1.5"
>
{item === "placeholder" ? (
<div className="h-4 w-12 animate-pulse rounded-md bg-neutral-200" />
) : (
<div className="flex items-center gap-2">
{title === "Top Countries" && (
<img
src={`https://hatscripts.github.io/circle-flags/flags/${item.toLowerCase()}.svg`}
alt={COUNTRIES[item]}
className="size-4"
/>
)}
<div className="flex gap-0.5">
<p className="font-medium text-black">
{title === "Top Links"
? smartTruncate(item, 33)
: COUNTRIES[item]}{" "}
</p>
<ExpandingArrow className="size-3" />
</div>
</div>
)}
<NumberFlow
value={count}
className={cn(
"text-neutral-600 group-hover:text-black",
count === 0 && "text-neutral-300",
)}
/>
</a>
</motion.div>
);
})}
</motion.div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Wordmark } from "@dub/ui";
import Link from "next/link";
import WrappedPageClient from "./client";

export default function WrappedPage({
params,
}: {
params: { slug: string; year: string };
}) {
return (
<div className="relative flex flex-col items-center">
<Link href={`/${params.slug}`}>
<Wordmark className="mt-6 h-8" />
</Link>
<WrappedPageClient />
</div>
);
}
9 changes: 9 additions & 0 deletions apps/web/app/app.dub.co/(onboarding)/[slug]/wrapped/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { redirect } from "next/navigation";

export default function WrappedParentPage({
params,
}: {
params: { slug: string };
}) {
redirect(`/${params.slug}/wrapped/2024`);
}
1 change: 1 addition & 0 deletions apps/web/lib/middleware/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default async function AppMiddleware(req: NextRequest) {
"/programs",
"/settings",
"/upgrade",
"/wrapped",
].includes(path) ||
path.startsWith("/settings/") ||
isTopLevelSettingsRedirect(path)
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SaleStatus,
UtmTemplate,
Webhook,
YearInReview,
} from "@dub/prisma/client";
import { WEBHOOK_TRIGGER_DESCRIPTIONS } from "./webhook/constants";
import { clickEventResponseSchema } from "./zod/schemas/clicks";
Expand Down Expand Up @@ -139,6 +140,7 @@ export type ExpandedWorkspaceProps = WorkspaceProps & {
id: string;
name: string;
}[];
yearInReview: YearInReview | null;
};

export type WorkspaceWithUsers = Omit<WorkspaceProps, "domains">;
Expand Down
10 changes: 8 additions & 2 deletions apps/web/tests/workspaces/retrieve-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ describe("GET /workspaces/{idOrSlug}", async () => {
slug: workspace.slug,
});

WorkspaceSchema.extend({ createdAt: z.string() })
WorkspaceSchema.extend({
createdAt: z.string(),
yearInReview: z.object({}).nullable(),
})
.strict()
.parse(workspaceFetched);
});
Expand All @@ -41,7 +44,10 @@ describe("GET /workspaces/{idOrSlug}", async () => {
slug: workspace.slug,
});

WorkspaceSchema.extend({ createdAt: z.string() })
WorkspaceSchema.extend({
createdAt: z.string(),
yearInReview: z.object({}).nullable(),
})
.strict()
.parse(workspaceFetched);
});
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const NAV_AREAS: SidebarNavAreas<{
href: `/${slug}/programs/${programs[0].id}/resources`,
},
{
name: "Settings",
name: "Configuration",
href: `/${slug}/programs/${programs[0].id}/settings`,
},
],
Expand Down
Loading
Loading