Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table cards now paginate independently #80

Merged
merged 21 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Refactor paths card to be full stack component
  • Loading branch information
benvinegar committed Jun 17, 2024
commit 8ae0898c1d58269027ab3d9bcf58fc37093f0016
21 changes: 18 additions & 3 deletions app/analytics/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,12 @@ export class AnalyticsEngineAPI {
column: T,
interval: string,
tz?: string,
page?: number,
limit?: number,
) {
// defaults to 1 day if not specified
limit = limit || 10;
page = page || 1;

const intervalSql = intervalToSql(interval, tz);

Expand All @@ -387,7 +389,7 @@ export class AnalyticsEngineAPI {
AND ${ColumnMappings.siteId} = '${siteId}'
GROUP BY ${_column}, ${ColumnMappings.newVisitor}, ${ColumnMappings.newSession}
ORDER BY count DESC
LIMIT ${limit}`;
LIMIT ${limit * page}`;

type SelectionSet = {
readonly count: number;
Expand All @@ -411,7 +413,14 @@ export class AnalyticsEngineAPI {
const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

const result = responseData.data.reduce(
// since CF AE doesn't support OFFSET clauses, we select up to LIMIT and
// then slice that into the individual requested page
const pageData = responseData.data.slice(
limit * (page - 1),
limit * page,
);

const result = pageData.reduce(
(acc, row) => {
const key =
row[_column] === ""
Expand All @@ -436,12 +445,18 @@ export class AnalyticsEngineAPI {
return returnPromise;
}

async getCountByPath(siteId: string, interval: string, tz?: string) {
async getCountByPath(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
const allCountsResultPromise = this.getAllCountsByColumn(
siteId,
"path",
interval,
tz,
page,
);

return allCountsResultPromise.then((allCountsResult) => {
Expand Down
38 changes: 8 additions & 30 deletions app/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,12 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export function useUpdateQueryStringValueWithoutNavigation(
queryKey: string,
queryValue: string,
) {
React.useEffect(() => {
const currentSearchParams = new URLSearchParams(window.location.search);
const oldQuery = currentSearchParams.get(queryKey) ?? "";
if (queryValue === oldQuery) return;

if (queryValue) {
currentSearchParams.set(queryKey, queryValue);
} else {
currentSearchParams.delete(queryKey);
}
const newUrl = [
window.location.pathname,
currentSearchParams.toString(),
]
.filter(Boolean)
.join("?");
// alright, let's talk about this...
// Normally with remix, you'd update the params via useSearchParams from react-router-dom
// and updating the search params will trigger the search to update for you.
// However, it also triggers a navigation to the new url, which will trigger
// the loader to run which we do not want because all our data is already
// on the client and we're just doing client-side filtering of data we
// already have. So we manually call `window.history.pushState` to avoid
// the router from triggering the loader.
window.history.replaceState(null, "", newUrl);
}, [queryKey, queryValue]);
export function paramsFromUrl(url: string) {
const searchParams = new URL(url).searchParams;
const params: Record<string, string> = {};
searchParams.forEach((value, key) => {
params[key] = value;
});
console.log(params);
return params;
}
8 changes: 2 additions & 6 deletions app/routes/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { AnalyticsEngineAPI } from "../analytics/query";

import TableCard from "~/components/TableCard";
import { ReferrerCard } from "./resources.referrer";
import { PathsCard } from "./resources.paths";

import TimeSeriesChart from "~/components/TimeSeriesChart";
import dayjs from "dayjs";
Expand Down Expand Up @@ -335,12 +336,7 @@ export default function Dashboard() {
</Card>
</div>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<Card>
<TableCard
countByProperty={data.countByPath}
columnHeaders={["Page", "Visitors", "Views"]}
/>
</Card>
<PathsCard siteId={data.siteId} interval={data.interval} />
<ReferrerCard
siteId={data.siteId}
interval={data.interval}
Expand Down
107 changes: 107 additions & 0 deletions app/routes/resources.paths.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useFetcher } from "@remix-run/react";

import { ArrowRight, ArrowLeft } from "lucide-react";

import { paramsFromUrl } from "~/lib/utils";

import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
import { json } from "@remix-run/cloudflare";

export async function loader({ context, request }: LoaderFunctionArgs) {
const { analyticsEngine } = context;

const { interval, site, page = 1 } = paramsFromUrl(request.url);
const tz = context.requestTimezone as string;

const countByPath = await analyticsEngine.getCountByPath(
site,
interval,
tz,
Number(page),
);

return json({
countByPath,
page: Number(page),
});
}

import { useEffect } from "react";
import TableCard from "~/components/TableCard";
import { Card } from "~/components/ui/card";

export const PathsCard = ({
siteId,
interval,
error,
}: {
siteId: string;
interval: string;
error?: string | null;
}) => {
const dataFetcher = useFetcher<typeof loader>();
const countByPath = dataFetcher.data?.countByPath || [];
const page = dataFetcher.data?.page || 1;

useEffect(() => {
// Your code here
if (dataFetcher.state === "idle") {
dataFetcher.load(
`/resources/paths?site=${siteId}&interval=${interval}`,
);
}
}, []);

useEffect(() => {
// NOTE: intentionally resets page to default when interval or site changes
if (dataFetcher.state === "idle") {
dataFetcher.load(
`/resources/paths?site=${siteId}&interval=${interval}`,
);
}
}, [siteId, interval]);

function handlePagination(page: number) {
// TODO: is there a way of updating the query string with this state without triggering a navigation?
dataFetcher.load(
`/resources/paths?site=${siteId}&interval=${interval}&paths_page=${page}`,
);
}

const hasMore = countByPath.length === 10;
return (
<Card>
{countByPath ? (
<div>
<TableCard
countByProperty={countByPath}
columnHeaders={["Page", "Visitors", "Views"]}
/>
<div className="p-2 pr-0 grid grid-cols-[auto,2rem,2rem] text-right">
<div></div>
<a
onClick={() => {
if (page > 1) handlePagination(page - 1);
}}
className={
page > 1 ? `text-primary` : `text-orange-300`
}
>
<ArrowLeft />
</a>
<a
onClick={() => {
if (hasMore) handlePagination(page + 1);
}}
className={
hasMore ? `text-primary` : `text-orange-300`
}
>
<ArrowRight />
</a>
</div>
</div>
) : null}
</Card>
);
};
18 changes: 6 additions & 12 deletions app/routes/resources.referrer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,39 @@ import { useFetcher } from "@remix-run/react";

import { ArrowRight, ArrowLeft } from "lucide-react";

// app/routes/resources/customers.tsx
import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
import { json } from "@remix-run/cloudflare";

export async function loader({ context, request }: LoaderFunctionArgs) {
const { analyticsEngine } = context;

const url = new URL(request.url);
const interval = url.searchParams.get("interval") || "";
const siteId = url.searchParams.get("site") || "";

const { interval, site, page = 1 } = paramsFromUrl(request.url);
const tz = context.requestTimezone as string;

const page = Number(url.searchParams.get("referrer_page") || 1);
const countByReferrer = await analyticsEngine.getCountByReferrer(
siteId,
site,
interval,
tz,
page,
Number(page),
);

return json({
countByReferrer: countByReferrer,
page,
page: Number(page),
});
}

import { useEffect } from "react";
import TableCard from "~/components/TableCard";
import { Card } from "~/components/ui/card";
import { paramsFromUrl } from "~/lib/utils";

export const ReferrerCard = ({
siteId,
interval,
error,
}: {
siteId: string;
interval: string;
error?: string | null;
}) => {
const dataFetcher = useFetcher<typeof loader>();
const countByReferrer = dataFetcher.data?.countByReferrer || [];
Expand All @@ -67,7 +61,7 @@ export const ReferrerCard = ({
function handlePagination(page: number) {
// TODO: is there a way of updating the query string with this state without triggering a navigation?
dataFetcher.load(
`/resources/referrer?site=${siteId}&interval=${interval}&referrer_page=${page}`,
`/resources/referrer?site=${siteId}&interval=${interval}&page=${page}`,
);
}

Expand Down