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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { fetchSanity } from "@/lib/integrations/sanity/live"
|
|
2
|
+
import {
|
|
3
|
+
ALL_BLOG_CATEGORIES_QUERY,
|
|
4
|
+
BLOG_ARTICLES_COUNT_QUERY,
|
|
5
|
+
BLOG_ARTICLES_PAGINATED_QUERY,
|
|
6
|
+
} from "@/lib/integrations/sanity/queries"
|
|
7
|
+
import { ContentFilters } from "./content-filters"
|
|
8
|
+
import { ContentGrid } from "./content-grid"
|
|
9
|
+
import { ContentPaginationNav } from "./content-pagination-nav"
|
|
10
|
+
import type {
|
|
11
|
+
ContentCollectionBlock,
|
|
12
|
+
ContentCollectionSearchParams,
|
|
13
|
+
ContentCategory,
|
|
14
|
+
ContentItemCard,
|
|
15
|
+
SearchParams,
|
|
16
|
+
} from "./types"
|
|
17
|
+
|
|
18
|
+
type ContentCollectionProps = {
|
|
19
|
+
block: ContentCollectionBlock
|
|
20
|
+
searchParams: ContentCollectionSearchParams
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FEATURED_COUNT = 2
|
|
24
|
+
const PAGE_SIZE = 6
|
|
25
|
+
|
|
26
|
+
const normalizePageValue = (value: string | string[] | undefined) => {
|
|
27
|
+
const page = Number(Array.isArray(value) ? value[0] : value)
|
|
28
|
+
return Number.isFinite(page) && page > 0 ? Math.floor(page) : 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getCollectionParams = (searchParams: SearchParams) => ({
|
|
32
|
+
currentPage: normalizePageValue(searchParams.page),
|
|
33
|
+
activeCategory:
|
|
34
|
+
typeof searchParams.category === "string" ? searchParams.category : "",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const getTotalPages = ({
|
|
38
|
+
activeCategory,
|
|
39
|
+
totalCount,
|
|
40
|
+
}: {
|
|
41
|
+
activeCategory: string
|
|
42
|
+
totalCount: number
|
|
43
|
+
}) => {
|
|
44
|
+
const regularTotal =
|
|
45
|
+
activeCategory === "" ? Math.max(0, totalCount - FEATURED_COUNT) : totalCount
|
|
46
|
+
|
|
47
|
+
return Math.max(1, Math.ceil(regularTotal / PAGE_SIZE))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const getCollectionWindow = ({
|
|
51
|
+
activeCategory,
|
|
52
|
+
currentPage,
|
|
53
|
+
}: {
|
|
54
|
+
activeCategory: string
|
|
55
|
+
currentPage: number
|
|
56
|
+
}) => {
|
|
57
|
+
const isFirstPageNoFilter = currentPage === 1 && activeCategory === ""
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
isFirstPageNoFilter,
|
|
61
|
+
fetchLimit: isFirstPageNoFilter ? PAGE_SIZE + FEATURED_COUNT : PAGE_SIZE,
|
|
62
|
+
fetchOffset: isFirstPageNoFilter
|
|
63
|
+
? 0
|
|
64
|
+
: (currentPage - 1) * PAGE_SIZE + (activeCategory === "" ? FEATURED_COUNT : 0),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function CollectionToolbarSection({
|
|
69
|
+
activeCategory,
|
|
70
|
+
categories,
|
|
71
|
+
totalCount,
|
|
72
|
+
}: {
|
|
73
|
+
activeCategory: string
|
|
74
|
+
categories: ContentCategory[]
|
|
75
|
+
totalCount: number
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<ContentFilters
|
|
79
|
+
categories={categories}
|
|
80
|
+
activeCategory={activeCategory}
|
|
81
|
+
totalCount={totalCount}
|
|
82
|
+
filteredCount={totalCount}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function CollectionResultsSection({
|
|
88
|
+
activeCategory,
|
|
89
|
+
items,
|
|
90
|
+
isFirstPageNoFilter,
|
|
91
|
+
subtitle,
|
|
92
|
+
}: {
|
|
93
|
+
activeCategory: string
|
|
94
|
+
items: ContentItemCard[]
|
|
95
|
+
isFirstPageNoFilter: boolean
|
|
96
|
+
subtitle: string | null | undefined
|
|
97
|
+
}) {
|
|
98
|
+
const featuredItems = isFirstPageNoFilter ? items.slice(0, FEATURED_COUNT) : []
|
|
99
|
+
const regularItems = isFirstPageNoFilter ? items.slice(FEATURED_COUNT) : items
|
|
100
|
+
const isEmpty = featuredItems.length === 0 && regularItems.length === 0
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
{featuredItems.length > 0 ? (
|
|
105
|
+
<ContentGrid
|
|
106
|
+
items={featuredItems}
|
|
107
|
+
className="grid grid-cols-1 gap-8 md:grid-cols-2"
|
|
108
|
+
/>
|
|
109
|
+
) : null}
|
|
110
|
+
{isFirstPageNoFilter && activeCategory === "" && subtitle ? (
|
|
111
|
+
<h2 className="font-medium text-2xl leading-tight">{subtitle}</h2>
|
|
112
|
+
) : null}
|
|
113
|
+
{isEmpty ? (
|
|
114
|
+
<div className="rounded-[1.5rem] border border-black/15 border-dashed bg-black/[0.02] px-6 py-12 text-center">
|
|
115
|
+
<p className="font-medium text-lg">No content found</p>
|
|
116
|
+
<p className="mt-2 text-current/60 text-sm">
|
|
117
|
+
Try a different category to explore more content.
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<ContentGrid items={regularItems} />
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function CollectionPaginationSection({
|
|
128
|
+
currentPage,
|
|
129
|
+
totalPages,
|
|
130
|
+
}: {
|
|
131
|
+
currentPage: number
|
|
132
|
+
totalPages: number
|
|
133
|
+
}) {
|
|
134
|
+
if (totalPages <= 1) return null
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ContentPaginationNav currentPage={currentPage} totalPages={totalPages} />
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function ContentCollection({
|
|
142
|
+
block,
|
|
143
|
+
searchParams,
|
|
144
|
+
}: ContentCollectionProps) {
|
|
145
|
+
const resolvedParams = (await searchParams) ?? {}
|
|
146
|
+
const { currentPage, activeCategory } = getCollectionParams(resolvedParams)
|
|
147
|
+
|
|
148
|
+
const { data: categoriesData } = await fetchSanity<ContentCategory[]>({
|
|
149
|
+
query: ALL_BLOG_CATEGORIES_QUERY,
|
|
150
|
+
tags: ["blog-categories"],
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const categories = categoriesData ?? []
|
|
154
|
+
const validCategorySlugs = new Set(
|
|
155
|
+
categories
|
|
156
|
+
.map((category) => category.slug?.current)
|
|
157
|
+
.filter((slug): slug is string => Boolean(slug))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const canonicalCategory =
|
|
161
|
+
activeCategory && validCategorySlugs.has(activeCategory) ? activeCategory : ""
|
|
162
|
+
|
|
163
|
+
const { data: totalCountData } = await fetchSanity<number>({
|
|
164
|
+
query: BLOG_ARTICLES_COUNT_QUERY,
|
|
165
|
+
params: { category: canonicalCategory },
|
|
166
|
+
tags: ["blog-articles-count"],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const totalCount = totalCountData ?? 0
|
|
170
|
+
const totalPages = getTotalPages({
|
|
171
|
+
activeCategory: canonicalCategory,
|
|
172
|
+
totalCount,
|
|
173
|
+
})
|
|
174
|
+
const canonicalPage = Math.min(currentPage, totalPages)
|
|
175
|
+
|
|
176
|
+
const { isFirstPageNoFilter, fetchLimit, fetchOffset } = getCollectionWindow({
|
|
177
|
+
activeCategory: canonicalCategory,
|
|
178
|
+
currentPage: canonicalPage,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const { data: itemsData } = await fetchSanity<ContentItemCard[]>({
|
|
182
|
+
query: BLOG_ARTICLES_PAGINATED_QUERY,
|
|
183
|
+
params: {
|
|
184
|
+
category: canonicalCategory,
|
|
185
|
+
offset: fetchOffset,
|
|
186
|
+
limit: fetchLimit,
|
|
187
|
+
},
|
|
188
|
+
tags: ["blog-articles"],
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const items = itemsData ?? []
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<section className="flex flex-col gap-8 py-8">
|
|
195
|
+
<CollectionToolbarSection
|
|
196
|
+
activeCategory={canonicalCategory}
|
|
197
|
+
categories={categories}
|
|
198
|
+
totalCount={totalCount}
|
|
199
|
+
/>
|
|
200
|
+
<CollectionResultsSection
|
|
201
|
+
activeCategory={canonicalCategory}
|
|
202
|
+
items={items}
|
|
203
|
+
isFirstPageNoFilter={isFirstPageNoFilter}
|
|
204
|
+
subtitle={block.subtitle}
|
|
205
|
+
/>
|
|
206
|
+
<CollectionPaginationSection
|
|
207
|
+
currentPage={canonicalPage}
|
|
208
|
+
totalPages={totalPages}
|
|
209
|
+
/>
|
|
210
|
+
</section>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { PageBuilderBlock } from "@/components/page-builder/types"
|
|
2
2
|
import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
3
3
|
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
>
|
|
4
|
+
export type SearchParams = Record<string, string | string[] | undefined>
|
|
5
|
+
|
|
6
|
+
export type ContentCollectionSearchParams = Promise<SearchParams> | undefined
|
|
8
7
|
|
|
9
8
|
export type ContentCollectionBlock = Extract<
|
|
10
9
|
PageBuilderBlock,
|
|
@@ -14,3 +13,5 @@ export type ContentCollectionBlock = Extract<
|
|
|
14
13
|
>
|
|
15
14
|
|
|
16
15
|
export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
|
|
16
|
+
export type ContentCategory = NonNullable<BlogArticleCard["blogContent"]>["categories"][number]
|
|
17
|
+
export type ContentItemCard = BlogArticleCard
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import { ArticleContent } from "./components/article-content"
|
|
2
|
+
import { ContentCollection } from "./components/content-collection"
|
|
2
3
|
import { Description } from "./components/description"
|
|
3
4
|
import { Hero } from "./components/hero"
|
|
4
|
-
import {
|
|
5
|
+
import type { ContentCollectionSearchParams } from "./components/content-collection/types"
|
|
5
6
|
import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
|
|
6
7
|
|
|
7
8
|
type RendererProps = {
|
|
8
9
|
blocks: PageBuilderBlocks
|
|
10
|
+
searchParams: ContentCollectionSearchParams
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function Renderer({ blocks }: RendererProps) {
|
|
13
|
+
export function Renderer({ blocks, searchParams }: RendererProps) {
|
|
12
14
|
if (!blocks?.length) return null
|
|
13
15
|
|
|
14
16
|
return (
|
|
15
17
|
<div className="space-y-16">
|
|
16
18
|
{blocks.map((block: PageBuilderBlock) => (
|
|
17
|
-
<PageBuilderNode key={block._key} block={block} />
|
|
19
|
+
<PageBuilderNode key={block._key} block={block} searchParams={searchParams} />
|
|
18
20
|
))}
|
|
19
21
|
</div>
|
|
20
22
|
)
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
function PageBuilderNode({
|
|
25
|
+
function PageBuilderNode({
|
|
26
|
+
block,
|
|
27
|
+
searchParams,
|
|
28
|
+
}: {
|
|
29
|
+
block: PageBuilderBlock
|
|
30
|
+
searchParams: ContentCollectionSearchParams
|
|
31
|
+
}) {
|
|
24
32
|
switch (block._type) {
|
|
25
33
|
case "blogCollection":
|
|
26
|
-
return <ContentCollection block={block} />
|
|
34
|
+
return <ContentCollection block={block} searchParams={searchParams} />
|
|
27
35
|
case "blogContent":
|
|
28
36
|
return <ArticleContent block={block} />
|
|
29
37
|
case "hero":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stegaClean } from "@sanity/client/stega"
|
|
2
|
+
import type { ContentCollectionSearchParams } from "@/components/page-builder/components/content-collection/types"
|
|
2
3
|
import { Renderer } from "@/components/page-builder/renderer"
|
|
3
4
|
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
4
5
|
import { urlForImage } from "@/lib/integrations/sanity/utils/image"
|
|
@@ -11,11 +12,15 @@ import {
|
|
|
11
12
|
type PageDocumentProps = {
|
|
12
13
|
page: NonNullable<PAGE_QUERY_RESULT>
|
|
13
14
|
path: string | null
|
|
15
|
+
searchParams: ContentCollectionSearchParams
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export const getPageSections = (
|
|
18
|
+
export const getPageSections = (
|
|
19
|
+
page: NonNullable<PAGE_QUERY_RESULT>,
|
|
20
|
+
searchParams: ContentCollectionSearchParams
|
|
21
|
+
) =>
|
|
17
22
|
page.pageBuilder?.length
|
|
18
|
-
? [<Renderer key="page-builder" blocks={page.pageBuilder} />]
|
|
23
|
+
? [<Renderer key="page-builder" blocks={page.pageBuilder} searchParams={searchParams} />]
|
|
19
24
|
: []
|
|
20
25
|
|
|
21
26
|
const buildBreadcrumbs = (
|
|
@@ -47,9 +52,9 @@ const buildBreadcrumbs = (
|
|
|
47
52
|
return items
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export const PageDocument = ({ page, path }: PageDocumentProps) => {
|
|
55
|
+
export const PageDocument = ({ page, path, searchParams }: PageDocumentProps) => {
|
|
51
56
|
const title = stegaClean(page.title) || page.title
|
|
52
|
-
const sections = getPageSections(page)
|
|
57
|
+
const sections = getPageSections(page, searchParams)
|
|
53
58
|
|
|
54
59
|
const cleanTitle = stegaClean(page.title) || "Basement"
|
|
55
60
|
const cleanDescription = stegaClean(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { draftMode } from "next/headers"
|
|
2
2
|
import { VisualEditing } from "next-sanity/visual-editing"
|
|
3
3
|
import { Suspense } from "react"
|
|
4
|
+
import { refreshAction } from "@/app/actions/refresh"
|
|
4
5
|
import { DraftModeToggle } from "@/components/sanity/draft-mode-toggle"
|
|
5
6
|
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
6
7
|
import { SanityLive } from "@/lib/integrations/sanity/live"
|
|
@@ -15,7 +16,7 @@ async function VisualEditingInner() {
|
|
|
15
16
|
<>
|
|
16
17
|
<DraftModeToggle />
|
|
17
18
|
<VisualEditing />
|
|
18
|
-
<SanityLive />
|
|
19
|
+
<SanityLive revalidateSyncTags={refreshAction} />
|
|
19
20
|
</>
|
|
20
21
|
)
|
|
21
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HOMEPAGE_DOCUMENT_ID = "page-homepage"
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchSanity } from "@/lib/integrations/sanity/live"
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
FOOTER_QUERY,
|
|
5
|
-
NAVBAR_QUERY,
|
|
3
|
+
SITE_LAYOUT_QUERY,
|
|
6
4
|
} from "@/lib/integrations/sanity/queries"
|
|
7
5
|
import type {
|
|
8
6
|
COMPANY_DATA_QUERY_RESULT,
|
|
@@ -14,22 +12,23 @@ export type NavbarData = NonNullable<NAVBAR_QUERY_RESULT>
|
|
|
14
12
|
export type FooterData = NonNullable<FOOTER_QUERY_RESULT>
|
|
15
13
|
export type CompanyData = NonNullable<COMPANY_DATA_QUERY_RESULT>
|
|
16
14
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return data
|
|
15
|
+
export type SiteLayoutData = {
|
|
16
|
+
navbar: NavbarData | null
|
|
17
|
+
footer: FooterData | null
|
|
18
|
+
companyData: CompanyData | null
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
export const
|
|
25
|
-
const { data } = await sanityFetch({ query: FOOTER_QUERY, tags: ["footer"] })
|
|
26
|
-
return data
|
|
27
|
-
}
|
|
21
|
+
export const SANITY_LAYOUT_TAGS = ["navbar", "footer", "companyData"]
|
|
28
22
|
|
|
29
|
-
export const
|
|
30
|
-
const { data } = await
|
|
31
|
-
query:
|
|
32
|
-
tags:
|
|
23
|
+
export const getSiteLayoutData = async (): Promise<SiteLayoutData> => {
|
|
24
|
+
const { data } = await fetchSanity<SiteLayoutData>({
|
|
25
|
+
query: SITE_LAYOUT_QUERY,
|
|
26
|
+
tags: SANITY_LAYOUT_TAGS,
|
|
33
27
|
})
|
|
34
|
-
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
navbar: data?.navbar ?? null,
|
|
31
|
+
footer: data?.footer ?? null,
|
|
32
|
+
companyData: data?.companyData ?? null,
|
|
33
|
+
}
|
|
35
34
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server"
|
|
2
|
+
import type { QueryParams } from "next-sanity"
|
|
2
3
|
import { defineLive } from "next-sanity/live"
|
|
3
4
|
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
4
5
|
import { client } from "../client"
|
|
@@ -10,8 +11,8 @@ const liveExports =
|
|
|
10
11
|
isConfigured && client
|
|
11
12
|
? defineLive({
|
|
12
13
|
client,
|
|
13
|
-
browserToken: sanityToken,
|
|
14
|
-
serverToken: sanityToken,
|
|
14
|
+
browserToken: sanityToken ?? false,
|
|
15
|
+
serverToken: sanityToken ?? false,
|
|
15
16
|
})
|
|
16
17
|
: null
|
|
17
18
|
|
|
@@ -20,6 +21,32 @@ export const sanityFetch =
|
|
|
20
21
|
|
|
21
22
|
export const SanityLive = liveExports?.SanityLive ?? (() => null)
|
|
22
23
|
|
|
24
|
+
type SanityFetchOptions = {
|
|
25
|
+
query: string
|
|
26
|
+
params?: QueryParams
|
|
27
|
+
tags?: string[]
|
|
28
|
+
stega?: boolean
|
|
29
|
+
perspective?: "published" | "drafts" | "previewDrafts"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function fetchSanity<T = unknown>({
|
|
33
|
+
query,
|
|
34
|
+
params = {},
|
|
35
|
+
tags = [],
|
|
36
|
+
stega,
|
|
37
|
+
perspective,
|
|
38
|
+
}: SanityFetchOptions): Promise<{ data: T | null }> {
|
|
39
|
+
const { data } = await sanityFetch({
|
|
40
|
+
query,
|
|
41
|
+
params,
|
|
42
|
+
tags,
|
|
43
|
+
...(typeof stega === "boolean" ? { stega } : {}),
|
|
44
|
+
...(perspective ? { perspective } : {}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { data: (data as T | null) ?? null }
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
export const SanityFetch = async <T,>(
|
|
24
51
|
slug: string,
|
|
25
52
|
query: string,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Observable } from "rxjs"
|
|
2
|
+
import { map } from "rxjs/operators"
|
|
3
|
+
import type { DocumentStore } from "sanity"
|
|
4
|
+
import {
|
|
5
|
+
type DocumentLocationsState,
|
|
6
|
+
defineDocuments,
|
|
7
|
+
presentationTool,
|
|
8
|
+
} from "sanity/presentation"
|
|
9
|
+
import { HOMEPAGE_DOCUMENT_ID } from "./constants"
|
|
10
|
+
import { previewURL } from "./env"
|
|
11
|
+
|
|
12
|
+
const MAX_FOLDER_DEPTH = 6
|
|
13
|
+
|
|
14
|
+
const buildNestedSlugExpression = (
|
|
15
|
+
parentReferenceField: string,
|
|
16
|
+
leafSlugField: string
|
|
17
|
+
) => {
|
|
18
|
+
const cases = Array.from({ length: MAX_FOLDER_DEPTH }, (_, index) => {
|
|
19
|
+
const depth = MAX_FOLDER_DEPTH - index
|
|
20
|
+
const parentSlugRefs = Array.from({ length: depth }, (_, parentIndex) => {
|
|
21
|
+
const remainingDepth = depth - parentIndex - 1
|
|
22
|
+
return `${parentReferenceField}${"->parentFolder".repeat(remainingDepth)}->slug.current`
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const pathDefinedChecks = parentSlugRefs
|
|
26
|
+
.map((field) => `defined(${field})`)
|
|
27
|
+
.join(" && ")
|
|
28
|
+
|
|
29
|
+
const childPath = [...parentSlugRefs, leafSlugField].join(' + "/" + ')
|
|
30
|
+
const folderRootPath = parentSlugRefs.join(' + "/" + ')
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
`${pathDefinedChecks} && ${leafSlugField} == "/" => ${folderRootPath}`,
|
|
34
|
+
`${pathDefinedChecks} && defined(${leafSlugField}) => ${childPath}`,
|
|
35
|
+
]
|
|
36
|
+
}).flat()
|
|
37
|
+
|
|
38
|
+
return `select(${cases.join(",\n ")},${leafSlugField})`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pageResolvedSlugExpression = buildNestedSlugExpression(
|
|
42
|
+
"pageFolder",
|
|
43
|
+
"slug.current"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const CONTENT_TYPE_PREFIXES: Record<string, string> = {
|
|
47
|
+
blogContent: "blog",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveSlugLocation = <T extends { title?: string }>(
|
|
51
|
+
documentStore: DocumentStore,
|
|
52
|
+
id: string,
|
|
53
|
+
projection: string,
|
|
54
|
+
toHref: (doc: T) => string | null,
|
|
55
|
+
fallbackTitle: string
|
|
56
|
+
): Observable<DocumentLocationsState> =>
|
|
57
|
+
documentStore
|
|
58
|
+
.listenQuery(`*[_id == $id][0]{ ${projection} }`, { id }, {})
|
|
59
|
+
.pipe(
|
|
60
|
+
map((doc: T | null) => {
|
|
61
|
+
const href = doc ? toHref(doc) : null
|
|
62
|
+
if (!href) return { locations: [] }
|
|
63
|
+
return { locations: [{ title: doc?.title || fallbackTitle, href }] }
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const presentation = presentationTool({
|
|
68
|
+
name: "preview",
|
|
69
|
+
title: "Preview",
|
|
70
|
+
resolve: {
|
|
71
|
+
mainDocuments: defineDocuments([
|
|
72
|
+
{
|
|
73
|
+
route: "/",
|
|
74
|
+
filter: `_type == "page" && _id == "${HOMEPAGE_DOCUMENT_ID}"`,
|
|
75
|
+
},
|
|
76
|
+
...Object.entries(CONTENT_TYPE_PREFIXES).map(([type, prefix]) => ({
|
|
77
|
+
route: `/${prefix}/:slug`,
|
|
78
|
+
filter: `_type == "${type}" && slug.current == $slug`,
|
|
79
|
+
})),
|
|
80
|
+
{
|
|
81
|
+
route: "/:slug+",
|
|
82
|
+
filter: `_type == "page" && ${pageResolvedSlugExpression} == $slug`,
|
|
83
|
+
},
|
|
84
|
+
]),
|
|
85
|
+
locations: (params, { documentStore }) => {
|
|
86
|
+
if (params.type === "page") {
|
|
87
|
+
if (params.id === HOMEPAGE_DOCUMENT_ID) {
|
|
88
|
+
return { locations: [{ title: "Homepage", href: "/" }] }
|
|
89
|
+
}
|
|
90
|
+
return resolveSlugLocation<{ title?: string; resolvedSlug?: string }>(
|
|
91
|
+
documentStore,
|
|
92
|
+
params.id,
|
|
93
|
+
`title, "resolvedSlug": ${pageResolvedSlugExpression}`,
|
|
94
|
+
(doc) => (doc.resolvedSlug ? `/${doc.resolvedSlug}` : null),
|
|
95
|
+
"Page"
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const prefix = CONTENT_TYPE_PREFIXES[params.type]
|
|
100
|
+
if (!prefix) return null
|
|
101
|
+
|
|
102
|
+
return resolveSlugLocation<{ title?: string; slug?: string }>(
|
|
103
|
+
documentStore,
|
|
104
|
+
params.id,
|
|
105
|
+
`title, "slug": slug.current`,
|
|
106
|
+
(doc) => (doc.slug ? `/${prefix}/${doc.slug}` : null),
|
|
107
|
+
"Untitled"
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
previewUrl: {
|
|
112
|
+
origin: previewURL,
|
|
113
|
+
draftMode: {
|
|
114
|
+
enable: "/api/draft-mode/enable",
|
|
115
|
+
disable: "/api/draft-mode/disable",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
})
|