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
15 changes: 3 additions & 12 deletions apps/dashboard/src/@/api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { getAuthToken } from "../../app/api/lib/getAuthToken";

export type Project = {
id: string;
Expand All @@ -21,11 +20,7 @@ export type Project = {
};

export async function getProjects(teamSlug: string) {
const cookiesManager = await cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
const token = await getAuthToken();

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

export async function getProject(teamSlug: string, projectSlug: string) {
const cookiesManager = await cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
const token = await getAuthToken();

if (!token) {
return null;
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/components/ui/CopyTextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ToolTipLabel } from "./tooltip";
export function CopyTextButton(props: {
textToShow: string;
textToCopy: string;
tooltip: string;
tooltip: string | undefined;
className?: string;
iconClassName?: string;
variant?:
Expand Down
36 changes: 34 additions & 2 deletions apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getProjects } from "@/api/projects";
import { getWalletConnections } from "@/api/analytics";
import { type Project, getProjects } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { Changelog } from "components/dashboard/Changelog";
import { redirect } from "next/navigation";
Expand All @@ -15,12 +16,13 @@ export default async function Page(props: {
}

const projects = await getProjects(params.team_slug);
const projectsWithTotalWallets = await getProjectsWithTotalWallets(projects);

return (
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
<div className="flex grow flex-col">
<h1 className="mb-4 font-semibold text-2xl tracking-tight">Projects</h1>
<TeamProjectsPage projects={projects} team={team} />
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
</div>
<div className="shrink-0 lg:w-[320px]">
<h2 className="mb-4 font-semibold text-2xl tracking-tight">
Expand All @@ -31,3 +33,33 @@ export default async function Page(props: {
</div>
);
}

async function getProjectsWithTotalWallets(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this going to slow down the page load quite significantly?

Copy link
Member Author

@MananTank MananTank Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my concern as well - but it seems to be pretty quick ( tested with ~15 projects )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about with projects that have 100k+ wallets in them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The total wallets query is aggregated by walletType - returned response array only includes one object per unique wallet type - so the response size won't be huge

Let me check with @gregfromstl If it that scenario involves significantly more processing time in analytics server ( afaik - its already preprocessed )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's preprocessed up to the day. So you'll be aggregating as many rows as there are days in the time period.

projects: Project[],
): Promise<Array<Project & { totalConnections: number }>> {
return Promise.all(
projects.map(async (p) => {
try {
const data = await getWalletConnections({
clientId: p.publishableKey,
period: "all",
});

let totalConnections = 0;
for (const d of data) {
totalConnections += d.totalConnections;
}

return {
...p,
totalConnections,
};
} catch {
return {
...p,
totalConnections: 0,
};
}
}),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
import { CopyButton } from "@/components/ui/CopyButton";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
Expand All @@ -19,43 +24,58 @@ import {
} from "@/components/ui/select";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { ChevronDownIcon, PlusIcon, SearchIcon } from "lucide-react";
import {
ChevronDownIcon,
EllipsisVerticalIcon,
PlusIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useMemo, useState } from "react";

type SortById = "name" | "createdAt" | "totalConnections";

type SortById = "name" | "createdAt";
type ProjectWithTotalConnections = Project & { totalConnections: number };

export function TeamProjectsPage(props: {
projects: Project[];
projects: ProjectWithTotalConnections[];
team: Team;
}) {
const { projects } = props;
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<SortById>("createdAt");
const [sortBy, setSortBy] = useState<SortById>("totalConnections");
const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] =
useState(false);
const router = useDashboardRouter();

let projectsToShow = !searchTerm
? projects
: projects.filter(
(project) =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.publishableKey
.toLowerCase()
.includes(searchTerm.toLowerCase()),
const projectsToShow = useMemo(() => {
let _projectsToShow = !searchTerm
? projects
: projects.filter(
(project) =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.publishableKey
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);

if (sortBy === "name") {
_projectsToShow = _projectsToShow.sort((a, b) =>
a.name.localeCompare(b.name),
);
} else if (sortBy === "createdAt") {
_projectsToShow = _projectsToShow.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
} else if (sortBy === "totalConnections") {
_projectsToShow = _projectsToShow.sort(
(a, b) => b.totalConnections - a.totalConnections,
);
}

if (sortBy === "name") {
projectsToShow = projectsToShow.sort((a, b) =>
a.name.localeCompare(b.name),
);
} else if (sortBy === "createdAt") {
projectsToShow = projectsToShow.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
}
return _projectsToShow;
}, [searchTerm, sortBy, projects]);

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

{/* Projects */}
{projectsToShow.length === 0 ? (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects created</p>
<Button
className="gap-2"
onClick={() => setIsCreateProjectDialogOpen(true)}
variant="outline"
>
<PlusIcon className="size-4" />
Create a Project
</Button>
</div>
</div>
<>
{searchTerm !== "" ? (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects found</p>
</div>
</div>
) : (
<div className="flex min-h-[450px] grow items-center justify-center rounded-lg border border-border">
<div className="flex flex-col items-center">
<p className="mb-5 text-center">No projects created</p>
<Button
className="gap-2"
onClick={() => setIsCreateProjectDialogOpen(true)}
variant="outline"
>
<PlusIcon className="size-4" />
Create a Project
</Button>
</div>
</div>
)}
</>
) : (
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
{projectsToShow.map((project) => {
Expand Down Expand Up @@ -118,7 +148,7 @@ export function TeamProjectsPage(props: {
}

function ProjectCard(props: {
project: Project;
project: ProjectWithTotalConnections;
team_slug: string;
}) {
const { project, team_slug } = props;
Expand All @@ -130,34 +160,51 @@ function ProjectCard(props: {
{/* TODO - set image */}
<ProjectAvatar className="size-10 rounded-full" src="" />

<div className="flex-grow flex-col gap-1">
<div className="flex items-center justify-between gap-2">
<Link
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
// remove /connect when we have overview page
href={`/team/${team_slug}/${project.slug}`}
>
<h2 className="font-medium text-base">{project.name}</h2>
</Link>
<CopyButton
text={project.publishableKey}
iconClassName="z-10 size-3"
className="!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
/>
</div>
<div className="flex-grow flex-col gap-1.5">
<Link
className="group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
// remove /connect when we have overview page
href={`/team/${team_slug}/${project.slug}`}
>
<h2 className="font-medium text-base">{project.name}</h2>
</Link>

<p className="flex items-center text-muted-foreground text-sm">
{truncate(project.publishableKey, 32)}
<p className="flex items-center gap-1 text-muted-foreground text-sm">
<span>{project.totalConnections}</span>
Total Users
</p>
</div>

<Popover>
<PopoverTrigger asChild>
<Button className="z-10 h-auto w-auto p-2" variant="ghost">
<EllipsisVerticalIcon className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-1">
<CopyTextButton
textToCopy={project.publishableKey}
textToShow="Copy Client ID"
copyIconPosition="right"
tooltip={undefined}
variant="ghost"
className="flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
/>
<Button
variant="ghost"
className="w-full justify-start gap-3"
asChild
>
<Link href={`/team/${team_slug}/${project.slug}/settings`}>
Settings
</Link>
</Button>
</PopoverContent>
</Popover>
</div>
);
}

function truncate(str: string, stringLimit: number) {
return str.length > stringLimit ? `${str.slice(0, stringLimit)}...` : str;
}

function SearchInput(props: {
value: string;
onValueChange: (value: string) => void;
Expand Down Expand Up @@ -209,10 +256,11 @@ function SelectBy(props: {
value: SortById;
onChange: (value: SortById) => void;
}) {
const values: SortById[] = ["name", "createdAt"];
const values: SortById[] = ["name", "createdAt", "totalConnections"];
const valueToLabel: Record<SortById, string> = {
name: "Name",
createdAt: "Creation Date",
totalConnections: "Total Users",
};

return (
Expand All @@ -223,7 +271,12 @@ function SelectBy(props: {
}}
>
<SelectTrigger className="min-w-[200px] bg-card capitalize">
Sort by {valueToLabel[props.value]}
<div className="flex items-center gap-1.5">
<span className="!hidden lg:!inline text-muted-foreground">
Sort by
</span>
{valueToLabel[props.value]}
</div>
</SelectTrigger>
<SelectContent>
{values.map((value) => (
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/dashboard/Changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function Changelog() {

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