diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 4ea0ee32339..923263fcd3f 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -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",
@@ -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",
@@ -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",
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
index 67dcd524548..68e28f0badb 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/layout.tsx
@@ -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",
+ },
]}
/>
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/ChartUI.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/ChartUI.tsx
new file mode 100644
index 00000000000..9c374e107e6
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/ChartUI.tsx
@@ -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 (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/date.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/date.ts
new file mode 100644
index 00000000000..fb6e216ee43
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/date.ts
@@ -0,0 +1,5 @@
+export function ignoreTime(date: Date) {
+ const newDate = new Date(date);
+ newDate.setHours(1, 0, 0, 0);
+ return newDate;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/delays.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/delays.ts
new file mode 100644
index 00000000000..1e197ebce2c
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/delays.ts
@@ -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));
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/fetchTestData.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/fetchTestData.ts
new file mode 100644
index 00000000000..6ab36325bd1
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/fetchTestData.ts
@@ -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;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getCachedFetchTestData.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getCachedFetchTestData.ts
new file mode 100644
index 00000000000..b2df93867f3
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getCachedFetchTestData.ts
@@ -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
+ },
+);
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getRange.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getRange.ts
new file mode 100644
index 00000000000..2d7cd4db49a
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/getRange.ts
@@ -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;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/pageProcessing.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/pageProcessing.ts
new file mode 100644
index 00000000000..8d50fc94d25
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/_common/pageProcessing.ts
@@ -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;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/client.tsx
new file mode 100644
index 00000000000..18d884c9359
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/client.tsx
@@ -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 (
+
+ );
+}
+
+export function RangeSelector() {
+ const range = useRange();
+ const setRange = useSetRange();
+ return (
+ {
+ 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()}`);
+ }}
+ />
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/contexts.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/contexts.tsx
new file mode 100644
index 00000000000..56e901fe127
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/contexts.tsx
@@ -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(null);
+// eslint-disable-next-line no-restricted-syntax
+const SetRangeCtx = createContext(null);
+
+export function RangeProvider(props: {
+ value: Range;
+ children: React.ReactNode;
+}) {
+ const [range, setRange] = useState(props.value);
+
+ return (
+
+
+ {props.children}
+
+
+ );
+}
+
+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;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/page.tsx
new file mode 100644
index 00000000000..ebdb9590e14
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/page.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/server.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/server.tsx
new file mode 100644
index 00000000000..4907926d998
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/cache-client/server.tsx
@@ -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 (
+ }
+ >
+
+
+ );
+}
+
+async function AsyncChartUI(props: {
+ range: {
+ from: Date;
+ to: Date;
+ };
+}) {
+ const data = await cachedFetchTestData(props.range);
+ return ;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/idb-persister.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/idb-persister.tsx
new file mode 100644
index 00000000000..fcf913f34a2
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/idb-persister.tsx
@@ -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(idbValidKey);
+ },
+ removeClient: async () => {
+ await del(idbValidKey);
+ },
+ } satisfies Persister;
+}
+
+const idbPersister = createIDBPersister();
+
+export function IdbPersistProvider(props: {
+ children?: React.ReactNode;
+}) {
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/page.tsx
new file mode 100644
index 00000000000..9e5d8679f0f
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client-persist/page.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client/_page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client/_page.tsx
new file mode 100644
index 00000000000..df89245d2dd
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/client/_page.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
+import {
+ DateRangeSelector,
+ type Range,
+} from "../../../../../../../components/analytics/date-range-selector";
+import { ChartUI } from "../_common/ChartUI";
+import { fetchTestData } from "../_common/fetchTestData";
+
+export function ClientPage(props: {
+ range: Range;
+}) {
+ const [range, setRange] = useState(props.range);
+ const chartDataQuery = useQuery({
+ queryKey: [
+ "fetchTestDate",
+ {
+ from: range.from.toDateString(),
+ to: range.to.toDateString(),
+ },
+ ],
+ queryFn: () => fetchTestData(range),
+ staleTime: 3600 * 1000, // 1 hour
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ });
+
+ return (
+
+
{
+ 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()}`);
+ }}
+ />
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/layout.tsx
new file mode 100644
index 00000000000..311c2dde5b1
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/layout.tsx
@@ -0,0 +1,40 @@
+import { SidebarLayout } from "@/components/blocks/SidebarLayout";
+
+export default async function Layout(props: {
+ children: React.ReactNode;
+ params: Promise<{
+ team_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/page.tsx
new file mode 100644
index 00000000000..4bafbd2c7df
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/page.tsx
@@ -0,0 +1,10 @@
+import { pageProcessing } from "./_common/pageProcessing";
+import { ClientPage } from "./client/_page";
+
+export default async function Page(props: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const range = await pageProcessing(props);
+
+ return ;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/page.tsx
new file mode 100644
index 00000000000..a55b7840a47
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/page.tsx
@@ -0,0 +1,50 @@
+import {
+ ResponsiveSearchParamsProvider,
+ ResponsiveSuspense,
+} from "responsive-rsc";
+import { ChartUI } from "../_common/ChartUI";
+import { simulatePageProcessingDelay } from "../_common/delays";
+import { getCachedFetchTestData } from "../_common/getCachedFetchTestData";
+import { getRange } from "../_common/getRange";
+import { RangeSelector } from "./range-selector";
+
+export default async function Page(props: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ await simulatePageProcessingDelay();
+
+ const searchParams = await props.searchParams;
+ const from =
+ typeof searchParams.from === "string" ? searchParams.from : undefined;
+ const to = typeof searchParams.to === "string" ? searchParams.to : undefined;
+
+ return (
+
+
+
+ );
+}
+
+export async function AsyncChartUI(props: {
+ from: string | undefined;
+ to: string | undefined;
+}) {
+ const range = getRange(props);
+ const data = await getCachedFetchTestData(range);
+ return ;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/range-selector.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/range-selector.tsx
new file mode 100644
index 00000000000..18269db956f
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/responsive-rsc/range-selector.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import {
+ useResponsiveSearchParams,
+ useSetResponsiveSearchParams,
+} from "responsive-rsc";
+import { DateRangeSelector } from "../../../../../../../components/analytics/date-range-selector";
+import { getRange } from "../_common/getRange";
+
+export function RangeSelector() {
+ const responsiveSearchParams = useResponsiveSearchParams();
+ const setResponsiveSearchParams = useSetResponsiveSearchParams();
+
+ const range = getRange({
+ from:
+ typeof responsiveSearchParams.from === "string"
+ ? responsiveSearchParams.from
+ : undefined,
+ to:
+ typeof responsiveSearchParams.to === "string"
+ ? responsiveSearchParams.to
+ : undefined,
+ });
+
+ return (
+ {
+ setResponsiveSearchParams((v) => {
+ return {
+ ...v,
+ from: newRange.from.toDateString(),
+ to: newRange.to.toDateString(),
+ };
+ });
+ }}
+ />
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/page.tsx
new file mode 100644
index 00000000000..e5c3fe1a52e
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/page.tsx
@@ -0,0 +1,50 @@
+import { Suspense } from "react";
+import { ChartUI } from "../_common/ChartUI";
+import { getCachedFetchTestData } from "../_common/getCachedFetchTestData";
+import { pageProcessing } from "../_common/pageProcessing";
+import { RangeSelector } from "./range-selector";
+
+export default async function Page(props: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const range = await pageProcessing(props);
+
+ return (
+
+ );
+}
+
+function RSCChart(props: {
+ range: {
+ from: Date;
+ to: Date;
+ };
+}) {
+ return (
+ }
+ >
+
+
+ );
+}
+
+async function AsyncChartUI(props: {
+ range: {
+ from: Date;
+ to: Date;
+ };
+}) {
+ const data = await getCachedFetchTestData(props.range);
+ return ;
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/range-selector.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/range-selector.tsx
new file mode 100644
index 00000000000..13ae64ac65f
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/test/server/range-selector.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { usePathname } from "next/navigation";
+import {
+ DateRangeSelector,
+ type Range,
+} from "../../../../../../../components/analytics/date-range-selector";
+
+export function RangeSelector(props: {
+ range: Range;
+}) {
+ const pathname = usePathname();
+ const router = useDashboardRouter();
+
+ return (
+ {
+ const searchParams = new URLSearchParams(window.location.search);
+ searchParams.set("from", newRange.from.toDateString());
+ searchParams.set("to", newRange.to.toDateString());
+
+ // triggers update
+ router.replace(`${pathname}?${searchParams.toString()}`);
+ }}
+ />
+ );
+}
diff --git a/apps/dashboard/src/components/analytics/date-range-selector.tsx b/apps/dashboard/src/components/analytics/date-range-selector.tsx
index e4365b0f544..1de6c9c93a8 100644
--- a/apps/dashboard/src/components/analytics/date-range-selector.tsx
+++ b/apps/dashboard/src/components/analytics/date-range-selector.tsx
@@ -6,13 +6,17 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { format, subDays } from "date-fns";
+import { differenceInCalendarDays, format, subDays } from "date-fns";
export function DateRangeSelector(props: {
range: Range;
setRange: (range: Range) => void;
}) {
const { range, setRange } = props;
+ const daysDiff = differenceInCalendarDays(range.to, range.from);
+ const rangeType =
+ durationPresets.find((preset) => preset.days === daysDiff)?.id ||
+ range.type;
return (