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,110 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { Image } from "@/components/ui/image"
|
|
6
|
+
import { Link } from "@/components/ui/link"
|
|
7
|
+
import type { NavbarData } from "@/lib/integrations/sanity/fetchers/layout"
|
|
8
|
+
|
|
9
|
+
import { CtaButtonRenderer } from "./components/cta-button"
|
|
10
|
+
import { NavItemRenderer } from "./components/nav-item-renderer"
|
|
11
|
+
|
|
12
|
+
export const HeaderClient = ({ data: navbar }: { data: NavbarData | null }) => {
|
|
13
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
|
14
|
+
const navRef = useRef<HTMLElement>(null)
|
|
15
|
+
const triggerRefs = useRef<Map<number, HTMLButtonElement>>(new Map())
|
|
16
|
+
|
|
17
|
+
const logoUrl = navbar?.logo?.asset?.url
|
|
18
|
+
const navigationItems = navbar?.navigationItems ?? []
|
|
19
|
+
const ctaButtons = navbar?.ctaButtons ?? []
|
|
20
|
+
|
|
21
|
+
// Close on click outside & Escape
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (openIndex === null) return
|
|
24
|
+
|
|
25
|
+
const onClick = (e: MouseEvent) => {
|
|
26
|
+
if (
|
|
27
|
+
navRef.current &&
|
|
28
|
+
e.target instanceof Node &&
|
|
29
|
+
!navRef.current.contains(e.target)
|
|
30
|
+
) {
|
|
31
|
+
setOpenIndex(null)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
36
|
+
if (e.key === "Escape") {
|
|
37
|
+
const prevIndex = openIndex
|
|
38
|
+
setOpenIndex(null)
|
|
39
|
+
triggerRefs.current.get(prevIndex)?.focus()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
document.addEventListener("click", onClick)
|
|
44
|
+
document.addEventListener("keydown", onKeyDown)
|
|
45
|
+
return () => {
|
|
46
|
+
document.removeEventListener("click", onClick)
|
|
47
|
+
document.removeEventListener("keydown", onKeyDown)
|
|
48
|
+
}
|
|
49
|
+
}, [openIndex])
|
|
50
|
+
|
|
51
|
+
const handleToggle = (index: number) => {
|
|
52
|
+
setOpenIndex((prev) => (prev === index ? null : index))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<nav
|
|
57
|
+
ref={navRef}
|
|
58
|
+
className="sticky top-0 z-50 border-black/10 border-b bg-white"
|
|
59
|
+
aria-label="Main navigation"
|
|
60
|
+
>
|
|
61
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
|
|
62
|
+
{/* Logo */}
|
|
63
|
+
<Link href="/" className="shrink-0" aria-label="Home">
|
|
64
|
+
{logoUrl ? (
|
|
65
|
+
<Image
|
|
66
|
+
src={logoUrl}
|
|
67
|
+
alt="Basement"
|
|
68
|
+
width={131}
|
|
69
|
+
height={23}
|
|
70
|
+
className="h-8 w-auto"
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<span className="font-semibold text-black text-lg">Basement</span>
|
|
74
|
+
)}
|
|
75
|
+
</Link>
|
|
76
|
+
|
|
77
|
+
{/* Navigation items */}
|
|
78
|
+
{navigationItems.length > 0 ? (
|
|
79
|
+
<ul className="hidden items-center gap-8 lg:flex">
|
|
80
|
+
{navigationItems.map((item, i) => (
|
|
81
|
+
<NavItemRenderer
|
|
82
|
+
key={item._key}
|
|
83
|
+
item={item}
|
|
84
|
+
isOpen={openIndex === i}
|
|
85
|
+
onToggle={() => handleToggle(i)}
|
|
86
|
+
triggerRef={(el) => {
|
|
87
|
+
if (el) {
|
|
88
|
+
triggerRefs.current.set(i, el)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
triggerRefs.current.delete(i)
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
) : null}
|
|
98
|
+
|
|
99
|
+
{/* CTA buttons */}
|
|
100
|
+
{ctaButtons.length > 0 ? (
|
|
101
|
+
<div className="hidden items-center gap-3 lg:flex">
|
|
102
|
+
{ctaButtons.map((cta) => (
|
|
103
|
+
<CtaButtonRenderer key={cta._key} cta={cta} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
</nav>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { stegaClean } from "next-sanity"
|
|
2
|
+
import {
|
|
3
|
+
getCompanyData,
|
|
4
|
+
getNavbar,
|
|
5
|
+
} from "@/lib/integrations/sanity/fetchers/layout"
|
|
6
|
+
import {
|
|
7
|
+
generateOrganizationJsonLd,
|
|
8
|
+
generateWebSiteJsonLd,
|
|
9
|
+
JsonLd as JsonLdBase,
|
|
10
|
+
} from "@/lib/utils/json-ld"
|
|
11
|
+
|
|
12
|
+
const APP_DESCRIPTION =
|
|
13
|
+
"Basement is the AI-native platform for audit and advisory firms. Automate engagement workflows using AI agents trusted by 50% of top 100 firms."
|
|
14
|
+
|
|
15
|
+
export const JsonLd = async () => {
|
|
16
|
+
const [navbarData, companyData] = await Promise.all([
|
|
17
|
+
getNavbar(),
|
|
18
|
+
getCompanyData(),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const sameAs = companyData?.socialLinks
|
|
22
|
+
?.map((link) => stegaClean(link.url))
|
|
23
|
+
.filter((url): url is string => Boolean(url))
|
|
24
|
+
|
|
25
|
+
const logoUrl = navbarData?.logo?.asset?.url
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<JsonLdBase
|
|
30
|
+
data={generateWebSiteJsonLd({
|
|
31
|
+
name: "Basement",
|
|
32
|
+
description: APP_DESCRIPTION,
|
|
33
|
+
})}
|
|
34
|
+
/>
|
|
35
|
+
<JsonLdBase
|
|
36
|
+
data={generateOrganizationJsonLd({
|
|
37
|
+
name: "Basement",
|
|
38
|
+
...(logoUrl ? { logo: logoUrl } : {}),
|
|
39
|
+
description: APP_DESCRIPTION,
|
|
40
|
+
...(sameAs?.length ? { sameAs } : {}),
|
|
41
|
+
})}
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { Footer } from "@/components/layout/footer"
|
|
3
|
+
import { Header } from "@/components/layout/header"
|
|
4
|
+
import { cn } from "@/lib/styles/cn"
|
|
5
|
+
|
|
6
|
+
interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
7
|
+
|
|
8
|
+
const SkipToMainContent = () => (
|
|
9
|
+
<Link
|
|
10
|
+
href="#main-content"
|
|
11
|
+
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-9999 focus:bg-black focus:p-2 focus:text-white focus:outline-none focus:ring-2 focus:ring-white"
|
|
12
|
+
>
|
|
13
|
+
Skip to main content
|
|
14
|
+
</Link>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const Wrapper = ({ children, className, ...props }: WrapperProps) => (
|
|
18
|
+
<>
|
|
19
|
+
<SkipToMainContent />
|
|
20
|
+
<Header />
|
|
21
|
+
<main
|
|
22
|
+
id="main-content"
|
|
23
|
+
className={cn("relative flex grow flex-col", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</main>
|
|
28
|
+
<Footer />
|
|
29
|
+
</>
|
|
30
|
+
)
|
package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RichText } from "@/components/sanity/rich-text"
|
|
2
|
+
import { SanityImage } from "@/components/ui/sanity-image"
|
|
3
|
+
import { formatDate } from "@/lib/utils/format-date"
|
|
4
|
+
import {RelatedPostItem} from "./related-post-item"
|
|
5
|
+
import type { ArticleContentProps } from "@/components/page-builder/types"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function ArticleContent({
|
|
9
|
+
block
|
|
10
|
+
}: ArticleContentProps) {
|
|
11
|
+
const { author, body, categories, date, relatedPosts, thumbnail } = block
|
|
12
|
+
return (
|
|
13
|
+
<div className="py-8 md:py-12">
|
|
14
|
+
<header className="mx-auto max-w-3xl space-y-6">
|
|
15
|
+
{categories && categories.length > 0 ? (
|
|
16
|
+
<div className="flex flex-wrap gap-2">
|
|
17
|
+
{/* @ts-ignore */}
|
|
18
|
+
{categories.map((category) => (
|
|
19
|
+
<span
|
|
20
|
+
key={category._id}
|
|
21
|
+
className="rounded-full border border-current/20 px-3 py-1 text-xs"
|
|
22
|
+
>
|
|
23
|
+
{category.title}
|
|
24
|
+
</span>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
) : null}
|
|
28
|
+
|
|
29
|
+
<div className="flex items-center gap-3">
|
|
30
|
+
{author?.avatar ? (
|
|
31
|
+
<SanityImage
|
|
32
|
+
image={author.avatar}
|
|
33
|
+
alt={author.name || ""}
|
|
34
|
+
className="size-10 rounded-full object-cover"
|
|
35
|
+
/>
|
|
36
|
+
) : null}
|
|
37
|
+
<div className="flex flex-col">
|
|
38
|
+
{author?.name ? (
|
|
39
|
+
<span className="font-medium text-sm">{author.name}</span>
|
|
40
|
+
) : null}
|
|
41
|
+
<div className="flex items-center gap-2 text-current/60 text-sm">
|
|
42
|
+
{author?.role ? <span>{author.role}</span> : null}
|
|
43
|
+
{author?.role && date ? <span aria-hidden>·</span> : null}
|
|
44
|
+
{date ? <time dateTime={date}>{formatDate(date)}</time> : null}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
{thumbnail ? (
|
|
51
|
+
<div className="mx-auto mt-8 max-w-4xl overflow-hidden rounded-lg">
|
|
52
|
+
<SanityImage
|
|
53
|
+
image={thumbnail}
|
|
54
|
+
alt={thumbnail.alt || ""}
|
|
55
|
+
priority
|
|
56
|
+
className="aspect-video w-full object-cover"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
{body ? (
|
|
62
|
+
<div className="mx-auto mt-10 max-w-3xl space-y-4 text-base/7 text-current/80 md:text-lg/8">
|
|
63
|
+
<RichText content={body} />
|
|
64
|
+
</div>
|
|
65
|
+
) : null}
|
|
66
|
+
|
|
67
|
+
{relatedPosts && relatedPosts.length > 0 ? (
|
|
68
|
+
<aside className="mx-auto mt-16 max-w-3xl">
|
|
69
|
+
<h2 className="font-semibold text-2xl">Related Articles</h2>
|
|
70
|
+
<ul className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
71
|
+
{relatedPosts.map((post) => {
|
|
72
|
+
if (!post.resolvedSlug) return null
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<RelatedPostItem key={post._id} post={post} />
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</ul>
|
|
79
|
+
</aside>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Link } from "@/components/ui/link"
|
|
2
|
+
import { SanityImage } from "@/components/ui/sanity-image"
|
|
3
|
+
import type { ArticleRelatedPost } from "@/components/page-builder/types"
|
|
4
|
+
|
|
5
|
+
export const RelatedPostItem = ({ post }: {post: ArticleRelatedPost}) => {
|
|
6
|
+
return (
|
|
7
|
+
<li key={post._id}>
|
|
8
|
+
<Link
|
|
9
|
+
href={`/${post.resolvedSlug}`}
|
|
10
|
+
className="group flex flex-col gap-3"
|
|
11
|
+
>
|
|
12
|
+
{post.metadata?.image ? (
|
|
13
|
+
<div className="overflow-hidden rounded-lg">
|
|
14
|
+
<SanityImage
|
|
15
|
+
image={post.metadata.image}
|
|
16
|
+
alt={post.title || ""}
|
|
17
|
+
className="aspect-[16/9] w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
) : null}
|
|
21
|
+
<h3 className="text-balance font-medium text-base leading-snug">
|
|
22
|
+
{post.title}
|
|
23
|
+
</h3>
|
|
24
|
+
</Link>
|
|
25
|
+
</li>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageBuilderBlock } from "../types"
|
|
2
|
+
|
|
3
|
+
type DescriptionProps = {
|
|
4
|
+
block: Extract<PageBuilderBlock, { _type: "description" }>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Description({ block }: DescriptionProps) {
|
|
8
|
+
if (!block.description) return null
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="py-4">
|
|
12
|
+
<p className="max-w-[62ch] text-base/7 text-current/72 md:text-lg/8">
|
|
13
|
+
{block.description}
|
|
14
|
+
</p>
|
|
15
|
+
</section>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageBuilderBlock } from "../types"
|
|
2
|
+
|
|
3
|
+
type HeroProps = {
|
|
4
|
+
block: Extract<PageBuilderBlock, { _type: "hero" }>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Hero({ block }: HeroProps) {
|
|
8
|
+
if (!block.headline) return null
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="py-8 md:py-12">
|
|
12
|
+
<h1 className="max-w-4xl font-semibold text-4xl tracking-tight md:text-6xl">
|
|
13
|
+
{block.headline}
|
|
14
|
+
</h1>
|
|
15
|
+
</section>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Link } from "@/components/ui/link"
|
|
2
|
+
import { SanityImage } from "@/components/ui/sanity-image"
|
|
3
|
+
import { formatDate } from "@/lib/utils/format-date"
|
|
4
|
+
import type { BlogArticleCard } from "./types"
|
|
5
|
+
|
|
6
|
+
type ContentCardProps = {
|
|
7
|
+
article: BlogArticleCard
|
|
8
|
+
className?: string | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ContentCard = ({ article, className }: ContentCardProps) => {
|
|
12
|
+
const href = article.resolvedSlug ? `/${article.resolvedSlug}` : null
|
|
13
|
+
if (!href) return null
|
|
14
|
+
|
|
15
|
+
const blog = article.blogContent
|
|
16
|
+
const title = article.title ?? ""
|
|
17
|
+
const categories = blog?.categories ?? []
|
|
18
|
+
const authorName = blog?.author?.name
|
|
19
|
+
const publishedAt = blog?.date ?? null
|
|
20
|
+
const formattedDate = publishedAt ? formatDate(publishedAt) : null
|
|
21
|
+
const thumbnail = blog?.thumbnail ?? article.metadata?.image
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<li className={className}>
|
|
25
|
+
<Link href={href} className="group flex flex-col gap-3">
|
|
26
|
+
<div className="relative aspect-16/9 w-full overflow-hidden rounded-lg bg-gray-100">
|
|
27
|
+
{thumbnail ? (
|
|
28
|
+
<SanityImage
|
|
29
|
+
image={thumbnail}
|
|
30
|
+
alt=""
|
|
31
|
+
fill
|
|
32
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
33
|
+
className="w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
34
|
+
/>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex flex-col gap-2">
|
|
39
|
+
{categories.length > 0 ? (
|
|
40
|
+
<div className="flex flex-wrap gap-1.5">
|
|
41
|
+
{categories.map((category) => (
|
|
42
|
+
<span key={category._id} className="text-current/60 text-xs">
|
|
43
|
+
{category.title}
|
|
44
|
+
</span>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
) : null}
|
|
48
|
+
|
|
49
|
+
<h3 className="text-balance font-medium text-lg leading-snug">
|
|
50
|
+
{title}
|
|
51
|
+
</h3>
|
|
52
|
+
|
|
53
|
+
{authorName || formattedDate ? (
|
|
54
|
+
<div className="flex items-center gap-2 text-current/60 text-sm">
|
|
55
|
+
{authorName ? <span>{authorName}</span> : null}
|
|
56
|
+
{authorName && formattedDate ? <span aria-hidden>·</span> : null}
|
|
57
|
+
{publishedAt && formattedDate ? (
|
|
58
|
+
<time dateTime={publishedAt}>{formattedDate}</time>
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
</Link>
|
|
64
|
+
</li>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { BlogArticleCard } from "./types"
|
|
2
|
+
import { ContentCard } from "./content-card"
|
|
3
|
+
|
|
4
|
+
type ContentGridProps = {
|
|
5
|
+
articles: BlogArticleCard[]
|
|
6
|
+
start?: number
|
|
7
|
+
end?: number
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ContentGrid({
|
|
12
|
+
articles,
|
|
13
|
+
start = 0,
|
|
14
|
+
end,
|
|
15
|
+
className,
|
|
16
|
+
}: ContentGridProps) {
|
|
17
|
+
const slice = articles.slice(start, end)
|
|
18
|
+
|
|
19
|
+
if (slice.length === 0) {
|
|
20
|
+
if (start === 0) {
|
|
21
|
+
return <p className="text-current/70 text-sm">No articles found.</p>
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ul
|
|
28
|
+
className={
|
|
29
|
+
className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{slice.map((article) => {
|
|
33
|
+
const slug = article.resolvedSlug
|
|
34
|
+
if (!slug) return null
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ContentCard key={article._id} article={article} />
|
|
38
|
+
)
|
|
39
|
+
})}
|
|
40
|
+
</ul>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sanityFetch } from "@/lib/integrations/sanity/live"
|
|
2
|
+
import { ALL_BLOG_ARTICLES_QUERY } from "@/lib/integrations/sanity/queries"
|
|
3
|
+
import { ContentGrid } from "./content-grid"
|
|
4
|
+
import type { ContentCollectionBlock } from "./types"
|
|
5
|
+
|
|
6
|
+
type ContentCollectionProps = {
|
|
7
|
+
block: ContentCollectionBlock
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function ContentCollection({ block }: ContentCollectionProps) {
|
|
11
|
+
const { data: articles } = await sanityFetch({
|
|
12
|
+
query: ALL_BLOG_ARTICLES_QUERY,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
if (!articles?.length) return null
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section className="flex flex-col gap-8 py-8">
|
|
20
|
+
{block.subtitle ? (
|
|
21
|
+
<h2 className="font-medium text-2xl leading-tight">
|
|
22
|
+
{block.subtitle}
|
|
23
|
+
</h2>
|
|
24
|
+
) : null}
|
|
25
|
+
<ContentGrid articles={articles} />
|
|
26
|
+
</section>
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PageBuilderBlock } from "@/components/page-builder/types"
|
|
2
|
+
import type { ALL_BLOG_ARTICLES_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
3
|
+
|
|
4
|
+
export type BlogCollectionBlock = Extract<
|
|
5
|
+
PageBuilderBlock,
|
|
6
|
+
{ _type: "blogCollection" }
|
|
7
|
+
>
|
|
8
|
+
|
|
9
|
+
export type ContentCollectionBlock = Extract<
|
|
10
|
+
PageBuilderBlock,
|
|
11
|
+
{
|
|
12
|
+
_type: "blogCollection"
|
|
13
|
+
}
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type BlogArticleCard = ALL_BLOG_ARTICLES_QUERY_RESULT[number]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ArticleContent } from "./components/article-content"
|
|
2
|
+
import { Description } from "./components/description"
|
|
3
|
+
import { Hero } from "./components/hero"
|
|
4
|
+
import { ContentCollection } from "./components/post-collection"
|
|
5
|
+
import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
|
|
6
|
+
|
|
7
|
+
type RendererProps = {
|
|
8
|
+
blocks: PageBuilderBlocks
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Renderer({ blocks }: RendererProps) {
|
|
12
|
+
if (!blocks?.length) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-16">
|
|
16
|
+
{blocks.map((block: PageBuilderBlock) => (
|
|
17
|
+
<PageBuilderNode key={block._key} block={block} />
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PageBuilderNode({ block }: { block: PageBuilderBlock }) {
|
|
24
|
+
switch (block._type) {
|
|
25
|
+
case "blogCollection":
|
|
26
|
+
return <ContentCollection block={block} />
|
|
27
|
+
case "blogContent":
|
|
28
|
+
return <ArticleContent block={block} />
|
|
29
|
+
case "hero":
|
|
30
|
+
return <Hero block={block} />
|
|
31
|
+
case "description":
|
|
32
|
+
return <Description block={block} />
|
|
33
|
+
default:
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
2
|
+
|
|
3
|
+
export type PageBuilderBlock = NonNullable<
|
|
4
|
+
NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
|
|
5
|
+
>[number]
|
|
6
|
+
|
|
7
|
+
export type PageBuilderBlocks = NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
|
|
8
|
+
|
|
9
|
+
export type BlogContentBlock = Extract<PageBuilderBlock, { _type: "blogContent" }>
|
|
10
|
+
|
|
11
|
+
export type ArticleContentProps = {
|
|
12
|
+
block: BlogContentBlock
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ArticleRelatedPost = {
|
|
17
|
+
_id: string;
|
|
18
|
+
title: string | null;
|
|
19
|
+
resolvedSlug: string | null;
|
|
20
|
+
metadata?: {
|
|
21
|
+
image?: unknown;
|
|
22
|
+
} | null
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { stegaClean } from "@sanity/client/stega"
|
|
2
|
+
import { Renderer } from "@/components/page-builder/renderer"
|
|
3
|
+
import type { PAGE_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
4
|
+
import { urlForImage } from "@/lib/integrations/sanity/utils/image"
|
|
5
|
+
import {
|
|
6
|
+
generateBreadcrumbJsonLd,
|
|
7
|
+
generateSanityWebPageJsonLd,
|
|
8
|
+
JsonLd,
|
|
9
|
+
} from "@/lib/utils/json-ld"
|
|
10
|
+
|
|
11
|
+
type PageDocumentProps = {
|
|
12
|
+
page: NonNullable<PAGE_QUERY_RESULT>
|
|
13
|
+
path: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const getPageSections = (page: NonNullable<PAGE_QUERY_RESULT>) =>
|
|
17
|
+
page.pageBuilder?.length
|
|
18
|
+
? [<Renderer key="page-builder" blocks={page.pageBuilder} />]
|
|
19
|
+
: []
|
|
20
|
+
|
|
21
|
+
const buildBreadcrumbs = (
|
|
22
|
+
slugPath: string | null,
|
|
23
|
+
pageTitle: string | null
|
|
24
|
+
): Array<{ name: string; url: string }> => {
|
|
25
|
+
const items: Array<{ name: string; url: string }> = [
|
|
26
|
+
{ name: "Home", url: "/" },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if (!slugPath) return items
|
|
30
|
+
|
|
31
|
+
const segments = slugPath.split("/").filter(Boolean)
|
|
32
|
+
for (let i = 0; i < segments.length; i++) {
|
|
33
|
+
const isLast = i === segments.length - 1
|
|
34
|
+
const url = `/${segments.slice(0, i + 1).join("/")}`
|
|
35
|
+
|
|
36
|
+
//TODO: we might change this in the future if slug title and folder diverge too much :)
|
|
37
|
+
|
|
38
|
+
const name =
|
|
39
|
+
isLast && pageTitle
|
|
40
|
+
? pageTitle
|
|
41
|
+
: (segments[i]
|
|
42
|
+
?.replace(/-/g, " ")
|
|
43
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()) ?? "")
|
|
44
|
+
items.push({ name, url })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return items
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const PageDocument = ({ page, path }: PageDocumentProps) => {
|
|
51
|
+
const title = stegaClean(page.title) || page.title
|
|
52
|
+
const sections = getPageSections(page)
|
|
53
|
+
|
|
54
|
+
const cleanTitle = stegaClean(page.title) || "Basement"
|
|
55
|
+
const cleanDescription = stegaClean(
|
|
56
|
+
page.metadata?.metaDescription || page.metadata?.description
|
|
57
|
+
)
|
|
58
|
+
const pageUrl = path ? `/${path}` : "/"
|
|
59
|
+
|
|
60
|
+
const ogImage = page.metadata?.image
|
|
61
|
+
? urlForImage(page.metadata.image).width(1200).height(630).url()
|
|
62
|
+
: undefined
|
|
63
|
+
|
|
64
|
+
const breadcrumbs = buildBreadcrumbs(path, cleanTitle)
|
|
65
|
+
const hasBreadcrumbs = breadcrumbs.length > 1
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<JsonLd
|
|
70
|
+
data={generateSanityWebPageJsonLd({
|
|
71
|
+
title: cleanTitle,
|
|
72
|
+
url: pageUrl,
|
|
73
|
+
description: cleanDescription || undefined,
|
|
74
|
+
image: ogImage,
|
|
75
|
+
dateModified: page._updatedAt,
|
|
76
|
+
})}
|
|
77
|
+
/>
|
|
78
|
+
{hasBreadcrumbs ? (
|
|
79
|
+
<JsonLd data={generateBreadcrumbJsonLd(breadcrumbs)} />
|
|
80
|
+
) : null}
|
|
81
|
+
<article className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-14 px-4 py-14 md:px-6 md:py-20">
|
|
82
|
+
<h1>{title}</h1>
|
|
83
|
+
{sections.length ? (
|
|
84
|
+
sections
|
|
85
|
+
) : (
|
|
86
|
+
<section>No page content has been added yet.</section>
|
|
87
|
+
)}
|
|
88
|
+
</article>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation"
|
|
4
|
+
import { useTransition } from "react"
|
|
5
|
+
|
|
6
|
+
export function DraftModeToggle() {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const [isPending, startTransition] = useTransition()
|
|
9
|
+
|
|
10
|
+
const handleDisable = () => {
|
|
11
|
+
startTransition(async () => {
|
|
12
|
+
await fetch("/api/draft-mode/disable")
|
|
13
|
+
router.refresh()
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={handleDisable}
|
|
21
|
+
disabled={isPending}
|
|
22
|
+
className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full border border-white/10 bg-black/80 px-2 py-1 font-medium text-sm text-white shadow-lg backdrop-blur-sm transition-colors hover:bg-black/90 disabled:opacity-50"
|
|
23
|
+
>
|
|
24
|
+
{isPending ? "Disabling..." : "Disable Draft Mode"}
|
|
25
|
+
</button>
|
|
26
|
+
)
|
|
27
|
+
}
|