Skip to content

Commit feb4fd0

Browse files
committed
[TOOL-3123] Dashboard: Order projects by total wallets (#6026)
<!-- start pr-codex --> ## PR-Codex overview This PR focuses on enhancing the `CopyTextButton` component, optimizing the project fetching logic, and improving project display features by integrating total wallet connections in the dashboard. ### Detailed summary - Changed `tooltip` type in `CopyTextButton` to `string | undefined`. - Updated the API call in `getChangelog` to limit results to 7. - Replaced cookie-based token retrieval with `getAuthToken` in `getProjects`. - Added `getProjectsWithTotalWallets` to calculate total wallet connections. - Updated `TeamProjectsPage` to display projects with total connections. - Modified sorting logic in `TeamProjectsPage` to include total connections. - Replaced `CopyButton` with `CopyTextButton` in `ProjectCard`. - Enhanced project display to show total users. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 1d8695e commit feb4fd0

File tree

5 files changed

+153
-77
lines changed

5 files changed

+153
-77
lines changed

apps/dashboard/src/@/api/projects.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import "server-only";
2-
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
32
import { API_SERVER_URL } from "@/constants/env";
4-
import { cookies } from "next/headers";
3+
import { getAuthToken } from "../../app/api/lib/getAuthToken";
54

65
export type Project = {
76
id: string;
@@ -21,11 +20,7 @@ export type Project = {
2120
};
2221

2322
export async function getProjects(teamSlug: string) {
24-
const cookiesManager = await cookies();
25-
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
26-
const token = activeAccount
27-
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
28-
: null;
23+
const token = await getAuthToken();
2924

3025
if (!token) {
3126
return [];
@@ -46,11 +41,7 @@ export async function getProjects(teamSlug: string) {
4641
}
4742

4843
export async function getProject(teamSlug: string, projectSlug: string) {
49-
const cookiesManager = await cookies();
50-
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
51-
const token = activeAccount
52-
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
53-
: null;
44+
const token = await getAuthToken();
5445

5546
if (!token) {
5647
return null;

apps/dashboard/src/@/components/ui/CopyTextButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
99
export function CopyTextButton(props: {
1010
textToShow: string;
1111
textToCopy: string;
12-
tooltip: string;
12+
tooltip: string | undefined;
1313
className?: string;
1414
iconClassName?: string;
1515
variant?:

apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { getProjects } from "@/api/projects";
1+
import { getWalletConnections } from "@/api/analytics";
2+
import { type Project, getProjects } from "@/api/projects";
23
import { getTeamBySlug } from "@/api/team";
34
import { Changelog } from "components/dashboard/Changelog";
45
import { redirect } from "next/navigation";
@@ -15,12 +16,13 @@ export default async function Page(props: {
1516
}
1617

1718
const projects = await getProjects(params.team_slug);
19+
const projectsWithTotalWallets = await getProjectsWithTotalWallets(projects);
1820

1921
return (
2022
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
2123
<div className="flex grow flex-col">
2224
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
23-
<TeamProjectsPage projects={projects} team={team} />
25+
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
2426
</div>
2527
<div className="shrink-0 lg:w-[320px]">
2628
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
@@ -31,3 +33,33 @@ export default async function Page(props: {
3133
</div>
3234
);
3335
}
36+
37+
async function getProjectsWithTotalWallets(
38+
projects: Project[],
39+
): Promise<Array<Project & { totalConnections: number }>> {
40+
return Promise.all(
41+
projects.map(async (p) => {
42+
try {
43+
const data = await getWalletConnections({
44+
clientId: p.publishableKey,
45+
period: "all",
46+
});
47+
48+
let totalConnections = 0;
49+
for (const d of data) {
50+
totalConnections += d.totalConnections;
51+
}
52+
53+
return {
54+
...p,
55+
totalConnections,
56+
};
57+
} catch {
58+
return {
59+
...p,
60+
totalConnections: 0,
61+
};
62+
}
63+
}),
64+
);
65+
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx

Lines changed: 114 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
import type { Project } from "@/api/projects";
44
import type { Team } from "@/api/team";
55
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
6-
import { CopyButton } from "@/components/ui/CopyButton";
6+
import { CopyTextButton } from "@/components/ui/CopyTextButton";
77
import { Button } from "@/components/ui/button";
88
import {
99
DropdownMenu,
1010
DropdownMenuContent,
1111
DropdownMenuTrigger,
1212
} from "@/components/ui/dropdown-menu";
1313
import { Input } from "@/components/ui/input";
14+
import {
15+
Popover,
16+
PopoverContent,
17+
PopoverTrigger,
18+
} from "@/components/ui/popover";
1419
import {
1520
Select,
1621
SelectContent,
@@ -19,43 +24,58 @@ import {
1924
} from "@/components/ui/select";
2025
import { useDashboardRouter } from "@/lib/DashboardRouter";
2126
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
22-
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
27+
import {
28+
ChevronDownIcon,
29+
EllipsisVerticalIcon,
30+
PlusIcon,
31+
SearchIcon,
32+
} from "lucide-react";
2333
import Link from "next/link";
24-
import { useState } from "react";
34+
import { useMemo, useState } from "react";
35+
36+
type SortById = "name" | "createdAt" | "totalConnections";
2537

26-
type SortById = "name" | "createdAt";
38+
type ProjectWithTotalConnections = Project & { totalConnections: number };
2739

2840
export function TeamProjectsPage(props: {
29-
projects: Project[];
41+
projects: ProjectWithTotalConnections[];
3042
team: Team;
3143
}) {
3244
const { projects } = props;
3345
const [searchTerm, setSearchTerm] = useState("");
34-
const [sortBy, setSortBy] = useState<SortById>("createdAt");
46+
const [sortBy, setSortBy] = useState<SortById>("totalConnections");
3547
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
3648
useState(false);
3749
const router = useDashboardRouter();
3850

39-
let projectsToShow = !searchTerm
40-
? projects
41-
: projects.filter(
42-
(project) =>
43-
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
44-
project.publishableKey
45-
.toLowerCase()
46-
.includes(searchTerm.toLowerCase()),
51+
const projectsToShow = useMemo(() => {
52+
let _projectsToShow = !searchTerm
53+
? projects
54+
: projects.filter(
55+
(project) =>
56+
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
57+
project.publishableKey
58+
.toLowerCase()
59+
.includes(searchTerm.toLowerCase()),
60+
);
61+
62+
if (sortBy === "name") {
63+
_projectsToShow = _projectsToShow.sort((a, b) =>
64+
a.name.localeCompare(b.name),
65+
);
66+
} else if (sortBy === "createdAt") {
67+
_projectsToShow = _projectsToShow.sort(
68+
(a, b) =>
69+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
70+
);
71+
} else if (sortBy === "totalConnections") {
72+
_projectsToShow = _projectsToShow.sort(
73+
(a, b) => b.totalConnections - a.totalConnections,
4774
);
75+
}
4876

49-
if (sortBy === "name") {
50-
projectsToShow = projectsToShow.sort((a, b) =>
51-
a.name.localeCompare(b.name),
52-
);
53-
} else if (sortBy === "createdAt") {
54-
projectsToShow = projectsToShow.sort(
55-
(a, b) =>
56-
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
57-
);
58-
}
77+
return _projectsToShow;
78+
}, [searchTerm, sortBy, projects]);
5979

6080
return (
6181
<div className="flex grow flex-col">
@@ -75,19 +95,29 @@ export function TeamProjectsPage(props: {
7595

7696
{/* Projects */}
7797
{projectsToShow.length === 0 ? (
78-
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
79-
<div className="flex flex-col items-center">
80-
<p className="mb-5 text-center">No projects created</p>
81-
<Button
82-
className="gap-2"
83-
onClick={() => setIsCreateProjectDialogOpen(true)}
84-
variant="outline"
85-
>
86-
<PlusIcon className="size-4" />
87-
Create a Project
88-
</Button>
89-
</div>
90-
</div>
98+
<>
99+
{searchTerm !== "" ? (
100+
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
101+
<div className="flex flex-col items-center">
102+
<p className="mb-5 text-center">No projects found</p>
103+
</div>
104+
</div>
105+
) : (
106+
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
107+
<div className="flex flex-col items-center">
108+
<p className="mb-5 text-center">No projects created</p>
109+
<Button
110+
className="gap-2"
111+
onClick={() => setIsCreateProjectDialogOpen(true)}
112+
variant="outline"
113+
>
114+
<PlusIcon className="size-4" />
115+
Create a Project
116+
</Button>
117+
</div>
118+
</div>
119+
)}
120+
</>
91121
) : (
92122
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
93123
{projectsToShow.map((project) => {
@@ -118,7 +148,7 @@ export function TeamProjectsPage(props: {
118148
}
119149

120150
function ProjectCard(props: {
121-
project: Project;
151+
project: ProjectWithTotalConnections;
122152
team_slug: string;
123153
}) {
124154
const { project, team_slug } = props;
@@ -130,34 +160,51 @@ function ProjectCard(props: {
130160
{/* TODO - set image */}
131161
<ProjectAvatar className="size-10 rounded-full" src="" />
132162

133-
<div className="flex-grow flex-col gap-1">
134-
<div className="flex items-center justify-between gap-2">
135-
<Link
136-
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
137-
// remove /connect when we have overview page
138-
href={`/team/${team_slug}/${project.slug}`}
139-
>
140-
<h2 className="font-medium text-base">{project.name}</h2>
141-
</Link>
142-
<CopyButton
143-
text={project.publishableKey}
144-
iconClassName="z-10 size-3"
145-
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
146-
/>
147-
</div>
163+
<div className="flex-grow flex-col gap-1.5">
164+
<Link
165+
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
166+
// remove /connect when we have overview page
167+
href={`/team/${team_slug}/${project.slug}`}
168+
>
169+
<h2 className="font-medium text-base">{project.name}</h2>
170+
</Link>
148171

149-
<p className="flex items-center text-muted-foreground text-sm">
150-
{truncate(project.publishableKey, 32)}
172+
<p className="flex items-center gap-1 text-muted-foreground text-sm">
173+
<span>{project.totalConnections}</span>
174+
Total Users
151175
</p>
152176
</div>
177+
178+
<Popover>
179+
<PopoverTrigger asChild>
180+
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
181+
<EllipsisVerticalIcon className="size-4" />
182+
</Button>
183+
</PopoverTrigger>
184+
<PopoverContent className="w-[180px] p-1">
185+
<CopyTextButton
186+
textToCopy={project.publishableKey}
187+
textToShow="Copy Client ID"
188+
copyIconPosition="right"
189+
tooltip={undefined}
190+
variant="ghost"
191+
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
192+
/>
193+
<Button
194+
variant="ghost"
195+
className="w-full justify-start gap-3"
196+
asChild
197+
>
198+
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
199+
Settings
200+
</Link>
201+
</Button>
202+
</PopoverContent>
203+
</Popover>
153204
</div>
154205
);
155206
}
156207

157-
function truncate(str: string, stringLimit: number) {
158-
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
159-
}
160-
161208
function SearchInput(props: {
162209
value: string;
163210
onValueChange: (value: string) => void;
@@ -209,10 +256,11 @@ function SelectBy(props: {
209256
value: SortById;
210257
onChange: (value: SortById) => void;
211258
}) {
212-
const values: SortById[] = ["name", "createdAt"];
259+
const values: SortById[] = ["name", "createdAt", "totalConnections"];
213260
const valueToLabel: Record<SortById, string> = {
214261
name: "Name",
215262
createdAt: "Creation Date",
263+
totalConnections: "Total Users",
216264
};
217265

218266
return (
@@ -223,7 +271,12 @@ function SelectBy(props: {
223271
}}
224272
>
225273
<SelectTrigger className="min-w-[200px] bg-card capitalize">
226-
Sort by {valueToLabel[props.value]}
274+
<div className="flex items-center gap-1.5">
275+
<span className="!hidden lg:!inline text-muted-foreground">
276+
Sort by
277+
</span>
278+
{valueToLabel[props.value]}
279+
</div>
227280
</SelectTrigger>
228281
<SelectContent>
229282
{values.map((value) => (

apps/dashboard/src/components/dashboard/Changelog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export async function Changelog() {
4747

4848
async function getChangelog() {
4949
const res = await fetch(
50-
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=10",
50+
"https://thirdweb.ghost.io/ghost/api/content/posts/?key=49c62b5137df1c17ab6b9e46e3&fields=title,url,published_at&filter=tag:changelog&visibility:public&limit=7",
5151
);
5252
const json = await res.json();
5353
return json.posts as ChangelogItem[];

0 commit comments

Comments
 (0)