Skip to content

Commit dba18b0

Browse files
authored
Merge pull request #383 from mfts/feat/paginate
feat: add pagination to visitor data
2 parents 55e7758 + 04f272b commit dba18b0

File tree

5 files changed

+276
-55
lines changed

5 files changed

+276
-55
lines changed

Diff for: components/ui/pagination.tsx

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as React from "react";
2+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
3+
4+
import { cn } from "@/lib/utils";
5+
import { ButtonProps, buttonVariants } from "@/components/ui/button";
6+
7+
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8+
<nav
9+
role="navigation"
10+
aria-label="pagination"
11+
className={cn("mx-auto flex w-full justify-center", className)}
12+
{...props}
13+
/>
14+
);
15+
Pagination.displayName = "Pagination";
16+
17+
const PaginationContent = React.forwardRef<
18+
HTMLUListElement,
19+
React.ComponentProps<"ul">
20+
>(({ className, ...props }, ref) => (
21+
<ul
22+
ref={ref}
23+
className={cn("flex flex-row items-center gap-1", className)}
24+
{...props}
25+
/>
26+
));
27+
PaginationContent.displayName = "PaginationContent";
28+
29+
const PaginationItem = React.forwardRef<
30+
HTMLLIElement,
31+
React.ComponentProps<"li">
32+
>(({ className, ...props }, ref) => (
33+
<li ref={ref} className={cn("", className)} {...props} />
34+
));
35+
PaginationItem.displayName = "PaginationItem";
36+
37+
type PaginationLinkProps = {
38+
isActive?: boolean;
39+
disabled?: boolean;
40+
} & Pick<ButtonProps, "size"> &
41+
React.ComponentProps<"a">;
42+
43+
const PaginationLink = ({
44+
className,
45+
isActive,
46+
size = "icon",
47+
...props
48+
}: PaginationLinkProps) => (
49+
<a
50+
aria-current={isActive ? "page" : undefined}
51+
className={cn(
52+
buttonVariants({
53+
variant: isActive ? "outline" : "ghost",
54+
size,
55+
}),
56+
className,
57+
)}
58+
{...props}
59+
/>
60+
);
61+
PaginationLink.displayName = "PaginationLink";
62+
63+
const PaginationPrevious = ({
64+
className,
65+
disabled,
66+
...props
67+
}: React.ComponentProps<typeof PaginationLink>) => (
68+
<PaginationLink
69+
aria-label="Go to previous page"
70+
size="default"
71+
className={cn(
72+
"gap-1 pl-2.5",
73+
disabled ? "pointer-events-none opacity-50" : "",
74+
className,
75+
)}
76+
{...props}
77+
>
78+
<ChevronLeft className="h-4 w-4" />
79+
<span>Previous</span>
80+
</PaginationLink>
81+
);
82+
PaginationPrevious.displayName = "PaginationPrevious";
83+
84+
const PaginationNext = ({
85+
className,
86+
disabled,
87+
...props
88+
}: React.ComponentProps<typeof PaginationLink>) => (
89+
<PaginationLink
90+
aria-label="Go to next page"
91+
size="default"
92+
className={cn(
93+
"gap-1 pr-2.5",
94+
disabled ? "pointer-events-none opacity-50" : "",
95+
className,
96+
)}
97+
{...props}
98+
>
99+
<span>Next</span>
100+
<ChevronRight className="h-4 w-4" />
101+
</PaginationLink>
102+
);
103+
PaginationNext.displayName = "PaginationNext";
104+
105+
const PaginationEllipsis = ({
106+
className,
107+
...props
108+
}: React.ComponentProps<"span">) => (
109+
<span
110+
aria-hidden
111+
className={cn("flex h-9 w-9 items-center justify-center", className)}
112+
{...props}
113+
>
114+
<MoreHorizontal className="h-4 w-4" />
115+
<span className="sr-only">More pages</span>
116+
</span>
117+
);
118+
PaginationEllipsis.displayName = "PaginationEllipsis";
119+
120+
export {
121+
Pagination,
122+
PaginationContent,
123+
PaginationEllipsis,
124+
PaginationItem,
125+
PaginationLink,
126+
PaginationNext,
127+
PaginationPrevious,
128+
};

Diff for: components/visitors/visitors-table.tsx

+64-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,22 @@ import { BadgeTooltip } from "@/components/ui/tooltip";
3232
import { usePlan } from "@/lib/swr/use-billing";
3333
import { UpgradePlanModal } from "../billing/upgrade-plan-modal";
3434
import { Button } from "../ui/button";
35+
import { useState } from "react";
36+
import {
37+
Pagination,
38+
PaginationContent,
39+
PaginationEllipsis,
40+
PaginationItem,
41+
PaginationLink,
42+
PaginationNext,
43+
PaginationPrevious,
44+
} from "../ui/pagination";
3545

3646
export default function VisitorsTable({ numPages }: { numPages: number }) {
37-
const { views } = useDocumentVisits();
47+
const [currentPage, setCurrentPage] = useState<number>(1);
48+
const limit = 10; // Set the number of items per page
3849

50+
const { views, loading, error } = useDocumentVisits(currentPage, limit);
3951
const { plan } = usePlan();
4052
const isFreePlan = plan?.plan === "free";
4153

@@ -228,6 +240,57 @@ export default function VisitorsTable({ numPages }: { numPages: number }) {
228240
</TableBody>
229241
</Table>
230242
</div>
243+
{/* Pagination Controls */}
244+
<div className="mt-2 w-full flex items-center">
245+
<div className="text-sm w-full">
246+
Showing <span className="font-semibold">10</span> of{" "}
247+
{views?.totalViews} visits
248+
</div>
249+
<Pagination className="justify-end">
250+
<PaginationContent>
251+
<PaginationItem>
252+
<PaginationPrevious
253+
onClick={() => setCurrentPage(currentPage - 1)}
254+
disabled={currentPage === 1}
255+
/>
256+
</PaginationItem>
257+
{currentPage !== 1 ? (
258+
<PaginationItem>
259+
<PaginationLink onClick={() => setCurrentPage(1)}>
260+
{1}
261+
</PaginationLink>
262+
</PaginationItem>
263+
) : null}
264+
265+
<PaginationItem>
266+
<PaginationLink isActive>{currentPage}</PaginationLink>
267+
</PaginationItem>
268+
269+
{views?.totalViews &&
270+
currentPage !== Math.ceil(views?.totalViews / 10) ? (
271+
<PaginationItem>
272+
<PaginationLink
273+
onClick={() =>
274+
setCurrentPage(Math.ceil(views?.totalViews / 10))
275+
}
276+
>
277+
{Math.ceil(views?.totalViews / 10)}
278+
</PaginationLink>
279+
</PaginationItem>
280+
) : null}
281+
<PaginationItem>
282+
<PaginationNext
283+
onClick={() => setCurrentPage(currentPage + 1)}
284+
disabled={
285+
views?.totalViews
286+
? currentPage === Math.ceil(views?.totalViews / 10)
287+
: true
288+
}
289+
/>
290+
</PaginationItem>
291+
</PaginationContent>
292+
</Pagination>
293+
</div>
231294
</div>
232295
);
233296
}

Diff for: lib/swr/use-document.ts

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useRouter } from "next/router";
22
import useSWR from "swr";
3+
import useSWRImmutable from "swr/immutable";
34
import { fetcher } from "@/lib/utils";
45
import { DocumentWithVersion, LinkWithViews } from "@/lib/types";
56
import { View } from "@prisma/client";
@@ -82,27 +83,29 @@ interface ViewWithDuration extends View {
8283
type TStatsData = {
8384
hiddenViewCount: number;
8485
viewsWithDuration: ViewWithDuration[];
86+
totalViews: number;
8587
};
8688

87-
export function useDocumentVisits() {
89+
export function useDocumentVisits(page: number, limit: number) {
8890
const router = useRouter();
8991
const teamInfo = useTeam();
92+
const teamId = teamInfo?.currentTeam?.id;
9093

9194
const { id } = router.query as {
9295
id: string;
9396
};
9497

95-
const { data: views, error } = useSWR<TStatsData>(
96-
teamInfo?.currentTeam?.id &&
97-
id &&
98-
`/api/teams/${teamInfo?.currentTeam?.id}/documents/${encodeURIComponent(
99-
id,
100-
)}/views`,
101-
fetcher,
102-
{
103-
dedupingInterval: 10000,
104-
},
105-
);
98+
const cacheKey =
99+
teamId && id
100+
? `/api/teams/${teamInfo?.currentTeam?.id}/documents/${encodeURIComponent(
101+
id,
102+
)}/views?page=${page}&limit=${limit}`
103+
: null;
104+
105+
const { data: views, error } = useSWR<TStatsData>(cacheKey, fetcher, {
106+
dedupingInterval: 20000,
107+
revalidateOnFocus: false,
108+
});
106109

107110
return {
108111
views,

Diff for: pages/api/auth/[...nextauth].ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const authOptions: NextAuthOptions = {
3434
authorization: {
3535
params: { scope: "openid profile email" },
3636
},
37-
issuer: "https://www.linkedin.com",
37+
issuer: "https://www.linkedin.com/oauth",
3838
jwks_endpoint: "https://www.linkedin.com/oauth/openid/jwks",
3939
profile(profile, tokens) {
4040
const defaultImage =

0 commit comments

Comments
 (0)