bsmnt 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/helpers/create/copy-template.d.ts +1 -1
  2. package/dist/helpers/create/copy-template.d.ts.map +1 -1
  3. package/dist/helpers/create/index.d.ts.map +1 -1
  4. package/dist/helpers/create/index.js +2 -1
  5. package/dist/helpers/create/index.js.map +1 -1
  6. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  7. package/dist/helpers/integrate/merge-config.js +0 -2
  8. package/dist/helpers/integrate/merge-config.js.map +1 -1
  9. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  10. package/dist/helpers/integrate/sanity/config.js +2 -4
  11. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  12. package/dist/index.js +84 -35
  13. package/dist/index.js.map +1 -1
  14. package/index.js +2 -2
  15. package/package.json +1 -1
  16. package/src/templates/next-default/.vscode/settings.json +1 -1
  17. package/src/templates/next-default/README.md +6 -7
  18. package/src/templates/next-default/app/layout.tsx +17 -4
  19. package/src/templates/next-default/biome.json +1 -1
  20. package/src/templates/next-default/css.d.ts +1 -0
  21. package/src/templates/next-default/lib/README.md +4 -8
  22. package/src/templates/next-default/lib/hooks/use-media.ts +3 -1
  23. package/src/templates/next-default/lib/styles/global.css +182 -0
  24. package/src/templates/next-default/lib/utils/json-ld.tsx +13 -18
  25. package/src/templates/next-default/lib/utils/portable-text-to-markdown.ts +83 -0
  26. package/src/templates/next-default/package.json +3 -3
  27. package/src/templates/next-experiments/.vscode/settings.json +1 -1
  28. package/src/templates/next-experiments/README.md +6 -7
  29. package/src/templates/next-experiments/app/layout.tsx +17 -4
  30. package/src/templates/next-experiments/biome.json +1 -1
  31. package/src/templates/next-experiments/css.d.ts +1 -0
  32. package/src/templates/next-experiments/lib/README.md +4 -8
  33. package/src/templates/next-experiments/lib/hooks/use-media.ts +3 -1
  34. package/src/templates/next-experiments/lib/styles/global.css +182 -0
  35. package/src/templates/next-experiments/lib/utils/json-ld.tsx +13 -18
  36. package/src/templates/next-experiments/lib/utils/portable-text-to-markdown.ts +83 -0
  37. package/src/templates/next-experiments/package.json +3 -3
  38. package/src/templates/next-pagebuilder/.env.example +11 -0
  39. package/src/templates/next-pagebuilder/README.md +23 -0
  40. package/src/templates/next-pagebuilder/_gitignore +67 -0
  41. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
  42. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
  43. package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
  44. package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
  45. package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
  46. package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
  47. package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
  48. package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
  49. package/src/templates/next-pagebuilder/app/robots.ts +15 -0
  50. package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
  51. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
  52. package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
  53. package/src/templates/next-pagebuilder/biome.json +239 -0
  54. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
  55. package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
  56. package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
  57. package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
  58. package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
  59. package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
  60. package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
  61. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
  62. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
  63. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
  64. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
  65. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  66. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
  67. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
  68. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
  69. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
  70. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
  71. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
  72. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
  73. package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
  74. package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
  75. package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
  76. package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
  77. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
  78. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
  79. package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
  80. package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
  81. package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
  82. package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
  83. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  84. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
  85. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  86. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
  87. package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  88. package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
  89. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
  90. package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
  91. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
  92. package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  93. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
  94. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
  95. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
  96. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
  97. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
  98. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
  99. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  100. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  101. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  102. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  103. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
  104. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  105. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
  106. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  107. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  108. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
  109. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  110. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  111. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  112. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  113. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  114. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
  116. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  117. package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
  118. package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
  119. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
  120. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
  121. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  122. package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
  123. package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
  124. package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
  125. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
  126. package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
  127. package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
  128. package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
  129. package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
  130. package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
  131. package/src/templates/next-pagebuilder/next.config.ts +134 -0
  132. package/src/templates/next-pagebuilder/package.json +71 -0
  133. package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
  134. package/src/templates/next-pagebuilder/proxy.ts +81 -0
  135. package/src/templates/next-pagebuilder/svg.d.ts +5 -0
  136. package/src/templates/next-pagebuilder/tsconfig.json +38 -0
  137. package/src/templates/next-webgl/.vscode/settings.json +1 -1
  138. package/src/templates/next-webgl/README.md +6 -7
  139. package/src/templates/next-webgl/app/layout.tsx +17 -4
  140. package/src/templates/next-webgl/biome.json +1 -1
  141. package/src/templates/next-webgl/css.d.ts +1 -0
  142. package/src/templates/next-webgl/lib/README.md +4 -8
  143. package/src/templates/next-webgl/lib/hooks/use-media.ts +3 -1
  144. package/src/templates/next-webgl/lib/styles/global.css +182 -0
  145. package/src/templates/next-webgl/lib/utils/json-ld.tsx +13 -18
  146. package/src/templates/next-webgl/lib/utils/portable-text-to-markdown.ts +83 -0
  147. package/src/templates/next-webgl/package.json +3 -3
  148. package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
  149. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
  150. package/src/templates/next-default/lib/scripts/dev.ts +0 -32
  151. package/src/templates/next-default/lib/styles/README.md +0 -13
  152. package/src/templates/next-default/lib/styles/fonts.ts +0 -20
  153. package/src/templates/next-default/lib/styles/index.css +0 -3
  154. package/src/templates/next-default/lib/styles/tokens.css +0 -179
  155. package/src/templates/next-default/lib/utils/README.md +0 -40
  156. package/src/templates/next-default/lib/utils/easings.ts +0 -240
  157. package/src/templates/next-default/lib/utils/fetch.ts +0 -84
  158. package/src/templates/next-default/lib/utils/global-css.d.ts +0 -1
  159. package/src/templates/next-default/lib/utils/math.ts +0 -236
  160. package/src/templates/next-default/lib/utils/strings.ts +0 -246
  161. package/src/templates/next-default/lib/utils/types.d.ts +0 -15
  162. package/src/templates/next-default/lib/utils/viewport.ts +0 -199
  163. package/src/templates/next-experiments/lib/scripts/dev.ts +0 -32
  164. package/src/templates/next-experiments/lib/styles/README.md +0 -13
  165. package/src/templates/next-experiments/lib/styles/fonts.ts +0 -20
  166. package/src/templates/next-experiments/lib/styles/index.css +0 -3
  167. package/src/templates/next-experiments/lib/styles/tokens.css +0 -179
  168. package/src/templates/next-experiments/lib/utils/README.md +0 -40
  169. package/src/templates/next-experiments/lib/utils/easings.ts +0 -240
  170. package/src/templates/next-experiments/lib/utils/fetch.ts +0 -84
  171. package/src/templates/next-experiments/lib/utils/global-css.d.ts +0 -1
  172. package/src/templates/next-experiments/lib/utils/math.ts +0 -236
  173. package/src/templates/next-experiments/lib/utils/strings.ts +0 -246
  174. package/src/templates/next-experiments/lib/utils/types.d.ts +0 -15
  175. package/src/templates/next-experiments/lib/utils/viewport.ts +0 -199
  176. package/src/templates/next-webgl/lib/scripts/dev.ts +0 -32
  177. package/src/templates/next-webgl/lib/styles/README.md +0 -13
  178. package/src/templates/next-webgl/lib/styles/fonts.ts +0 -20
  179. package/src/templates/next-webgl/lib/styles/index.css +0 -3
  180. package/src/templates/next-webgl/lib/styles/tokens.css +0 -179
  181. package/src/templates/next-webgl/lib/utils/README.md +0 -40
  182. package/src/templates/next-webgl/lib/utils/easings.ts +0 -240
  183. package/src/templates/next-webgl/lib/utils/fetch.ts +0 -84
  184. package/src/templates/next-webgl/lib/utils/global-css.d.ts +0 -1
  185. package/src/templates/next-webgl/lib/utils/math.ts +0 -236
  186. package/src/templates/next-webgl/lib/utils/strings.ts +0 -246
  187. package/src/templates/next-webgl/lib/utils/types.d.ts +0 -15
  188. package/src/templates/next-webgl/lib/utils/viewport.ts +0 -199
@@ -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
+ }
@@ -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
+ }