bsmnt 0.2.11 → 0.3.0
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/dist/helpers/create/copy-template.d.ts +1 -1
- package/dist/helpers/create/copy-template.d.ts.map +1 -1
- package/dist/helpers/create/index.d.ts.map +1 -1
- package/dist/helpers/create/index.js +2 -1
- package/dist/helpers/create/index.js.map +1 -1
- package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-config.js +0 -2
- package/dist/helpers/integrate/merge-config.js.map +1 -1
- package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/config.js +3 -14
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/index.js +84 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/next-pagebuilder/.env.example +11 -0
- package/src/templates/next-pagebuilder/README.md +23 -0
- package/src/templates/next-pagebuilder/_gitignore +67 -0
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
- package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
- package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
- package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
- package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
- package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
- package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
- package/src/templates/next-pagebuilder/app/robots.ts +15 -0
- package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
- package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
- package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
- package/src/templates/next-pagebuilder/biome.json +239 -0
- package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
- package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
- package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
- package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
- package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
- package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
- package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
- package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
- package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
- package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
- package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
- package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
- package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
- package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
- package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
- package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
- package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
- package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
- package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
- package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
- package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
- package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
- package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
- package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
- package/src/templates/next-pagebuilder/next.config.ts +134 -0
- package/src/templates/next-pagebuilder/package.json +71 -0
- package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
- package/src/templates/next-pagebuilder/proxy.ts +81 -0
- package/src/templates/next-pagebuilder/svg.d.ts +5 -0
- package/src/templates/next-pagebuilder/tsconfig.json +38 -0
- package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
- package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageBuilderBlock } from "../types"
|
|
2
|
+
|
|
3
|
+
type HeroProps = {
|
|
4
|
+
block: Extract<PageBuilderBlock, { _type: "hero" }>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Hero({ block }: HeroProps) {
|
|
8
|
+
if (!block.headline) return null
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="py-8 md:py-12">
|
|
12
|
+
<h1 className="max-w-4xl font-semibold text-4xl tracking-tight md:text-6xl">
|
|
13
|
+
{block.headline}
|
|
14
|
+
</h1>
|
|
15
|
+
</section>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Link } from "@/components/ui/link"
|
|
2
|
+
import { SanityImage } from "@/components/ui/sanity-image"
|
|
3
|
+
import { formatDate } from "@/lib/utils/format-date"
|
|
4
|
+
import type { BlogArticleCard } from "./types"
|
|
5
|
+
|
|
6
|
+
type ContentCardProps = {
|
|
7
|
+
article: BlogArticleCard
|
|
8
|
+
className?: string | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ContentCard = ({ article, className }: ContentCardProps) => {
|
|
12
|
+
const href = article.resolvedSlug ? `/${article.resolvedSlug}` : null
|
|
13
|
+
if (!href) return null
|
|
14
|
+
|
|
15
|
+
const blog = article.blogContent
|
|
16
|
+
const title = article.title ?? ""
|
|
17
|
+
const categories = blog?.categories ?? []
|
|
18
|
+
const authorName = blog?.author?.name
|
|
19
|
+
const publishedAt = blog?.date ?? null
|
|
20
|
+
const formattedDate = publishedAt ? formatDate(publishedAt) : null
|
|
21
|
+
const thumbnail = blog?.thumbnail ?? article.metadata?.image
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<li className={className}>
|
|
25
|
+
<Link href={href} className="group flex flex-col gap-3">
|
|
26
|
+
<div className="relative aspect-16/9 w-full overflow-hidden rounded-lg bg-gray-100">
|
|
27
|
+
{thumbnail ? (
|
|
28
|
+
<SanityImage
|
|
29
|
+
image={thumbnail}
|
|
30
|
+
alt=""
|
|
31
|
+
fill
|
|
32
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
33
|
+
className="w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
34
|
+
/>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex flex-col gap-2">
|
|
39
|
+
{categories.length > 0 ? (
|
|
40
|
+
<div className="flex flex-wrap gap-1.5">
|
|
41
|
+
{categories.map((category) => (
|
|
42
|
+
<span key={category._id} className="text-current/60 text-xs">
|
|
43
|
+
{category.title}
|
|
44
|
+
</span>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
) : null}
|
|
48
|
+
|
|
49
|
+
<h3 className="text-balance font-medium text-lg leading-snug">
|
|
50
|
+
{title}
|
|
51
|
+
</h3>
|
|
52
|
+
|
|
53
|
+
{authorName || formattedDate ? (
|
|
54
|
+
<div className="flex items-center gap-2 text-current/60 text-sm">
|
|
55
|
+
{authorName ? <span>{authorName}</span> : null}
|
|
56
|
+
{authorName && formattedDate ? <span aria-hidden>·</span> : null}
|
|
57
|
+
{publishedAt && formattedDate ? (
|
|
58
|
+
<time dateTime={publishedAt}>{formattedDate}</time>
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
</Link>
|
|
64
|
+
</li>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BlogArticleCard } from "./types"
|
|
2
|
+
import { ContentCard } from "./content-card"
|
|
3
|
+
|
|
4
|
+
type ContentGridProps = {
|
|
5
|
+
articles: BlogArticleCard[]
|
|
6
|
+
start?: number
|
|
7
|
+
end?: number
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ContentGrid({
|
|
12
|
+
articles,
|
|
13
|
+
start = 0,
|
|
14
|
+
end,
|
|
15
|
+
className,
|
|
16
|
+
}: ContentGridProps) {
|
|
17
|
+
const slice = articles.slice(start, end)
|
|
18
|
+
|
|
19
|
+
if (slice.length === 0) {
|
|
20
|
+
if (start === 0) {
|
|
21
|
+
return <p className="text-current/70 text-sm">No articles found.</p>
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ul
|
|
28
|
+
className={
|
|
29
|
+
className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{slice.map((article) => {
|
|
33
|
+
const slug = article.resolvedSlug
|
|
34
|
+
if (!slug) return null
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ContentCard key={article._id} article={article} />
|
|
38
|
+
)
|
|
39
|
+
})}
|
|
40
|
+
</ul>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sanityFetch } from "@/lib/integrations/sanity/live"
|
|
2
|
+
import { ALL_BLOG_ARTICLES_QUERY } from "@/lib/integrations/sanity/queries"
|
|
3
|
+
import { ContentGrid } from "./content-grid"
|
|
4
|
+
import type { ContentCollectionBlock } from "./types"
|
|
5
|
+
|
|
6
|
+
type ContentCollectionProps = {
|
|
7
|
+
block: ContentCollectionBlock
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function ContentCollection({ block }: ContentCollectionProps) {
|
|
11
|
+
const { data: articles } = await sanityFetch({
|
|
12
|
+
query: ALL_BLOG_ARTICLES_QUERY,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
if (!articles?.length) return null
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section className="flex flex-col gap-8 py-8">
|
|
20
|
+
{block.subtitle ? (
|
|
21
|
+
<h2 className="font-medium text-2xl leading-tight">
|
|
22
|
+
{block.subtitle}
|
|
23
|
+
</h2>
|
|
24
|
+
) : null}
|
|
25
|
+
<ContentGrid articles={articles} />
|
|
26
|
+
</section>
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PageBuilderBlock } from "@/components/page-builder/types"
|
|
2
|
+
import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
3
|
+
|
|
4
|
+
export type BlogCollectionBlock = Extract<
|
|
5
|
+
PageBuilderBlock,
|
|
6
|
+
{ _type: "blogCollection" }
|
|
7
|
+
>
|
|
8
|
+
|
|
9
|
+
export type ContentCollectionBlock = Extract<
|
|
10
|
+
PageBuilderBlock,
|
|
11
|
+
{
|
|
12
|
+
_type: "blogCollection"
|
|
13
|
+
}
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ArticleContent } from "./components/article-content"
|
|
2
|
+
import { Description } from "./components/description"
|
|
3
|
+
import { Hero } from "./components/hero"
|
|
4
|
+
import { ContentCollection } from "./components/post-collection"
|
|
5
|
+
import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
|
|
6
|
+
|
|
7
|
+
type RendererProps = {
|
|
8
|
+
blocks: PageBuilderBlocks
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Renderer({ blocks }: RendererProps) {
|
|
12
|
+
if (!blocks?.length) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-16">
|
|
16
|
+
{blocks.map((block: PageBuilderBlock) => (
|
|
17
|
+
<PageBuilderNode key={block._key} block={block} />
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PageBuilderNode({ block }: { block: PageBuilderBlock }) {
|
|
24
|
+
switch (block._type) {
|
|
25
|
+
case "blogCollection":
|
|
26
|
+
return <ContentCollection block={block} />
|
|
27
|
+
case "blogContent":
|
|
28
|
+
return <ArticleContent block={block} />
|
|
29
|
+
case "hero":
|
|
30
|
+
return <Hero block={block} />
|
|
31
|
+
case "description":
|
|
32
|
+
return <Description block={block} />
|
|
33
|
+
default:
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
2
|
+
|
|
3
|
+
export type PageBuilderBlock = NonNullable<
|
|
4
|
+
NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
|
|
5
|
+
>[number]
|
|
6
|
+
|
|
7
|
+
export type PageBuilderBlocks = NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
|
|
8
|
+
|
|
9
|
+
export type BlogContentBlock = Extract<PageBuilderBlock, { _type: "blogContent" }>
|
|
10
|
+
|
|
11
|
+
export type ArticleContentProps = {
|
|
12
|
+
block: BlogContentBlock
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ArticleRelatedPost = {
|
|
17
|
+
_id: string;
|
|
18
|
+
title: string | null;
|
|
19
|
+
resolvedSlug: string | null;
|
|
20
|
+
metadata?: {
|
|
21
|
+
image?: unknown;
|
|
22
|
+
} | null
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { stegaClean } from "@sanity/client/stega"
|
|
2
|
+
import { Renderer } from "@/components/page-builder/renderer"
|
|
3
|
+
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
4
|
+
import { urlForImage } from "@/lib/integrations/sanity/utils/image"
|
|
5
|
+
import {
|
|
6
|
+
generateBreadcrumbJsonLd,
|
|
7
|
+
generateSanityWebPageJsonLd,
|
|
8
|
+
JsonLd,
|
|
9
|
+
} from "@/lib/utils/json-ld"
|
|
10
|
+
|
|
11
|
+
type PageDocumentProps = {
|
|
12
|
+
page: NonNullable<PAGE_QUERY_RESULT>
|
|
13
|
+
path: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const getPageSections = (page: NonNullable<PAGE_QUERY_RESULT>) =>
|
|
17
|
+
page.pageBuilder?.length
|
|
18
|
+
? [<Renderer key="page-builder" blocks={page.pageBuilder} />]
|
|
19
|
+
: []
|
|
20
|
+
|
|
21
|
+
const buildBreadcrumbs = (
|
|
22
|
+
slugPath: string | null,
|
|
23
|
+
pageTitle: string | null
|
|
24
|
+
): Array<{ name: string; url: string }> => {
|
|
25
|
+
const items: Array<{ name: string; url: string }> = [
|
|
26
|
+
{ name: "Home", url: "/" },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if (!slugPath) return items
|
|
30
|
+
|
|
31
|
+
const segments = slugPath.split("/").filter(Boolean)
|
|
32
|
+
for (let i = 0; i < segments.length; i++) {
|
|
33
|
+
const isLast = i === segments.length - 1
|
|
34
|
+
const url = `/${segments.slice(0, i + 1).join("/")}`
|
|
35
|
+
|
|
36
|
+
//TODO: we might change this in the future if slug title and folder diverge too much :)
|
|
37
|
+
|
|
38
|
+
const name =
|
|
39
|
+
isLast && pageTitle
|
|
40
|
+
? pageTitle
|
|
41
|
+
: (segments[i]
|
|
42
|
+
?.replace(/-/g, " ")
|
|
43
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()) ?? "")
|
|
44
|
+
items.push({ name, url })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return items
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const PageDocument = ({ page, path }: PageDocumentProps) => {
|
|
51
|
+
const title = stegaClean(page.title) || page.title
|
|
52
|
+
const sections = getPageSections(page)
|
|
53
|
+
|
|
54
|
+
const cleanTitle = stegaClean(page.title) || "Basement"
|
|
55
|
+
const cleanDescription = stegaClean(
|
|
56
|
+
page.metadata?.metaDescription || page.metadata?.description
|
|
57
|
+
)
|
|
58
|
+
const pageUrl = path ? `/${path}` : "/"
|
|
59
|
+
|
|
60
|
+
const ogImage = page.metadata?.image
|
|
61
|
+
? urlForImage(page.metadata.image).width(1200).height(630).url()
|
|
62
|
+
: undefined
|
|
63
|
+
|
|
64
|
+
const breadcrumbs = buildBreadcrumbs(path, cleanTitle)
|
|
65
|
+
const hasBreadcrumbs = breadcrumbs.length > 1
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<JsonLd
|
|
70
|
+
data={generateSanityWebPageJsonLd({
|
|
71
|
+
title: cleanTitle,
|
|
72
|
+
url: pageUrl,
|
|
73
|
+
description: cleanDescription || undefined,
|
|
74
|
+
image: ogImage,
|
|
75
|
+
dateModified: page._updatedAt,
|
|
76
|
+
})}
|
|
77
|
+
/>
|
|
78
|
+
{hasBreadcrumbs ? (
|
|
79
|
+
<JsonLd data={generateBreadcrumbJsonLd(breadcrumbs)} />
|
|
80
|
+
) : null}
|
|
81
|
+
<article className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-14 px-4 py-14 md:px-6 md:py-20">
|
|
82
|
+
<h1>{title}</h1>
|
|
83
|
+
{sections.length ? (
|
|
84
|
+
sections
|
|
85
|
+
) : (
|
|
86
|
+
<section>No page content has been added yet.</section>
|
|
87
|
+
)}
|
|
88
|
+
</article>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation"
|
|
4
|
+
import { useTransition } from "react"
|
|
5
|
+
|
|
6
|
+
export function DraftModeToggle() {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const [isPending, startTransition] = useTransition()
|
|
9
|
+
|
|
10
|
+
const handleDisable = () => {
|
|
11
|
+
startTransition(async () => {
|
|
12
|
+
await fetch("/api/draft-mode/disable")
|
|
13
|
+
router.refresh()
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={handleDisable}
|
|
21
|
+
disabled={isPending}
|
|
22
|
+
className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full border border-white/10 bg-black/80 px-2 py-1 font-medium text-sm text-white shadow-lg backdrop-blur-sm transition-colors hover:bg-black/90 disabled:opacity-50"
|
|
23
|
+
>
|
|
24
|
+
{isPending ? "Disabling..." : "Disable Draft Mode"}
|
|
25
|
+
</button>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { PortableText, type PortableTextProps } from "@portabletext/react"
|
|
2
|
+
import { Link } from "@/components/ui/link"
|
|
3
|
+
import { SanityImage } from "@/components/ui/sanity-image"
|
|
4
|
+
import {
|
|
5
|
+
getLinkAttributes,
|
|
6
|
+
urlForReference,
|
|
7
|
+
} from "../../lib/integrations/sanity/utils/link"
|
|
8
|
+
|
|
9
|
+
interface RichTextProps {
|
|
10
|
+
content: PortableTextProps["value"]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type LinkMarkProps = {
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
value?: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LinkMark = ({ children, value }: LinkMarkProps) => {
|
|
19
|
+
const href = urlForReference(value as Parameters<typeof urlForReference>[0])
|
|
20
|
+
const attrs = getLinkAttributes(
|
|
21
|
+
value as Parameters<typeof getLinkAttributes>[0]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Link
|
|
26
|
+
href={href}
|
|
27
|
+
target={attrs.target}
|
|
28
|
+
rel={attrs.rel}
|
|
29
|
+
data-sanity-edit-target
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</Link>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const RichText = ({ content }: RichTextProps) => {
|
|
37
|
+
if (!content) return null
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<PortableText
|
|
41
|
+
value={content}
|
|
42
|
+
components={{
|
|
43
|
+
types: {
|
|
44
|
+
image: ({ value }) => <SanityImage image={value} maxWidth={1920} />,
|
|
45
|
+
table: ({ value }) => {
|
|
46
|
+
const rows = value?.rows as
|
|
47
|
+
| Array<{ _key?: string; cells?: string[] }>
|
|
48
|
+
| undefined
|
|
49
|
+
if (!rows?.length) return null
|
|
50
|
+
return (
|
|
51
|
+
<table>
|
|
52
|
+
<tbody>
|
|
53
|
+
{rows.map((row, i) => (
|
|
54
|
+
<tr key={row._key ?? `row-${i}`}>
|
|
55
|
+
{row.cells?.map((cell) => {
|
|
56
|
+
const cellKey = `${row._key}-${cell}`
|
|
57
|
+
return i === 0 ? (
|
|
58
|
+
<th key={cellKey}>{cell}</th>
|
|
59
|
+
) : (
|
|
60
|
+
<td key={cellKey}>{cell}</td>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
</tr>
|
|
64
|
+
))}
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
)
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
marks: {
|
|
71
|
+
link: LinkMark,
|
|
72
|
+
pageReference: LinkMark,
|
|
73
|
+
externalLink: LinkMark,
|
|
74
|
+
},
|
|
75
|
+
block: {
|
|
76
|
+
h1: ({ children }) => <h1 className="h1">{children}</h1>,
|
|
77
|
+
h2: ({ children }) => <h2 className="h2">{children}</h2>,
|
|
78
|
+
h3: ({ children }) => <h3 className="h3">{children}</h3>,
|
|
79
|
+
h4: ({ children }) => <h4 className="h4">{children}</h4>,
|
|
80
|
+
h5: ({ children }) => <h5 className="h5">{children}</h5>,
|
|
81
|
+
h6: ({ children }) => <h6 className="h6">{children}</h6>,
|
|
82
|
+
normal: ({ children }) => <p className="p">{children}</p>,
|
|
83
|
+
},
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { draftMode } from "next/headers"
|
|
2
|
+
import { VisualEditing } from "next-sanity/visual-editing"
|
|
3
|
+
import { Suspense } from "react"
|
|
4
|
+
import { DraftModeToggle } from "@/components/sanity/draft-mode-toggle"
|
|
5
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
6
|
+
import { SanityLive } from "@/lib/integrations/sanity/live"
|
|
7
|
+
|
|
8
|
+
async function VisualEditingInner() {
|
|
9
|
+
const { isEnabled: isDraftMode } = await draftMode()
|
|
10
|
+
const sanityConfigured = isSanityConfigured()
|
|
11
|
+
|
|
12
|
+
if (!(sanityConfigured && isDraftMode)) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<DraftModeToggle />
|
|
17
|
+
<VisualEditing />
|
|
18
|
+
<SanityLive />
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SanityVisualEditing = () => (
|
|
24
|
+
<Suspense fallback={null}>
|
|
25
|
+
<VisualEditingInner />
|
|
26
|
+
</Suspense>
|
|
27
|
+
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Image Component
|
|
3
|
+
*
|
|
4
|
+
* Next.js Image wrapper with optimized defaults and error handling.
|
|
5
|
+
* Always use this component instead of next/image directly.
|
|
6
|
+
*/
|
|
7
|
+
"use client"
|
|
8
|
+
|
|
9
|
+
import cn from "clsx"
|
|
10
|
+
import NextImage, { type ImageProps as NextImageProps } from "next/image"
|
|
11
|
+
import type { CSSProperties, Ref } from "react"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enhanced Image component props extending Next.js Image.
|
|
15
|
+
*
|
|
16
|
+
* Adds responsive sizing, aspect ratio support, and automatic blur placeholders.
|
|
17
|
+
* Always use this component instead of next/image directly.
|
|
18
|
+
*/
|
|
19
|
+
export type ImageProps = Omit<NextImageProps, "objectFit" | "alt"> & {
|
|
20
|
+
/** CSS object-fit property for image positioning */
|
|
21
|
+
objectFit?: CSSProperties["objectFit"]
|
|
22
|
+
/** Display as block element (adds display: block) */
|
|
23
|
+
block?: boolean
|
|
24
|
+
/** Size on mobile devices (e.g., "100vw", "50vw") */
|
|
25
|
+
mobileSize?: `${number}vw`
|
|
26
|
+
/** Size on desktop devices (e.g., "33vw", "25vw") */
|
|
27
|
+
desktopSize?: `${number}vw`
|
|
28
|
+
/** Ref for accessing the underlying img element */
|
|
29
|
+
ref?: Ref<HTMLImageElement>
|
|
30
|
+
/** Alt text for accessibility (required for meaningful images) */
|
|
31
|
+
alt?: string
|
|
32
|
+
/** Aspect ratio for automatic placeholder and layout stability */
|
|
33
|
+
aspectRatio?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Memoize helper functions to avoid recreation
|
|
37
|
+
const toBase64 = (str: string) =>
|
|
38
|
+
typeof window === "undefined"
|
|
39
|
+
? Buffer.from(str).toString("base64")
|
|
40
|
+
: window.btoa(str)
|
|
41
|
+
|
|
42
|
+
// Helper to generate blur placeholder with transparent background by default
|
|
43
|
+
const generateShimmer = (w: number, h: number) => `
|
|
44
|
+
<svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
45
|
+
<defs>
|
|
46
|
+
<linearGradient id="g">
|
|
47
|
+
<stop stop-color="rgba(255,255,255,0.1)" offset="20%" />
|
|
48
|
+
<stop stop-color="rgba(255,255,255,0.2)" offset="50%" />
|
|
49
|
+
<stop stop-color="rgba(255,255,255,0.1)" offset="70%" />
|
|
50
|
+
</linearGradient>
|
|
51
|
+
</defs>
|
|
52
|
+
<rect width="${w}" height="${h}" fill="rgba(0,0,0,0)" />
|
|
53
|
+
<rect id="r" width="${w}" height="${h}" fill="url(#g)" />
|
|
54
|
+
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
|
|
55
|
+
</svg>`
|
|
56
|
+
|
|
57
|
+
// Helper to determine if blur placeholder should be used
|
|
58
|
+
const shouldUseBlurPlaceholder = (
|
|
59
|
+
src: NextImageProps["src"],
|
|
60
|
+
placeholder: string,
|
|
61
|
+
blurDataURL: string | undefined
|
|
62
|
+
): boolean => {
|
|
63
|
+
if (!src) return false
|
|
64
|
+
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
65
|
+
return !isSvg && placeholder === "blur" && !blurDataURL
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper to generate blur data URL
|
|
69
|
+
const generateBlurDataURL = (
|
|
70
|
+
shouldUse: boolean,
|
|
71
|
+
aspectRatio: number | undefined,
|
|
72
|
+
existingBlurDataURL: string | undefined
|
|
73
|
+
): string | undefined => {
|
|
74
|
+
if (!(shouldUse && aspectRatio)) return existingBlurDataURL
|
|
75
|
+
|
|
76
|
+
const shimmerSvg = generateShimmer(700, Math.round(700 / aspectRatio))
|
|
77
|
+
return `data:image/svg+xml;base64,${toBase64(shimmerSvg)}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Helper to determine final placeholder value
|
|
81
|
+
const getFinalPlaceholder = (
|
|
82
|
+
shouldUse: boolean,
|
|
83
|
+
aspectRatio: number | undefined,
|
|
84
|
+
blurDataURL: string | undefined,
|
|
85
|
+
originalPlaceholder: NextImageProps["placeholder"]
|
|
86
|
+
): NextImageProps["placeholder"] => {
|
|
87
|
+
if (!shouldUse) {
|
|
88
|
+
return originalPlaceholder === "blur" && !blurDataURL
|
|
89
|
+
? "empty"
|
|
90
|
+
: originalPlaceholder
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return aspectRatio || blurDataURL ? "blur" : "empty"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enhanced Image component with responsive sizing and automatic optimizations.
|
|
98
|
+
*
|
|
99
|
+
* Always use this component instead of next/image directly. Provides:
|
|
100
|
+
* - Automatic responsive sizes generation
|
|
101
|
+
* - Smart blur placeholders with aspect ratio support
|
|
102
|
+
* - Performance optimizations (lazy loading by default)
|
|
103
|
+
* - Priority flag for LCP images
|
|
104
|
+
*
|
|
105
|
+
* @param props - Image props extending Next.js Image
|
|
106
|
+
* @param props.aspectRatio - Aspect ratio for layout stability and blur placeholder
|
|
107
|
+
* @param props.mobileSize - Size on mobile (e.g., "100vw")
|
|
108
|
+
* @param props.desktopSize - Size on desktop (e.g., "50vw")
|
|
109
|
+
* @param props.block - Display as block element
|
|
110
|
+
* @param props.priority - Prioritize loading for LCP images
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* // Basic usage with aspect ratio
|
|
115
|
+
* <Image
|
|
116
|
+
* src="/hero.jpg"
|
|
117
|
+
* alt="Hero image"
|
|
118
|
+
* aspectRatio={16/9}
|
|
119
|
+
* />
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* // LCP image with priority
|
|
125
|
+
* <Image
|
|
126
|
+
* src="/hero.jpg"
|
|
127
|
+
* alt="Hero image"
|
|
128
|
+
* aspectRatio={16/9}
|
|
129
|
+
* priority // Preloads image for LCP
|
|
130
|
+
* />
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* // Responsive grid image
|
|
136
|
+
* <Image
|
|
137
|
+
* src="/product.jpg"
|
|
138
|
+
* alt="Product"
|
|
139
|
+
* aspectRatio={1}
|
|
140
|
+
* mobileSize="100vw"
|
|
141
|
+
* desktopSize="33vw"
|
|
142
|
+
* />
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function Image({
|
|
146
|
+
style,
|
|
147
|
+
className,
|
|
148
|
+
objectFit = "cover",
|
|
149
|
+
quality = 90,
|
|
150
|
+
alt = "",
|
|
151
|
+
fill,
|
|
152
|
+
block = !fill,
|
|
153
|
+
width = block ? 1 : undefined,
|
|
154
|
+
height = block ? 1 : undefined,
|
|
155
|
+
mobileSize = "100vw",
|
|
156
|
+
desktopSize = "100vw",
|
|
157
|
+
sizes,
|
|
158
|
+
src,
|
|
159
|
+
unoptimized,
|
|
160
|
+
ref,
|
|
161
|
+
aspectRatio,
|
|
162
|
+
placeholder = "blur",
|
|
163
|
+
priority = false,
|
|
164
|
+
...props
|
|
165
|
+
}: ImageProps) {
|
|
166
|
+
// Generate responsive sizes if not provided
|
|
167
|
+
const finalSizes =
|
|
168
|
+
sizes || `(max-width: 1440px) ${mobileSize}, ${desktopSize}`
|
|
169
|
+
|
|
170
|
+
// Early return after hooks
|
|
171
|
+
if (!src) return null
|
|
172
|
+
|
|
173
|
+
// Determine SVG status and placeholder logic
|
|
174
|
+
const isSvg = typeof src === "string" && src.includes(".svg")
|
|
175
|
+
const shouldUsePlaceholder = shouldUseBlurPlaceholder(
|
|
176
|
+
src,
|
|
177
|
+
placeholder,
|
|
178
|
+
props.blurDataURL
|
|
179
|
+
)
|
|
180
|
+
const blurDataURL = generateBlurDataURL(
|
|
181
|
+
shouldUsePlaceholder,
|
|
182
|
+
aspectRatio,
|
|
183
|
+
props.blurDataURL
|
|
184
|
+
)
|
|
185
|
+
const finalPlaceholder = getFinalPlaceholder(
|
|
186
|
+
shouldUsePlaceholder,
|
|
187
|
+
aspectRatio,
|
|
188
|
+
props.blurDataURL,
|
|
189
|
+
placeholder
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<NextImage
|
|
194
|
+
ref={ref}
|
|
195
|
+
fill={!block}
|
|
196
|
+
{...(width !== undefined && { width })}
|
|
197
|
+
{...(height !== undefined && { height })}
|
|
198
|
+
priority={priority}
|
|
199
|
+
quality={quality}
|
|
200
|
+
alt={alt}
|
|
201
|
+
style={{
|
|
202
|
+
objectFit,
|
|
203
|
+
...style,
|
|
204
|
+
}}
|
|
205
|
+
className={cn(className, block && "block w-full")}
|
|
206
|
+
sizes={finalSizes}
|
|
207
|
+
src={src}
|
|
208
|
+
unoptimized={unoptimized || isSvg}
|
|
209
|
+
draggable={false}
|
|
210
|
+
onDragStart={(e) => e.preventDefault()}
|
|
211
|
+
{...(finalPlaceholder && { placeholder: finalPlaceholder })}
|
|
212
|
+
{...(blurDataURL && { blurDataURL })}
|
|
213
|
+
{...props}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
}
|