bsmnt 0.4.0 → 0.4.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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +17 -8
  3. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +19 -7
  4. package/src/templates/next-pagebuilder/app/actions/refresh.ts +5 -0
  5. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +15 -19
  6. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +3 -5
  7. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +11 -10
  8. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +14 -4
  9. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-card.tsx +3 -5
  10. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-filters.tsx +93 -0
  11. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/content-grid.tsx +7 -9
  12. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/content-pagination-nav.tsx +71 -0
  13. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +212 -0
  14. package/src/templates/next-pagebuilder/components/page-builder/components/{post-collection → content-collection}/types.ts +5 -4
  15. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +13 -5
  16. package/src/templates/next-pagebuilder/components/page-document/index.tsx +9 -4
  17. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +2 -1
  18. package/src/templates/next-pagebuilder/lib/integrations/sanity/constants.ts +1 -0
  19. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +17 -18
  20. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +29 -2
  21. package/src/templates/next-pagebuilder/lib/integrations/sanity/presentation.ts +118 -0
  22. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +144 -31
  23. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +5 -100
  24. package/src/templates/next-pagebuilder/next.config.ts +3 -0
  25. package/src/templates/next-pagebuilder/package.json +1 -2
  26. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +0 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bsmnt",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "packageManager": "bun@1.2.20",
5
5
  "description": "CLI to scaffold basement projects and add integrations",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  import { notFound } from "next/navigation"
2
2
  import { client } from "@/lib/integrations/sanity/client"
3
- import { sanityFetch } from "@/lib/integrations/sanity/live"
3
+ import { fetchSanity } from "@/lib/integrations/sanity/live"
4
4
  import {
5
5
  ALL_PAGE_SLUGS_QUERY,
6
6
  PAGE_QUERY,
@@ -9,12 +9,12 @@ import type {
9
9
  ALL_PAGE_SLUGS_QUERY_RESULT,
10
10
  PAGE_QUERY_RESULT,
11
11
  } from "@/lib/integrations/sanity/sanity.types"
12
+ import { PageDocument } from "@/components/page-document"
12
13
  import { generateSanityMetadata } from "@/lib/utils/metadata"
13
14
  import { getSlugTag } from "@/lib/utils/slug-tag"
14
- import { PageDocument } from "@/components/page-document"
15
15
 
16
16
  const getPageDocument = async (slug: string | null, stega = true) =>
17
- sanityFetch({
17
+ fetchSanity<PAGE_QUERY_RESULT>({
18
18
  query: PAGE_QUERY,
19
19
  params: { slug },
20
20
  tags: [getSlugTag(slug)],
@@ -32,15 +32,19 @@ const getResolvedPage = async (
32
32
  return (page as NonNullable<PAGE_QUERY_RESULT> | null) ?? null
33
33
  }
34
34
 
35
+ type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
36
+
35
37
  export const generateStaticParams = async () => {
36
- if (!client) return []
38
+ if (!client) return [{ slug: [] }]
37
39
 
38
40
  const slugs =
39
41
  await client.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY)
40
42
 
43
+ if (!slugs?.length) return [{ slug: [] }]
44
+
41
45
  return (slugs ?? []).flatMap((page) => {
42
46
  if (!page.slug) return []
43
- return { slug: page.slug.split("/") }
47
+ return page.slug === "/" ? { slug: [] } : { slug: page.slug.split("/") }
44
48
  })
45
49
  }
46
50
 
@@ -56,13 +60,18 @@ export const generateMetadata = async ({ params }: Props) => {
56
60
  : {}
57
61
  }
58
62
 
59
- export default async function CatchAllPage({ params }: Props) {
63
+ export default async function CatchAllPage({
64
+ params,
65
+ searchParams,
66
+ }: Props & {
67
+ searchParams: SearchParams
68
+ }) {
60
69
  const { slug } = await params
61
70
 
62
71
  const path = toPath(slug)
63
72
  const page = await getResolvedPage(path)
64
73
 
65
- if (!page) return notFound()
74
+ if (!page) notFound()
66
75
 
67
- return <PageDocument page={page} path={path} />
76
+ return <PageDocument page={page} path={path} searchParams={searchParams} />
68
77
  }
@@ -1,13 +1,25 @@
1
1
  import { JsonLd } from "@/components/layout/json-ld"
2
2
  import { Wrapper } from "@/components/layout/wrapper"
3
3
  import { SanityVisualEditing } from "@/components/sanity/visual-editing"
4
+ import { getSiteLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
4
5
 
5
- const Layout = async ({ children }: { children: React.ReactNode }) => (
6
- <>
7
- <JsonLd />
8
- <Wrapper>{children}</Wrapper>
9
- <SanityVisualEditing />
10
- </>
11
- )
6
+
7
+ const Layout = async ({ children }: { children: React.ReactNode }) => {
8
+ const layoutData = await getSiteLayoutData()
9
+
10
+ return (
11
+ <>
12
+ <JsonLd navbar={layoutData.navbar} companyData={layoutData.companyData} />
13
+ <Wrapper
14
+ navbar={layoutData.navbar}
15
+ footer={layoutData.footer}
16
+ companyData={layoutData.companyData}
17
+ >
18
+ {children}
19
+ </Wrapper>
20
+ <SanityVisualEditing />
21
+ </>
22
+ )
23
+ }
12
24
 
13
25
  export default Layout
@@ -0,0 +1,5 @@
1
+ "use client"
2
+
3
+ export async function refreshAction(): Promise<"refresh"> {
4
+ return "refresh"
5
+ }
@@ -3,30 +3,19 @@ import { SanityImage } from "@/components/ui/sanity-image"
3
3
  import {
4
4
  type CompanyData,
5
5
  type FooterData,
6
- getCompanyData,
7
- getFooter,
8
6
  } from "@/lib/integrations/sanity/fetchers/layout"
9
7
 
10
8
  type FooterLinkGroup = NonNullable<FooterData["links"]>[number]
11
9
  type FooterLinkItem = NonNullable<FooterLinkGroup["items"]>[number]
12
10
  type SocialLink = NonNullable<CompanyData["socialLinks"]>[number]
13
11
 
14
- export const Footer = async () => {
15
- const [footer, companyData] = await Promise.all([
16
- getFooter(),
17
- getCompanyData(),
18
- ])
19
-
20
- const year = new Date().getFullYear()
21
-
22
- if (!(footer || companyData)) {
23
- return (
24
- <footer className="border-black/10 border-t px-6 py-3">
25
- <p>&copy; {year} Basement</p>
26
- </footer>
27
- )
28
- }
29
-
12
+ export const Footer = ({
13
+ footer,
14
+ companyData,
15
+ }: {
16
+ footer: FooterData | null
17
+ companyData: CompanyData | null
18
+ }) => {
30
19
  const links = footer?.links ?? []
31
20
 
32
21
  return (
@@ -58,7 +47,9 @@ export const Footer = async () => {
58
47
  )}
59
48
 
60
49
  <div className="flex flex-row items-center justify-between gap-3 border-black/10 border-t pt-4">
61
- <p className="w-fit">&copy; {year} Basement</p>
50
+ <p className="w-fit">
51
+ &copy; <CurrentYear /> Basement
52
+ </p>
62
53
 
63
54
  {companyData?.socialLinks && companyData.socialLinks.length > 0 ? (
64
55
  <div className="flex w-fit flex-wrap items-center gap-x-4 gap-y-2">
@@ -93,3 +84,8 @@ export const Footer = async () => {
93
84
  </footer>
94
85
  )
95
86
  }
87
+
88
+
89
+ const CurrentYear = () => {
90
+ return <span>{new Date().getFullYear()}</span>
91
+ }
@@ -1,8 +1,6 @@
1
- import { getNavbar } from "@/lib/integrations/sanity/fetchers/layout"
2
-
3
1
  import { HeaderClient } from "./header-client"
2
+ import type { NavbarData } from "@/lib/integrations/sanity/fetchers/layout"
4
3
 
5
- export const Header = async () => {
6
- const navbar = await getNavbar()
7
- return <HeaderClient data={navbar} />
4
+ export const Header = ({ data }: { data: NavbarData | null }) => {
5
+ return <HeaderClient data={data} />
8
6
  }
@@ -1,7 +1,7 @@
1
1
  import { stegaClean } from "next-sanity"
2
- import {
3
- getCompanyData,
4
- getNavbar,
2
+ import type {
3
+ CompanyData,
4
+ NavbarData,
5
5
  } from "@/lib/integrations/sanity/fetchers/layout"
6
6
  import {
7
7
  generateOrganizationJsonLd,
@@ -12,17 +12,18 @@ import {
12
12
  const APP_DESCRIPTION =
13
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
14
 
15
- export const JsonLd = async () => {
16
- const [navbarData, companyData] = await Promise.all([
17
- getNavbar(),
18
- getCompanyData(),
19
- ])
20
-
15
+ export const JsonLd = ({
16
+ navbar,
17
+ companyData,
18
+ }: {
19
+ navbar: NavbarData | null
20
+ companyData: CompanyData | null
21
+ }) => {
21
22
  const sameAs = companyData?.socialLinks
22
23
  ?.map((link) => stegaClean(link.url))
23
24
  .filter((url): url is string => Boolean(url))
24
25
 
25
- const logoUrl = navbarData?.logo?.asset?.url
26
+ const logoUrl = navbar?.logo?.asset?.url
26
27
 
27
28
  return (
28
29
  <>
@@ -1,9 +1,12 @@
1
1
  import Link from "next/link"
2
2
  import { Footer } from "@/components/layout/footer"
3
3
  import { Header } from "@/components/layout/header"
4
+ import type { SiteLayoutData } from "@/lib/integrations/sanity/fetchers/layout"
4
5
  import { cn } from "@/lib/styles/cn"
5
6
 
6
- interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {}
7
+ interface WrapperProps
8
+ extends React.HTMLAttributes<HTMLDivElement>,
9
+ SiteLayoutData {}
7
10
 
8
11
  const SkipToMainContent = () => (
9
12
  <Link
@@ -14,10 +17,17 @@ const SkipToMainContent = () => (
14
17
  </Link>
15
18
  )
16
19
 
17
- export const Wrapper = ({ children, className, ...props }: WrapperProps) => (
20
+ export const Wrapper = ({
21
+ children,
22
+ className,
23
+ navbar,
24
+ footer,
25
+ companyData,
26
+ ...props
27
+ }: WrapperProps) => (
18
28
  <>
19
29
  <SkipToMainContent />
20
- <Header />
30
+ <Header data={navbar} />
21
31
  <main
22
32
  id="main-content"
23
33
  className={cn("relative flex grow flex-col", className)}
@@ -25,6 +35,6 @@ export const Wrapper = ({ children, className, ...props }: WrapperProps) => (
25
35
  >
26
36
  {children}
27
37
  </main>
28
- <Footer />
38
+ <Footer footer={footer} companyData={companyData} />
29
39
  </>
30
40
  )
@@ -1,10 +1,10 @@
1
1
  import { Link } from "@/components/ui/link"
2
2
  import { SanityImage } from "@/components/ui/sanity-image"
3
3
  import { formatDate } from "@/lib/utils/format-date"
4
- import type { BlogArticleCard } from "./types"
4
+ import type { ContentItemCard } from "./types"
5
5
 
6
6
  type ContentCardProps = {
7
- article: BlogArticleCard
7
+ article: ContentItemCard
8
8
  className?: string | undefined
9
9
  }
10
10
 
@@ -46,9 +46,7 @@ export const ContentCard = ({ article, className }: ContentCardProps) => {
46
46
  </div>
47
47
  ) : null}
48
48
 
49
- <h3 className="text-balance font-medium text-lg leading-snug">
50
- {title}
51
- </h3>
49
+ <h3 className="text-balance font-medium text-lg leading-snug">{title}</h3>
52
50
 
53
51
  {authorName || formattedDate ? (
54
52
  <div className="flex items-center gap-2 text-current/60 text-sm">
@@ -0,0 +1,93 @@
1
+ "use client"
2
+
3
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
4
+ import { cn } from "@/lib/styles/cn"
5
+ import type { ContentCategory } from "./types"
6
+
7
+ type ContentFiltersProps = {
8
+ categories: ContentCategory[]
9
+ activeCategory: string
10
+ totalCount: number
11
+ filteredCount: number
12
+ }
13
+
14
+ function FilterChip({
15
+ active,
16
+ label,
17
+ onClick,
18
+ }: {
19
+ active: boolean
20
+ label: string
21
+ onClick: () => void
22
+ }) {
23
+ return (
24
+ <button
25
+ type="button"
26
+ onClick={onClick}
27
+ className={cn(
28
+ "rounded-full border px-4 py-2 text-sm transition-colors",
29
+ active
30
+ ? "border-black bg-black text-white"
31
+ : "border-black/10 bg-white hover:border-black/30"
32
+ )}
33
+ >
34
+ {label}
35
+ </button>
36
+ )
37
+ }
38
+
39
+ export function ContentFilters({
40
+ categories,
41
+ activeCategory,
42
+ totalCount,
43
+ filteredCount,
44
+ }: ContentFiltersProps) {
45
+ const pathname = usePathname()
46
+ const router = useRouter()
47
+ const searchParams = useSearchParams()
48
+
49
+ const navigate = (category: string) => {
50
+ const nextParams = new URLSearchParams(searchParams.toString())
51
+
52
+ if (category) nextParams.set("category", category)
53
+ else nextParams.delete("category")
54
+
55
+ nextParams.delete("page")
56
+
57
+ const href = nextParams.toString()
58
+ ? `${pathname}?${nextParams.toString()}`
59
+ : pathname
60
+
61
+ // biome-ignore lint/suspicious/noExplicitAny: typed routes do not accept dynamic query string helpers cleanly
62
+ router.push(href as any, { scroll: false })
63
+ }
64
+
65
+ return (
66
+ <div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
67
+ <div className="flex flex-wrap gap-2">
68
+ <FilterChip
69
+ active={activeCategory === ""}
70
+ label="All content"
71
+ onClick={() => navigate("")}
72
+ />
73
+ {categories.map((category) => {
74
+ const slug = category.slug?.current
75
+ if (!slug) return null
76
+
77
+ return (
78
+ <FilterChip
79
+ key={category._id}
80
+ active={activeCategory === slug}
81
+ label={category.title || "Untitled"}
82
+ onClick={() => navigate(slug)}
83
+ />
84
+ )
85
+ })}
86
+ </div>
87
+ <p className="text-current/60 text-sm">
88
+ Showing {filteredCount} {filteredCount === 1 ? "item" : "items"}
89
+ {filteredCount !== totalCount ? ` out of ${totalCount}` : ""}
90
+ </p>
91
+ </div>
92
+ )
93
+ }
@@ -1,20 +1,20 @@
1
- import type { BlogArticleCard } from "./types"
2
1
  import { ContentCard } from "./content-card"
2
+ import type { ContentItemCard } from "./types"
3
3
 
4
4
  type ContentGridProps = {
5
- articles: BlogArticleCard[]
5
+ items: ContentItemCard[]
6
6
  start?: number
7
7
  end?: number
8
8
  className?: string
9
9
  }
10
10
 
11
11
  export function ContentGrid({
12
- articles,
12
+ items,
13
13
  start = 0,
14
14
  end,
15
15
  className,
16
16
  }: ContentGridProps) {
17
- const slice = articles.slice(start, end)
17
+ const slice = items.slice(start, end)
18
18
 
19
19
  if (slice.length === 0) {
20
20
  if (start === 0) {
@@ -29,13 +29,11 @@ export function ContentGrid({
29
29
  className ?? "grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3"
30
30
  }
31
31
  >
32
- {slice.map((article) => {
33
- const slug = article.resolvedSlug
32
+ {slice.map((item) => {
33
+ const slug = item.resolvedSlug
34
34
  if (!slug) return null
35
35
 
36
- return (
37
- <ContentCard key={article._id} article={article} />
38
- )
36
+ return <ContentCard key={item._id} article={item} />
39
37
  })}
40
38
  </ul>
41
39
  )
@@ -0,0 +1,71 @@
1
+ "use client"
2
+
3
+ import { usePathname, useSearchParams } from "next/navigation"
4
+ import { Link } from "@/components/ui/link"
5
+
6
+ type ContentPaginationNavProps = {
7
+ currentPage: number
8
+ totalPages: number
9
+ }
10
+
11
+ function buildPaginationHref({
12
+ pathname,
13
+ page,
14
+ searchParams,
15
+ }: {
16
+ pathname: string
17
+ page: number
18
+ searchParams: ReturnType<typeof useSearchParams>
19
+ }) {
20
+ const nextParams = new URLSearchParams(searchParams.toString())
21
+
22
+ if (page > 1) nextParams.set("page", String(page))
23
+ else nextParams.delete("page")
24
+
25
+ const query = nextParams.toString()
26
+ return query ? `${pathname}?${query}` : pathname
27
+ }
28
+
29
+ export function ContentPaginationNav({
30
+ currentPage,
31
+ totalPages,
32
+ }: ContentPaginationNavProps) {
33
+ const pathname = usePathname()
34
+ const searchParams = useSearchParams()
35
+
36
+ if (!pathname || totalPages <= 1) return null
37
+
38
+ return (
39
+ <nav className="flex flex-wrap items-center gap-2" aria-label="Pagination">
40
+ {currentPage > 1 ? (
41
+ <Link
42
+ href={buildPaginationHref({
43
+ pathname,
44
+ page: currentPage - 1,
45
+ searchParams,
46
+ })}
47
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
48
+ >
49
+ Previous
50
+ </Link>
51
+ ) : null}
52
+
53
+ <span className="text-current/60 text-sm">
54
+ Page {currentPage} of {totalPages}
55
+ </span>
56
+
57
+ {currentPage < totalPages ? (
58
+ <Link
59
+ href={buildPaginationHref({
60
+ pathname,
61
+ page: currentPage + 1,
62
+ searchParams,
63
+ })}
64
+ className="rounded-full border border-black/10 px-4 py-2 text-sm transition-colors hover:border-black/30"
65
+ >
66
+ Next
67
+ </Link>
68
+ ) : null}
69
+ </nav>
70
+ )
71
+ }