bsmnt 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +17 -8
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +19 -7
- package/src/templates/next-pagebuilder/app/actions/refresh.ts +5 -0
- package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +15 -19
- package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -5
- package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +11 -10
- package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +14 -4
- package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-card.tsx +3 -5
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +93 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-grid.tsx +7 -9
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +212 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/types.ts +5 -4
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +13 -5
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +9 -4
- package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +2 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/constants.ts +1 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +17 -18
- package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +29 -2
- package/src/templates/next-pagebuilder/lib/integrations/sanity/presentation.ts +118 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +144 -31
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +5 -100
- package/src/templates/next-pagebuilder/next.config.ts +3 -0
- package/src/templates/next-pagebuilder/package.json +1 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +0 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { notFound } from "next/navigation"
|
|
2
2
|
import { client } from "@/lib/integrations/sanity/client"
|
|
3
|
-
import {
|
|
3
|
+
import { fetchSanity } from "@/lib/integrations/sanity/live"
|
|
4
4
|
import {
|
|
5
5
|
ALL_PAGE_SLUGS_QUERY,
|
|
6
6
|
PAGE_QUERY,
|
|
@@ -9,12 +9,12 @@ import type {
|
|
|
9
9
|
ALL_PAGE_SLUGS_QUERY_RESULT,
|
|
10
10
|
PAGE_QUERY_RESULT,
|
|
11
11
|
} from "@/lib/integrations/sanity/sanity.types"
|
|
12
|
+
import { PageDocument } from "@/components/page-document"
|
|
12
13
|
import { generateSanityMetadata } from "@/lib/utils/metadata"
|
|
13
14
|
import { getSlugTag } from "@/lib/utils/slug-tag"
|
|
14
|
-
import { PageDocument } from "@/components/page-document"
|
|
15
15
|
|
|
16
16
|
const getPageDocument = async (slug: string | null, stega = true) =>
|
|
17
|
-
|
|
17
|
+
fetchSanity<PAGE_QUERY_RESULT>({
|
|
18
18
|
query: PAGE_QUERY,
|
|
19
19
|
params: { slug },
|
|
20
20
|
tags: [getSlugTag(slug)],
|
|
@@ -32,15 +32,19 @@ const getResolvedPage = async (
|
|
|
32
32
|
return (page as NonNullable<PAGE_QUERY_RESULT> | null) ?? null
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
|
|
36
|
+
|
|
35
37
|
export const generateStaticParams = async () => {
|
|
36
|
-
if (!client) return []
|
|
38
|
+
if (!client) return [{ slug: [] }]
|
|
37
39
|
|
|
38
40
|
const slugs =
|
|
39
41
|
await client.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY)
|
|
40
42
|
|
|
43
|
+
if (!slugs?.length) return [{ slug: [] }]
|
|
44
|
+
|
|
41
45
|
return (slugs ?? []).flatMap((page) => {
|
|
42
46
|
if (!page.slug) return []
|
|
43
|
-
return { slug: page.slug.split("/") }
|
|
47
|
+
return page.slug === "/" ? { slug: [] } : { slug: page.slug.split("/") }
|
|
44
48
|
})
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -56,13 +60,18 @@ export const generateMetadata = async ({ params }: Props) => {
|
|
|
56
60
|
: {}
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export default async function CatchAllPage({
|
|
63
|
+
export default async function CatchAllPage({
|
|
64
|
+
params,
|
|
65
|
+
searchParams,
|
|
66
|
+
}: Props & {
|
|
67
|
+
searchParams: SearchParams
|
|
68
|
+
}) {
|
|
60
69
|
const { slug } = await params
|
|
61
70
|
|
|
62
71
|
const path = toPath(slug)
|
|
63
72
|
const page = await getResolvedPage(path)
|
|
64
73
|
|
|
65
|
-
if (!page)
|
|
74
|
+
if (!page) notFound()
|
|
66
75
|
|
|
67
|
-
return <PageDocument page={page} path={path} />
|
|
76
|
+
return <PageDocument page={page} path={path} searchParams={searchParams} />
|
|
68
77
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { JsonLd } from "@/components/layout/json-ld"
|
|
2
2
|
import { Wrapper } from "@/components/layout/wrapper"
|
|
3
3
|
import { SanityVisualEditing } from "@/components/sanity/visual-editing"
|
|
4
|
+
import { getSiteLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
|
|
7
|
+
const Layout = async ({ children }: { children: React.ReactNode }) => {
|
|
8
|
+
const layoutData = await getSiteLayoutData()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<JsonLd navbar={layoutData.navbar} companyData={layoutData.companyData} />
|
|
13
|
+
<Wrapper
|
|
14
|
+
navbar={layoutData.navbar}
|
|
15
|
+
footer={layoutData.footer}
|
|
16
|
+
companyData={layoutData.companyData}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</Wrapper>
|
|
20
|
+
<SanityVisualEditing />
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
export default Layout
|
|
@@ -3,30 +3,19 @@ import { SanityImage } from "@/components/ui/sanity-image"
|
|
|
3
3
|
import {
|
|
4
4
|
type CompanyData,
|
|
5
5
|
type FooterData,
|
|
6
|
-
getCompanyData,
|
|
7
|
-
getFooter,
|
|
8
6
|
} from "@/lib/integrations/sanity/fetchers/layout"
|
|
9
7
|
|
|
10
8
|
type FooterLinkGroup = NonNullable<FooterData["links"]>[number]
|
|
11
9
|
type FooterLinkItem = NonNullable<FooterLinkGroup["items"]>[number]
|
|
12
10
|
type SocialLink = NonNullable<CompanyData["socialLinks"]>[number]
|
|
13
11
|
|
|
14
|
-
export const Footer =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!(footer || companyData)) {
|
|
23
|
-
return (
|
|
24
|
-
<footer className="border-black/10 border-t px-6 py-3">
|
|
25
|
-
<p>© {year} Basement</p>
|
|
26
|
-
</footer>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
12
|
+
export const Footer = ({
|
|
13
|
+
footer,
|
|
14
|
+
companyData,
|
|
15
|
+
}: {
|
|
16
|
+
footer: FooterData | null
|
|
17
|
+
companyData: CompanyData | null
|
|
18
|
+
}) => {
|
|
30
19
|
const links = footer?.links ?? []
|
|
31
20
|
|
|
32
21
|
return (
|
|
@@ -58,7 +47,9 @@ export const Footer = async () => {
|
|
|
58
47
|
)}
|
|
59
48
|
|
|
60
49
|
<div className="flex flex-row items-center justify-between gap-3 border-black/10 border-t pt-4">
|
|
61
|
-
<p className="w-fit"
|
|
50
|
+
<p className="w-fit">
|
|
51
|
+
© <CurrentYear /> Basement
|
|
52
|
+
</p>
|
|
62
53
|
|
|
63
54
|
{companyData?.socialLinks && companyData.socialLinks.length > 0 ? (
|
|
64
55
|
<div className="flex w-fit flex-wrap items-center gap-x-4 gap-y-2">
|
|
@@ -93,3 +84,8 @@ export const Footer = async () => {
|
|
|
93
84
|
</footer>
|
|
94
85
|
)
|
|
95
86
|
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
const CurrentYear = () => {
|
|
90
|
+
return <span>{new Date().getFullYear()}</span>
|
|
91
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { getNavbar } from "@/lib/integrations/sanity/fetchers/layout"
|
|
2
|
-
|
|
3
1
|
import { HeaderClient } from "./header-client"
|
|
2
|
+
import type { NavbarData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
4
3
|
|
|
5
|
-
export const Header =
|
|
6
|
-
|
|
7
|
-
return <HeaderClient data={navbar} />
|
|
4
|
+
export const Header = ({ data }: { data: NavbarData | null }) => {
|
|
5
|
+
return <HeaderClient data={data} />
|
|
8
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stegaClean } from "next-sanity"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type {
|
|
3
|
+
CompanyData,
|
|
4
|
+
NavbarData,
|
|
5
5
|
} from "@/lib/integrations/sanity/fetchers/layout"
|
|
6
6
|
import {
|
|
7
7
|
generateOrganizationJsonLd,
|
|
@@ -12,17 +12,18 @@ import {
|
|
|
12
12
|
const APP_DESCRIPTION =
|
|
13
13
|
"Basement is the AI-native platform for audit and advisory firms. Automate engagement workflows using AI agents trusted by 50% of top 100 firms."
|
|
14
14
|
|
|
15
|
-
export const JsonLd =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export const JsonLd = ({
|
|
16
|
+
navbar,
|
|
17
|
+
companyData,
|
|
18
|
+
}: {
|
|
19
|
+
navbar: NavbarData | null
|
|
20
|
+
companyData: CompanyData | null
|
|
21
|
+
}) => {
|
|
21
22
|
const sameAs = companyData?.socialLinks
|
|
22
23
|
?.map((link) => stegaClean(link.url))
|
|
23
24
|
.filter((url): url is string => Boolean(url))
|
|
24
25
|
|
|
25
|
-
const logoUrl =
|
|
26
|
+
const logoUrl = navbar?.logo?.asset?.url
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import Link from "next/link"
|
|
2
2
|
import { Footer } from "@/components/layout/footer"
|
|
3
3
|
import { Header } from "@/components/layout/header"
|
|
4
|
+
import type { SiteLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
4
5
|
import { cn } from "@/lib/styles/cn"
|
|
5
6
|
|
|
6
|
-
interface WrapperProps
|
|
7
|
+
interface WrapperProps
|
|
8
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
9
|
+
SiteLayoutData {}
|
|
7
10
|
|
|
8
11
|
const SkipToMainContent = () => (
|
|
9
12
|
<Link
|
|
@@ -14,10 +17,17 @@ const SkipToMainContent = () => (
|
|
|
14
17
|
</Link>
|
|
15
18
|
)
|
|
16
19
|
|
|
17
|
-
export const Wrapper = ({
|
|
20
|
+
export const Wrapper = ({
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
navbar,
|
|
24
|
+
footer,
|
|
25
|
+
companyData,
|
|
26
|
+
...props
|
|
27
|
+
}: WrapperProps) => (
|
|
18
28
|
<>
|
|
19
29
|
<SkipToMainContent />
|
|
20
|
-
<Header />
|
|
30
|
+
<Header data={navbar} />
|
|
21
31
|
<main
|
|
22
32
|
id="main-content"
|
|
23
33
|
className={cn("relative flex grow flex-col", className)}
|
|
@@ -25,6 +35,6 @@ export const Wrapper = ({ children, className, ...props }: WrapperProps) => (
|
|
|
25
35
|
>
|
|
26
36
|
{children}
|
|
27
37
|
</main>
|
|
28
|
-
<Footer />
|
|
38
|
+
<Footer footer={footer} companyData={companyData} />
|
|
29
39
|
</>
|
|
30
40
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Link } from "@/components/ui/link"
|
|
2
2
|
import { SanityImage } from "@/components/ui/sanity-image"
|
|
3
3
|
import { formatDate } from "@/lib/utils/format-date"
|
|
4
|
-
import type {
|
|
4
|
+
import type { ContentItemCard } from "./types"
|
|
5
5
|
|
|
6
6
|
type ContentCardProps = {
|
|
7
|
-
article:
|
|
7
|
+
article: ContentItemCard
|
|
8
8
|
className?: string | undefined
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -46,9 +46,7 @@ export const ContentCard = ({ article, className }: ContentCardProps) => {
|
|
|
46
46
|
</div>
|
|
47
47
|
) : null}
|
|
48
48
|
|
|
49
|
-
<h3 className="text-balance font-medium text-lg leading-snug">
|
|
50
|
-
{title}
|
|
51
|
-
</h3>
|
|
49
|
+
<h3 className="text-balance font-medium text-lg leading-snug">{title}</h3>
|
|
52
50
|
|
|
53
51
|
{authorName || formattedDate ? (
|
|
54
52
|
<div className="flex items-center gap-2 text-current/60 text-sm">
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
4
|
+
import { cn } from "@/lib/styles/cn"
|
|
5
|
+
import type { ContentCategory } from "./types"
|
|
6
|
+
|
|
7
|
+
type ContentFiltersProps = {
|
|
8
|
+
categories: ContentCategory[]
|
|
9
|
+
activeCategory: string
|
|
10
|
+
totalCount: number
|
|
11
|
+
filteredCount: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function FilterChip({
|
|
15
|
+
active,
|
|
16
|
+
label,
|
|
17
|
+
onClick,
|
|
18
|
+
}: {
|
|
19
|
+
active: boolean
|
|
20
|
+
label: string
|
|
21
|
+
onClick: () => void
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={onClick}
|
|
27
|
+
className={cn(
|
|
28
|
+
"rounded-full border px-4 py-2 text-sm transition-colors",
|
|
29
|
+
active
|
|
30
|
+
? "border-black bg-black text-white"
|
|
31
|
+
: "border-black/10 bg-white hover:border-black/30"
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{label}
|
|
35
|
+
</button>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ContentFilters({
|
|
40
|
+
categories,
|
|
41
|
+
activeCategory,
|
|
42
|
+
totalCount,
|
|
43
|
+
filteredCount,
|
|
44
|
+
}: ContentFiltersProps) {
|
|
45
|
+
const pathname = usePathname()
|
|
46
|
+
const router = useRouter()
|
|
47
|
+
const searchParams = useSearchParams()
|
|
48
|
+
|
|
49
|
+
const navigate = (category: string) => {
|
|
50
|
+
const nextParams = new URLSearchParams(searchParams.toString())
|
|
51
|
+
|
|
52
|
+
if (category) nextParams.set("category", category)
|
|
53
|
+
else nextParams.delete("category")
|
|
54
|
+
|
|
55
|
+
nextParams.delete("page")
|
|
56
|
+
|
|
57
|
+
const href = nextParams.toString()
|
|
58
|
+
? `${pathname}?${nextParams.toString()}`
|
|
59
|
+
: pathname
|
|
60
|
+
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: typed routes do not accept dynamic query string helpers cleanly
|
|
62
|
+
router.push(href as any, { scroll: false })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
|
|
67
|
+
<div className="flex flex-wrap gap-2">
|
|
68
|
+
<FilterChip
|
|
69
|
+
active={activeCategory === ""}
|
|
70
|
+
label="All content"
|
|
71
|
+
onClick={() => navigate("")}
|
|
72
|
+
/>
|
|
73
|
+
{categories.map((category) => {
|
|
74
|
+
const slug = category.slug?.current
|
|
75
|
+
if (!slug) return null
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<FilterChip
|
|
79
|
+
key={category._id}
|
|
80
|
+
active={activeCategory === slug}
|
|
81
|
+
label={category.title || "Untitled"}
|
|
82
|
+
onClick={() => navigate(slug)}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
<p className="text-current/60 text-sm">
|
|
88
|
+
Showing {filteredCount} {filteredCount === 1 ? "item" : "items"}
|
|
89
|
+
{filteredCount !== totalCount ? ` out of ${totalCount}` : ""}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { BlogArticleCard } from "./types"
|
|
2
1
|
import { ContentCard } from "./content-card"
|
|
2
|
+
import type { ContentItemCard } from "./types"
|
|
3
3
|
|
|
4
4
|
type ContentGridProps = {
|
|
5
|
-
|
|
5
|
+
items: ContentItemCard[]
|
|
6
6
|
start?: number
|
|
7
7
|
end?: number
|
|
8
8
|
className?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function ContentGrid({
|
|
12
|
-
|
|
12
|
+
items,
|
|
13
13
|
start = 0,
|
|
14
14
|
end,
|
|
15
15
|
className,
|
|
16
16
|
}: ContentGridProps) {
|
|
17
|
-
const slice =
|
|
17
|
+
const slice = items.slice(start, end)
|
|
18
18
|
|
|
19
19
|
if (slice.length === 0) {
|
|
20
20
|
if (start === 0) {
|
|
@@ -29,13 +29,11 @@ export function ContentGrid({
|
|
|
29
29
|
className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
|
|
30
30
|
}
|
|
31
31
|
>
|
|
32
|
-
{slice.map((
|
|
33
|
-
const slug =
|
|
32
|
+
{slice.map((item) => {
|
|
33
|
+
const slug = item.resolvedSlug
|
|
34
34
|
if (!slug) return null
|
|
35
35
|
|
|
36
|
-
return
|
|
37
|
-
<ContentCard key={article._id} article={article} />
|
|
38
|
-
)
|
|
36
|
+
return <ContentCard key={item._id} article={item} />
|
|
39
37
|
})}
|
|
40
38
|
</ul>
|
|
41
39
|
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { usePathname, useSearchParams } from "next/navigation"
|
|
4
|
+
import { Link } from "@/components/ui/link"
|
|
5
|
+
|
|
6
|
+
type ContentPaginationNavProps = {
|
|
7
|
+
currentPage: number
|
|
8
|
+
totalPages: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildPaginationHref({
|
|
12
|
+
pathname,
|
|
13
|
+
page,
|
|
14
|
+
searchParams,
|
|
15
|
+
}: {
|
|
16
|
+
pathname: string
|
|
17
|
+
page: number
|
|
18
|
+
searchParams: ReturnType<typeof useSearchParams>
|
|
19
|
+
}) {
|
|
20
|
+
const nextParams = new URLSearchParams(searchParams.toString())
|
|
21
|
+
|
|
22
|
+
if (page > 1) nextParams.set("page", String(page))
|
|
23
|
+
else nextParams.delete("page")
|
|
24
|
+
|
|
25
|
+
const query = nextParams.toString()
|
|
26
|
+
return query ? `${pathname}?${query}` : pathname
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ContentPaginationNav({
|
|
30
|
+
currentPage,
|
|
31
|
+
totalPages,
|
|
32
|
+
}: ContentPaginationNavProps) {
|
|
33
|
+
const pathname = usePathname()
|
|
34
|
+
const searchParams = useSearchParams()
|
|
35
|
+
|
|
36
|
+
if (!pathname || totalPages <= 1) return null
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<nav className="flex flex-wrap items-center gap-2" aria-label="Pagination">
|
|
40
|
+
{currentPage > 1 ? (
|
|
41
|
+
<Link
|
|
42
|
+
href={buildPaginationHref({
|
|
43
|
+
pathname,
|
|
44
|
+
page: currentPage - 1,
|
|
45
|
+
searchParams,
|
|
46
|
+
})}
|
|
47
|
+
className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
|
|
48
|
+
>
|
|
49
|
+
Previous
|
|
50
|
+
</Link>
|
|
51
|
+
) : null}
|
|
52
|
+
|
|
53
|
+
<span className="text-current/60 text-sm">
|
|
54
|
+
Page {currentPage} of {totalPages}
|
|
55
|
+
</span>
|
|
56
|
+
|
|
57
|
+
{currentPage < totalPages ? (
|
|
58
|
+
<Link
|
|
59
|
+
href={buildPaginationHref({
|
|
60
|
+
pathname,
|
|
61
|
+
page: currentPage + 1,
|
|
62
|
+
searchParams,
|
|
63
|
+
})}
|
|
64
|
+
className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
|
|
65
|
+
>
|
|
66
|
+
Next
|
|
67
|
+
</Link>
|
|
68
|
+
) : null}
|
|
69
|
+
</nav>
|
|
70
|
+
)
|
|
71
|
+
}
|