bsmnt 0.7.1 → 0.10.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/application/add-hooks/index.d.ts.map +1 -1
- package/dist/application/add-hooks/index.js +1 -0
- package/dist/application/add-hooks/index.js.map +1 -1
- package/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +20 -2
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/core/create/execute-plan.d.ts +2 -0
- package/dist/core/create/execute-plan.d.ts.map +1 -1
- package/dist/core/create/execute-plan.js +3 -0
- package/dist/core/create/execute-plan.js.map +1 -1
- package/dist/domain/version.d.ts +14 -0
- package/dist/domain/version.d.ts.map +1 -0
- package/dist/domain/version.js +81 -0
- package/dist/domain/version.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/executor.d.ts.map +1 -1
- package/dist/infrastructure/create/executor.js +2 -0
- package/dist/infrastructure/create/executor.js.map +1 -1
- package/dist/infrastructure/create/render-generated-docs.d.ts +18 -0
- package/dist/infrastructure/create/render-generated-docs.d.ts.map +1 -0
- package/dist/infrastructure/create/render-generated-docs.js +72 -0
- package/dist/infrastructure/create/render-generated-docs.js.map +1 -0
- package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-dotenvx.js +2 -1
- package/dist/infrastructure/create/setup-dotenvx.js.map +1 -1
- package/dist/infrastructure/create/setup-remote.d.ts +6 -1
- package/dist/infrastructure/create/setup-remote.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-remote.js +7 -2
- package/dist/infrastructure/create/setup-remote.js.map +1 -1
- package/dist/infrastructure/integrations/ensure-studio-headers.d.ts +6 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.d.ts.map +1 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.js +153 -0
- package/dist/infrastructure/integrations/ensure-studio-headers.js.map +1 -0
- package/dist/infrastructure/update/cache.d.ts +14 -0
- package/dist/infrastructure/update/cache.d.ts.map +1 -0
- package/dist/infrastructure/update/cache.js +41 -0
- package/dist/infrastructure/update/cache.js.map +1 -0
- package/dist/infrastructure/update/check-version.d.ts +2 -0
- package/dist/infrastructure/update/check-version.d.ts.map +1 -0
- package/dist/infrastructure/update/check-version.js +33 -0
- package/dist/infrastructure/update/check-version.js.map +1 -0
- package/dist/infrastructure/update/notifier.d.ts +8 -0
- package/dist/infrastructure/update/notifier.d.ts.map +1 -0
- package/dist/infrastructure/update/notifier.js +141 -0
- package/dist/infrastructure/update/notifier.js.map +1 -0
- package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity/config.js +2 -1
- package/dist/modules/features/cms/sanity/config.js.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js +4 -1
- package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/features/cms/sanity/files/app/api/blog/[slug]/route.ts +4 -1
- package/src/modules/features/cms/sanity/files/app/sitemap.md/route.ts +2 -1
- package/src/modules/features/cms/sanity/files/components/ui/sanity-image/index.tsx +19 -5
- package/src/modules/features/cms/sanity/files/lib/integrations/sanity/README.md +31 -0
- package/src/modules/features/cms/sanity/files/proxy.ts +9 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +8 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +7 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +15 -5
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +81 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +13 -7
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-notices.tsx +135 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +1 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +72 -21
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +1 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +178 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +9 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +35 -4
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +54 -1
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +11 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +4 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +20 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +27 -5
- package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +9 -0
- package/src/modules/features/env/dotenvx/files/setup-remote.ts +3 -1
- package/src/templates/next-default/.env.example +10 -0
- package/src/templates/next-default/.github/workflows/ci.yml +7 -0
- package/src/templates/next-default/README.md +14 -0
- package/src/templates/next-default/app/robots.ts +2 -3
- package/src/templates/next-default/knip.json +8 -6
- package/src/templates/next-default/lefthook.yml +0 -5
- package/src/templates/next-default/next.config.ts +36 -2
- package/src/templates/next-default/package.json +9 -9
- package/src/templates/next-experiments/.env.example +10 -0
- package/src/templates/next-experiments/.github/workflows/ci.yml +7 -0
- package/src/templates/next-experiments/README.md +14 -0
- package/src/templates/next-experiments/app/robots.ts +2 -3
- package/src/templates/next-experiments/knip.json +8 -6
- package/src/templates/next-experiments/lefthook.yml +0 -5
- package/src/templates/next-experiments/next.config.ts +36 -2
- package/src/templates/next-experiments/package.json +9 -9
- package/src/templates/next-pagebuilder/.env.example +10 -0
- package/src/templates/next-pagebuilder/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/src/templates/next-pagebuilder/.github/workflows/ci.yml +15 -0
- package/src/templates/next-pagebuilder/README.md +45 -0
- package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +26 -11
- package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +39 -0
- package/src/templates/next-pagebuilder/app/robots.ts +2 -3
- package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +6 -3
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/skeleton.tsx +53 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +169 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +10 -1
- package/src/templates/next-pagebuilder/knip.json +8 -6
- package/src/templates/next-pagebuilder/lefthook.yml +0 -5
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +81 -0
- package/src/templates/next-pagebuilder/next.config.ts +33 -2
- package/src/templates/next-pagebuilder/package.json +4 -4
- package/src/templates/next-webgl/.env.example +10 -0
- package/src/templates/next-webgl/.github/workflows/ci.yml +7 -0
- package/src/templates/next-webgl/README.md +14 -0
- package/src/templates/next-webgl/app/robots.ts +2 -3
- package/src/templates/next-webgl/knip.json +8 -6
- package/src/templates/next-webgl/lefthook.yml +0 -5
- package/src/templates/next-webgl/next.config.ts +36 -2
- package/src/templates/next-webgl/package.json +9 -9
|
@@ -14,7 +14,7 @@ import { checkRedirect } from "@/lib/utils/check-redirect"
|
|
|
14
14
|
import { generateSanityMetadata } from "@/lib/utils/metadata"
|
|
15
15
|
import { getSlugTag } from "@/lib/utils/slug-tag"
|
|
16
16
|
|
|
17
|
-
const getPageDocument = async (slug: string | null, stega
|
|
17
|
+
const getPageDocument = async (slug: string | null, stega?: boolean) =>
|
|
18
18
|
fetchSanity<PAGE_QUERY_RESULT>({
|
|
19
19
|
query: PAGE_QUERY,
|
|
20
20
|
params: { slug },
|
|
@@ -22,12 +22,18 @@ const getPageDocument = async (slug: string | null, stega = true) =>
|
|
|
22
22
|
stega,
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const isValidSegment = (segment: string) =>
|
|
26
|
+
segment.length > 0 && segment !== "." && segment !== ".."
|
|
27
|
+
|
|
28
|
+
const toPath = (segments?: string[]) => {
|
|
29
|
+
if (!segments?.length) return null
|
|
30
|
+
if (!segments.every(isValidSegment)) return null
|
|
31
|
+
return segments.join("/")
|
|
32
|
+
}
|
|
27
33
|
|
|
28
34
|
const getResolvedPage = async (
|
|
29
35
|
path: string | null,
|
|
30
|
-
stega
|
|
36
|
+
stega?: boolean
|
|
31
37
|
): Promise<NonNullable<PAGE_QUERY_RESULT> | null> => {
|
|
32
38
|
const { data: page } = await getPageDocument(path, stega)
|
|
33
39
|
return (page as NonNullable<PAGE_QUERY_RESULT> | null) ?? null
|
|
@@ -35,18 +41,21 @@ const getResolvedPage = async (
|
|
|
35
41
|
|
|
36
42
|
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
|
|
37
43
|
|
|
44
|
+
const HOMEPAGE_PARAM: { slug: string[] } = { slug: [] }
|
|
45
|
+
|
|
38
46
|
export const generateStaticParams = async () => {
|
|
39
|
-
if (!client) return [
|
|
47
|
+
if (!client) return [HOMEPAGE_PARAM]
|
|
40
48
|
|
|
41
49
|
const slugs =
|
|
42
50
|
await client.fetch<ALL_PAGE_SLUGS_QUERY_RESULT>(ALL_PAGE_SLUGS_QUERY)
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
return [
|
|
53
|
+
HOMEPAGE_PARAM,
|
|
54
|
+
...(slugs ?? []).flatMap((page) => {
|
|
55
|
+
if (!page.slug || page.slug === "/") return []
|
|
56
|
+
return { slug: page.slug.split("/") }
|
|
57
|
+
}),
|
|
58
|
+
]
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
type Props = { params: Promise<{ slug?: string[] }> }
|
|
@@ -54,6 +63,9 @@ type Props = { params: Promise<{ slug?: string[] }> }
|
|
|
54
63
|
export const generateMetadata = async ({ params }: Props) => {
|
|
55
64
|
const { slug } = await params
|
|
56
65
|
const path = toPath(slug)
|
|
66
|
+
|
|
67
|
+
if (slug?.length && path === null) return {}
|
|
68
|
+
|
|
57
69
|
const page = await getResolvedPage(path, false)
|
|
58
70
|
|
|
59
71
|
return page
|
|
@@ -70,6 +82,9 @@ export default async function CatchAllPage({
|
|
|
70
82
|
const { slug } = await params
|
|
71
83
|
|
|
72
84
|
const path = toPath(slug)
|
|
85
|
+
|
|
86
|
+
if (slug?.length && path === null) notFound()
|
|
87
|
+
|
|
73
88
|
const page = await getResolvedPage(path)
|
|
74
89
|
|
|
75
90
|
if (!page) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { fetchSanity } from "@/lib/integrations/sanity/live"
|
|
4
|
+
import { GATED_CONTENT_QUERY } from "@/lib/integrations/sanity/queries"
|
|
5
|
+
import type { GATED_CONTENT_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types"
|
|
6
|
+
|
|
7
|
+
export type GatedContentPayload = NonNullable<GATED_CONTENT_QUERY_RESULT>
|
|
8
|
+
|
|
9
|
+
export type UnlockResult =
|
|
10
|
+
| { ok: true; payload: GatedContentPayload }
|
|
11
|
+
| { ok: false; error: string }
|
|
12
|
+
|
|
13
|
+
type UnlockInput = { documentId: string; email: string }
|
|
14
|
+
|
|
15
|
+
export async function unlockGatedContent({
|
|
16
|
+
documentId,
|
|
17
|
+
email,
|
|
18
|
+
}: UnlockInput): Promise<UnlockResult> {
|
|
19
|
+
if (!documentId) return { ok: false, error: "Missing content id." }
|
|
20
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
21
|
+
return { ok: false, error: "Enter a valid email address." }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Lead-capture seam: forward `email` to your CRM / email provider here.
|
|
25
|
+
// This is not access control — the payload is withheld from the page until
|
|
26
|
+
// this action runs, but anyone who submits the form receives it.
|
|
27
|
+
|
|
28
|
+
const { data } = await fetchSanity<GATED_CONTENT_QUERY_RESULT>({
|
|
29
|
+
query: GATED_CONTENT_QUERY,
|
|
30
|
+
params: { id: documentId },
|
|
31
|
+
// stega must stay off: it injects invisible characters into strings, which
|
|
32
|
+
// would corrupt the asset URLs returned for download/playback.
|
|
33
|
+
stega: false,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!data) return { ok: false, error: "Content not found." }
|
|
37
|
+
|
|
38
|
+
return { ok: true, payload: data }
|
|
39
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next"
|
|
2
|
-
|
|
3
|
-
const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
|
|
2
|
+
import { getBaseUrl } from "@/lib/utils/base-url"
|
|
4
3
|
|
|
5
4
|
export default function robots(): MetadataRoute.Robots {
|
|
6
5
|
return {
|
|
@@ -9,6 +8,6 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
9
8
|
allow: "/",
|
|
10
9
|
disallow: [],
|
|
11
10
|
},
|
|
12
|
-
sitemap: `${
|
|
11
|
+
sitemap: `${getBaseUrl()}/sitemap.xml`,
|
|
13
12
|
}
|
|
14
13
|
}
|
|
@@ -51,8 +51,11 @@ const renderSitemap = (entries: SitemapEntry[]) =>
|
|
|
51
51
|
"</urlset>",
|
|
52
52
|
].join("\n")
|
|
53
53
|
|
|
54
|
-
const getSitemapResponse = (entries: SitemapEntry[]) =>
|
|
55
|
-
new NextResponse(renderSitemap(entries)
|
|
54
|
+
const getSitemapResponse = (entries: SitemapEntry[], status = 200) =>
|
|
55
|
+
new NextResponse(renderSitemap(entries), {
|
|
56
|
+
status,
|
|
57
|
+
headers: { "Content-Type": "application/xml; charset=utf-8" },
|
|
58
|
+
})
|
|
56
59
|
|
|
57
60
|
export const GET = async () => {
|
|
58
61
|
const baseUrl = getSitemapBaseUrl()
|
|
@@ -75,6 +78,6 @@ export const GET = async () => {
|
|
|
75
78
|
)
|
|
76
79
|
} catch (error) {
|
|
77
80
|
console.error("Error generating sitemap.xml:", error)
|
|
78
|
-
return getSitemapResponse([homepageEntry])
|
|
81
|
+
return getSitemapResponse([homepageEntry], 503)
|
|
79
82
|
}
|
|
80
83
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const FEATURED_PLACEHOLDERS = ["featured-1", "featured-2"]
|
|
2
|
+
const CARD_PLACEHOLDERS = [
|
|
3
|
+
"card-1",
|
|
4
|
+
"card-2",
|
|
5
|
+
"card-3",
|
|
6
|
+
"card-4",
|
|
7
|
+
"card-5",
|
|
8
|
+
"card-6",
|
|
9
|
+
]
|
|
10
|
+
const CHIP_PLACEHOLDERS = ["chip-1", "chip-2", "chip-3", "chip-4"]
|
|
11
|
+
|
|
12
|
+
function CardPlaceholder() {
|
|
13
|
+
return (
|
|
14
|
+
<li className="flex flex-col gap-3">
|
|
15
|
+
<div className="aspect-16/9 w-full rounded-lg bg-black/10" />
|
|
16
|
+
<div className="flex flex-col gap-2">
|
|
17
|
+
<div className="h-3 w-16 rounded bg-black/10" />
|
|
18
|
+
<div className="h-5 w-4/5 rounded bg-black/10" />
|
|
19
|
+
<div className="h-4 w-1/2 rounded bg-black/10" />
|
|
20
|
+
</div>
|
|
21
|
+
</li>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ContentCollectionSkeleton() {
|
|
26
|
+
return (
|
|
27
|
+
<section
|
|
28
|
+
className="flex animate-pulse flex-col gap-8 py-8"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
>
|
|
31
|
+
<div className="flex flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-4 sm:p-5">
|
|
32
|
+
<div className="flex flex-wrap gap-2">
|
|
33
|
+
{CHIP_PLACEHOLDERS.map((key) => (
|
|
34
|
+
<div className="h-9 w-28 rounded-full bg-black/10" key={key} />
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
<div className="h-4 w-40 rounded bg-black/10" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<ul className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
|
41
|
+
{FEATURED_PLACEHOLDERS.map((key) => (
|
|
42
|
+
<CardPlaceholder key={key} />
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
|
|
46
|
+
<ul className="grid grid-cols-2 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
47
|
+
{CARD_PLACEHOLDERS.map((key) => (
|
|
48
|
+
<CardPlaceholder key={key} />
|
|
49
|
+
))}
|
|
50
|
+
</ul>
|
|
51
|
+
</section>
|
|
52
|
+
)
|
|
53
|
+
}
|
package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
type GatedContentPayload,
|
|
6
|
+
unlockGatedContent,
|
|
7
|
+
} from "@/app/actions/unlock-gated-content"
|
|
8
|
+
import type { PageBuilderBlock } from "@/components/page-builder/types"
|
|
9
|
+
import { RichText } from "@/components/sanity/rich-text"
|
|
10
|
+
import { Link } from "@/components/ui/link"
|
|
11
|
+
|
|
12
|
+
type GatedContentBlock = Extract<PageBuilderBlock, { _type: "gatedContent" }>
|
|
13
|
+
|
|
14
|
+
type GatedContentProps = {
|
|
15
|
+
block: GatedContentBlock
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GateState = "locked" | "loading" | "unlocked"
|
|
19
|
+
|
|
20
|
+
const STORAGE_PREFIX = "gated-content:"
|
|
21
|
+
|
|
22
|
+
export function GatedContent({ block }: GatedContentProps) {
|
|
23
|
+
const storageKey = `${STORAGE_PREFIX}${block._id}`
|
|
24
|
+
const [state, setState] = useState<GateState>("locked")
|
|
25
|
+
const [payload, setPayload] = useState<GatedContentPayload | null>(null)
|
|
26
|
+
const [error, setError] = useState<string | null>(null)
|
|
27
|
+
const [justUnlocked, setJustUnlocked] = useState(false)
|
|
28
|
+
|
|
29
|
+
// The payload is withheld from the page query, so it never reaches the client
|
|
30
|
+
// until this action runs. We remember the submitted email so a returning
|
|
31
|
+
// visitor re-fetches without seeing the form again.
|
|
32
|
+
const unlock = async (email: string, persist: boolean) => {
|
|
33
|
+
setState("loading")
|
|
34
|
+
setError(null)
|
|
35
|
+
|
|
36
|
+
const result = await unlockGatedContent({ documentId: block._id, email })
|
|
37
|
+
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
setError(result.error)
|
|
40
|
+
setState("locked")
|
|
41
|
+
if (!persist) window.localStorage.removeItem(storageKey)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setPayload(result.payload)
|
|
46
|
+
setState("unlocked")
|
|
47
|
+
if (persist) {
|
|
48
|
+
window.localStorage.setItem(storageKey, email)
|
|
49
|
+
setJustUnlocked(true)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-fetch only when the block changes; unlock closes over stable values
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const savedEmail = window.localStorage.getItem(storageKey)
|
|
56
|
+
if (savedEmail) void unlock(savedEmail, false)
|
|
57
|
+
}, [storageKey])
|
|
58
|
+
|
|
59
|
+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
const email =
|
|
62
|
+
new FormData(event.currentTarget).get("email")?.toString() ?? ""
|
|
63
|
+
void unlock(email, true)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<section className="py-8 md:py-12">
|
|
68
|
+
{block.teaserHeading ? (
|
|
69
|
+
<h2 className="mb-4 font-semibold text-2xl tracking-tight md:text-4xl">
|
|
70
|
+
{block.teaserHeading}
|
|
71
|
+
</h2>
|
|
72
|
+
) : null}
|
|
73
|
+
|
|
74
|
+
{block.teaserBody ? (
|
|
75
|
+
<div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
|
|
76
|
+
<RichText content={block.teaserBody} />
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
|
|
80
|
+
{state === "unlocked" && payload ? (
|
|
81
|
+
<div className="mt-8">
|
|
82
|
+
{justUnlocked && block.successMessage ? (
|
|
83
|
+
<p className="mb-6 rounded-lg bg-black/[0.04] px-4 py-3 text-current/72 text-sm">
|
|
84
|
+
{block.successMessage}
|
|
85
|
+
</p>
|
|
86
|
+
) : null}
|
|
87
|
+
<GatedReveal payload={payload} />
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<form
|
|
91
|
+
onSubmit={handleSubmit}
|
|
92
|
+
className="mt-8 flex max-w-md flex-col gap-4 rounded-[1.5rem] border border-black/10 bg-black/[0.02] p-6"
|
|
93
|
+
>
|
|
94
|
+
{block.gateHeadline ? (
|
|
95
|
+
<h3 className="font-semibold text-lg">{block.gateHeadline}</h3>
|
|
96
|
+
) : null}
|
|
97
|
+
{block.gateDescription ? (
|
|
98
|
+
<p className="text-current/72 text-sm">{block.gateDescription}</p>
|
|
99
|
+
) : null}
|
|
100
|
+
<input
|
|
101
|
+
type="email"
|
|
102
|
+
name="email"
|
|
103
|
+
required
|
|
104
|
+
placeholder="you@example.com"
|
|
105
|
+
className="rounded-lg border border-black/10 bg-white px-4 py-2.5 text-sm outline-none focus:border-black/40"
|
|
106
|
+
/>
|
|
107
|
+
{error ? <p className="text-red-600 text-sm">{error}</p> : null}
|
|
108
|
+
<button
|
|
109
|
+
type="submit"
|
|
110
|
+
disabled={state === "loading"}
|
|
111
|
+
className="rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80 disabled:opacity-60"
|
|
112
|
+
>
|
|
113
|
+
{state === "loading" ? "Unlocking…" : block.submitLabel || "Unlock"}
|
|
114
|
+
</button>
|
|
115
|
+
</form>
|
|
116
|
+
)}
|
|
117
|
+
</section>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function GatedReveal({ payload }: { payload: GatedContentPayload }) {
|
|
122
|
+
if (payload.kind === "richText" && payload.content) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="max-w-[62ch] space-y-4 text-base/7 text-current/80 md:text-lg/8">
|
|
125
|
+
<RichText content={payload.content} />
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (payload.kind === "file" && payload.file?.url) {
|
|
131
|
+
return (
|
|
132
|
+
<Link
|
|
133
|
+
href={payload.file.url}
|
|
134
|
+
download
|
|
135
|
+
className="inline-flex items-center gap-2 rounded-lg bg-black px-4 py-2.5 font-medium text-sm text-white transition-colors hover:bg-black/80"
|
|
136
|
+
>
|
|
137
|
+
Download {payload.file.originalFilename ?? "file"}
|
|
138
|
+
</Link>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (payload.kind === "video") {
|
|
143
|
+
if (payload.video.url) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="aspect-video w-full max-w-3xl overflow-hidden rounded-lg">
|
|
146
|
+
<iframe
|
|
147
|
+
src={payload.video.url}
|
|
148
|
+
title="Gated video"
|
|
149
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
150
|
+
allowFullScreen
|
|
151
|
+
className="h-full w-full border-0"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
if (payload.video.file?.url) {
|
|
157
|
+
return (
|
|
158
|
+
// biome-ignore lint/a11y/useMediaCaption: editor-supplied video without a caption track
|
|
159
|
+
<video
|
|
160
|
+
controls
|
|
161
|
+
src={payload.video.file.url}
|
|
162
|
+
className="w-full max-w-3xl rounded-lg"
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { Suspense } from "react"
|
|
1
2
|
import { ArticleContent } from "./components/article-content"
|
|
2
3
|
import { ContentCollection } from "./components/content-collection"
|
|
4
|
+
import { ContentCollectionSkeleton } from "./components/content-collection/skeleton"
|
|
3
5
|
import type { ContentCollectionSearchParams } from "./components/content-collection/types"
|
|
4
6
|
import { Description } from "./components/description"
|
|
7
|
+
import { GatedContent } from "./components/gated-content"
|
|
5
8
|
import { Hero } from "./components/hero"
|
|
6
9
|
import type { PageBuilderBlock, PageBuilderBlocks } from "./types"
|
|
7
10
|
|
|
@@ -35,13 +38,19 @@ function PageBuilderNode({
|
|
|
35
38
|
}) {
|
|
36
39
|
switch (block._type) {
|
|
37
40
|
case "blogCollection":
|
|
38
|
-
return
|
|
41
|
+
return (
|
|
42
|
+
<Suspense fallback={<ContentCollectionSkeleton />}>
|
|
43
|
+
<ContentCollection block={block} searchParams={searchParams} />
|
|
44
|
+
</Suspense>
|
|
45
|
+
)
|
|
39
46
|
case "blogContent":
|
|
40
47
|
return <ArticleContent block={block} />
|
|
41
48
|
case "hero":
|
|
42
49
|
return <Hero block={block} />
|
|
43
50
|
case "description":
|
|
44
51
|
return <Description block={block} />
|
|
52
|
+
case "gatedContent":
|
|
53
|
+
return <GatedContent block={block} />
|
|
45
54
|
default:
|
|
46
55
|
return null
|
|
47
56
|
}
|
|
@@ -22,11 +22,13 @@
|
|
|
22
22
|
"rxjs",
|
|
23
23
|
"gsap",
|
|
24
24
|
"@gsap/react",
|
|
25
|
-
"motion"
|
|
25
|
+
"motion",
|
|
26
|
+
"detect-gpu",
|
|
27
|
+
"lodash-es",
|
|
28
|
+
"@types/lodash-es",
|
|
29
|
+
"react-device-detect",
|
|
30
|
+
"zustand"
|
|
26
31
|
],
|
|
27
|
-
"ignoreBinaries": [
|
|
28
|
-
|
|
29
|
-
"op",
|
|
30
|
-
"vercel"
|
|
31
|
-
]
|
|
32
|
+
"ignoreBinaries": ["gh", "op", "vercel"],
|
|
33
|
+
"ignoreFiles": [".agents/**", ".claude/**", "hooks/**"]
|
|
32
34
|
}
|
|
@@ -50,6 +50,13 @@ export type SanityImageAssetReference = {
|
|
|
50
50
|
[internalGroqTypeReferenceTo]?: "sanity.imageAsset"
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export type SanityFileAssetReference = {
|
|
54
|
+
_ref: string
|
|
55
|
+
_type: "reference"
|
|
56
|
+
_weak?: boolean
|
|
57
|
+
[internalGroqTypeReferenceTo]?: "sanity.fileAsset"
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
export type StructuredData = {
|
|
54
61
|
_id: string
|
|
55
62
|
_type: "structuredData"
|
|
@@ -307,6 +314,33 @@ export type BlogCollection = {
|
|
|
307
314
|
subtitle?: string
|
|
308
315
|
}
|
|
309
316
|
|
|
317
|
+
export type GatedContent = {
|
|
318
|
+
_id: string
|
|
319
|
+
_type: "gatedContent"
|
|
320
|
+
_createdAt: string
|
|
321
|
+
_updatedAt: string
|
|
322
|
+
_rev: string
|
|
323
|
+
internalTitle?: string
|
|
324
|
+
teaserHeading?: string
|
|
325
|
+
teaserBody?: RichText
|
|
326
|
+
gateType?: "form"
|
|
327
|
+
gateHeadline?: string
|
|
328
|
+
gateDescription?: string
|
|
329
|
+
submitLabel?: string
|
|
330
|
+
successMessage?: string
|
|
331
|
+
kind?: "richText" | "file" | "video"
|
|
332
|
+
content?: RichText
|
|
333
|
+
file?: {
|
|
334
|
+
asset?: SanityFileAssetReference
|
|
335
|
+
_type: "file"
|
|
336
|
+
}
|
|
337
|
+
videoUrl?: string
|
|
338
|
+
videoFile?: {
|
|
339
|
+
asset?: SanityFileAssetReference
|
|
340
|
+
_type: "file"
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
310
344
|
export type NavItem = {
|
|
311
345
|
_type: "navItem"
|
|
312
346
|
title?: string
|
|
@@ -475,6 +509,13 @@ export type BlogCollectionReference = {
|
|
|
475
509
|
[internalGroqTypeReferenceTo]?: "blogCollection"
|
|
476
510
|
}
|
|
477
511
|
|
|
512
|
+
export type GatedContentReference = {
|
|
513
|
+
_ref: string
|
|
514
|
+
_type: "reference"
|
|
515
|
+
_weak?: boolean
|
|
516
|
+
[internalGroqTypeReferenceTo]?: "gatedContent"
|
|
517
|
+
}
|
|
518
|
+
|
|
478
519
|
export type PageBuilder = Array<
|
|
479
520
|
| ({
|
|
480
521
|
_key: string
|
|
@@ -488,6 +529,9 @@ export type PageBuilder = Array<
|
|
|
488
529
|
| ({
|
|
489
530
|
_key: string
|
|
490
531
|
} & BlogCollectionReference)
|
|
532
|
+
| ({
|
|
533
|
+
_key: string
|
|
534
|
+
} & GatedContentReference)
|
|
491
535
|
>
|
|
492
536
|
|
|
493
537
|
export type Metadata = {
|
|
@@ -699,6 +743,7 @@ export type AllSanitySchemaTypes =
|
|
|
699
743
|
| Slug
|
|
700
744
|
| Redirect
|
|
701
745
|
| SanityImageAssetReference
|
|
746
|
+
| SanityFileAssetReference
|
|
702
747
|
| StructuredData
|
|
703
748
|
| SanityImageCrop
|
|
704
749
|
| SanityImageHotspot
|
|
@@ -715,6 +760,7 @@ export type AllSanitySchemaTypes =
|
|
|
715
760
|
| RichText
|
|
716
761
|
| Author
|
|
717
762
|
| BlogCollection
|
|
763
|
+
| GatedContent
|
|
718
764
|
| NavItem
|
|
719
765
|
| NavMegaMenu
|
|
720
766
|
| NavColumn
|
|
@@ -730,6 +776,7 @@ export type AllSanitySchemaTypes =
|
|
|
730
776
|
| DescriptionReference
|
|
731
777
|
| BlogContentReference
|
|
732
778
|
| BlogCollectionReference
|
|
779
|
+
| GatedContentReference
|
|
733
780
|
| PageBuilder
|
|
734
781
|
| Metadata
|
|
735
782
|
| ExternalLink
|
|
@@ -768,6 +815,20 @@ export type PAGE_QUERY_RESULT = {
|
|
|
768
815
|
_rev: string
|
|
769
816
|
subtitle?: string
|
|
770
817
|
}
|
|
818
|
+
| {
|
|
819
|
+
_key: string
|
|
820
|
+
_id: string
|
|
821
|
+
_type: "gatedContent"
|
|
822
|
+
internalTitle: string | null
|
|
823
|
+
teaserHeading: string | null
|
|
824
|
+
teaserBody: RichText | null
|
|
825
|
+
gateType: string | null
|
|
826
|
+
gateHeadline: string | null
|
|
827
|
+
gateDescription: string | null
|
|
828
|
+
submitLabel: string | null
|
|
829
|
+
successMessage: string | null
|
|
830
|
+
kind: "richText" | "file" | "video" | null
|
|
831
|
+
}
|
|
771
832
|
| {
|
|
772
833
|
_key: string
|
|
773
834
|
_id: string
|
|
@@ -897,6 +958,26 @@ export type PAGE_QUERY_RESULT = {
|
|
|
897
958
|
_updatedAt: string
|
|
898
959
|
} | null
|
|
899
960
|
|
|
961
|
+
// Source: queries.ts
|
|
962
|
+
// Variable: GATED_CONTENT_QUERY
|
|
963
|
+
export type GATED_CONTENT_QUERY_RESULT = {
|
|
964
|
+
kind: "richText" | "file" | "video" | null
|
|
965
|
+
content: RichText | null
|
|
966
|
+
file: {
|
|
967
|
+
url: string | null
|
|
968
|
+
originalFilename: string | null
|
|
969
|
+
mimeType: string | null
|
|
970
|
+
size: number | null
|
|
971
|
+
} | null
|
|
972
|
+
video: {
|
|
973
|
+
url: string | null
|
|
974
|
+
file: {
|
|
975
|
+
url: string | null
|
|
976
|
+
mimeType: string | null
|
|
977
|
+
} | null
|
|
978
|
+
}
|
|
979
|
+
} | null
|
|
980
|
+
|
|
900
981
|
// Source: queries.ts
|
|
901
982
|
// Variable: PAGE_METADATA_QUERY
|
|
902
983
|
// Query: *[ _type == "page" && ( ($slug == null && !defined(pageFolder._ref) && (!defined(slug.current) || slug.current == "" || slug.current == "/")) || select( defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current, defined(pageFolder->slug.current) && (!defined(slug.current) || slug.current == "" || slug.current == "/") => pageFolder->slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->parentFolder->slug.current) && defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->parentFolder->slug.current + "/" + pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->parentFolder->slug.current) && defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->parentFolder->slug.current + "/" + pageFolder->slug.current + "/" + slug.current, defined(pageFolder->slug.current) && defined(slug.current) && slug.current != "" && slug.current != "/" => pageFolder->slug.current + "/" + slug.current, slug.current ) == $slug ) ][0] { _id, title, metadata, _updatedAt }
|
|
@@ -68,7 +68,6 @@ const nextConfig: NextConfig = {
|
|
|
68
68
|
browserToTerminal: true,
|
|
69
69
|
},
|
|
70
70
|
experimental: {
|
|
71
|
-
prefetchInlining: true,
|
|
72
71
|
optimizePackageImports: [
|
|
73
72
|
"@react-three/drei",
|
|
74
73
|
"@react-three/fiber",
|
|
@@ -94,7 +93,8 @@ const nextConfig: NextConfig = {
|
|
|
94
93
|
},
|
|
95
94
|
headers: async () => [
|
|
96
95
|
{
|
|
97
|
-
|
|
96
|
+
// Excludes /studio* — must stay mutually exclusive with the rule below.
|
|
97
|
+
source: "/((?!studio(?:/|$)).*)",
|
|
98
98
|
headers: [
|
|
99
99
|
{
|
|
100
100
|
key: "X-Content-Type-Options",
|
|
@@ -126,6 +126,37 @@ const nextConfig: NextConfig = {
|
|
|
126
126
|
},
|
|
127
127
|
],
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
// X-Frame-Options can't allow a third-party origin, and Next can't unset
|
|
131
|
+
// it from another rule — the iframe needs it absent, not overridden.
|
|
132
|
+
source: "/studio/:path*",
|
|
133
|
+
headers: [
|
|
134
|
+
{
|
|
135
|
+
key: "X-Content-Type-Options",
|
|
136
|
+
value: "nosniff",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "Content-Security-Policy",
|
|
140
|
+
value: "frame-ancestors 'self' https://*.sanity.io;",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "X-XSS-Protection",
|
|
144
|
+
value: "1; mode=block",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: "X-DNS-Prefetch-Control",
|
|
148
|
+
value: "on",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "Strict-Transport-Security",
|
|
152
|
+
value: "max-age=63072000; includeSubDomains; preload",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: "Permissions-Policy",
|
|
156
|
+
value: "camera=(), microphone=(), geolocation=()",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
129
160
|
],
|
|
130
161
|
}
|
|
131
162
|
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"class-variance-authority": "^0.7.1",
|
|
31
|
-
"next": "^16",
|
|
32
|
-
"react": "^19",
|
|
33
|
-
"react-dom": "^19",
|
|
31
|
+
"next": "^16.3.0",
|
|
32
|
+
"react": "^19.2.8",
|
|
33
|
+
"react-dom": "^19.2.8",
|
|
34
34
|
"react-use": "^17.6.0",
|
|
35
35
|
"tailwind-merge": "^3.5.0",
|
|
36
36
|
"zod": "^4.3.6"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@biomejs/biome": "^2.4.14",
|
|
40
40
|
"@csstools/postcss-global-data": "^4.0.0",
|
|
41
|
-
"@next/bundle-analyzer": "^16.
|
|
41
|
+
"@next/bundle-analyzer": "^16.3.0",
|
|
42
42
|
"@svgr/webpack": "^8.1.0",
|
|
43
43
|
"@tailwindcss/postcss": "^4.2.2",
|
|
44
44
|
"@types/bun": "^1.3.11",
|