bsmnt 0.2.10 → 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/configs/skills.d.ts +27 -0
- package/dist/configs/skills.d.ts.map +1 -0
- package/dist/configs/skills.js +18 -0
- package/dist/configs/skills.js.map +1 -0
- package/dist/configs/skills.json +26 -0
- package/dist/helpers/add/hooks-config.d.ts.map +1 -1
- package/dist/helpers/add/hooks-config.js +0 -6
- package/dist/helpers/add/hooks-config.js.map +1 -1
- 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/create/setup-agent.d.ts.map +1 -1
- package/dist/helpers/create/setup-agent.js +15 -5
- package/dist/helpers/create/setup-agent.js.map +1 -1
- package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-config.js +1 -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 +5 -10
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/layout-merger.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/layout-merger.js +13 -12
- package/dist/helpers/integrate/sanity/mergers/layout-merger.js.map +1 -1
- package/dist/helpers/skills/index.d.ts +10 -0
- package/dist/helpers/skills/index.d.ts.map +1 -0
- package/dist/helpers/skills/index.js +136 -0
- package/dist/helpers/skills/index.js.map +1 -0
- package/dist/index.js +102 -35
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/confirm-publish-action.ts +31 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +17 -0
- package/src/helpers/integrate/sanity/files/lib/utils/json-ld.tsx +249 -0
- package/src/template-hooks/config.js +0 -6
- package/src/templates/next-default/app/layout.tsx +18 -0
- package/src/templates/next-default/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-default/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-default/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-default/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-default/package.json +1 -1
- package/src/templates/next-default/tsconfig.json +1 -0
- package/src/templates/next-experiments/app/layout.tsx +18 -0
- package/src/templates/next-experiments/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-experiments/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-experiments/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-experiments/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-experiments/package.json +1 -1
- package/src/templates/next-experiments/tsconfig.json +1 -0
- 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/templates/next-webgl/app/layout.tsx +18 -0
- package/src/templates/next-webgl/lib/hooks/use-device-detection.ts +1 -1
- package/src/templates/next-webgl/lib/hooks/use-media-breakpoint.ts +1 -1
- package/src/templates/next-webgl/lib/hooks/use-media.ts +29 -0
- package/src/templates/next-webgl/lib/utils/json-ld.tsx +199 -0
- package/src/templates/next-webgl/package.json +1 -1
- package/src/templates/next-webgl/tsconfig.json +1 -0
- package/plugins/no-anchor-element.grit +0 -11
- package/plugins/no-relative-parent-imports.grit +0 -6
- package/plugins/no-unnecessary-forwardref.grit +0 -5
- 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
- package/src/template-hooks/use-media.ts +0 -33
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import NextLink from "next/link"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import {
|
|
6
|
+
type AnchorHTMLAttributes,
|
|
7
|
+
type ComponentProps,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react"
|
|
12
|
+
|
|
13
|
+
// Helper to extract props safe for button elements
|
|
14
|
+
function getButtonProps(props: Record<string, unknown>) {
|
|
15
|
+
const {
|
|
16
|
+
href,
|
|
17
|
+
target,
|
|
18
|
+
rel,
|
|
19
|
+
"data-external": _dataExternal,
|
|
20
|
+
...buttonProps
|
|
21
|
+
} = props
|
|
22
|
+
return buttonProps
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Helper to extract props safe for div elements
|
|
26
|
+
function getDivProps(props: Record<string, unknown>) {
|
|
27
|
+
const {
|
|
28
|
+
href,
|
|
29
|
+
target,
|
|
30
|
+
rel,
|
|
31
|
+
onClick,
|
|
32
|
+
"data-external": _dataExternal,
|
|
33
|
+
...divProps
|
|
34
|
+
} = props
|
|
35
|
+
return divProps
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type CustomLinkProps = Omit<
|
|
39
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
40
|
+
keyof ComponentProps<typeof NextLink> | "href"
|
|
41
|
+
> &
|
|
42
|
+
Omit<ComponentProps<typeof NextLink>, "href"> & {
|
|
43
|
+
href?: string
|
|
44
|
+
onClick?: (e: MouseEvent<HTMLElement>) => void
|
|
45
|
+
scroll?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Link({
|
|
49
|
+
href,
|
|
50
|
+
children,
|
|
51
|
+
onClick,
|
|
52
|
+
scroll = false, // Default to false to prevent scroll restoration warnings with fixed/sticky elements
|
|
53
|
+
...props
|
|
54
|
+
}: CustomLinkProps) {
|
|
55
|
+
const [shouldPrefetch, setShouldPrefetch] = useState(false)
|
|
56
|
+
const [isExternal, setIsExternal] = useState(false)
|
|
57
|
+
const [isActive, setIsActive] = useState(false)
|
|
58
|
+
|
|
59
|
+
// Get pathname - deferred to avoid blocking static generation
|
|
60
|
+
// usePathname is safe to call but we defer the active check to useEffect
|
|
61
|
+
const pathname = usePathname()
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Check if this link is active (current page)
|
|
65
|
+
if (href && pathname) {
|
|
66
|
+
setIsActive(pathname === href)
|
|
67
|
+
}
|
|
68
|
+
}, [href, pathname])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
// Skip if no href
|
|
72
|
+
if (!href) return
|
|
73
|
+
|
|
74
|
+
// Check if external link
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(href, window.location.href)
|
|
77
|
+
setIsExternal(url.host !== window.location.host)
|
|
78
|
+
} catch {
|
|
79
|
+
setIsExternal(false)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Only prefetch on good connections
|
|
83
|
+
const connection = (
|
|
84
|
+
navigator as Navigator & {
|
|
85
|
+
connection?: { effectiveType: string; saveData: boolean }
|
|
86
|
+
}
|
|
87
|
+
).connection
|
|
88
|
+
if (connection) {
|
|
89
|
+
const { effectiveType, saveData } = connection
|
|
90
|
+
setShouldPrefetch(effectiveType === "4g" && !saveData)
|
|
91
|
+
} else {
|
|
92
|
+
// Default to prefetching if API not available
|
|
93
|
+
setShouldPrefetch(true)
|
|
94
|
+
}
|
|
95
|
+
}, [href])
|
|
96
|
+
|
|
97
|
+
// If no href is provided but there's an onClick, render a button
|
|
98
|
+
if (!href && onClick) {
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
onClick={(e: MouseEvent<HTMLButtonElement>) => onClick(e)}
|
|
102
|
+
type="button"
|
|
103
|
+
{...getButtonProps(props)}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</button>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no href and no onClick, render a div
|
|
111
|
+
if (!href) {
|
|
112
|
+
return <div {...getDivProps(props)}>{children}</div>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Block dangerous URIs (javascript:, data:, vbscript:)
|
|
116
|
+
const isDangerousHref = /^(javascript|data|vbscript):/i.test(href)
|
|
117
|
+
if (isDangerousHref) {
|
|
118
|
+
return <span {...getDivProps(props)}>{children}</span>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For SSR, check if it's external based on the href pattern
|
|
122
|
+
const isExternalSSR =
|
|
123
|
+
href.startsWith("http://") || href.startsWith("https://")
|
|
124
|
+
|
|
125
|
+
if (isExternalSSR || isExternal) {
|
|
126
|
+
return (
|
|
127
|
+
<a
|
|
128
|
+
href={href}
|
|
129
|
+
target="_blank"
|
|
130
|
+
rel="noopener noreferrer"
|
|
131
|
+
data-external
|
|
132
|
+
onClick={onClick}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</a>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<NextLink
|
|
142
|
+
href={href as ComponentProps<typeof NextLink>["href"]}
|
|
143
|
+
prefetch={shouldPrefetch}
|
|
144
|
+
scroll={scroll}
|
|
145
|
+
data-active={isActive || undefined}
|
|
146
|
+
{...(onClick && { onClick })}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</NextLink>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getImageDimensions } from "@sanity/asset-utils"
|
|
2
|
+
import { Image, type ImageProps } from "@/components/ui/image"
|
|
3
|
+
import { urlForImage } from "@/lib/integrations/sanity/utils/image"
|
|
4
|
+
|
|
5
|
+
interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
|
|
6
|
+
image: {
|
|
7
|
+
asset?: {
|
|
8
|
+
_ref: string
|
|
9
|
+
_type: "reference"
|
|
10
|
+
}
|
|
11
|
+
alt?: string
|
|
12
|
+
hotspot?: object
|
|
13
|
+
crop?: object
|
|
14
|
+
}
|
|
15
|
+
maxWidth?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SanityImage({
|
|
19
|
+
image,
|
|
20
|
+
maxWidth = 1920,
|
|
21
|
+
alt,
|
|
22
|
+
fill,
|
|
23
|
+
...props
|
|
24
|
+
}: SanityImageProps) {
|
|
25
|
+
if (!image?.asset) return null
|
|
26
|
+
|
|
27
|
+
const { width, height } = getImageDimensions(image.asset)
|
|
28
|
+
const aspectRatio = width / height
|
|
29
|
+
|
|
30
|
+
const imageProps = fill ? { fill: true } : { width, height }
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Image
|
|
34
|
+
src={urlForImage(image).width(maxWidth).url()}
|
|
35
|
+
alt={alt || image.alt || ""}
|
|
36
|
+
aspectRatio={aspectRatio}
|
|
37
|
+
{...imageProps}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createClient, type SanityClient } from "next-sanity"
|
|
2
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
3
|
+
import { apiVersion, dataset, projectId, sanityToken, studioUrl } from "./env"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanity client instance
|
|
7
|
+
*
|
|
8
|
+
* Returns null if Sanity is not configured (missing env vars).
|
|
9
|
+
* Always check with isSanityConfigured() before using.
|
|
10
|
+
*/
|
|
11
|
+
export const client: SanityClient | null = isSanityConfigured()
|
|
12
|
+
? createClient({
|
|
13
|
+
projectId,
|
|
14
|
+
dataset,
|
|
15
|
+
apiVersion,
|
|
16
|
+
useCdn: true,
|
|
17
|
+
perspective: "published",
|
|
18
|
+
token: sanityToken,
|
|
19
|
+
stega: {
|
|
20
|
+
studioUrl,
|
|
21
|
+
filter: (props) => {
|
|
22
|
+
if (props.sourcePath.at(-1) === "title") return true
|
|
23
|
+
return props.filterDefault(props)
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
: null
|
package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import { useDraftModeEnvironment } from "next-sanity/hooks"
|
|
6
|
+
|
|
7
|
+
export function DisableDraftMode() {
|
|
8
|
+
const environment = useDraftModeEnvironment()
|
|
9
|
+
const pathname = usePathname()
|
|
10
|
+
|
|
11
|
+
if (environment !== "live" && environment !== "unknown") return null
|
|
12
|
+
if (pathname.startsWith("/studio")) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Link
|
|
16
|
+
href="/api/draft-mode/disable"
|
|
17
|
+
scroll={false}
|
|
18
|
+
className="dr-p-4 fixed top-safe right-safe z-50 bg-red font-mono text-primary text-sm uppercase"
|
|
19
|
+
>
|
|
20
|
+
Disable Draft Mode
|
|
21
|
+
</Link>
|
|
22
|
+
)
|
|
23
|
+
}
|
package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ArrayOfObjectsInputProps, useFormValue } from "sanity"
|
|
2
|
+
import {
|
|
3
|
+
getVisiblePageBuilderInsertMenuGroups,
|
|
4
|
+
getVisiblePageBuilderReferenceMembers,
|
|
5
|
+
type PageBuilderGroupName,
|
|
6
|
+
} from "@/lib/integrations/sanity/page-builder-config"
|
|
7
|
+
|
|
8
|
+
export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
|
|
9
|
+
const pageTypeValue = useFormValue(["type"])
|
|
10
|
+
const pageType =
|
|
11
|
+
typeof pageTypeValue === "string"
|
|
12
|
+
? (pageTypeValue as PageBuilderGroupName)
|
|
13
|
+
: undefined
|
|
14
|
+
|
|
15
|
+
const visibleMemberNames = new Set(
|
|
16
|
+
getVisiblePageBuilderReferenceMembers(pageType).map((member) => member.name)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return props.renderDefault({
|
|
20
|
+
...props,
|
|
21
|
+
schemaType: {
|
|
22
|
+
...props.schemaType,
|
|
23
|
+
of: props.schemaType.of.filter(
|
|
24
|
+
(member) =>
|
|
25
|
+
typeof member.name === "string" && visibleMemberNames.has(member.name)
|
|
26
|
+
),
|
|
27
|
+
options: {
|
|
28
|
+
...props.schemaType.options,
|
|
29
|
+
insertMenu: {
|
|
30
|
+
...props.schemaType.options?.insertMenu,
|
|
31
|
+
groups: getVisiblePageBuilderInsertMenuGroups(pageType),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Select } from "@sanity/ui"
|
|
2
|
+
import { useEffect } from "react"
|
|
3
|
+
import { type StringInputProps, set, setIfMissing } from "sanity"
|
|
4
|
+
|
|
5
|
+
type PageCategoryOption = {
|
|
6
|
+
title: string
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const isPageCategoryOption = (option: unknown): option is PageCategoryOption =>
|
|
11
|
+
typeof option === "object" &&
|
|
12
|
+
option !== null &&
|
|
13
|
+
"title" in option &&
|
|
14
|
+
"value" in option &&
|
|
15
|
+
typeof option.title === "string" &&
|
|
16
|
+
typeof option.value === "string"
|
|
17
|
+
|
|
18
|
+
export function PageCategoryInput(props: StringInputProps) {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (props.value === undefined) {
|
|
21
|
+
props.onChange(setIfMissing("generic"))
|
|
22
|
+
}
|
|
23
|
+
}, [props.onChange, props.value])
|
|
24
|
+
|
|
25
|
+
const options = Array.isArray(props.schemaType.options?.list)
|
|
26
|
+
? props.schemaType.options.list.filter(isPageCategoryOption)
|
|
27
|
+
: []
|
|
28
|
+
|
|
29
|
+
if (options.length === 0) {
|
|
30
|
+
return props.renderDefault({ ...props, value: props.value ?? "generic" })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Select
|
|
35
|
+
{...props.elementProps}
|
|
36
|
+
disabled={props.readOnly}
|
|
37
|
+
onChange={(event) => props.onChange(set(event.currentTarget.value))}
|
|
38
|
+
value={props.value ?? "generic"}
|
|
39
|
+
{...(props.validationError
|
|
40
|
+
? { customValidity: props.validationError }
|
|
41
|
+
: {})}
|
|
42
|
+
>
|
|
43
|
+
{options.map((option) => (
|
|
44
|
+
<option key={option.value} value={option.value}>
|
|
45
|
+
{option.title}
|
|
46
|
+
</option>
|
|
47
|
+
))}
|
|
48
|
+
</Select>
|
|
49
|
+
)
|
|
50
|
+
}
|