Skip to content

Commit 2127d0d

Browse files
committed
add analytics
1 parent 488dd45 commit 2127d0d

File tree

10 files changed

+563
-25
lines changed

10 files changed

+563
-25
lines changed

app/projects/[slug]/[[...tab]]/page.tsx

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ProjectAnalytics from "@/components/projects/project-analytics";
12
import { getProjectBySlug } from "@/lib/actions/get-project";
23
import prisma from "@/lib/prisma";
34
import { notFound } from "next/navigation";
@@ -23,9 +24,19 @@ export default async function Project({
2324
notFound();
2425
}
2526

26-
return (
27-
project.image && (
28-
<img src={project.image} alt={project.name} className="mt-4 rounded-xl" />
29-
)
30-
);
27+
if (!tab) {
28+
return (
29+
project.image && (
30+
<img
31+
src={project.image}
32+
alt={project.name}
33+
className="mt-4 rounded-xl"
34+
/>
35+
)
36+
);
37+
}
38+
39+
if (tab[0] === "analytics") {
40+
return <ProjectAnalytics project={project} />;
41+
}
3142
}

app/projects/[slug]/layout.tsx

+8-15
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@ export default async function ProjectLayout({
5050
notFound();
5151
}
5252

53-
const githubLink = project.links.find((link) => link.type === "GITHUB")!;
54-
const websiteLink = project.links.find((link) => link.type === "WEBSITE");
55-
56-
const { stars } = await getRepo(githubLink.url);
53+
const { stars } = await getRepo(project.githubLink.url);
5754

5855
if (stars !== project.stars) {
5956
await prisma.project.update({
@@ -67,13 +64,7 @@ export default async function ProjectLayout({
6764
}
6865

6966
return (
70-
<ProjectProvider
71-
props={{
72-
...project,
73-
githubLink,
74-
websiteLink,
75-
}}
76-
>
67+
<ProjectProvider props={project}>
7768
<div className="aspect-[4/1] w-full rounded-t-2xl bg-gradient-to-tr from-purple-100 via-violet-50 to-blue-100" />
7869
<div className="-mt-8 flex items-center justify-between px-4 sm:-mt-12 sm:items-end md:pr-0">
7970
<Image
@@ -88,16 +79,16 @@ export default async function ProjectLayout({
8879
<EditProjectButton projectId={project.id} />
8980
</Suspense>
9081
<a
91-
href={githubLink.shortLink}
82+
href={project.githubLink.shortLink}
9283
target="_blank"
9384
className={buttonLinkVariants({ variant: "secondary" })}
9485
>
9586
<Star className="h-4 w-4" />
9687
<p className="text-sm">{nFormatter(stars, { full: true })}</p>
9788
</a>
98-
{websiteLink && (
89+
{project.websiteLink && (
9990
<a
100-
href={websiteLink.shortLink}
91+
href={project.websiteLink.shortLink}
10192
target="_blank"
10293
className={buttonLinkVariants()}
10394
>
@@ -107,14 +98,16 @@ export default async function ProjectLayout({
10798
)}
10899
</div>
109100
</div>
110-
<div className="max-w-lg p-4">
101+
<div className="max-w-lg p-4 pb-0">
111102
<div className="flex items-center space-x-2">
112103
<h1 className="font-display text-3xl font-bold">{project.name}</h1>
113104
{project.verified && (
114105
<BadgeCheck className="h-8 w-8 text-white" fill="#1c9bef" />
115106
)}
116107
</div>
117108
<p className="mt-2 text-gray-500">{project.description}</p>
109+
</div>
110+
<div className="px-4">
118111
<ProjectLayoutTabs />
119112
{children}
120113
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
import { nFormatter } from "@dub/utils";
3+
import { AreaChart } from "@tremor/react";
4+
5+
export default function ProjectAnalyticsClient({
6+
chartData,
7+
categories,
8+
}: {
9+
chartData: any[];
10+
categories: string[];
11+
}) {
12+
return (
13+
<AreaChart
14+
className="h-80"
15+
data={chartData}
16+
index="start"
17+
categories={categories}
18+
colors={["blue", "rose"]}
19+
valueFormatter={nFormatter}
20+
yAxisWidth={60}
21+
onValueChange={(v) => console.log(v)}
22+
/>
23+
);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LoadingSpinner } from "@dub/ui";
2+
3+
export default function ProjectAnalyticsPlacholder() {
4+
return (
5+
<div className="flex h-80 w-full flex-col items-center justify-center space-y-4">
6+
<LoadingSpinner />
7+
<p className="text-sm text-gray-500">Loading analytics...</p>
8+
</div>
9+
);
10+
}
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { dub, getDomainAndKey } from "@/lib/dub";
2+
import { ProjectWithLinks } from "@/lib/types";
3+
import { Suspense } from "react";
4+
import ProjectAnalyticsClient from "./project-analytics-client";
5+
import ProjectAnalyticsPlacholder from "./project-analytics-placeholder";
6+
7+
export default function ProjectAnalytics({
8+
project,
9+
}: {
10+
project: ProjectWithLinks;
11+
}) {
12+
return (
13+
<Suspense fallback={<ProjectAnalyticsPlacholder />}>
14+
<ProjectAnalyticsRSC project={project} />
15+
</Suspense>
16+
);
17+
}
18+
19+
async function ProjectAnalyticsRSC({ project }: { project: ProjectWithLinks }) {
20+
const { links } = project;
21+
22+
const analytics = await Promise.all(
23+
links.map(async (link) => {
24+
const { domain, key } = getDomainAndKey(link.shortLink);
25+
return await dub.analytics.timeseries({
26+
domain,
27+
key,
28+
interval: "30d",
29+
});
30+
}),
31+
);
32+
33+
const chartData = analytics[0].map(
34+
(data: { start: string; clicks: number }, i) => {
35+
return {
36+
start: new Date(data.start).toLocaleDateString("en-US", {
37+
month: "short",
38+
day: "numeric",
39+
}),
40+
[links[0].type]: analytics[0][i].clicks,
41+
[links[1].type]: analytics[1][i].clicks,
42+
};
43+
},
44+
);
45+
46+
console.log({ analytics, chartData });
47+
48+
return (
49+
<div className="mt-4">
50+
<ProjectAnalyticsClient
51+
chartData={chartData}
52+
categories={[links[0].type, links[1].type]}
53+
/>
54+
</div>
55+
);
56+
}

lib/actions/get-project.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import { cache } from "react";
22
import prisma from "../prisma";
3+
import { ProjectWithLinks } from "../types";
34

45
export const getProjectBySlug = cache(async (slug: string) => {
5-
return await prisma.project.findUnique({
6+
const project = await prisma.project.findUnique({
67
where: {
78
slug,
89
},
910
include: {
1011
links: true,
1112
},
1213
});
14+
if (!project) {
15+
return null;
16+
}
17+
const githubLink = project.links.find((link) => link.type === "GITHUB")!;
18+
const websiteLink = project.links.find((link) => link.type === "WEBSITE");
19+
return {
20+
...project,
21+
githubLink,
22+
websiteLink,
23+
} as ProjectWithLinks;
1324
});

lib/dub.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Link } from "@prisma/client";
33
import { Dub } from "dub";
44

55
export const dub = new Dub({
6-
workspaceId: "ws_clv9jxuxp0006gpq847kwrcwj",
6+
// workspaceId: "ws_clv9jxuxp0006gpq847kwrcwj",
7+
workspaceId: "ws_cl7pj5kq4006835rbjlt2ofka",
78
});
89

910
export async function shortenAndCreateLink({
@@ -34,11 +35,11 @@ export async function editShortLink({
3435
link: Link;
3536
newUrl: string;
3637
}) {
37-
const shortLinkUrl = new URL(link.shortLink);
38+
const { domain, key } = getDomainAndKey(link.shortLink);
3839

3940
const { id: dubLinkId } = await dub.links.get({
40-
domain: shortLinkUrl.hostname,
41-
key: shortLinkUrl.pathname.slice(1),
41+
domain,
42+
key,
4243
});
4344

4445
await dub.links.update(dubLinkId, {
@@ -54,3 +55,11 @@ export async function editShortLink({
5455
},
5556
});
5657
}
58+
59+
export function getDomainAndKey(url: string) {
60+
const link = new URL(url);
61+
return {
62+
domain: link.hostname,
63+
key: link.pathname.slice(1),
64+
};
65+
}

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
"@dub/tailwind-config": "^0.0.4",
1616
"@dub/ui": "^0.1.11",
1717
"@dub/utils": "^0.0.48",
18+
"@headlessui/tailwindcss": "^0.2.0",
1819
"@prisma/client": "^4.16.2",
1920
"@sindresorhus/slugify": "^2.2.1",
21+
"@tremor/react": "^3.16.1",
2022
"class-variance-authority": "^0.7.0",
2123
"dub": "^0.24.1",
2224
"framer-motion": "^11.1.7",

0 commit comments

Comments
 (0)