Skip to content
Closed
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
3 changes: 3 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@sentry/nextjs": "8.45.1",
"@shazow/whatsabi": "^0.18.0",
"@tanstack/react-query": "5.62.16",
"@tanstack/react-query-persist-client": "^5.64.2",
"@tanstack/react-table": "^8.20.6",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "^1.5.2",
Expand All @@ -64,6 +65,7 @@
"flat": "^6.0.1",
"framer-motion": "11.15.0",
"fuse.js": "7.0.0",
"idb-keyval": "^6.2.1",
"input-otp": "^1.4.1",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
Expand All @@ -89,6 +91,7 @@
"react-table": "^7.8.0",
"recharts": "2.14.1",
"remark-gfm": "^4.0.0",
"responsive-rsc": "^0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "^1.7.1",
Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export default async function TeamLayout(props: {
path: `/team/${params.team_slug}/~/settings`,
name: "Settings",
},
{
path: `/team/${params.team_slug}/~/test`,
name: "Test",
},
]}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";

export function ChartUI(props: {
data: Array<{ time: Date; count: number }>;
isPending: boolean;
}) {
return (
<ThirdwebBarChart
title="Test"
data={props.data}
config={{
count: {
label: "Foo",
color: "hsl(var(--chart-1))",
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
isPending={props.isPending}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function ignoreTime(date: Date) {
const newDate = new Date(date);
newDate.setHours(1, 0, 0, 0);
return newDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This adds a key difference from the client side version -
// client side version can skip this entirely on subsequent date range changes
export async function simulatePageProcessingDelay() {
await new Promise((resolve) => setTimeout(resolve, 1000));
}

export async function simulateChartFetchingDelay() {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { addDays, differenceInCalendarDays } from "date-fns";
import { simulateChartFetchingDelay } from "./delays";

export type TestData = Array<{ time: Date; count: number }>;

export async function fetchTestData(params: {
from: Date;
to: Date;
}) {
await simulateChartFetchingDelay();

const days = differenceInCalendarDays(params.to, params.from);

const data: TestData = [];
for (let i = 0; i < days; i++) {
data.push({
time: addDays(params.from, i),
count: ((i + 1) % 10) + i,
});
}

return data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import "server-only";

import { unstable_cache } from "next/cache";
import { fetchTestData } from "./fetchTestData";

export const getCachedFetchTestData = unstable_cache(
fetchTestData,
["fetchTestData"],
{
revalidate: 3600, // 1 hour
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getLastNDaysRange } from "../../../../../../../components/analytics/date-range-selector";
import type { Range } from "../../../../../../../components/analytics/date-range-selector";
import { ignoreTime } from "./date";

export function getRange(params: {
from: string | undefined;
to: string | undefined;
}) {
const fromStr = params.from;
const toStr = params.to;

const defaultRange = getLastNDaysRange("last-30");
const range: Range =
fromStr && toStr && typeof fromStr === "string" && typeof toStr === "string"
? {
from: ignoreTime(new Date(fromStr)),
to: ignoreTime(new Date(toStr)),
type: "custom",
}
: {
from: ignoreTime(defaultRange.from),
to: ignoreTime(defaultRange.to),
type: defaultRange.type,
};

return range;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { simulatePageProcessingDelay } from "./delays";
import { getRange } from "./getRange";

export async function pageProcessing(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const fromStr = searchParams.from;
const toStr = searchParams.to;

await simulatePageProcessingDelay();

const range = getRange({
from: typeof fromStr === "string" ? fromStr : undefined,
to: typeof toStr === "string" ? toStr : undefined,
});

return range;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { useRef } from "react";
import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector";
import { ChartUI } from "../_common/ChartUI";
import { type TestData, fetchTestData } from "../_common/fetchTestData";
import { useRange, useSetRange } from "./contexts";

export function QueryChartUI(props: {
initialData: TestData;
}) {
const range = useRange();
const initialRange = useRef(range);
const chartDataQuery = useQuery({
queryKey: [
"fetchTestDate",
{
from: range.from.toDateString(),
to: range.to.toDateString(),
},
],
queryFn: () => {
console.log("client side query", range);
return fetchTestData(range);
},
initialData() {
const hasInitialData =
range.from.toString() === initialRange.current.from.toString() &&
range.to.toString() === initialRange.current.to.toString();

if (hasInitialData) {
return props.initialData;
}
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});

return (
<ChartUI
data={chartDataQuery.data || []}
isPending={chartDataQuery.isPending}
/>
);
}

export function RangeSelector() {
const range = useRange();
const setRange = useSetRange();
return (
<DateRangeSelector
range={range}
setRange={(v) => {
setRange(v);
// update search params without reloading the page
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("from", v.from.toDateString());
searchParams.set("to", v.to.toDateString());
window.history.pushState({}, "", `?${searchParams.toString()}`);
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { createContext, useContext, useState } from "react";
import invariant from "tiny-invariant";
import type { Range } from "../../../../../../../components/analytics/date-range-selector";

type SetRange = (range: Range) => void;

// eslint-disable-next-line no-restricted-syntax
const RangeCtx = createContext<Range | null>(null);
// eslint-disable-next-line no-restricted-syntax
const SetRangeCtx = createContext<SetRange | null>(null);

export function RangeProvider(props: {
value: Range;
children: React.ReactNode;
}) {
const [range, setRange] = useState<Range>(props.value);

return (
<RangeCtx.Provider value={range}>
<SetRangeCtx.Provider value={setRange}>
{props.children}
</SetRangeCtx.Provider>
</RangeCtx.Provider>
);
}

export function useRange() {
const range = useContext(RangeCtx);
invariant(range, "Not in RangeProvider");
return range;
}

export function useSetRange() {
const setRange = useContext(SetRangeCtx);
invariant(setRange, "Not in RangeProvider");
return setRange;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { pageProcessing } from "../_common/pageProcessing";
import { RangeSelector } from "./client";
import { RangeProvider } from "./contexts";
import { RSCQueryChart } from "./server";

export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const range = await pageProcessing(props);

return (
<RangeProvider value={range}>
<div>
<RangeSelector />
<div className="h-8" />
<RSCQueryChart
range={{
from: range.from,
to: range.to,
}}
/>
</div>
</RangeProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { unstable_cache } from "next/cache";
import { Suspense } from "react";
import { ChartUI } from "../_common/ChartUI";
import { fetchTestData } from "../_common/fetchTestData";
import { QueryChartUI } from "./client";

const cachedFetchTestData = unstable_cache(fetchTestData, ["fetchTestData"], {
// 1 hour
revalidate: 3600,
});

export function RSCQueryChart(props: {
range: {
from: Date;
to: Date;
};
}) {
return (
<Suspense
key={props.range.from.toDateString() + props.range.to.toDateString()}
fallback={<ChartUI data={[]} isPending={true} />}
>
<AsyncChartUI range={props.range} />
</Suspense>
);
}

async function AsyncChartUI(props: {
range: {
from: Date;
to: Date;
};
}) {
const data = await cachedFetchTestData(props.range);
return <QueryChartUI initialData={data} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import type {
PersistedClient,
Persister,
} from "@tanstack/react-query-persist-client";
import { del, get, set } from "idb-keyval";

const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 1 day
gcTime: 1000 * 60 * 60 * 24,
},
},
});

/**
* Creates an Indexed DB persister
* @see https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
function createIDBPersister(idbValidKey: IDBValidKey = "reactQuery") {
return {
persistClient: async (client: PersistedClient) => {
await set(idbValidKey, client);
},
restoreClient: async () => {
return await get<PersistedClient>(idbValidKey);
},
removeClient: async () => {
await del(idbValidKey);
},
} satisfies Persister;
}

const idbPersister = createIDBPersister();

export function IdbPersistProvider(props: {
children?: React.ReactNode;
}) {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: idbPersister }}
>
{props.children}
</PersistQueryClientProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { pageProcessing } from "../_common/pageProcessing";
import { ClientPage } from "../client/_page";
import { IdbPersistProvider } from "./idb-persister";

export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const range = await pageProcessing(props);

return (
<IdbPersistProvider>
<ClientPage range={range} />
</IdbPersistProvider>
);
}
Loading
Loading