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.
Files changed (163) hide show
  1. package/dist/configs/skills.d.ts +27 -0
  2. package/dist/configs/skills.d.ts.map +1 -0
  3. package/dist/configs/skills.js +18 -0
  4. package/dist/configs/skills.js.map +1 -0
  5. package/dist/configs/skills.json +26 -0
  6. package/dist/helpers/add/hooks-config.d.ts.map +1 -1
  7. package/dist/helpers/add/hooks-config.js +0 -6
  8. package/dist/helpers/add/hooks-config.js.map +1 -1
  9. package/dist/helpers/create/copy-template.d.ts +1 -1
  10. package/dist/helpers/create/copy-template.d.ts.map +1 -1
  11. package/dist/helpers/create/index.d.ts.map +1 -1
  12. package/dist/helpers/create/index.js +2 -1
  13. package/dist/helpers/create/index.js.map +1 -1
  14. package/dist/helpers/create/setup-agent.d.ts.map +1 -1
  15. package/dist/helpers/create/setup-agent.js +15 -5
  16. package/dist/helpers/create/setup-agent.js.map +1 -1
  17. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  18. package/dist/helpers/integrate/merge-config.js +1 -2
  19. package/dist/helpers/integrate/merge-config.js.map +1 -1
  20. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  21. package/dist/helpers/integrate/sanity/config.js +5 -10
  22. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  23. package/dist/helpers/integrate/sanity/mergers/layout-merger.d.ts.map +1 -1
  24. package/dist/helpers/integrate/sanity/mergers/layout-merger.js +13 -12
  25. package/dist/helpers/integrate/sanity/mergers/layout-merger.js.map +1 -1
  26. package/dist/helpers/skills/index.d.ts +10 -0
  27. package/dist/helpers/skills/index.d.ts.map +1 -0
  28. package/dist/helpers/skills/index.js +136 -0
  29. package/dist/helpers/skills/index.js.map +1 -0
  30. package/dist/index.js +102 -35
  31. package/dist/index.js.map +1 -1
  32. package/package.json +3 -2
  33. package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +2 -1
  34. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/confirm-publish-action.ts +31 -0
  35. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +17 -0
  36. package/src/helpers/integrate/sanity/files/lib/utils/json-ld.tsx +249 -0
  37. package/src/template-hooks/config.js +0 -6
  38. package/src/templates/next-default/app/layout.tsx +18 -0
  39. package/src/templates/next-default/lib/hooks/use-device-detection.ts +1 -1
  40. package/src/templates/next-default/lib/hooks/use-media-breakpoint.ts +1 -1
  41. package/src/templates/next-default/lib/hooks/use-media.ts +29 -0
  42. package/src/templates/next-default/lib/utils/json-ld.tsx +199 -0
  43. package/src/templates/next-default/package.json +1 -1
  44. package/src/templates/next-default/tsconfig.json +1 -0
  45. package/src/templates/next-experiments/app/layout.tsx +18 -0
  46. package/src/templates/next-experiments/lib/hooks/use-device-detection.ts +1 -1
  47. package/src/templates/next-experiments/lib/hooks/use-media-breakpoint.ts +1 -1
  48. package/src/templates/next-experiments/lib/hooks/use-media.ts +29 -0
  49. package/src/templates/next-experiments/lib/utils/json-ld.tsx +199 -0
  50. package/src/templates/next-experiments/package.json +1 -1
  51. package/src/templates/next-experiments/tsconfig.json +1 -0
  52. package/src/templates/next-pagebuilder/.env.example +11 -0
  53. package/src/templates/next-pagebuilder/README.md +23 -0
  54. package/src/templates/next-pagebuilder/_gitignore +67 -0
  55. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
  56. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
  57. package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
  58. package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
  59. package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
  60. package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
  61. package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
  62. package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
  63. package/src/templates/next-pagebuilder/app/robots.ts +15 -0
  64. package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
  65. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
  66. package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
  67. package/src/templates/next-pagebuilder/biome.json +239 -0
  68. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
  69. package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
  70. package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
  71. package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
  72. package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
  73. package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
  74. package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
  75. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
  76. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
  77. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
  78. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
  79. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  80. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
  81. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
  82. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
  83. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
  84. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
  85. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
  86. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
  87. package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
  88. package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
  89. package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
  90. package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
  91. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
  92. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
  93. package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
  94. package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
  95. package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
  96. package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
  97. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  98. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
  99. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  100. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
  101. package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  102. package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
  103. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
  104. package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
  105. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
  106. package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  107. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
  108. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
  109. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
  110. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
  111. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
  112. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
  113. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  114. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  116. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  117. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
  118. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  119. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
  120. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  121. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  122. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
  123. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  124. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  125. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  126. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  127. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  128. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  129. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
  130. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  131. package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
  132. package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
  133. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
  134. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
  135. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  136. package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
  137. package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
  138. package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
  139. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
  140. package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
  141. package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
  142. package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
  143. package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
  144. package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
  145. package/src/templates/next-pagebuilder/next.config.ts +134 -0
  146. package/src/templates/next-pagebuilder/package.json +71 -0
  147. package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
  148. package/src/templates/next-pagebuilder/proxy.ts +81 -0
  149. package/src/templates/next-pagebuilder/svg.d.ts +5 -0
  150. package/src/templates/next-pagebuilder/tsconfig.json +38 -0
  151. package/src/templates/next-webgl/app/layout.tsx +18 -0
  152. package/src/templates/next-webgl/lib/hooks/use-device-detection.ts +1 -1
  153. package/src/templates/next-webgl/lib/hooks/use-media-breakpoint.ts +1 -1
  154. package/src/templates/next-webgl/lib/hooks/use-media.ts +29 -0
  155. package/src/templates/next-webgl/lib/utils/json-ld.tsx +199 -0
  156. package/src/templates/next-webgl/package.json +1 -1
  157. package/src/templates/next-webgl/tsconfig.json +1 -0
  158. package/plugins/no-anchor-element.grit +0 -11
  159. package/plugins/no-relative-parent-imports.grit +0 -6
  160. package/plugins/no-unnecessary-forwardref.grit +0 -5
  161. package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
  162. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
  163. 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,5 @@
1
+ export const isSanityConfigured = () =>
2
+ Boolean(
3
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
4
+ process.env.NEXT_PUBLIC_SANITY_DATASET
5
+ )
@@ -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
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }