Skip to content

Commit fd9b0a5

Browse files
committed
[TOOL-3337] Dashboard: Add Transactions pages in project layout
1 parent 1e092a7 commit fd9b0a5

File tree

21 files changed

+1717
-56
lines changed

21 files changed

+1717
-56
lines changed

apps/dashboard/src/@/lib/time.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { differenceInCalendarDays } from "date-fns";
2+
import {
3+
type DurationId,
4+
type Range,
5+
getLastNDaysRange,
6+
} from "../../components/analytics/date-range-selector";
7+
8+
export function normalizeTime(date: Date) {
9+
const newDate = new Date(date);
10+
newDate.setHours(1, 0, 0, 0);
11+
return newDate;
12+
}
13+
14+
export function normalizeTimeISOString(date: Date) {
15+
return normalizeTime(date).toISOString();
16+
}
17+
18+
export function getFiltersFromSearchParams(params: {
19+
from: string | undefined | string[];
20+
to: string | undefined | string[];
21+
interval: string | undefined | string[];
22+
defaultRange: DurationId;
23+
}) {
24+
const fromStr = params.from;
25+
const toStr = params.to;
26+
const defaultRange = getLastNDaysRange(params.defaultRange);
27+
28+
const range: Range =
29+
fromStr && toStr && typeof fromStr === "string" && typeof toStr === "string"
30+
? {
31+
from: normalizeTime(new Date(fromStr)),
32+
to: normalizeTime(new Date(toStr)),
33+
type: "custom",
34+
}
35+
: {
36+
from: normalizeTime(defaultRange.from),
37+
to: normalizeTime(defaultRange.to),
38+
type: defaultRange.type,
39+
};
40+
41+
const defaultInterval =
42+
differenceInCalendarDays(range.to, range.from) > 30
43+
? "week"
44+
: ("day" as const);
45+
46+
return {
47+
range,
48+
interval:
49+
params.interval === "day"
50+
? ("day" as const)
51+
: params.interval === "week"
52+
? ("week" as const)
53+
: defaultInterval,
54+
};
55+
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
TableRow,
3434
} from "@/components/ui/table";
3535
import { ToolTipLabel } from "@/components/ui/tooltip";
36+
import { normalizeTime } from "@/lib/time";
3637
import {
3738
type Transaction,
3839
useEngineTransactions,
@@ -52,7 +53,6 @@ import Link from "next/link";
5253
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
5354
import { toTokens } from "thirdweb";
5455
import { FormLabel, LinkButton, Text } from "tw-components";
55-
import { normalizeTime } from "../../../../../../../../../../lib/time";
5656
import { TransactionTimeline } from "./transaction-timeline";
5757

5858
export type EngineStatus =

apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectSidebarLayout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
33
import {
4+
ArrowRightLeftIcon,
45
BookTextIcon,
56
BoxIcon,
67
HomeIcon,
@@ -76,6 +77,12 @@ export function ProjectSidebarLayout(props: {
7677
icon: InsightIcon,
7778
tracking: tracking("insight"),
7879
},
80+
{
81+
href: `${layoutPath}/transactions`,
82+
label: "Transactions",
83+
icon: ArrowRightLeftIcon,
84+
tracking: tracking("transactions"),
85+
},
7986
]}
8087
footerSidebarLinks={[
8188
{

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default async function Page(props: {
7070
});
7171

7272
return (
73-
<div>
73+
<div className="flex grow flex-col">
7474
<AccountAbstractionSummary
7575
teamId={project.teamId}
7676
projectId={project.id}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-filter.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
"use client";
22

3+
import { normalizeTimeISOString } from "@/lib/time";
34
import {
45
useResponsiveSearchParams,
56
useSetResponsiveSearchParams,
67
} from "responsive-rsc";
78
import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector";
89
import { IntervalSelector } from "../../../../../../../components/analytics/interval-selector";
9-
import {
10-
getNebulaFiltersFromSearchParams,
11-
normalizeTimeISOString,
12-
} from "../../../../../../../lib/time";
10+
import { getNebulaFiltersFromSearchParams } from "../../../../../../../lib/time";
1311

1412
export function NebulaAnalyticsFilter() {
1513
const responsiveSearchParams = useResponsiveSearchParams();
@@ -22,7 +20,7 @@ export function NebulaAnalyticsFilter() {
2220
});
2321

2422
return (
25-
<div className="no-scrollbar flex items-center gap-3 overflow-auto">
23+
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
2624
<DateRangeSelector
2725
range={range}
2826
popoverAlign="end"

apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Button } from "@/components/ui/button";
2+
import { normalizeTimeISOString } from "@/lib/time";
23
import { FileCode2Icon, MessageSquareQuoteIcon } from "lucide-react";
34
import Link from "next/link";
45
import {
56
ResponsiveSearchParamsProvider,
67
ResponsiveSuspense,
78
} from "responsive-rsc";
8-
import { normalizeTimeISOString } from "../../../../../../../lib/time";
99
import { fetchNebulaAnalytics } from "./fetch-nebula-analytics";
1010
import { NebulaAnalyticsFilter } from "./nebula-analytics-filter";
1111
import { NebulaAnalyticsDashboardUI } from "./nebula-analytics-ui";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
2+
import { TransactionAnalyticsFilter } from "./filter";
3+
import { TransactionsChartCard } from "./tx-chart/tx-chart";
4+
import { TransactionsTable } from "./tx-table/tx-table";
5+
6+
export function TransactionsAnalyticsPageContent(props: {
7+
searchParams: {
8+
from?: string | undefined | string[];
9+
to?: string | undefined | string[];
10+
interval?: string | undefined | string[];
11+
};
12+
}) {
13+
return (
14+
<ResponsiveSearchParamsProvider value={props.searchParams}>
15+
<div className="flex grow flex-col">
16+
<div className="flex justify-end">
17+
<TransactionAnalyticsFilter />
18+
</div>
19+
<div className="h-6" />
20+
<div className="flex grow flex-col gap-6">
21+
<TransactionsChartCard searchParams={props.searchParams} />
22+
<TransactionsTable />
23+
</div>
24+
</div>
25+
</ResponsiveSearchParamsProvider>
26+
);
27+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { normalizeTimeISOString } from "@/lib/time";
4+
import {
5+
useResponsiveSearchParams,
6+
useSetResponsiveSearchParams,
7+
} from "responsive-rsc";
8+
import { DateRangeSelector } from "../../../../../../components/analytics/date-range-selector";
9+
import { IntervalSelector } from "../../../../../../components/analytics/interval-selector";
10+
import { getTxAnalyticsFiltersFromSearchParams } from "./getTransactionAnalyticsFilter";
11+
12+
export function TransactionAnalyticsFilter() {
13+
const responsiveSearchParams = useResponsiveSearchParams();
14+
const setResponsiveSearchParams = useSetResponsiveSearchParams();
15+
16+
const { range, interval } = getTxAnalyticsFiltersFromSearchParams({
17+
from: responsiveSearchParams.from,
18+
to: responsiveSearchParams.to,
19+
interval: responsiveSearchParams.interval,
20+
});
21+
22+
return (
23+
<div className="no-scrollbar flex items-center gap-3 max-sm:overflow-auto">
24+
<DateRangeSelector
25+
range={range}
26+
popoverAlign="end"
27+
setRange={(newRange) => {
28+
setResponsiveSearchParams((v) => {
29+
return {
30+
...v,
31+
from: normalizeTimeISOString(newRange.from),
32+
to: normalizeTimeISOString(newRange.to),
33+
};
34+
});
35+
}}
36+
/>
37+
38+
<IntervalSelector
39+
intervalType={interval}
40+
setIntervalType={(newInterval) => {
41+
setResponsiveSearchParams((v) => {
42+
return {
43+
...v,
44+
interval: newInterval,
45+
};
46+
});
47+
}}
48+
/>
49+
</div>
50+
);
51+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { getFiltersFromSearchParams } from "@/lib/time";
2+
3+
export function getTxAnalyticsFiltersFromSearchParams(params: {
4+
from?: string | undefined | string[];
5+
to?: string | undefined | string[];
6+
interval?: string | undefined | string[];
7+
}) {
8+
return getFiltersFromSearchParams({
9+
from: params.from,
10+
to: params.to,
11+
interval: params.interval,
12+
defaultRange: "last-30",
13+
});
14+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { StatCard } from "components/analytics/stat";
2+
import { ActivityIcon, CoinsIcon } from "lucide-react";
3+
import { Suspense } from "react";
4+
5+
// TODO: implement this
6+
async function getTransactionAnalyticsSummary(props: {
7+
teamId: string;
8+
projectId: string;
9+
}) {
10+
console.log("getTransactionAnalyticsSummary called with", props);
11+
await new Promise((resolve) => setTimeout(resolve, 1000));
12+
13+
return {
14+
foo: 100,
15+
bar: 200,
16+
};
17+
}
18+
19+
// TODO: rename props, change labels and icons
20+
function TransactionAnalyticsSummaryUI(props: {
21+
data:
22+
| {
23+
foo: number;
24+
bar: number;
25+
}
26+
| undefined;
27+
isPending: boolean;
28+
}) {
29+
return (
30+
<div className="grid grid-cols-2 gap-4">
31+
<StatCard
32+
label="Foo"
33+
value={props.data?.foo}
34+
icon={ActivityIcon}
35+
isPending={props.isPending}
36+
/>
37+
<StatCard
38+
label="Bar"
39+
value={props.data?.bar}
40+
icon={CoinsIcon}
41+
formatter={(value: number) =>
42+
new Intl.NumberFormat("en-US", {
43+
style: "currency",
44+
currency: "USD",
45+
}).format(value)
46+
}
47+
isPending={props.isPending}
48+
/>
49+
</div>
50+
);
51+
}
52+
53+
// fetches data and renders the UI
54+
async function AsyncTransactionsAnalyticsSummary(props: {
55+
teamId: string;
56+
projectId: string;
57+
}) {
58+
const data = await getTransactionAnalyticsSummary({
59+
teamId: props.teamId,
60+
projectId: props.projectId,
61+
});
62+
63+
return <TransactionAnalyticsSummaryUI data={data} isPending={false} />;
64+
}
65+
66+
// shows loading state while fetching data
67+
export function TransactionAnalyticsSummary(props: {
68+
teamId: string;
69+
projectId: string;
70+
}) {
71+
return (
72+
<Suspense
73+
fallback={
74+
<TransactionAnalyticsSummaryUI data={undefined} isPending={true} />
75+
}
76+
>
77+
<AsyncTransactionsAnalyticsSummary
78+
teamId={props.teamId}
79+
projectId={props.projectId}
80+
/>
81+
</Suspense>
82+
);
83+
}

0 commit comments

Comments
 (0)