diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd5f6b1b0..bd309e6cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: cache: pnpm node-version: lts/* - run: pnpm install --ignore-scripts - - run: pnpm build --filter=./packages/* + - run: pnpm build --filter=!./apps/* - run: pnpm lint test: diff --git a/.github/workflows/lint-fix.yml b/.github/workflows/lint-fix.yml index 45d8ee9f1..aa98f902f 100644 --- a/.github/workflows/lint-fix.yml +++ b/.github/workflows/lint-fix.yml @@ -31,7 +31,7 @@ jobs: with: app-id: ${{ secrets.ECOSPARK_APP_ID }} private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} - - uses: peter-evans/create-pull-request@8867c4aba1b742c39f8d0ba35429c2dfa4b6cb20 # v7 + - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7 with: body: I ran `pnpm lint:fix` 🧑‍💻 branch: actions/lint-fix diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 0e4bc021a..5eae35018 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -2,7 +2,7 @@ name: Prettier on: push: - branches: [main] + branches: [main, canary] workflow_dispatch: concurrency: @@ -23,8 +23,7 @@ jobs: with: cache: pnpm node-version: lts/* - - - run: pnpm install --dev --ignore-scripts + - run: pnpm install --ignore-scripts - uses: actions/cache@v4 with: path: node_modules/.cache/prettier/.prettier-cache @@ -36,10 +35,10 @@ jobs: with: app-id: ${{ secrets.ECOSPARK_APP_ID }} private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} - - uses: peter-evans/create-pull-request@8867c4aba1b742c39f8d0ba35429c2dfa4b6cb20 # v7 + - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7 with: body: I ran `pnpm format` 🧑‍💻 - branch: actions/prettier + branch: "actions/prettier-${{ github.ref_name }}" commit-message: 'chore(prettier): 🤖 ✨' labels: 🤖 bot sign-commits: true diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index ee6d266bc..a7dd41782 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -18,6 +18,8 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} + # Enable the new QRCode Preview URL menu on canary versions + PRESENTATION_ENABLE_QRCODE: true steps: - uses: actions/create-github-app-token@v1 id: generate-token diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 34fbc125c..9f4ba8a5e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -24,6 +24,8 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} + # Disable the new QRCode feature on production builds + PRESENTATION_ENABLE_QRCODE: false steps: - uses: actions/create-github-app-token@v1 id: generate-token @@ -38,25 +40,25 @@ jobs: # Publish to NPM on new releases - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} - uses: pnpm/action-setup@v4 - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} - uses: actions/setup-node@v4 - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} with: cache: pnpm node-version: lts/* - name: install deps & build - run: pnpm install --ignore-scripts && pnpm build --filter=./packages/* - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + run: pnpm install --ignore-scripts && pnpm build --filter=!./apps/* + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} - name: Set publishing config run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} env: NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} # Release Please has already incremented versions and published tags, so we just # need to publish all unpublished versions to NPM here - run: pnpm -r publish - if: ${{ steps.release.outputs.release_created || github.event.inputs.publish == 'true' }} + if: ${{ steps.release.outputs.releases_created || github.event.inputs.publish == 'true' }} env: NPM_CONFIG_PROVENANCE: true diff --git a/.prettierignore b/.prettierignore index 5d4232d3f..8c56ad49e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,10 +7,16 @@ apps/*/.next apps/*/build apps/*/dist +apps/*/.output apps/*/public/build +apps/*/sanity.types.ts CHANGELOG.md packages/*/dist packages/*/dist-svelte packages/*/storybook-static +packages/@repo/*/dist +packages/@repo/*/dist-svelte +packages/@repo/*/storybook-static pnpm-lock.yaml tsconfig.json + diff --git a/apps/astro/README.md b/apps/astro/README.md deleted file mode 100644 index 5558406ee..000000000 --- a/apps/astro/README.md +++ /dev/null @@ -1 +0,0 @@ -# Astro diff --git a/apps/astro/astro.config.mjs b/apps/astro/astro.config.mjs index e96e2121f..be39175f2 100644 --- a/apps/astro/astro.config.mjs +++ b/apps/astro/astro.config.mjs @@ -1,11 +1,12 @@ -import {defineConfig} from 'astro/config' -import {workspaces, studioUrl as baseUrl, apiVersion} from 'apps-common/env' -import sanity from '@sanity/astro' import react from '@astrojs/react' import tailwind from '@astrojs/tailwind' import vercel from '@astrojs/vercel/serverless' -const {projectId, dataset, workspace} = workspaces['astro'] -const studioUrl = `${baseUrl}/${workspace}` +import {apiVersion, workspaces} from '@repo/env' +import {studioUrl as baseUrl} from '@repo/studio-url' +import sanity from '@sanity/astro' +import {defineConfig} from 'astro/config' + +const {projectId, dataset} = workspaces['astro'] // https://astro.build/config export default defineConfig({ @@ -17,7 +18,17 @@ export default defineConfig({ useCdn: true, apiVersion, stega: { - studioUrl, + studioUrl: (sourceDocument) => { + if ( + sourceDocument._projectId === workspaces['cross-dataset-references'].projectId && + sourceDocument._dataset === workspaces['cross-dataset-references'].dataset + ) { + const {workspace, tool} = workspaces['cross-dataset-references'] + return {baseUrl, workspace, tool} + } + const {workspace, tool} = workspaces['astro'] + return {baseUrl, workspace, tool} + }, }, }), react(), diff --git a/apps/astro/package.json b/apps/astro/package.json index 17f709274..0f691e782 100644 --- a/apps/astro/package.json +++ b/apps/astro/package.json @@ -10,22 +10,28 @@ "preview": "astro preview", "start": "astro dev" }, + "prettier": "@repo/prettier-config", "dependencies": { - "@astrojs/check": "^0.9.3", + "@astrojs/check": "^0.9.4", "@astrojs/react": "^3.6.2", - "@astrojs/tailwind": "^5.1.0", - "@astrojs/vercel": "^7.8.0", + "@astrojs/tailwind": "^5.1.2", + "@astrojs/vercel": "^7.8.1", + "@repo/env": "workspace:*", + "@repo/studio-url": "workspace:*", "@sanity/astro": "^3.1.6", - "@sanity/client": "^6.21.3", + "@sanity/client": "^6.22.1", "@sanity/image-url": "^1.0.2", "@sanity/visual-editing": "workspace:*", - "apps-common": "workspace:^", - "astro": "^4.15.4", + "@vercel/stega": "^0.1.2", + "astro": "^4.16.5", "astro-portabletext": "^0.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "sanity": "3.57.2", - "tailwindcss": "^3.4.10", - "typescript": "5.6.2" + "sanity": "3.61.0", + "tailwindcss": "^3.4.14", + "typescript": "5.6.3" + }, + "devDependencies": { + "@repo/prettier-config": "workspace:*" } } diff --git a/apps/astro/src/layouts/layout.astro b/apps/astro/src/layouts/layout.astro index 1bfe99615..46b2b0e05 100644 --- a/apps/astro/src/layouts/layout.astro +++ b/apps/astro/src/layouts/layout.astro @@ -1,5 +1,6 @@ --- import {VisualEditing} from '@sanity/astro/visual-editing' + const visualEditingEnabled = import.meta.env.SANITY_VISUAL_EDITING_ENABLED == 'true' export type props = { diff --git a/apps/astro/src/pages/shoes/[slug].astro b/apps/astro/src/pages/shoes/[slug].astro index f601fcff9..23e4cbd4f 100644 --- a/apps/astro/src/pages/shoes/[slug].astro +++ b/apps/astro/src/pages/shoes/[slug].astro @@ -1,10 +1,10 @@ --- -import {shoe, shoesList, type ShoeResult, type ShoesListResult} from 'apps-common/queries' -import {formatCurrency} from 'apps-common/utils' import {PortableText} from 'astro-portabletext' import Layout from '../../layouts/layout.astro' import {loadQuery} from '../../load-query' +import {shoe, shoesList, type ShoeResult, type ShoesListResult} from '../../queries' import {urlFor, urlForCrossDatasetReference} from '../../sanity' +import {formatCurrency} from '../../utils' const {slug} = Astro.params diff --git a/apps/astro/src/pages/shoes/index.astro b/apps/astro/src/pages/shoes/index.astro index b3ad74127..96bc60dd8 100644 --- a/apps/astro/src/pages/shoes/index.astro +++ b/apps/astro/src/pages/shoes/index.astro @@ -1,9 +1,9 @@ --- -import {shoesList, type ShoesListResult} from 'apps-common/queries' -import {formatCurrency} from 'apps-common/utils' import Layout from '../../layouts/layout.astro' import {loadQuery} from '../../load-query' +import {shoesList, type ShoesListResult} from '../../queries' import {urlFor, urlForCrossDatasetReference} from '../../sanity' +import {formatCurrency} from '../../utils' const {data: products} = await loadQuery({query: shoesList}) const _loading = false diff --git a/apps/common/src/queries.ts b/apps/astro/src/queries.ts similarity index 100% rename from apps/common/src/queries.ts rename to apps/astro/src/queries.ts diff --git a/apps/astro/src/sanity.ts b/apps/astro/src/sanity.ts index 042390d07..39e07c5f5 100644 --- a/apps/astro/src/sanity.ts +++ b/apps/astro/src/sanity.ts @@ -1,5 +1,5 @@ +import {workspaces} from '@repo/env' import imageUrlBuilder from '@sanity/image-url' -import {workspaces} from 'apps-common/env' const {projectId, dataset} = workspaces['astro'] diff --git a/apps/astro/src/utils.ts b/apps/astro/src/utils.ts new file mode 100644 index 000000000..e60534636 --- /dev/null +++ b/apps/astro/src/utils.ts @@ -0,0 +1,18 @@ +import {vercelStegaSplit} from '@vercel/stega' + +export function formatCurrency(_value: number | string): string { + let value = typeof _value === 'string' ? undefined : _value + let encoded = '' + if (typeof _value === 'string') { + const split = vercelStegaSplit(_value) + value = parseInt(split.cleaned, 10) + encoded = split.encoded + } + const formatter = new Intl.NumberFormat('en', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }) + return `${formatter.format(value!)}${encoded}` +} diff --git a/apps/common/package.json b/apps/common/package.json deleted file mode 100644 index 70ef68ffc..000000000 --- a/apps/common/package.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "name": "apps-common", - "version": "0.0.0", - "private": true, - "license": "MIT", - "author": "Sanity.io ", - "sideEffects": false, - "type": "module", - "exports": { - ".": "./src/index.ts", - "./env": "./src/env.ts", - "./queries": "./src/queries.ts", - "./utils": "./src/utils.ts", - "./package.json": "./package.json" - }, - "files": [ - "src" - ], - "scripts": { - "lint": "eslint .", - "test": "tsc --noEmit" - }, - "prettier": "@sanity/prettier-config", - "eslintConfig": { - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "settings": { - "react": { - "version": "detect" - } - }, - "plugins": [ - "@typescript-eslint", - "react", - "react-hooks", - "simple-import-sort", - "prettier" - ], - "extends": [ - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:react/jsx-runtime", - "eslint:recommended", - "plugin:prettier/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "no-console": "error", - "no-warning-comments": [ - "warn", - { - "location": "start", - "terms": [ - "todo", - "@todo", - "fixme" - ] - } - ], - "@typescript-eslint/explicit-module-boundary-types": "error", - "@typescript-eslint/member-delimiter-style": "off", - "@typescript-eslint/no-empty-interface": "off", - "prettier/prettier": "warn", - "react-hooks/exhaustive-deps": "error", - "react-hooks/rules-of-hooks": "error", - "react/prop-types": "off", - "simple-import-sort/exports": "warn", - "simple-import-sort/imports": "warn" - }, - "root": true - }, - "dependencies": { - "@sanity/icons": "^3.4.0", - "@sanity/react-loader": "workspace:*", - "@sanity/ui": "2.8.9", - "@vercel/stega": "0.1.2" - }, - "devDependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "rxjs": "^7.8.1", - "sanity": "^3.57.2", - "styled-components": "6.1.13", - "typescript": "5.6.2" - }, - "peerDependencies": { - "react": "^18.3.0", - "react-dom": "^18.3.0", - "rxjs": "^7.8.1", - "sanity": "^3.57.2", - "styled-components": "^6.1.8" - }, - "engines": { - "node": ">=18" - } -} diff --git a/apps/common/src/index.ts b/apps/common/src/index.ts deleted file mode 100644 index cb7cdd48b..000000000 --- a/apps/common/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './schema' diff --git a/apps/common/src/lib/findField.ts b/apps/common/src/lib/findField.ts deleted file mode 100644 index de4224157..000000000 --- a/apps/common/src/lib/findField.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {FieldMember, ObjectFormNode} from 'sanity' - -import {isFieldMember} from './isFieldMember' - -/** @alpha */ -export function findFieldMember( - node: Omit, - name: string, -): N | undefined { - return node.members.filter(isFieldMember).find((m) => m.name === name) as N | undefined -} diff --git a/apps/common/src/lib/isFieldMember.ts b/apps/common/src/lib/isFieldMember.ts deleted file mode 100644 index 0542b550a..000000000 --- a/apps/common/src/lib/isFieldMember.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type {FieldMember, ObjectMember} from 'sanity' - -/** @alpha */ -export function isFieldMember(member: ObjectMember): member is FieldMember { - return member.kind === 'field' -} diff --git a/apps/common/src/lib/useReferenceEditState.ts b/apps/common/src/lib/useReferenceEditState.ts deleted file mode 100644 index 49a7bd68e..000000000 --- a/apps/common/src/lib/useReferenceEditState.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {useEffect, useState} from 'react' -import {EMPTY, switchMap} from 'rxjs' -import {type EditStateFor, getPublishedId, useDocumentPreviewStore, useDocumentStore} from 'sanity' - -export function useReferenceEditState(documentId?: string): EditStateFor | undefined { - const documentStore = useDocumentStore() - const previewStore = useDocumentPreviewStore() - const [editState, setEditState] = useState() - - useEffect(() => { - setEditState(undefined) - - if (!documentId) return undefined - - const type$ = previewStore.observeDocumentTypeFromId(documentId) - const editState$ = type$.pipe( - switchMap((type) => { - if (!type) return EMPTY - - return documentStore.pair.editState(getPublishedId(documentId), type) - }), - ) - - const sub = editState$.subscribe({next: setEditState}) - - return () => sub.unsubscribe() - }, [documentId, documentStore, previewStore]) - - return editState -} diff --git a/apps/common/src/schema/defineSchema.ts b/apps/common/src/schema/defineSchema.ts deleted file mode 100644 index 7d89088b9..000000000 --- a/apps/common/src/schema/defineSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {isArray, type SchemaTypeDefinition} from 'sanity' - -export function defineSchema(options: SchemaTypeDefinition[]): { - types: SchemaTypeDefinition[] -} { - if (isArray(options)) return {types: options} - - return options -} diff --git a/apps/common/src/schema/index.ts b/apps/common/src/schema/index.ts deleted file mode 100644 index e459951ba..000000000 --- a/apps/common/src/schema/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {defineSchema} from './defineSchema' -import {pageSchema} from './page' -import {productSchema} from './product' -import {projectSchema} from './project' -import {shoeSchema} from './shoe' -import {siteSettingsSchema} from './siteSettings' - -/** @public */ -export const schema = defineSchema([ - ...pageSchema.types, - ...productSchema.types, - ...shoeSchema.types, - ...projectSchema.types, - ...siteSettingsSchema.types, -]) diff --git a/apps/common/src/schema/page/index.ts b/apps/common/src/schema/page/index.ts deleted file mode 100644 index 57e0063e1..000000000 --- a/apps/common/src/schema/page/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {defineSchema} from '../defineSchema' -import {sectionStyleType} from './objects/sectionStyle' -import {pageType} from './page' -import {pageSectionType} from './pageSectionType' - -export const pageSchema = defineSchema([ - // objects - sectionStyleType, - - // documents - pageType, - pageSectionType, -]) diff --git a/apps/common/src/schema/page/objects/sectionStyle.ts b/apps/common/src/schema/page/objects/sectionStyle.ts deleted file mode 100644 index 77a1d735f..000000000 --- a/apps/common/src/schema/page/objects/sectionStyle.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {defineField, defineType} from 'sanity' - -export const sectionStyleType = defineType({ - type: 'object', - name: 'sectionStyle', - title: 'Section style', - fields: [ - defineField({ - type: 'string', - name: 'variant', - title: 'Variant', - options: { - list: [ - { - title: 'Default', - value: 'default', - }, - { - title: 'Inverted', - value: 'inverted', - }, - ], - }, - initialValue: 'default', - validation: (rule) => rule.required(), - }), - ], -}) diff --git a/apps/common/src/schema/page/page.tsx b/apps/common/src/schema/page/page.tsx deleted file mode 100644 index fd10bc27c..000000000 --- a/apps/common/src/schema/page/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import {defineField, defineType} from 'sanity' - -import {pageSectionArrayMember} from './section' -import {featuredProductsSectionType} from './sections/featuredProducts' -import {featureHighlightSectionType} from './sections/featureHighlight' -import {heroSectionType} from './sections/hero' -import {introSectionType} from './sections/intro' - -export const pageType = defineType({ - type: 'document', - name: 'page', - fields: [ - defineField({ - type: 'string', - name: 'title', - title: 'Title', - }), - defineField({ - type: 'slug', - name: 'slug', - title: 'Slug', - options: {source: 'title'}, - hidden: (ctx) => ['home', 'drafts.home'].includes(ctx.document?._id as string), - }), - defineField({ - type: 'array', - name: 'sections', - title: 'Sections', - of: [ - heroSectionType, - introSectionType, - featuredProductsSectionType, - featureHighlightSectionType, - pageSectionArrayMember, - ], - }), - ], -}) diff --git a/apps/common/src/schema/page/pageSectionType.ts b/apps/common/src/schema/page/pageSectionType.ts deleted file mode 100644 index fa553eb57..000000000 --- a/apps/common/src/schema/page/pageSectionType.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {defineField, defineType} from 'sanity' - -export const pageSectionType = defineType({ - type: 'document', - name: 'page.section', - fields: [ - defineField({ - type: 'string', - name: 'headline', - title: 'Headline', - }), - defineField({ - type: 'string', - name: 'tagline', - title: 'Tagline', - }), - defineField({ - type: 'string', - name: 'subline', - title: 'Subline', - }), - ], -}) diff --git a/apps/common/src/schema/page/section.ts b/apps/common/src/schema/page/section.ts deleted file mode 100644 index fa094ce30..000000000 --- a/apps/common/src/schema/page/section.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {defineArrayMember, defineField} from 'sanity' - -import {PageSectionInput} from './components/PageSectionInput' -import {pageSectionType} from './pageSectionType' - -export const pageSectionArrayMember = defineArrayMember({ - type: 'object', - name: 'section', - fields: [ - defineField({ - type: 'reference', - name: 'symbol', - title: 'Symbol', - to: [{type: 'page.section'}], - }), - - // overrides - ...pageSectionType.fields, - ], - components: { - input: PageSectionInput, - }, -}) diff --git a/apps/common/src/schema/page/sections/featureHighlight.ts b/apps/common/src/schema/page/sections/featureHighlight.ts deleted file mode 100644 index 186b77d17..000000000 --- a/apps/common/src/schema/page/sections/featureHighlight.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {defineArrayMember, defineField} from 'sanity' - -export const featureHighlightSectionType = defineArrayMember({ - type: 'object', - name: 'featureHighlight', - title: 'Feature Highlight', - fields: [ - defineField({ - type: 'string', - name: 'headline', - title: 'Headline', - }), - defineField({ - type: 'text', - name: 'description', - title: 'Description', - }), - defineField({ - type: 'image', - name: 'image', - title: 'Image', - }), - defineField({ - type: 'array', - name: 'ctas', - title: 'CTAs', - of: [ - { - type: 'object', - name: 'cta', - title: 'CTA', - fields: [ - defineField({ - type: 'string', - name: 'title', - title: 'Title', - }), - defineField({ - type: 'string', - name: 'href', - title: 'Href', - }), - ], - }, - ], - }), - defineField({ - type: 'reference', - name: 'product', - title: 'Product', - to: [{type: 'product'}], - }), - defineField({ - type: 'sectionStyle', - name: 'style', - title: 'Style', - }), - ], - preview: { - select: { - title: 'headline', - }, - prepare(data) { - return { - ...data, - subtitle: 'Feature', - } - }, - }, -}) diff --git a/apps/common/src/schema/page/sections/featuredProducts.ts b/apps/common/src/schema/page/sections/featuredProducts.ts deleted file mode 100644 index e043f142e..000000000 --- a/apps/common/src/schema/page/sections/featuredProducts.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {defineArrayMember, defineField} from 'sanity' - -export const featuredProductsSectionType = defineArrayMember({ - type: 'object', - name: 'featuredProducts', - title: 'Featured Products', - fields: [ - defineField({ - type: 'string', - name: 'headline', - title: 'Headline', - }), - defineField({ - type: 'array', - name: 'products', - title: 'Products', - of: [ - { - type: 'reference', - to: [{type: 'product'}], - }, - ], - }), - defineField({ - type: 'sectionStyle', - name: 'style', - title: 'Style', - }), - ], - preview: { - select: { - title: 'headline', - }, - prepare(data) { - return { - ...data, - subtitle: 'Featured Products', - } - }, - }, -}) diff --git a/apps/common/src/schema/page/sections/hero.ts b/apps/common/src/schema/page/sections/hero.ts deleted file mode 100644 index 94bc2c5b3..000000000 --- a/apps/common/src/schema/page/sections/hero.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {defineArrayMember, defineField} from 'sanity' - -export const heroSectionType = defineArrayMember({ - type: 'object', - name: 'hero', - title: 'Hero', - description: 'The hero section of the page', - fields: [ - defineField({ - type: 'string', - name: 'headline', - title: 'Headline', - }), - defineField({ - type: 'string', - name: 'tagline', - title: 'Tagline', - }), - defineField({ - type: 'string', - name: 'subline', - title: 'Subline', - }), - defineField({ - type: 'sectionStyle', - name: 'style', - title: 'Style', - }), - ], - preview: { - select: { - title: 'headline', - }, - prepare(data) { - return { - ...data, - subtitle: 'Hero', - } - }, - }, -}) diff --git a/apps/common/src/schema/page/sections/intro.ts b/apps/common/src/schema/page/sections/intro.ts deleted file mode 100644 index ada5c056f..000000000 --- a/apps/common/src/schema/page/sections/intro.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {defineArrayMember, defineField} from 'sanity' - -export const introSectionType = defineArrayMember({ - type: 'object', - name: 'intro', - title: 'Intro', - fields: [ - defineField({ - type: 'string', - name: 'headline', - title: 'Headline', - }), - defineField({ - type: 'text', - name: 'intro', - title: 'Intro', - }), - defineField({ - type: 'sectionStyle', - name: 'style', - title: 'Style', - }), - ], - preview: { - select: { - title: 'headline', - }, - prepare(data) { - return { - ...data, - subtitle: 'Intro', - } - }, - }, -}) diff --git a/apps/common/src/schema/product/index.ts b/apps/common/src/schema/product/index.ts deleted file mode 100644 index ee74d47e8..000000000 --- a/apps/common/src/schema/product/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {defineSchema} from '../defineSchema' -import {productType} from './product' - -export const productSchema = defineSchema([productType]) diff --git a/apps/common/src/schema/product/product.ts b/apps/common/src/schema/product/product.ts deleted file mode 100644 index f73481a0f..000000000 --- a/apps/common/src/schema/product/product.ts +++ /dev/null @@ -1,129 +0,0 @@ -import {defineArrayMember, defineField, defineType} from 'sanity' - -export const productType = defineType({ - type: 'document', - name: 'product', - fields: [ - defineField({ - type: 'string', - name: 'title', - title: 'Title', - }), - defineField({ - type: 'slug', - name: 'slug', - title: 'Slug', - options: {source: 'title'}, - }), - defineField({ - type: 'array', - name: 'media', - title: 'Media', - of: [ - { - type: 'image', - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alt text', - }), - ], - }, - ], - options: {layout: 'grid'}, - }), - defineField({ - type: 'array', - name: 'description', - title: 'Description', - of: [{type: 'block'}], - }), - defineField({ - title: 'Brand', - name: 'brandReference', - type: 'crossDatasetReference', - dataset: 'cross-dataset-references', - studioUrl: ({type, id}) => - new URL( - `/cross-dataset-references/desk/intent/edit/id=${id};type=${type}/`, - location.href, - ).toString(), - to: [ - { - type: 'brand', - preview: { - select: { - title: 'name', - media: 'logo', - }, - }, - }, - ], - }), - defineField({ - type: 'object', - name: 'details', - title: 'Details', - fields: [ - defineField({ - type: 'string', - name: 'materials', - title: 'Materials', - }), - defineField({ - type: 'array', - name: 'collectionNotes', - title: 'Collection notes', - of: [{type: 'block'}], - }), - defineField({ - type: 'array', - name: 'performance', - title: 'Performance', - of: [{type: 'block'}], - }), - defineField({ - type: 'string', - name: 'ledLifespan', - title: 'LED lifespan', - }), - defineField({ - type: 'array', - name: 'certifications', - title: 'Certifications', - of: [{type: 'string'}], - }), - ], - }), - defineField({ - type: 'array', - name: 'variants', - title: 'Variants', - of: [ - defineArrayMember({ - type: 'object', - name: 'variant', - title: 'Variant', - fields: [ - defineField({ - type: 'string', - name: 'title', - title: 'Title', - }), - defineField({ - type: 'string', - name: 'price', - title: 'Price', - }), - defineField({ - type: 'string', - name: 'sku', - title: 'SKU', - }), - ], - }), - ], - }), - ], -}) diff --git a/apps/common/src/schema/project/index.ts b/apps/common/src/schema/project/index.ts deleted file mode 100644 index 1d7a0472b..000000000 --- a/apps/common/src/schema/project/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {defineSchema} from '../defineSchema' -import {projectType} from './project' - -export const projectSchema = defineSchema([projectType]) diff --git a/apps/common/src/schema/project/project.ts b/apps/common/src/schema/project/project.ts deleted file mode 100644 index 5c5eae55a..000000000 --- a/apps/common/src/schema/project/project.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {defineField, defineType} from 'sanity' - -export const projectType = defineType({ - type: 'document', - name: 'project', - fields: [ - defineField({ - type: 'string', - name: 'title', - title: 'Title', - }), - defineField({ - type: 'slug', - name: 'slug', - title: 'Slug', - options: {source: 'title'}, - }), - ], -}) diff --git a/apps/common/src/schema/shoe/index.ts b/apps/common/src/schema/shoe/index.ts deleted file mode 100644 index 9bb6ade36..000000000 --- a/apps/common/src/schema/shoe/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {defineSchema} from '../defineSchema' -import {shoeType} from './shoe' - -export const shoeSchema = defineSchema([shoeType]) diff --git a/apps/common/src/schema/siteSettings/index.ts b/apps/common/src/schema/siteSettings/index.ts deleted file mode 100644 index c479ec518..000000000 --- a/apps/common/src/schema/siteSettings/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {defineSchema} from '../defineSchema' -import {siteSettingsType} from './siteSettings' - -export const siteSettingsSchema = defineSchema([siteSettingsType]) diff --git a/apps/common/src/schema/siteSettings/siteSettings.ts b/apps/common/src/schema/siteSettings/siteSettings.ts deleted file mode 100644 index 90f631ce4..000000000 --- a/apps/common/src/schema/siteSettings/siteSettings.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {ControlsIcon} from '@sanity/icons' -import {defineType} from 'sanity' - -export const siteSettingsType = defineType({ - type: 'document', - icon: ControlsIcon, - name: 'siteSettings', - title: 'Site Settings', - fields: [ - { - type: 'string', - name: 'title', - title: 'Site Title', - }, - { - type: 'string', - name: 'description', - title: 'Site Description', - }, - { - type: 'reference', - name: 'frontPage', - title: 'Front page', - to: [{type: 'page'}], - }, - { - type: 'string', - name: 'copyrightText', - title: 'Copyright text', - }, - ], - preview: { - prepare() { - return {title: 'Site Settings'} - }, - }, -}) diff --git a/apps/common/src/utils.ts b/apps/common/src/utils.ts deleted file mode 100644 index bdc65a275..000000000 --- a/apps/common/src/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable no-console */ -// Common utils used in templates, demos and tests. -// The JS in here needs to be able to run server side, browser side, in any fw - -import {vercelStegaSplit} from '@vercel/stega' - -export function formatCurrency(_value: number | string): string { - let value = typeof _value === 'string' ? undefined : _value - let encoded = '' - if (typeof _value === 'string') { - const split = vercelStegaSplit(_value) - value = parseInt(split.cleaned, 10) - encoded = split.encoded - } - const formatter = new Intl.NumberFormat('en', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }) - return `${formatter.format(value!)}${encoded}` -} - -const rtf = new Intl.RelativeTimeFormat('en', {style: 'short'}) -export function formatTimeSince(from: Date, to: Date): string { - const seconds = Math.floor((from.getTime() - to.getTime()) / 1000) - if (seconds > -60) { - return rtf.format(Math.min(seconds, -1), 'second') - } - const minutes = Math.ceil(seconds / 60) - if (minutes > -60) { - return rtf.format(minutes, 'minute') - } - const hours = Math.ceil(minutes / 60) - if (hours > -24) { - return rtf.format(hours, 'hour') - } - const days = Math.ceil(hours / 24) - return rtf.format(days, 'day') -} diff --git a/apps/common/tsconfig.json b/apps/common/tsconfig.json deleted file mode 100644 index ec754db61..000000000 --- a/apps/common/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "@sanity/pkg-utils/tsconfig/strictest.json", - "compilerOptions": { - "baseUrl": ".", - "rootDir": ".", - "noUnusedLocals": false, - "noUnusedParameters": false, - "paths": { - "@sanity/react-loader": ["../../packages/react-loader/src"], - "@sanity/visual-editing": ["../../packages/visual-editing/src"] - } - }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["dist", "node_modules"] -} diff --git a/apps/live-next/.env.local.example b/apps/live-next/.env.local.example new file mode 100644 index 000000000..7657fd92a --- /dev/null +++ b/apps/live-next/.env.local.example @@ -0,0 +1,3 @@ +SANITY_API_READ_TOKEN= +SANITY_API_WRITE_TOKEN= +SANITY_API_BROWSER_TOKEN= diff --git a/apps/live-next/.eslintignore b/apps/live-next/.eslintignore new file mode 100644 index 000000000..30764a1a8 --- /dev/null +++ b/apps/live-next/.eslintignore @@ -0,0 +1,2 @@ +# Ignoring generated files +./sanity.types.ts diff --git a/apps/live-next/.eslintrc b/apps/live-next/.eslintrc new file mode 100644 index 000000000..32a4796f4 --- /dev/null +++ b/apps/live-next/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": "next/core-web-vitals", + "root": true, + "plugins": ["react-compiler"], + "rules": { + "react-compiler/react-compiler": "error" + } +} diff --git a/apps/next-no-cache/.gitignore b/apps/live-next/.gitignore similarity index 79% rename from apps/next-no-cache/.gitignore rename to apps/live-next/.gitignore index fd3dbb571..7a1e9b7c3 100644 --- a/apps/next-no-cache/.gitignore +++ b/apps/live-next/.gitignore @@ -13,8 +13,9 @@ /.next/ /out/ -# production -/build +# sanity +/.sanity/ +/dist/ # misc .DS_Store @@ -34,3 +35,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Env files created by scripts for working locally +.env +.env.local diff --git a/apps/live-next/app/actions.ts b/apps/live-next/app/actions.ts new file mode 100644 index 000000000..addc89862 --- /dev/null +++ b/apps/live-next/app/actions.ts @@ -0,0 +1,47 @@ +'use server' + +// import {verifyPreviewSecret} from '@/sanity/lib/live' +import {revalidatePath, revalidateTag} from 'next/cache' +import {draftMode} from 'next/headers' + +// @TODO revisit this later +// export async function handleDraftModeAction(secret: string): Promise { +// console.log('Server Action wants to enable Draft Mode', {secret}) + +// if ((await draftMode()).isEnabled) { +// // eslint-disable-next-line no-console +// console.log('Draft Mode is already enabled') +// return +// } + +// try { +// const {isValid} = await verifyPreviewSecret(secret) + +// if (!isValid) { +// return 'Invalid secret provided' +// } + +// console.log('Enabling Draft Mode') +// ;(await draftMode()).enable() +// } catch (err) { +// console.error('Failed to verify preview secret', {secret}, err) +// return 'Unexpected error' +// } +// } + +export async function disableDraftMode() { + 'use server' + await Promise.allSettled([ + (await draftMode()).disable(), + // Simulate a delay to show the loading state + new Promise((resolve) => setTimeout(resolve, 1000)), + ]) +} + +export async function purgeEverything() { + revalidatePath('/', 'layout') +} + +export async function purgeSanity() { + revalidateTag('sanity') +} diff --git a/apps/live-next/app/alert-banner.tsx b/apps/live-next/app/alert-banner.tsx new file mode 100644 index 000000000..225d7a0b2 --- /dev/null +++ b/apps/live-next/app/alert-banner.tsx @@ -0,0 +1,51 @@ +'use client' + +import {useRouter} from 'next/navigation' +import {useSyncExternalStore, useTransition} from 'react' +import {disableDraftMode} from './actions' + +const emptySubscribe = () => () => {} + +export default function AlertBanner() { + const router = useRouter() + const [pending, startTransition] = useTransition() + + const shouldShow = useSyncExternalStore( + emptySubscribe, + () => window.top === window, + () => false, + ) + + if (!shouldShow) return null + + return ( +
+
+ {pending ? ( + 'Disabling draft mode...' + ) : ( + <> + {'Previewing drafts. '} + + + )} +
+
+ ) +} diff --git a/apps/live-next/app/animated-h1.tsx b/apps/live-next/app/animated-h1.tsx new file mode 100644 index 000000000..d6ad37b2c --- /dev/null +++ b/apps/live-next/app/animated-h1.tsx @@ -0,0 +1,89 @@ +'use client' + +import {AnimatePresence, motion, Variants} from 'framer-motion' +import {stegaClean} from 'next-sanity' + +const containerVariants: Variants = { + hidden: {}, + visible: (i = 1) => ({ + transition: { + staggerChildren: 0.3 * i, + delayChildren: 0.4 * i, + }, + }), + exit: (i = 1) => ({ + transition: { + staggerChildren: 0.3 * i, + staggerDirection: -1, + delayChildren: 0.4 * i, + }, + }), +} + +const characterVariants: Variants = { + hidden: { + opacity: 0, + scale: 0, + width: 0, + transition: {type: 'spring', bounce: 0}, + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + width: 'auto', + transition: {type: 'spring', bounce: 0}, + }, + exit: { + opacity: 0, + scale: 0, + width: 0, + transition: {type: 'spring', bounce: 0}, + }, +} + +export function AnimatedH1({text: stegaText, className}: {text: string; className: string}) { + const text = stegaClean(stegaText) + const transformChars = (text: string) => { + const incs = new Map() + return text.split('').map((char) => { + let n = incs.get(char) || 0 + const key = `${char}-${n++}` + incs.set(char, n) + return ( + + ) + }) + } + const characters = transformChars(text) + + return ( + <> + + {stegaText} + + {characters.map((character) => character)} + + + + ) +} +AnimatedH1.displayName = 'AnimatedH1' diff --git a/apps/live-next/app/api/draft-mode/enable/route.ts b/apps/live-next/app/api/draft-mode/enable/route.ts new file mode 100644 index 000000000..6e8d0a19d --- /dev/null +++ b/apps/live-next/app/api/draft-mode/enable/route.ts @@ -0,0 +1,8 @@ +import {client} from '@/sanity/lib/client' +import {defineEnableDraftMode} from 'next-sanity/draft-mode' + +export const {GET} = defineEnableDraftMode({ + client: client.withConfig({ + token: process.env.SANITY_API_READ_TOKEN, + }), +}) diff --git a/apps/live-next/app/avatar.tsx b/apps/live-next/app/avatar.tsx new file mode 100644 index 000000000..b0acf4554 --- /dev/null +++ b/apps/live-next/app/avatar.tsx @@ -0,0 +1,29 @@ +import type {Author} from '@/sanity.types' +import {urlForImage} from '@/sanity/lib/utils' +import {Image} from 'next-sanity/image' + +interface Props { + name: string + picture: Exclude | null +} + +export default function Avatar({name, picture}: Props) { + return ( +
+ {picture?.asset?._ref ? ( +
+ {picture?.alt +
+ ) : ( +
By
+ )} +
{name}
+
+ ) +} diff --git a/apps/live-next/app/cover-image.tsx b/apps/live-next/app/cover-image.tsx new file mode 100644 index 000000000..31c163924 --- /dev/null +++ b/apps/live-next/app/cover-image.tsx @@ -0,0 +1,30 @@ +import {urlForImage} from '@/sanity/lib/utils' +import {Image} from 'next-sanity/image' + +interface CoverImageProps { + image: any + priority?: boolean +} + +export default function CoverImage(props: CoverImageProps) { + const {image: source, priority} = props + const image = source?.asset?._ref ? ( + {source?.alt + ) : ( +
+ ) + + return ( +
+ {image} +
+ ) +} diff --git a/apps/live-next/app/date.tsx b/apps/live-next/app/date.tsx new file mode 100644 index 000000000..7b6518770 --- /dev/null +++ b/apps/live-next/app/date.tsx @@ -0,0 +1,5 @@ +import {format} from 'date-fns' + +export default function DateComponent({dateString}: {dateString: string}) { + return +} diff --git a/apps/live-next/app/draft-mode-status.tsx b/apps/live-next/app/draft-mode-status.tsx new file mode 100644 index 000000000..05c32e047 --- /dev/null +++ b/apps/live-next/app/draft-mode-status.tsx @@ -0,0 +1,14 @@ +'use client' + +import {useDraftModeEnvironment, useDraftModePerspective} from '@sanity/next-loader/hooks' + +export function DraftModeStatus() { + const perspective = useDraftModePerspective() + const environment = useDraftModeEnvironment() + return ( +
+

perspective: {perspective}

+

environment: {environment}

+
+ ) +} diff --git a/apps/live-next/app/favicon.ico b/apps/live-next/app/favicon.ico new file mode 100644 index 000000000..af9845059 Binary files /dev/null and b/apps/live-next/app/favicon.ico differ diff --git a/apps/live-next/app/globals.css b/apps/live-next/app/globals.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/apps/live-next/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/live-next/app/layout.tsx b/apps/live-next/app/layout.tsx new file mode 100644 index 000000000..d5ea5cba9 --- /dev/null +++ b/apps/live-next/app/layout.tsx @@ -0,0 +1,100 @@ +import './globals.css' +import {sanityFetch, SanityLive} from '@/sanity/lib/live' +import {settingsQuery} from '@/sanity/lib/queries' +import {resolveOpenGraphImage} from '@/sanity/lib/utils' +import {SpeedInsights} from '@vercel/speed-insights/next' +import type {Metadata} from 'next' +import {toPlainText, VisualEditing, type PortableTextBlock} from 'next-sanity' +import {Inter} from 'next/font/google' +import {draftMode} from 'next/headers' +import {Suspense} from 'react' +import AlertBanner from './alert-banner' +import {DraftModeStatus} from './draft-mode-status' +import PortableText from './portable-text' + +export async function generateMetadata(): Promise { + const {data: settings} = await sanityFetch({ + query: settingsQuery, + // Metadata should never contain stega + stega: false, + }) + + const ogImage = resolveOpenGraphImage(settings?.ogImage) + let metadataBase: URL | undefined = undefined + try { + metadataBase = settings?.ogImage?.metadataBase + ? new URL(settings.ogImage.metadataBase) + : undefined + } catch { + // ignore + } + return { + metadataBase, + title: settings?.title + ? { + template: `%s | ${settings.title}`, + default: settings.title, + } + : undefined, + description: settings?.description ? toPlainText(settings.description) : undefined, + openGraph: { + images: ogImage ? [ogImage] : [], + }, + } +} + +const inter = Inter({ + variable: '--font-inter', + subsets: ['latin'], + display: 'swap', +}) + +async function Footer() { + const {data} = await sanityFetch({query: settingsQuery}) + const footer = data?.footer || [] + + return ( +
+
+ {footer?.length > 0 && ( + + )} +
+
+ ) +} + +export default async function RootLayout({children}: {children: React.ReactNode}) { + const {data} = await sanityFetch({query: settingsQuery}) + return ( + + +
+ {(await draftMode()).isEnabled && ( + <> + + + + )} +
{children}
+ +
+ +
+ {(await draftMode()).isEnabled && } + + + + + ) +} diff --git a/apps/live-next/app/more-stories.tsx b/apps/live-next/app/more-stories.tsx new file mode 100644 index 000000000..31e279438 --- /dev/null +++ b/apps/live-next/app/more-stories.tsx @@ -0,0 +1,44 @@ +import type {MoreStoriesQueryResult} from '@/sanity.types' +import {SanityLiveStream} from '@/sanity/lib/live' +import {moreStoriesQuery} from '@/sanity/lib/queries' +import Link from 'next/link' +import Avatar from './avatar' +import CoverImage from './cover-image' +import DateComponent from './date' + +export default async function MoreStories(params: {skip: string; limit: number}) { + // const {data} = await sanityFetch({query: moreStoriesQuery, params}) + + return ( + + {async ({data}: {data: MoreStoriesQueryResult}) => { + 'use server' + + return ( +
+ {data?.map((post) => { + const {_id, title, slug, coverImage, excerpt, author} = post + return ( +
+ + + +

+ + {title} + +

+
+ +
+ {excerpt &&

{excerpt}

} + {author && } +
+ ) + })} +
+ ) + }} +
+ ) +} diff --git a/apps/live-next/app/page.tsx b/apps/live-next/app/page.tsx new file mode 100644 index 000000000..26337f010 --- /dev/null +++ b/apps/live-next/app/page.tsx @@ -0,0 +1,119 @@ +import type {HeroQueryResult} from '@/sanity.types' +import {sanityFetch, SanityLiveStream} from '@/sanity/lib/live' +import {heroQuery, settingsQuery} from '@/sanity/lib/queries' +import Link from 'next/link' +import {Suspense} from 'react' +import {AnimatedH1} from './animated-h1' +import Avatar from './avatar' +import CoverImage from './cover-image' +import DateComponent from './date' +import MoreStories from './more-stories' +import PortableText from './portable-text' + +function Intro(props: {title: string | null | undefined; description: any}) { + const {title, description} = props + + if (!title && !description?.length) { + return null + } + + return ( +
+ {title && ( + + )} + {description?.length > 0 && ( +

+ +

+ )} +
+ ) +} + +function HeroPost({ + title, + slug, + excerpt, + coverImage, + date, + author, +}: Pick< + Exclude, + 'title' | 'coverImage' | 'date' | 'excerpt' | 'author' | 'slug' +>) { + return ( +
+ + + +
+
+

+ + {title} + +

+
+ +
+
+
+ {excerpt &&

{excerpt}

} + {author && } +
+
+
+ ) +} + +export default async function Page() { + // const [{data: settings}, {data: heroPost}] = await Promise.all([ + // sanityFetch({query: settingsQuery}), + // sanityFetch({query: heroQuery}), + // ]) + const {data: heroPost} = await sanityFetch({query: heroQuery}) + + return ( +
+ + {async ({data: settings}) => { + 'use server' + return + }} + + + {async ({data: heroPost}) => { + 'use server' + return ( + <> + {heroPost && ( + + )} + + ) + }} + + {heroPost?._id && ( + + )} +
+ ) +} diff --git a/apps/live-next/app/portable-text.tsx b/apps/live-next/app/portable-text.tsx new file mode 100644 index 000000000..3a90ee9e4 --- /dev/null +++ b/apps/live-next/app/portable-text.tsx @@ -0,0 +1,41 @@ +/** + * This component uses Portable Text to render a post body. + * + * You can learn more about Portable Text on: + * https://www.sanity.io/docs/block-content + * https://github.com/portabletext/react-portabletext + * https://portabletext.org/ + * + */ + +import {PortableText, type PortableTextBlock, type PortableTextComponents} from 'next-sanity' + +export default function CustomPortableText({ + className, + value, +}: { + className?: string + value: PortableTextBlock[] +}) { + const components: PortableTextComponents = { + block: { + h5: ({children}) =>
{children}
, + h6: ({children}) =>
{children}
, + }, + marks: { + link: ({children, value}) => { + return ( + + {children} + + ) + }, + }, + } + + return ( +
+ +
+ ) +} diff --git a/apps/live-next/app/posts/[slug]/page.tsx b/apps/live-next/app/posts/[slug]/page.tsx new file mode 100644 index 000000000..3ba27b4fe --- /dev/null +++ b/apps/live-next/app/posts/[slug]/page.tsx @@ -0,0 +1,136 @@ +import {sanityFetch, SanityLiveStream} from '@/sanity/lib/live' +import {postQuery, settingsQuery} from '@/sanity/lib/queries' +import {resolveOpenGraphImage} from '@/sanity/lib/utils' +import type {Metadata, ResolvingMetadata} from 'next' +import {defineQuery, type PortableTextBlock} from 'next-sanity' +import Link from 'next/link' +import {Suspense} from 'react' +import Avatar from '../../avatar' +import CoverImage from '../../cover-image' +import DateComponent from '../../date' +import MoreStories from '../../more-stories' +import PortableText from '../../portable-text' + +const postSlugs = defineQuery(`*[_type == "post" && defined(slug.current)]{"slug": slug.current}`) + +export async function generateStaticParams() { + const {data} = await sanityFetch({ + query: postSlugs, + perspective: 'published', + stega: false, + }) + return data +} + +export async function generateMetadata( + {params}: {params: Promise<{slug: string}>}, + parent: ResolvingMetadata, +): Promise { + const {slug} = await params + const {data: post} = await sanityFetch({query: postQuery, params: {slug}, stega: false}) + const previousImages = (await parent).openGraph?.images || [] + const ogImage = resolveOpenGraphImage(post?.coverImage) + + return { + authors: post?.author?.name ? [{name: post?.author?.name}] : [], + title: post?.title || '404 - Post not found', + description: post?.excerpt, + openGraph: { + images: ogImage ? [ogImage, ...previousImages] : previousImages, + }, + } satisfies Metadata +} + +export default async function PostPage({params}: {params: Promise<{slug: string}>}) { + const {slug} = await params + // const [{data: post}, {data: settings}] = await Promise.all([ + // sanityFetch({query: postQuery, params: {slug}}), + // sanityFetch({query: settingsQuery}), + // ]) + const {data: post} = await sanityFetch({query: postQuery, params: {slug}}) + + return ( +
+ + {async ({data: settings}) => { + 'use server' + return ( + <> + {settings?.title && ( +

+ + {settings.title} + +

+ )} + + ) + }} +
+ {post?._id ? ( + <> + + {async ({data: post}) => { + 'use server' + + if (!post?._id) { + return ( +

+ 404 - Post not found with slug: {slug} +

+ ) + } + + return ( +
+

+ {post.title} +

+
+ {post.author && ( + + )} +
+
+ +
+
+
+ {post.author && ( + + )} +
+
+
+ +
+
+
+ {post.content?.length && post.content.length > 0 && ( + + )} +
+ ) + }} +
+ + + ) : ( +

+ 404 - Post not found with slug: {slug} +

+ )} +
+ ) +} diff --git a/apps/live-next/next.config.ts b/apps/live-next/next.config.ts new file mode 100644 index 000000000..051a32b1a --- /dev/null +++ b/apps/live-next/next.config.ts @@ -0,0 +1,22 @@ +import withBundleAnalyzer from '@next/bundle-analyzer' +import type {NextConfig} from 'next' + +const nextConfig: NextConfig = { + logging: { + fetches: { + fullUrl: true, + }, + }, + + images: { + remotePatterns: [{hostname: 'cdn.sanity.io'}, {hostname: 'source.unsplash.com'}], + }, + + experimental: { + reactCompiler: true, + }, +} satisfies NextConfig + +export default withBundleAnalyzer({ + enabled: process.env.ANALYZE === 'true', +})(nextConfig) diff --git a/apps/live-next/package.json b/apps/live-next/package.json new file mode 100644 index 000000000..62d9e68c5 --- /dev/null +++ b/apps/live-next/package.json @@ -0,0 +1,49 @@ +{ + "name": "live-next", + "version": "0.0.0", + "private": true, + "scripts": { + "prebuild": "npm run typegen", + "build": "next build", + "debug": "NEXT_PRIVATE_DEBUG_CACHE=1 next build --profile && NEXT_PRIVATE_DEBUG_CACHE=1 next start -p 3009", + "predev": "npm run typegen", + "dev": "next dev -p 3009 --turbo", + "lint": "next lint", + "start": "next start", + "typegen": "sanity typegen generate" + }, + "prettier": "@repo/prettier-config", + "dependencies": { + "@repo/env": "workspace:*", + "@repo/sanity-extracted-schema": "workspace:*", + "@repo/studio-url": "workspace:*", + "@sanity/image-url": "^1.0.2", + "@sanity/next-loader": "workspace:*", + "@sanity/preview-url-secret": "workspace:*", + "@sanity/visual-editing": "workspace:*", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^20.14.13", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vercel/speed-insights": "^1.0.12", + "autoprefixer": "^10.4.20", + "babel-plugin-react-compiler": "0.0.0-experimental-fa06e2c-20241014", + "date-fns": "^4.1.0", + "framer-motion": "12.0.0-alpha.1", + "next": "15.0.0-canary.195", + "next-sanity": "9.7.0-canary.15", + "postcss": "^8.4.47", + "react": "19.0.0-rc-fb9a90fa48-20240614", + "react-dom": "19.0.0-rc-fb9a90fa48-20240614", + "server-only": "^0.0.1", + "tailwindcss": "^3.4.14", + "typescript": "5.6.3" + }, + "devDependencies": { + "@next/bundle-analyzer": "15.0.0-canary.195", + "@repo/prettier-config": "workspace:*", + "eslint": "^8.57.1", + "eslint-config-next": "15.0.0-canary.195", + "eslint-plugin-react-compiler": "0.0.0-experimental-fa06e2c-20241014" + } +} diff --git a/apps/next-no-cache/postcss.config.js b/apps/live-next/postcss.config.js similarity index 100% rename from apps/next-no-cache/postcss.config.js rename to apps/live-next/postcss.config.js diff --git a/apps/live-next/sanity-typegen.json b/apps/live-next/sanity-typegen.json new file mode 100644 index 000000000..8e8645f16 --- /dev/null +++ b/apps/live-next/sanity-typegen.json @@ -0,0 +1,3 @@ +{ + "schema": "./node_modules/@repo/sanity-extracted-schema/live-demo.json" +} diff --git a/apps/live-next/sanity.types.ts b/apps/live-next/sanity.types.ts new file mode 100644 index 000000000..411daeb67 --- /dev/null +++ b/apps/live-next/sanity.types.ts @@ -0,0 +1,695 @@ +// Query TypeMap +import '@sanity/client' + +/** + * --------------------------------------------------------------------------------- + * This file has been generated by Sanity TypeGen. + * Command: `sanity typegen generate` + * + * Any modifications made directly to this file will be overwritten the next time + * the TypeScript definitions are generated. Please make changes to the Sanity + * schema definitions and/or GROQ queries if you need to update these types. + * + * For more information on how to use Sanity TypeGen, visit the official documentation: + * https://www.sanity.io/docs/sanity-typegen + * --------------------------------------------------------------------------------- + */ + +// Source: schema.json +export type SanityImagePaletteSwatch = { + _type: 'sanity.imagePaletteSwatch' + background?: string + foreground?: string + population?: number + title?: string +} + +export type SanityImagePalette = { + _type: 'sanity.imagePalette' + darkMuted?: SanityImagePaletteSwatch + lightVibrant?: SanityImagePaletteSwatch + darkVibrant?: SanityImagePaletteSwatch + vibrant?: SanityImagePaletteSwatch + dominant?: SanityImagePaletteSwatch + lightMuted?: SanityImagePaletteSwatch + muted?: SanityImagePaletteSwatch +} + +export type SanityImageDimensions = { + _type: 'sanity.imageDimensions' + height?: number + width?: number + aspectRatio?: number +} + +export type SanityFileAsset = { + _id: string + _type: 'sanity.fileAsset' + _createdAt: string + _updatedAt: string + _rev: string + originalFilename?: string + label?: string + title?: string + description?: string + altText?: string + sha1hash?: string + extension?: string + mimeType?: string + size?: number + assetId?: string + uploadId?: string + path?: string + url?: string + source?: SanityAssetSourceData +} + +export type Geopoint = { + _type: 'geopoint' + lat?: number + lng?: number + alt?: number +} + +export type Post = { + _id: string + _type: 'post' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + slug?: Slug + content?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> + excerpt?: string + coverImage?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } + date?: string + author?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'author' + } +} + +export type Author = { + _id: string + _type: 'author' + _createdAt: string + _updatedAt: string + _rev: string + name?: string + picture?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } +} + +export type Slug = { + _type: 'slug' + current?: string + source?: string +} + +export type Settings = { + _id: string + _type: 'settings' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + description?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: never + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> + theme?: { + background?: Color + text?: Color + } + footer?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> + ogImage?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + metadataBase?: string + _type: 'image' + } +} + +export type SanityImageCrop = { + _type: 'sanity.imageCrop' + top?: number + bottom?: number + left?: number + right?: number +} + +export type SanityImageHotspot = { + _type: 'sanity.imageHotspot' + x?: number + y?: number + height?: number + width?: number +} + +export type SanityImageAsset = { + _id: string + _type: 'sanity.imageAsset' + _createdAt: string + _updatedAt: string + _rev: string + originalFilename?: string + label?: string + title?: string + description?: string + altText?: string + sha1hash?: string + extension?: string + mimeType?: string + size?: number + assetId?: string + uploadId?: string + path?: string + url?: string + metadata?: SanityImageMetadata + source?: SanityAssetSourceData +} + +export type SanityAssetSourceData = { + _type: 'sanity.assetSourceData' + name?: string + id?: string + url?: string +} + +export type SanityImageMetadata = { + _type: 'sanity.imageMetadata' + location?: Geopoint + dimensions?: SanityImageDimensions + palette?: SanityImagePalette + lqip?: string + blurHash?: string + hasAlpha?: boolean + isOpaque?: boolean +} + +export type SanityAssistInstructionTask = { + _type: 'sanity.assist.instructionTask' + path?: string + instructionKey?: string + started?: string + updated?: string + info?: string +} + +export type SanityAssistTaskStatus = { + _type: 'sanity.assist.task.status' + tasks?: Array< + { + _key: string + } & SanityAssistInstructionTask + > +} + +export type SanityAssistSchemaTypeAnnotations = { + _type: 'sanity.assist.schemaType.annotations' + title?: string + fields?: Array< + { + _key: string + } & SanityAssistSchemaTypeField + > +} + +export type SanityAssistOutputType = { + _type: 'sanity.assist.output.type' + type?: string +} + +export type SanityAssistOutputField = { + _type: 'sanity.assist.output.field' + path?: string +} + +export type SanityAssistInstructionContext = { + _type: 'sanity.assist.instruction.context' + reference?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'assist.instruction.context' + } +} + +export type AssistInstructionContext = { + _id: string + _type: 'assist.instruction.context' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + context?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: never + markDefs?: null + level?: number + _type: 'block' + _key: string + }> +} + +export type SanityAssistInstructionUserInput = { + _type: 'sanity.assist.instruction.userInput' + message?: string + description?: string +} + +export type SanityAssistInstructionPrompt = Array<{ + children?: Array< + | { + marks?: Array + text?: string + _type: 'span' + _key: string + } + | ({ + _key: string + } & SanityAssistInstructionFieldRef) + | ({ + _key: string + } & SanityAssistInstructionContext) + | ({ + _key: string + } & SanityAssistInstructionUserInput) + > + style?: 'normal' + listItem?: never + markDefs?: null + level?: number + _type: 'block' + _key: string +}> + +export type SanityAssistInstructionFieldRef = { + _type: 'sanity.assist.instruction.fieldRef' + path?: string +} + +export type SanityAssistInstruction = { + _type: 'sanity.assist.instruction' + prompt?: SanityAssistInstructionPrompt + icon?: string + title?: string + userId?: string + createdById?: string + output?: Array< + | ({ + _key: string + } & SanityAssistOutputField) + | ({ + _key: string + } & SanityAssistOutputType) + > +} + +export type SanityAssistSchemaTypeField = { + _type: 'sanity.assist.schemaType.field' + path?: string + instructions?: Array< + { + _key: string + } & SanityAssistInstruction + > +} + +export type Color = { + _type: 'color' + hex?: string + alpha?: number + hsl?: HslaColor + hsv?: HsvaColor + rgb?: RgbaColor +} + +export type RgbaColor = { + _type: 'rgbaColor' + r?: number + g?: number + b?: number + a?: number +} + +export type HsvaColor = { + _type: 'hsvaColor' + h?: number + s?: number + v?: number + a?: number +} + +export type HslaColor = { + _type: 'hslaColor' + h?: number + s?: number + l?: number + a?: number +} + +export type AllSanitySchemaTypes = + | SanityImagePaletteSwatch + | SanityImagePalette + | SanityImageDimensions + | SanityFileAsset + | Geopoint + | Post + | Author + | Slug + | Settings + | SanityImageCrop + | SanityImageHotspot + | SanityImageAsset + | SanityAssetSourceData + | SanityImageMetadata + | SanityAssistInstructionTask + | SanityAssistTaskStatus + | SanityAssistSchemaTypeAnnotations + | SanityAssistOutputType + | SanityAssistOutputField + | SanityAssistInstructionContext + | AssistInstructionContext + | SanityAssistInstructionUserInput + | SanityAssistInstructionPrompt + | SanityAssistInstructionFieldRef + | SanityAssistInstruction + | SanityAssistSchemaTypeField + | Color + | RgbaColor + | HsvaColor + | HslaColor +export declare const internalGroqTypeReferenceTo: unique symbol +// Source: ./sanity/lib/queries.ts +// Variable: settingsQuery +// Query: *[_type == "settings"][0] +export type SettingsQueryResult = { + _id: string + _type: 'settings' + _createdAt: string + _updatedAt: string + _rev: string + title?: string + description?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'normal' + listItem?: never + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> + theme?: { + background?: Color + text?: Color + } + footer?: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> + ogImage?: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + metadataBase?: string + _type: 'image' + } +} | null +// Variable: heroQuery +// Query: *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] { content, _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } +export type HeroQueryResult = { + content: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> | null + _id: string + status: 'draft' | 'published' + title: string | 'Untitled' + slug: string | null + excerpt: string | null + coverImage: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + date: string + author: { + name: string | 'Anonymous' + picture: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + } | null +} | null +// Variable: moreStoriesQuery +// Query: *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } +export type MoreStoriesQueryResult = Array<{ + _id: string + status: 'draft' | 'published' + title: string | 'Untitled' + slug: string | null + excerpt: string | null + coverImage: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + date: string + author: { + name: string | 'Anonymous' + picture: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + } | null +}> +// Variable: postQuery +// Query: *[_type == "post" && slug.current == $slug] [0] { content, _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture}, } +export type PostQueryResult = { + content: Array<{ + children?: Array<{ + marks?: Array + text?: string + _type: 'span' + _key: string + }> + style?: 'blockquote' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal' + listItem?: 'bullet' | 'number' + markDefs?: Array<{ + href?: string + _type: 'link' + _key: string + }> + level?: number + _type: 'block' + _key: string + }> | null + _id: string + status: 'draft' | 'published' + title: string | 'Untitled' + slug: string | null + excerpt: string | null + coverImage: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + date: string + author: { + name: string | 'Anonymous' + picture: { + asset?: { + _ref: string + _type: 'reference' + _weak?: boolean + [internalGroqTypeReferenceTo]?: 'sanity.imageAsset' + } + hotspot?: SanityImageHotspot + crop?: SanityImageCrop + alt?: string + _type: 'image' + } | null + } | null +} | null + +// Source: ./app/posts/[slug]/page.tsx +// Variable: postSlugs +// Query: *[_type == "post" && defined(slug.current)]{"slug": slug.current} +export type PostSlugsResult = Array<{ + slug: string | null +}> + +declare module '@sanity/client' { + interface SanityQueries { + '*[_type == "settings"][0]': SettingsQueryResult + '\n *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] {\n content,\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': HeroQueryResult + '\n *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] {\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': MoreStoriesQueryResult + '\n *[_type == "post" && slug.current == $slug] [0] {\n content,\n \n _id,\n "status": select(_originalId in path("drafts.**") => "draft", "published"),\n "title": coalesce(title, "Untitled"),\n "slug": slug.current,\n excerpt,\n coverImage,\n "date": coalesce(date, _updatedAt),\n "author": author->{"name": coalesce(name, "Anonymous"), picture},\n\n }\n': PostQueryResult + '*[_type == "post" && defined(slug.current)]{"slug": slug.current}': PostSlugsResult + } +} diff --git a/apps/live-next/sanity/lib/api.ts b/apps/live-next/sanity/lib/api.ts new file mode 100644 index 000000000..15e942e4b --- /dev/null +++ b/apps/live-next/sanity/lib/api.ts @@ -0,0 +1,5 @@ +import {workspaces} from '@repo/env' + +export const {projectId, dataset} = workspaces['live-demo'] + +export {apiVersion} from '@repo/env' diff --git a/apps/live-next/sanity/lib/client.ts b/apps/live-next/sanity/lib/client.ts new file mode 100644 index 000000000..9b17b0b9c --- /dev/null +++ b/apps/live-next/sanity/lib/client.ts @@ -0,0 +1,34 @@ +import {apiVersion, dataset, projectId} from '@/sanity/lib/api' +import {workspaces} from '@repo/env' +import {studioUrl as baseUrl} from '@repo/studio-url' +import {createClient} from 'next-sanity' + +export const client = createClient({ + projectId, + dataset, + apiVersion, + useCdn: true, + perspective: 'published', + // A token is needed since we're using a private dataset + token: process.env.SANITY_API_READ_TOKEN, + stega: { + studioUrl: (sourceDocument) => { + if ( + sourceDocument._projectId === workspaces['cross-dataset-references'].projectId && + sourceDocument._dataset === workspaces['cross-dataset-references'].dataset + ) { + const {workspace, tool} = workspaces['cross-dataset-references'] + return {baseUrl, workspace, tool} + } + const {workspace, tool} = workspaces['live-demo'] + return {baseUrl, workspace, tool} + }, + filter: (props) => { + if (props.sourcePath.at(-1) === 'title') { + return true + } + + return props.filterDefault(props) + }, + }, +}) diff --git a/apps/live-next/sanity/lib/live.ts b/apps/live-next/sanity/lib/live.ts new file mode 100644 index 000000000..b3ff52fd6 --- /dev/null +++ b/apps/live-next/sanity/lib/live.ts @@ -0,0 +1,24 @@ +import 'server-only' +import {defineLive} from '@sanity/next-loader' +import {client} from './client' + +const serverToken = process.env.SANITY_API_READ_TOKEN +const browserToken = process.env.SANITY_API_BROWSER_TOKEN + +if (!serverToken) { + throw new Error('Missing SANITY_API_READ_TOKEN') +} +if (!browserToken) { + throw new Error('Missing SANITY_API_BROWSER_TOKEN') +} + +export const { + sanityFetch, + SanityLive, + SanityLiveStream, + // verifyPreviewSecret +} = defineLive({ + client, + serverToken, + browserToken, +}) diff --git a/apps/live-next/sanity/lib/queries.ts b/apps/live-next/sanity/lib/queries.ts new file mode 100644 index 000000000..34906f5c9 --- /dev/null +++ b/apps/live-next/sanity/lib/queries.ts @@ -0,0 +1,34 @@ +import {defineQuery} from 'next-sanity' + +export const settingsQuery = defineQuery(`*[_type == "settings"][0]`) + +const postFields = /* groq */ ` + _id, + "status": select(_originalId in path("drafts.**") => "draft", "published"), + "title": coalesce(title, "Untitled"), + "slug": slug.current, + excerpt, + coverImage, + "date": coalesce(date, _updatedAt), + "author": author->{"name": coalesce(name, "Anonymous"), picture}, +` + +export const heroQuery = defineQuery(` + *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] { + content, + ${postFields} + } +`) + +export const moreStoriesQuery = defineQuery(` + *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { + ${postFields} + } +`) + +export const postQuery = defineQuery(` + *[_type == "post" && slug.current == $slug] [0] { + content, + ${postFields} + } +`) diff --git a/apps/live-next/sanity/lib/utils.ts b/apps/live-next/sanity/lib/utils.ts new file mode 100644 index 000000000..ad7f84e5a --- /dev/null +++ b/apps/live-next/sanity/lib/utils.ts @@ -0,0 +1,23 @@ +import {dataset, projectId} from '@/sanity/lib/api' +import createImageUrlBuilder from '@sanity/image-url' + +const imageBuilder = createImageUrlBuilder({ + projectId: projectId || '', + dataset: dataset || '', +}) + +export const urlForImage = (source: any) => { + // Ensure that source image contains a valid reference + if (!source?.asset?._ref) { + return undefined + } + + return imageBuilder?.image(source).auto('format').fit('max') +} + +export function resolveOpenGraphImage(image: any, width = 1200, height = 627) { + if (!image) return + const url = urlForImage(image)?.width(1200).height(627).fit('crop').url() + if (!url) return + return {url, alt: image?.alt as string, width, height} +} diff --git a/apps/live-next/tailwind.config.ts b/apps/live-next/tailwind.config.ts new file mode 100644 index 000000000..38b39aa28 --- /dev/null +++ b/apps/live-next/tailwind.config.ts @@ -0,0 +1,25 @@ +import typography from '@tailwindcss/typography' +import type {Config} from 'tailwindcss' + +export default { + content: ['./app/**/*.{ts,tsx}', './sanity/**/*.{ts,tsx}'], + theme: { + extend: { + backgroundColor: { + 'theme': 'var(--theme-background,#fff)', + 'theme-inverse': 'var(--theme-text,#fff)', + }, + fontFamily: { + sans: ['var(--font-inter)'], + }, + textColor: { + 'theme': 'var(--theme-text,#000)', + 'theme-inverse': 'var(--theme-background,#fff)', + }, + }, + }, + future: { + hoverOnlyWhenSupported: true, + }, + plugins: [typography], +} satisfies Config diff --git a/apps/next-no-cache/tsconfig.json b/apps/live-next/tsconfig.json similarity index 61% rename from apps/next-no-cache/tsconfig.json rename to apps/live-next/tsconfig.json index 3a476cd77..2d9e949e1 100644 --- a/apps/next-no-cache/tsconfig.json +++ b/apps/live-next/tsconfig.json @@ -1,15 +1,13 @@ { "compilerOptions": { - "target": "es2022", + "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, + "forceConsistentCasingInFileNames": true, "noEmit": true, - "esModuleInterop": true, "module": "preserve", - "moduleResolution": "bundler", - "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, @@ -19,14 +17,9 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/apps/next-server-only/turbo.json b/apps/live-next/turbo.json similarity index 50% rename from apps/next-server-only/turbo.json rename to apps/live-next/turbo.json index ff06c68ff..ab6e5ea66 100644 --- a/apps/next-server-only/turbo.json +++ b/apps/live-next/turbo.json @@ -4,7 +4,14 @@ "tasks": { "build": { "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"], - "env": ["NEXT_PUBLIC_SANITY_*", "NEXT_PUBLIC_VERCEL_ENV"], + "env": [ + "NEXT_PUBLIC_SANITY_*", + "NEXT_PUBLIC_VERCEL_ENV", + "SANITY_API_READ_TOKEN", + "SANITY_API_WRITE_TOKEN", + "SANITY_API_BROWSER_TOKEN", + "ANALYZE" + ], "outputs": [".next/**", "!.next/cache/**"] } } diff --git a/apps/next-no-cache/.eslintrc.json b/apps/next-no-cache/.eslintrc.json deleted file mode 100644 index bffb357a7..000000000 --- a/apps/next-no-cache/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/apps/next-no-cache/README.md b/apps/next-no-cache/README.md deleted file mode 100644 index c4033664f..000000000 --- a/apps/next-no-cache/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/next-no-cache/next.config.mjs b/apps/next-no-cache/next.config.mjs deleted file mode 100644 index 89fe90acc..000000000 --- a/apps/next-no-cache/next.config.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import path from 'node:path' -import withBundleAnalyzer from '@next/bundle-analyzer' -import sanityPkg from 'sanity/package.json' assert {type: 'json'} - -function requireResolve(id) { - return import.meta.resolve(id).replace('file://', '') -} - -const sanityExports = {} -for (const key of Object.keys(sanityPkg.exports)) { - if (key === '.') continue - const subexport = path.join('sanity', key) - sanityExports[subexport] = requireResolve(subexport) -} -sanityExports['sanity'] = requireResolve('sanity') - -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - // /* - logging: { - fetches: { - fullUrl: true, - }, - }, - // */ - - typescript: { - // @TODO figure out why TSC fails in the Next.js process but works fine when running `tsc --noEmit` - ignoreBuildErrors: true, - }, - - transpilePackages: ['apps-common'], - - images: { - remotePatterns: [{hostname: 'cdn.sanity.io'}, {hostname: 'source.unsplash.com'}], - }, - - experimental: { - taint: true, - }, - - webpack(config) { - config.resolve.alias = { - ...config.resolve.alias, - ...sanityExports, - '@sanity/presentation': requireResolve('@sanity/presentation'), - '@sanity/vision': requireResolve('@sanity/vision'), - '@sanity/ui/theme': requireResolve('@sanity/ui/theme'), - '@sanity/ui': requireResolve('@sanity/ui'), - 'styled-components': requireResolve('styled-components'), - } - return config - }, -} - -export default withBundleAnalyzer({ - enabled: process.env.ANALYZE === 'true', -})(nextConfig) diff --git a/apps/next-no-cache/package.json b/apps/next-no-cache/package.json deleted file mode 100644 index a5f83f45a..000000000 --- a/apps/next-no-cache/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "next-no-cache", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "next build", - "dev": "next dev", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@portabletext/react": "^3.1.0", - "@repo/channels": "workspace:*", - "@repo/visual-editing-helpers": "workspace:*", - "@sanity/client": "^6.21.3", - "@sanity/icons": "^3.4.0", - "@sanity/image-url": "^1.0.2", - "@sanity/presentation": "workspace:*", - "@sanity/preview-kit-compat": "workspace:*", - "@sanity/preview-url-secret": "workspace:*", - "@sanity/react-loader": "workspace:*", - "@sanity/vision": "^3.57.2", - "@sanity/visual-editing": "workspace:*", - "@tailwindcss/typography": "^0.5.15", - "@types/node": "^20.5.0", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vercel/speed-insights": "^1.0.12", - "apps-common": "workspace:*", - "autoprefixer": "^10.4.20", - "classnames": "^2.5.1", - "date-fns": "^3.6.0", - "fast-deep-equal": "3.1.3", - "next": "15.0.0-canary.148", - "next-sanity": "9.4.7", - "react": "19.0.0-rc-fb9a90fa48-20240614", - "react-dom": "19.0.0-rc-fb9a90fa48-20240614", - "react-wrap-balancer": "^1.1.1", - "sanity": "^3.57.2", - "sanity-plugin-asset-source-unsplash": "^3.0.1", - "server-only": "^0.0.1", - "styled-components": "^6.1.13" - }, - "devDependencies": { - "@next/bundle-analyzer": "15.0.0-canary.148", - "@types/node": "^20", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", - "eslint-config-next": "14.3.0-canary.87", - "postcss": "^8.4.45", - "tailwindcss": "^3.4.10", - "typescript": "5.6.2" - } -} diff --git a/apps/next-no-cache/public/next.svg b/apps/next-no-cache/public/next.svg deleted file mode 100644 index 5174b28c5..000000000 --- a/apps/next-no-cache/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/next-no-cache/public/vercel.svg b/apps/next-no-cache/public/vercel.svg deleted file mode 100644 index d2f842227..000000000 --- a/apps/next-no-cache/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/next-no-cache/sanity.cli.ts b/apps/next-no-cache/sanity.cli.ts deleted file mode 100644 index f47497f71..000000000 --- a/apps/next-no-cache/sanity.cli.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This configuration file lets you run `$ sanity [command]` in this folder - * Go to https://www.sanity.io/docs/cli to learn more. - **/ -import {loadEnvConfig} from '@next/env' -import {defineCliConfig} from 'sanity/cli' -import {projectId, datasets} from 'apps-common/env' - -const dev = process.env.NODE_ENV !== 'production' -loadEnvConfig(__dirname, dev, {info: () => null, error: console.error}) - -const dataset = datasets['blog'] - -export default defineCliConfig({ - api: {projectId, dataset}, - vite: (config) => { - return { - ...config, - envPrefix: ['NEXT_', 'SANITY_STUDIO_', 'VITE_'], - } - }, -}) diff --git a/apps/next-no-cache/sanity.config.ts b/apps/next-no-cache/sanity.config.ts deleted file mode 100644 index eee9c6f66..000000000 --- a/apps/next-no-cache/sanity.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -'use client' -/** - * This configuration is used to for the Sanity Studio that’s mounted on the `/app/studio/[[...index]]/page.tsx` route - */ - -import {visionTool} from '@sanity/vision' -import {defineConfig} from 'sanity' -import {presentationTool} from 'sanity/presentation' -import {structureTool} from 'sanity/structure' -import {unsplashImageAsset} from 'sanity-plugin-asset-source-unsplash' - -// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works -import {apiVersion, dataset, projectId} from '@/lib/env' -import {types} from '@/lib/sanity/schema' - -const config = defineConfig({ - basePath: '/studio', - projectId, - dataset, - // Add and edit the content schema in the './src/sanity/schema' folder - schema: {types}, - plugins: [ - presentationTool({ - previewUrl: { - previewMode: { - enable: '/api/draft', - disable: 'api/disable-draft', - }, - }, - }), - structureTool(), - // Add an image asset source for Unsplash - unsplashImageAsset(), - // Vision is a tool that lets you query your content with GROQ in the studio - // https://www.sanity.io/docs/the-vision-plugin - visionTool({defaultApiVersion: apiVersion}), - ], -}) -export default config diff --git a/apps/next-no-cache/src/app/(blog)/AuthorAvatar.tsx b/apps/next-no-cache/src/app/(blog)/AuthorAvatar.tsx deleted file mode 100644 index ec0eeeafa..000000000 --- a/apps/next-no-cache/src/app/(blog)/AuthorAvatar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import {urlForImage} from '@/lib/image' -import {loadQuery} from '@/lib/loadQuery' -import Image from 'next/image' -import Balancer from 'react-wrap-balancer' - -const query = /* groq */ `*[_type == "author" && _id == $id][0]` - -export async function AuthorAvatar(params: {id: string}) { - const data = await loadQuery({ - query, - params, - }) - const {name = 'Anonymous', image} = data ?? {} - return ( - <> -
-
- {image?.alt -
-
- {name} -
-
- - ) -} - -export function AuthorAvatarFallback() { - return ( -
-
-
Fetching author…
-
- ) -} diff --git a/apps/next-no-cache/src/app/(blog)/BlogHeader.tsx b/apps/next-no-cache/src/app/(blog)/BlogHeader.tsx deleted file mode 100644 index 9f91037c7..000000000 --- a/apps/next-no-cache/src/app/(blog)/BlogHeader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client' - -import Link from 'next/link' -import {usePathname} from 'next/navigation' -import Balancer from 'react-wrap-balancer' - -export default function BlogHeader({title}: {title: string}) { - const pathname = usePathname() - if (pathname === '/') { - return ( -
-

- {title} -

-
- ) - } - return ( -
-

- - {title} - -

-
- ) -} diff --git a/apps/next-no-cache/src/app/(blog)/CoverImage.tsx b/apps/next-no-cache/src/app/(blog)/CoverImage.tsx deleted file mode 100644 index 75c3c491e..000000000 --- a/apps/next-no-cache/src/app/(blog)/CoverImage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {urlForImage} from '@/lib/image' -import cn from 'classnames' -import Image from 'next/image' -import Link from 'next/link' - -interface CoverImageProps { - title: string - slug?: string - image?: {asset?: any; alt?: string} - priority?: boolean -} - -export default function CoverImage(props: CoverImageProps) { - const {title, slug, image: source, priority} = props - const image = source?.asset?._ref ? ( -
- {source?.alt -
- ) : ( -
- ) - - return ( -
- {slug ? ( - - {image} - - ) : ( - image - )} -
- ) -} diff --git a/apps/next-no-cache/src/app/(blog)/MoreStories.tsx b/apps/next-no-cache/src/app/(blog)/MoreStories.tsx deleted file mode 100644 index 0a34f2889..000000000 --- a/apps/next-no-cache/src/app/(blog)/MoreStories.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Link from 'next/link' -import {Suspense} from 'react' - -import {postFields} from '@/lib/queries' -import {AuthorAvatar, AuthorAvatarFallback} from './AuthorAvatar' -import CoverImage from './CoverImage' -import Date from './PostDate' -import {loadQuery} from '@/lib/loadQuery' - -const query = /* groq */ `*[_type == "post" && _id != $skip] | order(date desc, _updatedAt desc) [0...$limit] { - ${postFields} -}` - -export default async function MoreStories(params: {skip: string; limit: number}) { - const data = await loadQuery({ - query, - params, - }) - - return ( - <> -
- {data.map((post: any) => { - const {_id, title = 'Untitled', slug, mainImage, excerpt, author} = post - return ( -
-
- -
-

- {slug ? ( - - {title} - - ) : ( - title - )} -

-
- -
- {excerpt &&

{excerpt}

} - {author?._ref && ( - }> - - - )} -
- ) - })} -
- - ) -} diff --git a/apps/next-no-cache/src/app/(blog)/PostBody.module.css b/apps/next-no-cache/src/app/(blog)/PostBody.module.css deleted file mode 100644 index 87f49dffc..000000000 --- a/apps/next-no-cache/src/app/(blog)/PostBody.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.portableText { - @apply text-lg leading-relaxed; -} - -.portableText p, -.portableText ul, -.portableText ol, -.portableText blockquote { - @apply my-6; -} - -.portableText h2 { - @apply mb-4 mt-12 text-3xl leading-snug; -} - -.portableText h3 { - @apply mb-4 mt-8 text-2xl leading-snug; -} - -.portableText a { - @apply text-blue-500 underline; -} -.portableText a:hover { - @apply text-blue-800; -} diff --git a/apps/next-no-cache/src/app/(blog)/PostBody.tsx b/apps/next-no-cache/src/app/(blog)/PostBody.tsx deleted file mode 100644 index a9a1146e1..000000000 --- a/apps/next-no-cache/src/app/(blog)/PostBody.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * This component uses Portable Text to render a post body. - * - * You can learn more about Portable Text on: - * https://www.sanity.io/docs/block-content - * https://github.com/portabletext/react-portabletext - * https://portabletext.org/ - * - */ -import {PortableText} from '@portabletext/react' - -import styles from './PostBody.module.css' - -export default function PostBody({ - body, -}: { - body: React.ComponentProps['value'] -}) { - return ( -
- -
- ) -} diff --git a/apps/next-no-cache/src/app/(blog)/PostDate.tsx b/apps/next-no-cache/src/app/(blog)/PostDate.tsx deleted file mode 100644 index 873577100..000000000 --- a/apps/next-no-cache/src/app/(blog)/PostDate.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {format, parseISO} from 'date-fns' -import Balancer from 'react-wrap-balancer' - -export default function PostDate({dateString}: {dateString: string}) { - const date = parseISO(dateString) - return ( - - ) -} diff --git a/apps/next-no-cache/src/app/(blog)/PreviewBanner.tsx b/apps/next-no-cache/src/app/(blog)/PreviewBanner.tsx deleted file mode 100644 index 76f7176eb..000000000 --- a/apps/next-no-cache/src/app/(blog)/PreviewBanner.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default function PreviewBanner() { - return ( -
- {'Previewing drafts. '} - - Back to published - -
- ) -} diff --git a/apps/next-no-cache/src/app/(blog)/[slug]/page.tsx b/apps/next-no-cache/src/app/(blog)/[slug]/page.tsx deleted file mode 100644 index 0b927de49..000000000 --- a/apps/next-no-cache/src/app/(blog)/[slug]/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type {Metadata, ResolvingMetadata} from 'next' -import {notFound} from 'next/navigation' -import {Suspense} from 'react' -import Balancer from 'react-wrap-balancer' - -import {AuthorAvatar, AuthorAvatarFallback} from '../AuthorAvatar' -import CoverImage from '../CoverImage' -import MoreStories from '../MoreStories' -import PostBody from '../PostBody' -import PostDate from '../PostDate' -import {urlForImage} from '@/lib/image' -import {postFields} from '@/lib/queries' -import {loadQuery} from '@/lib/loadQuery' - -type Props = { - params: {slug: string} -} - -const query = /* groq */ `*[_type == "post" && slug.current == $slug][0] { - ${postFields} -}` - -export async function generateMetadata( - {params}: Props, - parent: ResolvingMetadata, -): Promise { - const [post, authorName] = await Promise.all([ - loadQuery({query, params}), - // @TODO necessary as there's problems with type inference when `author-{name,image}` is used - loadQuery({ - query: /* groq */ `*[_type == "post" && slug.current == $slug][0].author->name`, - params, - }), - ]) - // optionally access and extend (rather than replace) parent metadata - const parentTitle = (await parent).title?.absolute - const previousImages = (await parent).openGraph?.images || [] - - return { - authors: authorName ? [{name: authorName}] : [], - title: `${parentTitle} | ${post?.title}`, - openGraph: { - images: post?.mainImage?.asset?._ref - ? [urlForImage(post.mainImage).height(1000).width(2000).url(), ...previousImages] - : previousImages, - }, - } satisfies Metadata -} - -export default async function BlogPostPage({params}: Props) { - const {slug} = params - const data = await loadQuery({ - query, - params, - }) - - if (!data) { - return notFound() - } - - const {_id, title = 'Untitled', author, mainImage, body} = data ?? {} - return ( - <> -
-

- {title} -

-
- {author?._ref && ( - }> - - - )} -
-
- -
-
-
- {author?._ref && ( - }> - - - )} -
-
- -
-
- -
- - - ) -} diff --git a/apps/next-no-cache/src/app/(blog)/layout.tsx b/apps/next-no-cache/src/app/(blog)/layout.tsx deleted file mode 100644 index 9e6bd2dd6..000000000 --- a/apps/next-no-cache/src/app/(blog)/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import Link from 'next/link' - -import BlogHeader from './BlogHeader' -import {EXAMPLE_NAME} from '@/lib/constants' -import PreviewBanner from './PreviewBanner' -import {draftMode} from 'next/headers' -import {VisualEditing} from 'next-sanity' - -export default function BlogLayout({children}: {children: React.ReactNode}) { - return ( - <> - {draftMode().isEnabled && } -
- -
{children}
-
-
- - Studio - -
-
- {draftMode().isEnabled && } - - ) -} diff --git a/apps/next-no-cache/src/app/(blog)/page.tsx b/apps/next-no-cache/src/app/(blog)/page.tsx deleted file mode 100644 index 84284d490..000000000 --- a/apps/next-no-cache/src/app/(blog)/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Link from 'next/link' -import {Suspense} from 'react' -import Balancer from 'react-wrap-balancer' - -import {AuthorAvatar, AuthorAvatarFallback} from './AuthorAvatar' -import CoverImage from './CoverImage' -import MoreStories from './MoreStories' -import Date from './PostDate' -import {postFields} from '@/lib/queries' -import {loadQuery} from '@/lib/loadQuery' - -export default async function BlogIndexPage() { - const data = await loadQuery({ - query: /* groq */ ` -*[_type == "post"] | order(publishedAt desc, _updatedAt desc) [0] { - ${postFields} -}`, - }) - const {_id, author, excerpt, mainImage, slug, title, publishedAt} = data ?? {} - - return ( - <> - {data && ( -
-
- -
-
-
-

- {slug ? ( - - {title} - - ) : ( - {title} - )} -

-
- -
-
-
- {excerpt && ( -

- {excerpt} -

- )} - {author?._ref && ( - }> - - - )} -
-
-
- )} - {_id && ( - - )} - - ) -} diff --git a/apps/next-no-cache/src/app/api/disable-draft/route.ts b/apps/next-no-cache/src/app/api/disable-draft/route.ts deleted file mode 100644 index 5feca1a1d..000000000 --- a/apps/next-no-cache/src/app/api/disable-draft/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {draftMode} from 'next/headers' -import {NextRequest, NextResponse} from 'next/server' - -export function GET(request: NextRequest) { - draftMode().disable() - const url = new URL(request.nextUrl) - return NextResponse.redirect(new URL('/', url.origin)) -} diff --git a/apps/next-no-cache/src/app/api/draft/route.ts b/apps/next-no-cache/src/app/api/draft/route.ts deleted file mode 100644 index 95568a060..000000000 --- a/apps/next-no-cache/src/app/api/draft/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {draftMode} from 'next/headers' -import {redirect} from 'next/navigation' -import {validatePreviewUrl} from '@sanity/preview-url-secret' -import {client} from '@/lib/client' - -const clientWithToken = client.withConfig({ - token: process.env.SANITY_API_READ_TOKEN, -}) - -export async function GET(req: Request) { - const {isValid, redirectTo = '/'} = await validatePreviewUrl(clientWithToken, req.url) - if (!isValid) { - return new Response('Invalid secret', {status: 401}) - } - - draftMode().enable() - - redirect(redirectTo) -} diff --git a/apps/next-no-cache/src/app/favicon.ico b/apps/next-no-cache/src/app/favicon.ico deleted file mode 100644 index 718d6fea4..000000000 Binary files a/apps/next-no-cache/src/app/favicon.ico and /dev/null differ diff --git a/apps/next-no-cache/src/app/globals.css b/apps/next-no-cache/src/app/globals.css deleted file mode 100644 index e0936eda7..000000000 --- a/apps/next-no-cache/src/app/globals.css +++ /dev/null @@ -1,23 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) - rgb(var(--background-start-rgb)); -} diff --git a/apps/next-no-cache/src/app/layout.tsx b/apps/next-no-cache/src/app/layout.tsx deleted file mode 100644 index 676b2d5b0..000000000 --- a/apps/next-no-cache/src/app/layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import './globals.css' - -import type {Metadata} from 'next' -import {Inter} from 'next/font/google' -import {SpeedInsights} from '@vercel/speed-insights/next' - -import {CMS_NAME} from '@/lib/constants' - -const inter = Inter({ - variable: '--font-inter', - subsets: ['latin'], - display: 'swap', -}) - -export const metadata = { - title: `Next.js and ${CMS_NAME} Example`, - description: `This is a blog built with Next.js and ${CMS_NAME}.`, -} satisfies Metadata - -export default function RootLayout({children}: {children: React.ReactNode}) { - return ( - - - {children} - - - - ) -} diff --git a/apps/next-no-cache/src/app/studio/[[...index]]/page.tsx b/apps/next-no-cache/src/app/studio/[[...index]]/page.tsx deleted file mode 100644 index 71cd1cb67..000000000 --- a/apps/next-no-cache/src/app/studio/[[...index]]/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route is responsible for the built-in authoring environment using Sanity Studio v3. - * All routes under /studio will be handled by this file using Next.js' catch-all routes: - * https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes - * - * You can learn more about the next-sanity package here: - * https://github.com/sanity-io/next-sanity - */ - -import {NextStudio} from 'next-sanity/studio' - -import config from '@/sanity.config' - -export const dynamic = 'force-static' - -export {metadata, viewport} from 'next-sanity/studio' - -export default function StudioPage() { - return -} diff --git a/apps/next-no-cache/src/lib/client.ts b/apps/next-no-cache/src/lib/client.ts deleted file mode 100644 index 4d3a20582..000000000 --- a/apps/next-no-cache/src/lib/client.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {createClient} from '@sanity/client' - -import {apiVersion, dataset, projectId} from './env' - -export const client = createClient({ - apiVersion, - dataset, - projectId, - useCdn: true, - perspective: 'published', - stega: { - studioUrl: '/studio', - }, -}) diff --git a/apps/next-no-cache/src/lib/constants.ts b/apps/next-no-cache/src/lib/constants.ts deleted file mode 100644 index 7ef30f1c0..000000000 --- a/apps/next-no-cache/src/lib/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const EXAMPLE_NAME = 'Blog' -export const EXAMPLE_PATH = 'cms-sanity' -export const CMS_NAME = 'Sanity' -export const CMS_URL = 'https://sanity.io/' -export const HOME_OG_IMAGE_URL = - 'https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Sanity**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTA1IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMWVtIj48dGl0bGU%2BU2FuaXR5PC90aXRsZT48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03OC4xNzkzIDcuOTkyNjFWMjEuMDAyOEg3My45MDMxVjEwLjIxMzhMNzguMTc5MyA3Ljk5MjYxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMjAuOTUxMSAyMS4zM0wzMC45NDQgMTYuMTA1MUwyOS43MTIxIDEyLjkxNDFMMjMuMTMzMiAxNS45ODIxTDIwLjk1MTEgMjEuMzNaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik03My45MDMxIDEwLjIwMjdMODQuNzQ0MyA0LjY1NDc3TDgyLjkxMjYgMS41NTcxTDczLjkwMzEgNS45NTk5N1YxMC4yMDI3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNNDMuMzcwNSA2Ljk2MjMzVjIxLjAwMjhIMzkuMjkyN1YxLjAwNzE0TDQzLjM3MDUgNi45NjIzM1oiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTI3LjEyOTkgNi4xODYxN0wyMC45NTExIDIxLjMzTDE3Ljc3MzEgMTguNTk0M0wyNS4xMzUzIDEuMDA3MTRMMjcuMTI5OSA2LjE4NjE3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTI1LjEzNTMgMS4wMDcxNEgyOS4zNDc3TDM3LjEzODYgMjEuMDAyOEgzMi44MjY5TDI1LjEzNTMgMS4wMDcxNFoiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIGQ9Ik00NC4wMDEyIDEuMDA3MTRMNTIuOTgyNCAxNC42NjgyVjIxLjAwMjhMMzkuMjkyNyAxLjAwNzE0SDQ0LjAwMTJaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNjQuOTE4MyAxLjAwNzE0SDYwLjY3MzlWMjEuMDA2M0g2NC45MTgzVjEuMDA3MTRaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNzMuOTAzMSA0LjY1NDc0SDY3LjM3VjEuMDA3MTRIODIuNTg2N0w4NC43NDQzIDQuNjU0NzRINzguMTc5M0g3My45MDMxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC41IiBkPSJNOTcuMjc1NCAxMy40MTUzVjIxLjAwMjhIOTMuMDYyOVYxMy40MTUzIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNOTMuMDYyOSAxMy40MTUyTDEwMC4xOTEgMS4wMDcxNEgxMDQuNjY2TDk3LjI3NTQgMTMuNDE1Mkg5My4wNjI5WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNOTMuMDYzIDEzLjQxNTJMODUuNzM2MyAxLjAwNzE0SDkwLjM0NTZMOTUuMzA5MiA5LjUxMDA4TDkzLjA2MyAxMy40MTUyWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTEuOTYxMjYgMy4zMTQ3OUMxLjk2MTI2IDYuMDk5MjEgMy43MTE0NSA3Ljc1NTk1IDcuMjE1MzYgOC42Mjk1NkwxMC45MjgzIDkuNDc1MzNDMTQuMjQ0NCAxMC4yMjM2IDE2LjI2MzkgMTIuMDgyMiAxNi4yNjM5IDE1LjExMDNDMTYuMjg5NyAxNi40Mjk1IDE1Ljg1MzEgMTcuNzE3MyAxNS4wMjc0IDE4Ljc1NzlDMTUuMDI3NCAxNS43MzY4IDEzLjQzNjcgMTQuMTA0NCA5LjU5OTcyIDEzLjEyMjlMNS45NTQwOSAxMi4zMDg1QzMuMDM0NzUgMTEuNjU0MSAwLjc4MTQ3OCAxMC4xMjYyIDAuNzgxNDc4IDYuODM3MDlDMC43NjYxMjMgNS41NjY5MyAxLjE4MTE2IDQuMzI3ODEgMS45NjEyNiAzLjMxNDc5IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik01Mi45ODI0IDEzLjY0MTVWMS4wMDcxNEg1Ny4wNjAyVjIxLjAwMjhINTIuOTgyNFYxMy42NDE1WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMTIuNzQ1OCAxNC4zNjg5QzE0LjMyOTQgMTUuMzY0MyAxNS4wMjM4IDE2Ljc1NjUgMTUuMDIzOCAxOC43NTQ0QzEzLjcxMyAyMC40MDQxIDExLjQxMDEgMjEuMzMgOC43MDMzMyAyMS4zM0M0LjE0NzE4IDIxLjMzIDAuOTU4NTc3IDE5LjEyNjggMC4yNSAxNS4yOTgySDQuNjI1NDdDNS4xODg3OCAxNy4wNTU5IDYuNjgwMzQgMTcuODcwMyA4LjY3MTQ0IDE3Ljg3MDNDMTEuMTAxOSAxNy44NzAzIDEyLjcxNzQgMTYuNTk2NCAxMi43NDkzIDE0LjM2MTkiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNyIgZD0iTTQuMjM1NjcgNy40NDI2N0MzLjUxMjUgNy4wMjA0NSAyLjkxOTIgNi40MTM3NSAyLjUxODczIDUuNjg2OTdDMi4xMTgyNyA0Ljk2MDE5IDEuOTI1NTggNC4xNDA0NSAxLjk2MTEzIDMuMzE0NzZDMy4yMjU5NCAxLjY3ODkxIDUuNDI2MDggMC42Nzk5OTMgOC4xMDgwNCAwLjY3OTk5M0MxMi43NDkyIDAuNjc5OTkzIDE1LjQzNDcgMy4wODg1MiAxNi4wOTcyIDYuNDc4NTZIMTEuODg4M0MxMS40MjQyIDUuMTQyMDMgMTAuMjYyMSA0LjEwMTM2IDguMTQzNDcgNC4xMDEzNkM1Ljg3OTU3IDQuMTAxMzYgNC4zMzQ4NyA1LjM5NjExIDQuMjQ2MjkgNy40NDI2NyIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPC9zdmc%2B&widths=undefined&widths=auto&heights=250&heights=150' diff --git a/apps/next-no-cache/src/lib/env.ts b/apps/next-no-cache/src/lib/env.ts deleted file mode 100644 index badcecf73..000000000 --- a/apps/next-no-cache/src/lib/env.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-01-02' - -export const dataset = assertValue( - process.env.NEXT_PUBLIC_SANITY_DATASET, - 'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET', -) - -export const projectId = assertValue( - process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, - 'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID', -) - -// This is always an empty string client side and is only used server side -export const token = process.env.SANITY_API_READ_TOKEN || '' - -function assertValue(v: T | undefined, errorMessage: string): T { - if (v === undefined) { - throw new Error(errorMessage) - } - - return v -} diff --git a/apps/next-no-cache/src/lib/image.ts b/apps/next-no-cache/src/lib/image.ts deleted file mode 100644 index bb95050c0..000000000 --- a/apps/next-no-cache/src/lib/image.ts +++ /dev/null @@ -1,32 +0,0 @@ -import createImageUrlBuilder from '@sanity/image-url' -import type {ImageLoader} from 'next/image' -import type {Image} from 'sanity' - -import {dataset, projectId} from './env' - -const imageBuilder = createImageUrlBuilder({ - projectId: projectId || '', - dataset: dataset || '', -}) - -export const urlForImage = (source: Image) => { - return imageBuilder?.image(source).auto('format').fit('max') -} - -const imageloader: ImageLoader = ({src, width, quality}) => { - const url = new URL(src) - url.searchParams.set('auto', 'format') - url.searchParams.set('fit', 'max') - if (url.searchParams.has('h')) { - const originalHeight = parseInt(url.searchParams.get('h')!, 10) - const originalWidth = parseInt(url.searchParams.get('w')!, 10) - url.searchParams.set('h', Math.round((originalHeight / originalWidth) * width).toString()) - } - url.searchParams.set('w', width.toString()) - if (quality) { - url.searchParams.set('q', quality.toString()) - } - return url.href -} - -export default imageloader diff --git a/apps/next-no-cache/src/lib/links.ts b/apps/next-no-cache/src/lib/links.ts deleted file mode 100644 index 44421940b..000000000 --- a/apps/next-no-cache/src/lib/links.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function resolveHref(documentType?: string, slugOrId?: string): string | undefined { - switch (documentType) { - case 'post': - return slugOrId ? `/${slugOrId}` : undefined - default: - console.warn('Invalid document type:', documentType) - return undefined - } -} diff --git a/apps/next-no-cache/src/lib/loadQuery.tsx b/apps/next-no-cache/src/lib/loadQuery.tsx deleted file mode 100644 index 1c7833dba..000000000 --- a/apps/next-no-cache/src/lib/loadQuery.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type {QueryParams} from 'next-sanity' -import {draftMode} from 'next/headers' -import 'server-only' -import {client} from './client' -import {token} from './env' -import type {FilteredResponseQueryOptions} from '@sanity/client' - -const DEFAULT_PARAMS = {} as QueryParams - -export async function loadQuery({ - query, - params = DEFAULT_PARAMS, -}: { - query: string - params?: QueryParams -}): Promise { - const isDraftMode = draftMode().isEnabled - if (isDraftMode && !token) { - throw new Error('The `SANITY_API_READ_TOKEN` environment variable is required in Draft Mode.') - } - - const perspective = isDraftMode ? 'previewDrafts' : 'published' - const options = { - filterResponse: true, - resultSourceMap: isDraftMode ? 'withKeyArraySelector' : false, - useCdn: !isDraftMode, - token: isDraftMode ? token : undefined, - perspective, - // Disable the Data Cache for all requests when in Draft Mode, otherwise it's 60 seconds like the API CDN TTL - next: {revalidate: isDraftMode ? 0 : 60}, - } satisfies FilteredResponseQueryOptions - return await client.fetch(query, params, { - ...options, - stega: isDraftMode, - } as FilteredResponseQueryOptions) -} diff --git a/apps/next-no-cache/src/lib/queries.ts b/apps/next-no-cache/src/lib/queries.ts deleted file mode 100644 index 8c0c7b369..000000000 --- a/apps/next-no-cache/src/lib/queries.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const postFields = /* groq */ ` - ..., - "slug": slug.current, - "publishedAt": coalesce(publishedAt, _updatedAt), -` - -export const latestPostQuery = /* groq */ ` -*[_type == "post"] | order(publishedAt desc, _updatedAt desc) [0] { - ${postFields} -}` - -export const postQuery = /* groq */ ` -{ - "post": *[_type == "post" && slug.current == $slug] | order(_updatedAt desc) [0] { - content, - ${postFields} - }, - "morePosts": *[_type == "post" && slug.current != $slug] | order(date desc, _updatedAt desc) [0...2] { - content, - ${postFields} - } -}` - -export const postSlugsQuery = /* groq */ ` -*[_type == "post" && defined(slug.current)][].slug.current -` - -export const postBySlugQuery = /* groq */ ` -*[_type == "post" && slug.current == $slug][0] { - ${postFields} -} -` diff --git a/apps/next-no-cache/src/lib/sanity/schema.ts b/apps/next-no-cache/src/lib/sanity/schema.ts deleted file mode 100644 index fe7e9fb06..000000000 --- a/apps/next-no-cache/src/lib/sanity/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {author} from './schemas/author' -import {blockContent} from './schemas/blockContent' -import {post} from './schemas/post' - -export const types = [author, blockContent, post] diff --git a/apps/next-no-cache/src/lib/sanity/schemas/author.ts b/apps/next-no-cache/src/lib/sanity/schemas/author.ts deleted file mode 100644 index 99ee8d356..000000000 --- a/apps/next-no-cache/src/lib/sanity/schemas/author.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {UserIcon} from '@sanity/icons' -import {defineField, defineType} from 'sanity' - -export const author = defineType({ - name: 'author', - title: 'Author', - icon: UserIcon, - type: 'document', - fields: [ - defineField({ - name: 'name', - title: 'Name', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'image', - title: 'Image', - type: 'image', - options: { - hotspot: true, - }, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - ], - preview: { - select: { - title: 'name', - media: 'image', - }, - }, -}) diff --git a/apps/next-no-cache/src/lib/sanity/schemas/blockContent.ts b/apps/next-no-cache/src/lib/sanity/schemas/blockContent.ts deleted file mode 100644 index b8f0fb0e2..000000000 --- a/apps/next-no-cache/src/lib/sanity/schemas/blockContent.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {defineArrayMember, defineField, defineType} from 'sanity' - -/** - * This is the schema type for block content used in the post document type - * Importing this type into the studio configuration's `schema` property - * lets you reuse it in other document types with: - * { - * name: 'someName', - * title: 'Some title', - * type: 'blockContent' - * } - */ - -export const blockContent = defineType({ - title: 'Block Content', - name: 'blockContent', - type: 'array', - of: [ - defineArrayMember({ - title: 'Block', - type: 'block', - // Styles let you define what blocks can be marked up as. The default - // set corresponds with HTML tags, but you can set any title or value - // you want, and decide how you want to deal with it where you want to - // use your content. - styles: [ - {title: 'Normal', value: 'normal'}, - {title: 'H1', value: 'h1'}, - {title: 'H2', value: 'h2'}, - {title: 'H3', value: 'h3'}, - {title: 'H4', value: 'h4'}, - {title: 'Quote', value: 'blockquote'}, - ], - lists: [{title: 'Bullet', value: 'bullet'}], - // Marks let you mark up inline text in the Portable Text Editor - marks: { - // Decorators usually describe a single property – e.g. a typographic - // preference or highlighting - decorators: [ - {title: 'Strong', value: 'strong'}, - {title: 'Emphasis', value: 'em'}, - ], - // Annotations can be any object structure – e.g. a link or a footnote. - annotations: [ - { - title: 'URL', - name: 'link', - type: 'object', - fields: [ - { - title: 'URL', - name: 'href', - type: 'url', - }, - ], - }, - ], - }, - }), - // You can add additional types here. Note that you can't use - // primitive types such as 'string' and 'number' in the same array - // as a block type. - defineArrayMember({ - type: 'image', - options: {hotspot: true}, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - ], -}) diff --git a/apps/next-no-cache/src/lib/sanity/schemas/post.ts b/apps/next-no-cache/src/lib/sanity/schemas/post.ts deleted file mode 100644 index ff7a7cf83..000000000 --- a/apps/next-no-cache/src/lib/sanity/schemas/post.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {BookIcon} from '@sanity/icons' -import {defineField, defineType} from 'sanity' - -import {format, parseISO} from 'date-fns' -import {author as authorType} from './author' -import {blockContent} from './blockContent' - -export const post = defineType({ - name: 'post', - title: 'Post', - icon: BookIcon, - type: 'document', - fields: [ - defineField({ - name: 'title', - title: 'Title', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'slug', - title: 'Slug', - type: 'slug', - options: { - source: 'title', - maxLength: 96, - isUnique: (value, context) => context.defaultIsUnique(value, context), - }, - validation: (rule) => rule.required(), - }), - defineField({ - name: 'author', - title: 'Author', - type: 'reference', - to: [{type: authorType.name}], - }), - defineField({ - name: 'mainImage', - title: 'Main image', - type: 'image', - options: { - hotspot: true, - }, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - defineField({ - name: 'publishedAt', - title: 'Published at', - type: 'datetime', - initialValue: () => new Date().toISOString(), - }), - defineField({ - name: 'body', - title: 'Body', - type: blockContent.name, - }), - defineField({ - name: 'excerpt', - title: 'Excerpt', - type: 'text', - }), - ], - - preview: { - select: { - title: 'title', - author: 'author.name', - media: 'mainImage', - publishedAt: 'publishedAt', - }, - prepare({title, media, author, publishedAt}) { - const subtitles = [ - author && `by ${author}`, - publishedAt && `on ${format(parseISO(publishedAt), 'LLL d, yyyy')}`, - ].filter(Boolean) - - return {title, media, subtitle: subtitles.join(' ')} - }, - }, -}) diff --git a/apps/next-no-cache/src/sanity.config.ts b/apps/next-no-cache/src/sanity.config.ts deleted file mode 100644 index dcefe214f..000000000 --- a/apps/next-no-cache/src/sanity.config.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from '../sanity.config' diff --git a/apps/next-no-cache/tailwind.config.ts b/apps/next-no-cache/tailwind.config.ts deleted file mode 100644 index f62bb4077..000000000 --- a/apps/next-no-cache/tailwind.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {Config} from 'tailwindcss' -import typography from '@tailwindcss/typography' - -const config: Config = { - content: ['./src/app/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}', './src/lib/**/*.{ts,tsx}'], - theme: { - extend: { - fontFamily: { - sans: ['var(--font-inter)'], - }, - }, - }, - future: { - hoverOnlyWhenSupported: true, - }, - plugins: [typography], -} -export default config diff --git a/apps/next-no-cache/turbo.json b/apps/next-no-cache/turbo.json deleted file mode 100644 index ff06c68ff..000000000 --- a/apps/next-no-cache/turbo.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"], - "env": ["NEXT_PUBLIC_SANITY_*", "NEXT_PUBLIC_VERCEL_ENV"], - "outputs": [".next/**", "!.next/cache/**"] - } - } -} diff --git a/apps/next-server-only/.eslintrc.json b/apps/next-server-only/.eslintrc.json deleted file mode 100644 index bffb357a7..000000000 --- a/apps/next-server-only/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/apps/next-server-only/.gitignore b/apps/next-server-only/.gitignore deleted file mode 100644 index fd3dbb571..000000000 --- a/apps/next-server-only/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/apps/next-server-only/README.md b/apps/next-server-only/README.md deleted file mode 100644 index c4033664f..000000000 --- a/apps/next-server-only/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/next-server-only/next.config.mjs b/apps/next-server-only/next.config.mjs deleted file mode 100644 index 89fe90acc..000000000 --- a/apps/next-server-only/next.config.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import path from 'node:path' -import withBundleAnalyzer from '@next/bundle-analyzer' -import sanityPkg from 'sanity/package.json' assert {type: 'json'} - -function requireResolve(id) { - return import.meta.resolve(id).replace('file://', '') -} - -const sanityExports = {} -for (const key of Object.keys(sanityPkg.exports)) { - if (key === '.') continue - const subexport = path.join('sanity', key) - sanityExports[subexport] = requireResolve(subexport) -} -sanityExports['sanity'] = requireResolve('sanity') - -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - // /* - logging: { - fetches: { - fullUrl: true, - }, - }, - // */ - - typescript: { - // @TODO figure out why TSC fails in the Next.js process but works fine when running `tsc --noEmit` - ignoreBuildErrors: true, - }, - - transpilePackages: ['apps-common'], - - images: { - remotePatterns: [{hostname: 'cdn.sanity.io'}, {hostname: 'source.unsplash.com'}], - }, - - experimental: { - taint: true, - }, - - webpack(config) { - config.resolve.alias = { - ...config.resolve.alias, - ...sanityExports, - '@sanity/presentation': requireResolve('@sanity/presentation'), - '@sanity/vision': requireResolve('@sanity/vision'), - '@sanity/ui/theme': requireResolve('@sanity/ui/theme'), - '@sanity/ui': requireResolve('@sanity/ui'), - 'styled-components': requireResolve('styled-components'), - } - return config - }, -} - -export default withBundleAnalyzer({ - enabled: process.env.ANALYZE === 'true', -})(nextConfig) diff --git a/apps/next-server-only/package.json b/apps/next-server-only/package.json deleted file mode 100644 index 5540f6146..000000000 --- a/apps/next-server-only/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "next-server-only", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "next build", - "dev": "next dev", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@portabletext/react": "^3.1.0", - "@repo/channels": "workspace:*", - "@repo/visual-editing-helpers": "workspace:*", - "@sanity/client": "^6.21.3", - "@sanity/icons": "^3.4.0", - "@sanity/image-url": "^1.0.2", - "@sanity/presentation": "workspace:*", - "@sanity/preview-kit-compat": "workspace:*", - "@sanity/preview-url-secret": "workspace:*", - "@sanity/react-loader": "workspace:*", - "@sanity/vision": "^3.57.2", - "@sanity/visual-editing": "workspace:*", - "@tailwindcss/typography": "^0.5.15", - "@types/node": "^20.5.0", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vercel/speed-insights": "^1.0.12", - "apps-common": "workspace:*", - "autoprefixer": "^10.4.20", - "classnames": "^2.5.1", - "date-fns": "^3.6.0", - "fast-deep-equal": "3.1.3", - "next": "15.0.0-canary.148", - "next-sanity": "9.4.7", - "react": "19.0.0-rc-fb9a90fa48-20240614", - "react-dom": "19.0.0-rc-fb9a90fa48-20240614", - "react-wrap-balancer": "^1.1.1", - "sanity": "^3.57.2", - "sanity-plugin-asset-source-unsplash": "^3.0.1", - "server-only": "^0.0.1", - "styled-components": "^6.1.13" - }, - "devDependencies": { - "@next/bundle-analyzer": "15.0.0-canary.148", - "@types/node": "^20", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "autoprefixer": "^10.4.20", - "eslint": "^8.57.0", - "eslint-config-next": "14.3.0-canary.87", - "postcss": "^8.4.45", - "tailwindcss": "^3.4.10", - "typescript": "5.6.2" - } -} diff --git a/apps/next-server-only/postcss.config.js b/apps/next-server-only/postcss.config.js deleted file mode 100644 index 33ad091d2..000000000 --- a/apps/next-server-only/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/apps/next-server-only/public/next.svg b/apps/next-server-only/public/next.svg deleted file mode 100644 index 5174b28c5..000000000 --- a/apps/next-server-only/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/next-server-only/public/vercel.svg b/apps/next-server-only/public/vercel.svg deleted file mode 100644 index d2f842227..000000000 --- a/apps/next-server-only/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/next-server-only/sanity.cli.ts b/apps/next-server-only/sanity.cli.ts deleted file mode 100644 index f47497f71..000000000 --- a/apps/next-server-only/sanity.cli.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This configuration file lets you run `$ sanity [command]` in this folder - * Go to https://www.sanity.io/docs/cli to learn more. - **/ -import {loadEnvConfig} from '@next/env' -import {defineCliConfig} from 'sanity/cli' -import {projectId, datasets} from 'apps-common/env' - -const dev = process.env.NODE_ENV !== 'production' -loadEnvConfig(__dirname, dev, {info: () => null, error: console.error}) - -const dataset = datasets['blog'] - -export default defineCliConfig({ - api: {projectId, dataset}, - vite: (config) => { - return { - ...config, - envPrefix: ['NEXT_', 'SANITY_STUDIO_', 'VITE_'], - } - }, -}) diff --git a/apps/next-server-only/sanity.config.ts b/apps/next-server-only/sanity.config.ts deleted file mode 100644 index eee9c6f66..000000000 --- a/apps/next-server-only/sanity.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -'use client' -/** - * This configuration is used to for the Sanity Studio that’s mounted on the `/app/studio/[[...index]]/page.tsx` route - */ - -import {visionTool} from '@sanity/vision' -import {defineConfig} from 'sanity' -import {presentationTool} from 'sanity/presentation' -import {structureTool} from 'sanity/structure' -import {unsplashImageAsset} from 'sanity-plugin-asset-source-unsplash' - -// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works -import {apiVersion, dataset, projectId} from '@/lib/env' -import {types} from '@/lib/sanity/schema' - -const config = defineConfig({ - basePath: '/studio', - projectId, - dataset, - // Add and edit the content schema in the './src/sanity/schema' folder - schema: {types}, - plugins: [ - presentationTool({ - previewUrl: { - previewMode: { - enable: '/api/draft', - disable: 'api/disable-draft', - }, - }, - }), - structureTool(), - // Add an image asset source for Unsplash - unsplashImageAsset(), - // Vision is a tool that lets you query your content with GROQ in the studio - // https://www.sanity.io/docs/the-vision-plugin - visionTool({defaultApiVersion: apiVersion}), - ], -}) -export default config diff --git a/apps/next-server-only/src/app/(blog)/AuthorAvatar.tsx b/apps/next-server-only/src/app/(blog)/AuthorAvatar.tsx deleted file mode 100644 index a59513bbf..000000000 --- a/apps/next-server-only/src/app/(blog)/AuthorAvatar.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {urlForImage} from '@/lib/image' -import {loadQuery} from '@/lib/loadQuery' -import Image from 'next/image' -import Balancer from 'react-wrap-balancer' - -const query = /* groq */ `*[_type == "author" && _id == $id][0]` - -export async function AuthorAvatar(params: {id: string}) { - const data = await loadQuery({ - query, - params, - tags: [params.id], - }) - const {name = 'Anonymous', image} = data ?? {} - return ( - <> -
-
- {image?.alt -
-
- {name} -
-
- - ) -} - -export function AuthorAvatarFallback() { - return ( -
-
-
Fetching author…
-
- ) -} diff --git a/apps/next-server-only/src/app/(blog)/BlogHeader.tsx b/apps/next-server-only/src/app/(blog)/BlogHeader.tsx deleted file mode 100644 index 9f91037c7..000000000 --- a/apps/next-server-only/src/app/(blog)/BlogHeader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client' - -import Link from 'next/link' -import {usePathname} from 'next/navigation' -import Balancer from 'react-wrap-balancer' - -export default function BlogHeader({title}: {title: string}) { - const pathname = usePathname() - if (pathname === '/') { - return ( -
-

- {title} -

-
- ) - } - return ( -
-

- - {title} - -

-
- ) -} diff --git a/apps/next-server-only/src/app/(blog)/CoverImage.tsx b/apps/next-server-only/src/app/(blog)/CoverImage.tsx deleted file mode 100644 index 75c3c491e..000000000 --- a/apps/next-server-only/src/app/(blog)/CoverImage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {urlForImage} from '@/lib/image' -import cn from 'classnames' -import Image from 'next/image' -import Link from 'next/link' - -interface CoverImageProps { - title: string - slug?: string - image?: {asset?: any; alt?: string} - priority?: boolean -} - -export default function CoverImage(props: CoverImageProps) { - const {title, slug, image: source, priority} = props - const image = source?.asset?._ref ? ( -
- {source?.alt -
- ) : ( -
- ) - - return ( -
- {slug ? ( - - {image} - - ) : ( - image - )} -
- ) -} diff --git a/apps/next-server-only/src/app/(blog)/MoreStories.tsx b/apps/next-server-only/src/app/(blog)/MoreStories.tsx deleted file mode 100644 index 89e2a3e43..000000000 --- a/apps/next-server-only/src/app/(blog)/MoreStories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import Link from 'next/link' -import {Suspense} from 'react' - -import {postFields} from '@/lib/queries' -import {AuthorAvatar, AuthorAvatarFallback} from './AuthorAvatar' -import CoverImage from './CoverImage' -import Date from './PostDate' -import {loadQuery} from '@/lib/loadQuery' - -const query = /* groq */ `*[_type == "post" && _id != $skip] | order(date desc, _updatedAt desc) [0...$limit] { - ${postFields} -}` - -export default async function MoreStories(params: {skip: string; limit: number}) { - const data = await loadQuery({ - query, - params, - tags: ['post'], - }) - - return ( - <> -
- {data.map((post: any) => { - const {_id, title = 'Untitled', slug, mainImage, excerpt, author} = post - return ( -
-
- -
-

- {slug ? ( - - {title} - - ) : ( - title - )} -

-
- -
- {excerpt &&

{excerpt}

} - {author?._ref && ( - }> - - - )} -
- ) - })} -
- - ) -} diff --git a/apps/next-server-only/src/app/(blog)/PostBody.module.css b/apps/next-server-only/src/app/(blog)/PostBody.module.css deleted file mode 100644 index 87f49dffc..000000000 --- a/apps/next-server-only/src/app/(blog)/PostBody.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.portableText { - @apply text-lg leading-relaxed; -} - -.portableText p, -.portableText ul, -.portableText ol, -.portableText blockquote { - @apply my-6; -} - -.portableText h2 { - @apply mb-4 mt-12 text-3xl leading-snug; -} - -.portableText h3 { - @apply mb-4 mt-8 text-2xl leading-snug; -} - -.portableText a { - @apply text-blue-500 underline; -} -.portableText a:hover { - @apply text-blue-800; -} diff --git a/apps/next-server-only/src/app/(blog)/PostBody.tsx b/apps/next-server-only/src/app/(blog)/PostBody.tsx deleted file mode 100644 index a9a1146e1..000000000 --- a/apps/next-server-only/src/app/(blog)/PostBody.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * This component uses Portable Text to render a post body. - * - * You can learn more about Portable Text on: - * https://www.sanity.io/docs/block-content - * https://github.com/portabletext/react-portabletext - * https://portabletext.org/ - * - */ -import {PortableText} from '@portabletext/react' - -import styles from './PostBody.module.css' - -export default function PostBody({ - body, -}: { - body: React.ComponentProps['value'] -}) { - return ( -
- -
- ) -} diff --git a/apps/next-server-only/src/app/(blog)/PostDate.tsx b/apps/next-server-only/src/app/(blog)/PostDate.tsx deleted file mode 100644 index 873577100..000000000 --- a/apps/next-server-only/src/app/(blog)/PostDate.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {format, parseISO} from 'date-fns' -import Balancer from 'react-wrap-balancer' - -export default function PostDate({dateString}: {dateString: string}) { - const date = parseISO(dateString) - return ( - - ) -} diff --git a/apps/next-server-only/src/app/(blog)/PreviewBanner.tsx b/apps/next-server-only/src/app/(blog)/PreviewBanner.tsx deleted file mode 100644 index 76f7176eb..000000000 --- a/apps/next-server-only/src/app/(blog)/PreviewBanner.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default function PreviewBanner() { - return ( -
- {'Previewing drafts. '} - - Back to published - -
- ) -} diff --git a/apps/next-server-only/src/app/(blog)/[slug]/page.tsx b/apps/next-server-only/src/app/(blog)/[slug]/page.tsx deleted file mode 100644 index 890e08154..000000000 --- a/apps/next-server-only/src/app/(blog)/[slug]/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type {Metadata, ResolvingMetadata} from 'next' -import {notFound} from 'next/navigation' -import {Suspense} from 'react' -import Balancer from 'react-wrap-balancer' - -import {AuthorAvatar, AuthorAvatarFallback} from '../AuthorAvatar' -import CoverImage from '../CoverImage' -import MoreStories from '../MoreStories' -import PostBody from '../PostBody' -import PostDate from '../PostDate' -import {urlForImage} from '@/lib/image' -import {postFields} from '@/lib/queries' -import {loadQuery} from '@/lib/loadQuery' - -type Props = { - params: {slug: string} -} - -const query = /* groq */ `*[_type == "post" && slug.current == $slug][0] { - ${postFields} -}` - -export async function generateMetadata( - {params}: Props, - parent: ResolvingMetadata, -): Promise { - const [post, authorName] = await Promise.all([ - loadQuery({query, params, tags: [`post:${params.slug}`]}), - // @TODO necessary as there's problems with type inference when `author-{name,image}` is used - loadQuery({ - query: /* groq */ `*[_type == "post" && slug.current == $slug][0].author->name`, - params, - tags: [`post:${params.slug}`, 'author'], - }), - ]) - // optionally access and extend (rather than replace) parent metadata - const parentTitle = (await parent).title?.absolute - const previousImages = (await parent).openGraph?.images || [] - - return { - authors: authorName ? [{name: authorName}] : [], - title: `${parentTitle} | ${post?.title}`, - openGraph: { - images: post?.mainImage?.asset?._ref - ? [urlForImage(post.mainImage).height(1000).width(2000).url(), ...previousImages] - : previousImages, - }, - } satisfies Metadata -} - -export default async function BlogPostPage({params}: Props) { - const {slug} = params - const data = await loadQuery({ - query, - params, - tags: [`post:${params.slug}`], - }) - - if (!data) { - return notFound() - } - - const {_id, title = 'Untitled', author, mainImage, body} = data ?? {} - return ( - <> -
-

- {title} -

-
- {author?._ref && ( - }> - - - )} -
-
- -
-
-
- {author?._ref && ( - }> - - - )} -
-
- -
-
- -
- - - ) -} diff --git a/apps/next-server-only/src/app/(blog)/layout.tsx b/apps/next-server-only/src/app/(blog)/layout.tsx deleted file mode 100644 index 781034041..000000000 --- a/apps/next-server-only/src/app/(blog)/layout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Link from 'next/link' - -import BlogHeader from './BlogHeader' -import {EXAMPLE_NAME} from '@/lib/constants' -import PreviewBanner from './PreviewBanner' -import {draftMode} from 'next/headers' -import {revalidatePath, revalidateTag} from 'next/cache' -import {VisualEditing} from 'next-sanity' - -export default function BlogLayout({children}: {children: React.ReactNode}) { - return ( - <> - {draftMode().isEnabled && } -
- -
{children}
-
-
- - Studio - -
-
- {draftMode().isEnabled && ( - { - 'use server' - if (!draftMode().isEnabled) { - console.debug('Skipped manual refresh because draft mode is not enabled') - return - } - if (payload.source === 'mutation') { - if (payload.document.slug?.current) { - const tag = `${payload.document._type}:${payload.document.slug.current}` - console.log('Revalidate slug', tag) - await revalidateTag(tag) - } - console.log('Revalidate tag', payload.document._type) - return revalidateTag(payload.document._type) - } - await revalidatePath('/', 'layout') - }} - /> - )} - - ) -} diff --git a/apps/next-server-only/src/app/(blog)/page.tsx b/apps/next-server-only/src/app/(blog)/page.tsx deleted file mode 100644 index 77ab6f72a..000000000 --- a/apps/next-server-only/src/app/(blog)/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Link from 'next/link' -import {Suspense} from 'react' -import Balancer from 'react-wrap-balancer' - -import {AuthorAvatar, AuthorAvatarFallback} from './AuthorAvatar' -import CoverImage from './CoverImage' -import MoreStories from './MoreStories' -import Date from './PostDate' -import {postFields} from '@/lib/queries' -import {loadQuery} from '@/lib/loadQuery' - -export default async function BlogIndexPage() { - const data = await loadQuery({ - query: /* groq */ ` -*[_type == "post"] | order(publishedAt desc, _updatedAt desc) [0] { - ${postFields} -}`, - tags: ['post'], - }) - const {_id, author, excerpt, mainImage, slug, title, publishedAt} = data ?? {} - - return ( - <> - {data && ( -
-
- -
-
-
-

- {slug ? ( - - {title} - - ) : ( - {title} - )} -

-
- -
-
-
- {excerpt && ( -

- {excerpt} -

- )} - {author?._ref && ( - }> - - - )} -
-
-
- )} - {_id && ( - - )} - - ) -} diff --git a/apps/next-server-only/src/app/api/disable-draft/route.ts b/apps/next-server-only/src/app/api/disable-draft/route.ts deleted file mode 100644 index 5feca1a1d..000000000 --- a/apps/next-server-only/src/app/api/disable-draft/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {draftMode} from 'next/headers' -import {NextRequest, NextResponse} from 'next/server' - -export function GET(request: NextRequest) { - draftMode().disable() - const url = new URL(request.nextUrl) - return NextResponse.redirect(new URL('/', url.origin)) -} diff --git a/apps/next-server-only/src/app/api/draft/route.ts b/apps/next-server-only/src/app/api/draft/route.ts deleted file mode 100644 index 95568a060..000000000 --- a/apps/next-server-only/src/app/api/draft/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {draftMode} from 'next/headers' -import {redirect} from 'next/navigation' -import {validatePreviewUrl} from '@sanity/preview-url-secret' -import {client} from '@/lib/client' - -const clientWithToken = client.withConfig({ - token: process.env.SANITY_API_READ_TOKEN, -}) - -export async function GET(req: Request) { - const {isValid, redirectTo = '/'} = await validatePreviewUrl(clientWithToken, req.url) - if (!isValid) { - return new Response('Invalid secret', {status: 401}) - } - - draftMode().enable() - - redirect(redirectTo) -} diff --git a/apps/next-server-only/src/app/favicon.ico b/apps/next-server-only/src/app/favicon.ico deleted file mode 100644 index 718d6fea4..000000000 Binary files a/apps/next-server-only/src/app/favicon.ico and /dev/null differ diff --git a/apps/next-server-only/src/app/globals.css b/apps/next-server-only/src/app/globals.css deleted file mode 100644 index e0936eda7..000000000 --- a/apps/next-server-only/src/app/globals.css +++ /dev/null @@ -1,23 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) - rgb(var(--background-start-rgb)); -} diff --git a/apps/next-server-only/src/app/layout.tsx b/apps/next-server-only/src/app/layout.tsx deleted file mode 100644 index eb18f73fd..000000000 --- a/apps/next-server-only/src/app/layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import 'tailwindcss/tailwind.css' - -import type {Metadata} from 'next' -import {Inter} from 'next/font/google' -import {SpeedInsights} from '@vercel/speed-insights/next' - -import {CMS_NAME} from '@/lib/constants' - -const inter = Inter({ - variable: '--font-inter', - subsets: ['latin'], - display: 'swap', -}) - -export const metadata = { - title: `Next.js and ${CMS_NAME} Example`, - description: `This is a blog built with Next.js and ${CMS_NAME}.`, -} satisfies Metadata - -export default function RootLayout({children}: {children: React.ReactNode}) { - return ( - - - {children} - - - - ) -} diff --git a/apps/next-server-only/src/app/studio/[[...index]]/page.tsx b/apps/next-server-only/src/app/studio/[[...index]]/page.tsx deleted file mode 100644 index 71cd1cb67..000000000 --- a/apps/next-server-only/src/app/studio/[[...index]]/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This route is responsible for the built-in authoring environment using Sanity Studio v3. - * All routes under /studio will be handled by this file using Next.js' catch-all routes: - * https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes - * - * You can learn more about the next-sanity package here: - * https://github.com/sanity-io/next-sanity - */ - -import {NextStudio} from 'next-sanity/studio' - -import config from '@/sanity.config' - -export const dynamic = 'force-static' - -export {metadata, viewport} from 'next-sanity/studio' - -export default function StudioPage() { - return -} diff --git a/apps/next-server-only/src/lib/client.ts b/apps/next-server-only/src/lib/client.ts deleted file mode 100644 index a87a2dc3d..000000000 --- a/apps/next-server-only/src/lib/client.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {createClient} from '@sanity/client' - -import {apiVersion, dataset, projectId, revalidateSecret} from './env' - -export const client = createClient({ - apiVersion, - dataset, - projectId, - // If the GROQ revalidate hook is setup we use the Vercel Data Cache to handle on-demand revalidation, and the Sanity API CDN if not - useCdn: revalidateSecret ? false : true, - perspective: 'published', - stega: { - studioUrl: '/studio', - }, -}) diff --git a/apps/next-server-only/src/lib/constants.ts b/apps/next-server-only/src/lib/constants.ts deleted file mode 100644 index 7ef30f1c0..000000000 --- a/apps/next-server-only/src/lib/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const EXAMPLE_NAME = 'Blog' -export const EXAMPLE_PATH = 'cms-sanity' -export const CMS_NAME = 'Sanity' -export const CMS_URL = 'https://sanity.io/' -export const HOME_OG_IMAGE_URL = - 'https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**Sanity**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTA1IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMWVtIj48dGl0bGU%2BU2FuaXR5PC90aXRsZT48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03OC4xNzkzIDcuOTkyNjFWMjEuMDAyOEg3My45MDMxVjEwLjIxMzhMNzguMTc5MyA3Ljk5MjYxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMjAuOTUxMSAyMS4zM0wzMC45NDQgMTYuMTA1MUwyOS43MTIxIDEyLjkxNDFMMjMuMTMzMiAxNS45ODIxTDIwLjk1MTEgMjEuMzNaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik03My45MDMxIDEwLjIwMjdMODQuNzQ0MyA0LjY1NDc3TDgyLjkxMjYgMS41NTcxTDczLjkwMzEgNS45NTk5N1YxMC4yMDI3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNNDMuMzcwNSA2Ljk2MjMzVjIxLjAwMjhIMzkuMjkyN1YxLjAwNzE0TDQzLjM3MDUgNi45NjIzM1oiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTI3LjEyOTkgNi4xODYxN0wyMC45NTExIDIxLjMzTDE3Ljc3MzEgMTguNTk0M0wyNS4xMzUzIDEuMDA3MTRMMjcuMTI5OSA2LjE4NjE3WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTI1LjEzNTMgMS4wMDcxNEgyOS4zNDc3TDM3LjEzODYgMjEuMDAyOEgzMi44MjY5TDI1LjEzNTMgMS4wMDcxNFoiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIGQ9Ik00NC4wMDEyIDEuMDA3MTRMNTIuOTgyNCAxNC42NjgyVjIxLjAwMjhMMzkuMjkyNyAxLjAwNzE0SDQ0LjAwMTJaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNjQuOTE4MyAxLjAwNzE0SDYwLjY3MzlWMjEuMDA2M0g2NC45MTgzVjEuMDA3MTRaIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNNzMuOTAzMSA0LjY1NDc0SDY3LjM3VjEuMDA3MTRIODIuNTg2N0w4NC43NDQzIDQuNjU0NzRINzguMTc5M0g3My45MDMxWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC41IiBkPSJNOTcuMjc1NCAxMy40MTUzVjIxLjAwMjhIOTMuMDYyOVYxMy40MTUzIiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBkPSJNOTMuMDYyOSAxMy40MTUyTDEwMC4xOTEgMS4wMDcxNEgxMDQuNjY2TDk3LjI3NTQgMTMuNDE1Mkg5My4wNjI5WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNOTMuMDYzIDEzLjQxNTJMODUuNzM2MyAxLjAwNzE0SDkwLjM0NTZMOTUuMzA5MiA5LjUxMDA4TDkzLjA2MyAxMy40MTUyWiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggZD0iTTEuOTYxMjYgMy4zMTQ3OUMxLjk2MTI2IDYuMDk5MjEgMy43MTE0NSA3Ljc1NTk1IDcuMjE1MzYgOC42Mjk1NkwxMC45MjgzIDkuNDc1MzNDMTQuMjQ0NCAxMC4yMjM2IDE2LjI2MzkgMTIuMDgyMiAxNi4yNjM5IDE1LjExMDNDMTYuMjg5NyAxNi40Mjk1IDE1Ljg1MzEgMTcuNzE3MyAxNS4wMjc0IDE4Ljc1NzlDMTUuMDI3NCAxNS43MzY4IDEzLjQzNjcgMTQuMTA0NCA5LjU5OTcyIDEzLjEyMjlMNS45NTQwOSAxMi4zMDg1QzMuMDM0NzUgMTEuNjU0MSAwLjc4MTQ3OCAxMC4xMjYyIDAuNzgxNDc4IDYuODM3MDlDMC43NjYxMjMgNS41NjY5MyAxLjE4MTE2IDQuMzI3ODEgMS45NjEyNiAzLjMxNDc5IiBmaWxsPSJjdXJyZW50Q29sb3IiPjwvcGF0aD48cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik01Mi45ODI0IDEzLjY0MTVWMS4wMDcxNEg1Ny4wNjAyVjIxLjAwMjhINTIuOTgyNFYxMy42NDE1WiIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPHBhdGggb3BhY2l0eT0iMC43IiBkPSJNMTIuNzQ1OCAxNC4zNjg5QzE0LjMyOTQgMTUuMzY0MyAxNS4wMjM4IDE2Ljc1NjUgMTUuMDIzOCAxOC43NTQ0QzEzLjcxMyAyMC40MDQxIDExLjQxMDEgMjEuMzMgOC43MDMzMyAyMS4zM0M0LjE0NzE4IDIxLjMzIDAuOTU4NTc3IDE5LjEyNjggMC4yNSAxNS4yOTgySDQuNjI1NDdDNS4xODg3OCAxNy4wNTU5IDYuNjgwMzQgMTcuODcwMyA4LjY3MTQ0IDE3Ljg3MDNDMTEuMTAxOSAxNy44NzAzIDEyLjcxNzQgMTYuNTk2NCAxMi43NDkzIDE0LjM2MTkiIGZpbGw9ImN1cnJlbnRDb2xvciI%2BPC9wYXRoPjxwYXRoIG9wYWNpdHk9IjAuNyIgZD0iTTQuMjM1NjcgNy40NDI2N0MzLjUxMjUgNy4wMjA0NSAyLjkxOTIgNi40MTM3NSAyLjUxODczIDUuNjg2OTdDMi4xMTgyNyA0Ljk2MDE5IDEuOTI1NTggNC4xNDA0NSAxLjk2MTEzIDMuMzE0NzZDMy4yMjU5NCAxLjY3ODkxIDUuNDI2MDggMC42Nzk5OTMgOC4xMDgwNCAwLjY3OTk5M0MxMi43NDkyIDAuNjc5OTkzIDE1LjQzNDcgMy4wODg1MiAxNi4wOTcyIDYuNDc4NTZIMTEuODg4M0MxMS40MjQyIDUuMTQyMDMgMTAuMjYyMSA0LjEwMTM2IDguMTQzNDcgNC4xMDEzNkM1Ljg3OTU3IDQuMTAxMzYgNC4zMzQ4NyA1LjM5NjExIDQuMjQ2MjkgNy40NDI2NyIgZmlsbD0iY3VycmVudENvbG9yIj48L3BhdGg%2BPC9zdmc%2B&widths=undefined&widths=auto&heights=250&heights=150' diff --git a/apps/next-server-only/src/lib/env.ts b/apps/next-server-only/src/lib/env.ts deleted file mode 100644 index f1c06c0da..000000000 --- a/apps/next-server-only/src/lib/env.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-01-02' - -export const dataset = assertValue( - process.env.NEXT_PUBLIC_SANITY_DATASET, - 'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET', -) - -export const projectId = assertValue( - process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, - 'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID', -) - -// This is always an empty string client side and is only used server side -export const token = process.env.SANITY_API_READ_TOKEN || '' - -// Used to verify GROQ webhook revalidation requests, defining this will also disable time-based revalidation and only use on-demand revalidation -export const revalidateSecret = process.env.SANITY_REVALIDATE_SECRET || '' - -// Used by `sanity-plugin-iframe-pane` to verify that draft mode was initiated by a valid Studio session -export const urlSecretId = `preview.secret` - -function assertValue(v: T | undefined, errorMessage: string): T { - if (v === undefined) { - throw new Error(errorMessage) - } - - return v -} diff --git a/apps/next-server-only/src/lib/image.ts b/apps/next-server-only/src/lib/image.ts deleted file mode 100644 index bb95050c0..000000000 --- a/apps/next-server-only/src/lib/image.ts +++ /dev/null @@ -1,32 +0,0 @@ -import createImageUrlBuilder from '@sanity/image-url' -import type {ImageLoader} from 'next/image' -import type {Image} from 'sanity' - -import {dataset, projectId} from './env' - -const imageBuilder = createImageUrlBuilder({ - projectId: projectId || '', - dataset: dataset || '', -}) - -export const urlForImage = (source: Image) => { - return imageBuilder?.image(source).auto('format').fit('max') -} - -const imageloader: ImageLoader = ({src, width, quality}) => { - const url = new URL(src) - url.searchParams.set('auto', 'format') - url.searchParams.set('fit', 'max') - if (url.searchParams.has('h')) { - const originalHeight = parseInt(url.searchParams.get('h')!, 10) - const originalWidth = parseInt(url.searchParams.get('w')!, 10) - url.searchParams.set('h', Math.round((originalHeight / originalWidth) * width).toString()) - } - url.searchParams.set('w', width.toString()) - if (quality) { - url.searchParams.set('q', quality.toString()) - } - return url.href -} - -export default imageloader diff --git a/apps/next-server-only/src/lib/links.ts b/apps/next-server-only/src/lib/links.ts deleted file mode 100644 index 44421940b..000000000 --- a/apps/next-server-only/src/lib/links.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function resolveHref(documentType?: string, slugOrId?: string): string | undefined { - switch (documentType) { - case 'post': - return slugOrId ? `/${slugOrId}` : undefined - default: - console.warn('Invalid document type:', documentType) - return undefined - } -} diff --git a/apps/next-server-only/src/lib/loadQuery.tsx b/apps/next-server-only/src/lib/loadQuery.tsx deleted file mode 100644 index da44b204e..000000000 --- a/apps/next-server-only/src/lib/loadQuery.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type {QueryParams} from 'next-sanity' -import {draftMode} from 'next/headers' -import 'server-only' -import {client} from './client' -import {token} from './env' -import {UnfilteredResponseQueryOptions} from '@sanity/client' - -const DEFAULT_PARAMS = {} as QueryParams - -export async function loadQuery({ - query, - params = DEFAULT_PARAMS, - tags, -}: { - query: string - params?: QueryParams - tags: string[] -}): Promise { - const isDraftMode = draftMode().isEnabled - if (isDraftMode && !token) { - throw new Error('The `SANITY_API_READ_TOKEN` environment variable is required in Draft Mode.') - } - - // https://nextjs.org/docs/app/api-reference/functions/fetch#optionsnextrevalidate - /* - const REVALIDATE_SKIP_CACHE = 0 - const REVALIDATE_CACHE_FOREVER = false - const revalidate = ( - isDraftMode - ? // If we're in Draft Mode we want fresh content on every request so we skip the cache - REVALIDATE_SKIP_CACHE - : revalidateSecret - ? // If GROQ webhook revalidation is setup, then we only want to revalidate on-demand so the cache lives as long as possible - REVALIDATE_CACHE_FOREVER - : // No webhook means we don't know ahead of time when content changes, so we use the Sanity CDN API cache which has its own Stale-While-Revalidate logic - REVALIDATE_SKIP_CACHE - ) satisfies NextFetchRequestConfig['revalidate'] - // */ - - const perspective = isDraftMode ? 'previewDrafts' : 'published' - - const options = { - filterResponse: false, - useCdn: false, - resultSourceMap: isDraftMode ? 'withKeyArraySelector' : false, - token: isDraftMode ? token : undefined, - perspective, - next: { - tags, - // Cache forever in Draft Mode until expired with revalidateTag, use time-based cache in production that matches the CDN cache - revalidate: isDraftMode ? false : 60, - }, - } satisfies UnfilteredResponseQueryOptions - const result = await client.fetch(query, params, { - ...options, - stega: isDraftMode, - } as UnfilteredResponseQueryOptions) - return result.result -} diff --git a/apps/next-server-only/src/lib/queries.ts b/apps/next-server-only/src/lib/queries.ts deleted file mode 100644 index 8c0c7b369..000000000 --- a/apps/next-server-only/src/lib/queries.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const postFields = /* groq */ ` - ..., - "slug": slug.current, - "publishedAt": coalesce(publishedAt, _updatedAt), -` - -export const latestPostQuery = /* groq */ ` -*[_type == "post"] | order(publishedAt desc, _updatedAt desc) [0] { - ${postFields} -}` - -export const postQuery = /* groq */ ` -{ - "post": *[_type == "post" && slug.current == $slug] | order(_updatedAt desc) [0] { - content, - ${postFields} - }, - "morePosts": *[_type == "post" && slug.current != $slug] | order(date desc, _updatedAt desc) [0...2] { - content, - ${postFields} - } -}` - -export const postSlugsQuery = /* groq */ ` -*[_type == "post" && defined(slug.current)][].slug.current -` - -export const postBySlugQuery = /* groq */ ` -*[_type == "post" && slug.current == $slug][0] { - ${postFields} -} -` diff --git a/apps/next-server-only/src/lib/sanity/schema.ts b/apps/next-server-only/src/lib/sanity/schema.ts deleted file mode 100644 index fe7e9fb06..000000000 --- a/apps/next-server-only/src/lib/sanity/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {author} from './schemas/author' -import {blockContent} from './schemas/blockContent' -import {post} from './schemas/post' - -export const types = [author, blockContent, post] diff --git a/apps/next-server-only/src/lib/sanity/schemas/author.ts b/apps/next-server-only/src/lib/sanity/schemas/author.ts deleted file mode 100644 index 99ee8d356..000000000 --- a/apps/next-server-only/src/lib/sanity/schemas/author.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {UserIcon} from '@sanity/icons' -import {defineField, defineType} from 'sanity' - -export const author = defineType({ - name: 'author', - title: 'Author', - icon: UserIcon, - type: 'document', - fields: [ - defineField({ - name: 'name', - title: 'Name', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'image', - title: 'Image', - type: 'image', - options: { - hotspot: true, - }, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - ], - preview: { - select: { - title: 'name', - media: 'image', - }, - }, -}) diff --git a/apps/next-server-only/src/lib/sanity/schemas/blockContent.ts b/apps/next-server-only/src/lib/sanity/schemas/blockContent.ts deleted file mode 100644 index b8f0fb0e2..000000000 --- a/apps/next-server-only/src/lib/sanity/schemas/blockContent.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {defineArrayMember, defineField, defineType} from 'sanity' - -/** - * This is the schema type for block content used in the post document type - * Importing this type into the studio configuration's `schema` property - * lets you reuse it in other document types with: - * { - * name: 'someName', - * title: 'Some title', - * type: 'blockContent' - * } - */ - -export const blockContent = defineType({ - title: 'Block Content', - name: 'blockContent', - type: 'array', - of: [ - defineArrayMember({ - title: 'Block', - type: 'block', - // Styles let you define what blocks can be marked up as. The default - // set corresponds with HTML tags, but you can set any title or value - // you want, and decide how you want to deal with it where you want to - // use your content. - styles: [ - {title: 'Normal', value: 'normal'}, - {title: 'H1', value: 'h1'}, - {title: 'H2', value: 'h2'}, - {title: 'H3', value: 'h3'}, - {title: 'H4', value: 'h4'}, - {title: 'Quote', value: 'blockquote'}, - ], - lists: [{title: 'Bullet', value: 'bullet'}], - // Marks let you mark up inline text in the Portable Text Editor - marks: { - // Decorators usually describe a single property – e.g. a typographic - // preference or highlighting - decorators: [ - {title: 'Strong', value: 'strong'}, - {title: 'Emphasis', value: 'em'}, - ], - // Annotations can be any object structure – e.g. a link or a footnote. - annotations: [ - { - title: 'URL', - name: 'link', - type: 'object', - fields: [ - { - title: 'URL', - name: 'href', - type: 'url', - }, - ], - }, - ], - }, - }), - // You can add additional types here. Note that you can't use - // primitive types such as 'string' and 'number' in the same array - // as a block type. - defineArrayMember({ - type: 'image', - options: {hotspot: true}, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - ], -}) diff --git a/apps/next-server-only/src/lib/sanity/schemas/post.ts b/apps/next-server-only/src/lib/sanity/schemas/post.ts deleted file mode 100644 index ff7a7cf83..000000000 --- a/apps/next-server-only/src/lib/sanity/schemas/post.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {BookIcon} from '@sanity/icons' -import {defineField, defineType} from 'sanity' - -import {format, parseISO} from 'date-fns' -import {author as authorType} from './author' -import {blockContent} from './blockContent' - -export const post = defineType({ - name: 'post', - title: 'Post', - icon: BookIcon, - type: 'document', - fields: [ - defineField({ - name: 'title', - title: 'Title', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'slug', - title: 'Slug', - type: 'slug', - options: { - source: 'title', - maxLength: 96, - isUnique: (value, context) => context.defaultIsUnique(value, context), - }, - validation: (rule) => rule.required(), - }), - defineField({ - name: 'author', - title: 'Author', - type: 'reference', - to: [{type: authorType.name}], - }), - defineField({ - name: 'mainImage', - title: 'Main image', - type: 'image', - options: { - hotspot: true, - }, - fields: [ - defineField({ - name: 'alt', - type: 'string', - title: 'Alternative Text', - }), - ], - }), - defineField({ - name: 'publishedAt', - title: 'Published at', - type: 'datetime', - initialValue: () => new Date().toISOString(), - }), - defineField({ - name: 'body', - title: 'Body', - type: blockContent.name, - }), - defineField({ - name: 'excerpt', - title: 'Excerpt', - type: 'text', - }), - ], - - preview: { - select: { - title: 'title', - author: 'author.name', - media: 'mainImage', - publishedAt: 'publishedAt', - }, - prepare({title, media, author, publishedAt}) { - const subtitles = [ - author && `by ${author}`, - publishedAt && `on ${format(parseISO(publishedAt), 'LLL d, yyyy')}`, - ].filter(Boolean) - - return {title, media, subtitle: subtitles.join(' ')} - }, - }, -}) diff --git a/apps/next-server-only/src/sanity.config.ts b/apps/next-server-only/src/sanity.config.ts deleted file mode 100644 index dcefe214f..000000000 --- a/apps/next-server-only/src/sanity.config.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from '../sanity.config' diff --git a/apps/next-server-only/tailwind.config.ts b/apps/next-server-only/tailwind.config.ts deleted file mode 100644 index f62bb4077..000000000 --- a/apps/next-server-only/tailwind.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {Config} from 'tailwindcss' -import typography from '@tailwindcss/typography' - -const config: Config = { - content: ['./src/app/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}', './src/lib/**/*.{ts,tsx}'], - theme: { - extend: { - fontFamily: { - sans: ['var(--font-inter)'], - }, - }, - }, - future: { - hoverOnlyWhenSupported: true, - }, - plugins: [typography], -} -export default config diff --git a/apps/next-server-only/tsconfig.json b/apps/next-server-only/tsconfig.json deleted file mode 100644 index 3a476cd77..000000000 --- a/apps/next-server-only/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "target": "es2022", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "preserve", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ], - "exclude": ["node_modules"] -} diff --git a/apps/next-with-i18n/next.config.mjs b/apps/next-with-i18n/next.config.mjs index faa0565d0..9ed846b47 100644 --- a/apps/next-with-i18n/next.config.mjs +++ b/apps/next-with-i18n/next.config.mjs @@ -31,9 +31,6 @@ const config = { fullUrl: true, }, }, - experimental: { - taint: true, - }, webpack(config) { config.resolve.alias = { diff --git a/apps/next-with-i18n/package.json b/apps/next-with-i18n/package.json index bcd65931f..04cb4e158 100644 --- a/apps/next-with-i18n/package.json +++ b/apps/next-with-i18n/package.json @@ -15,36 +15,37 @@ "singleQuote": true }, "dependencies": { - "@sanity/assist": "^3.0.6", - "@sanity/client": "^6.21.3", + "@repo/env": "workspace:*", + "@sanity/assist": "^3.0.8", + "@sanity/client": "^6.22.1", "@sanity/preview-url-secret": "workspace:*", - "@sanity/vision": "^3.57.2", + "@sanity/vision": "^3.61.0", "@tailwindcss/typography": "0.5.15", - "@tinloof/sanity-studio": "1.3.3", + "@tinloof/sanity-studio": "1.3.5", "classnames": "2.5.1", - "lucide-react": "^0.439.0", - "next": "14.2.9", - "next-sanity": "^9.4.7", + "lucide-react": "^0.453.0", + "next": "14.2.15", + "next-sanity": "^9.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", - "sanity": "^3.57.2", + "sanity": "^3.61.0", "server-only": "0.0.1" }, "devDependencies": { "@sanity/presentation": "workspace:*", "@sanity/visual-editing": "workspace:*", - "@types/react": "^18.3.5", + "@types/react": "^18.3.11", "autoprefixer": "10.4.20", - "eslint": "^8.57.0", - "eslint-config-next": "14.2.9", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.15", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-simple-import-sort": "12.1.1", - "postcss": "8.4.45", + "postcss": "8.4.47", "prettier": "^3.3.3", - "prettier-plugin-packagejson": "^2.5.2", - "prettier-plugin-tailwindcss": "0.6.6", - "tailwindcss": "3.4.10", - "typescript": "5.6.2" + "prettier-plugin-packagejson": "^2.5.3", + "prettier-plugin-tailwindcss": "0.6.8", + "tailwindcss": "3.4.14", + "typescript": "5.6.3" } } diff --git a/apps/next/README.md b/apps/next/README.md deleted file mode 100644 index 702c7a04e..000000000 --- a/apps/next/README.md +++ /dev/null @@ -1,55 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, link the package to the Vercel project: - -```bash -npx vercel@latest link -``` - -You should select `Sanity` as the scope, and `visual-editing-next` as the project: - -```bash -npx vercel@latest link -Need to install the following packages: -vercel@33.5.4 -Ok to proceed? (y) y -Vercel CLI 33.5.4 -? Set up “~/Developer/GitHub/visual-editing/apps/next”? [Y/n] y -? Which scope should contain your project? Sanity -? Link to existing project? [y/N] y -? What’s the name of your existing project? visual-editing-next -✅ Linked to sanity-io/visual-editing-next (created .vercel) -``` - -Then, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/next/next.config.mjs b/apps/next/next.config.mjs index 51f2544ad..212af2731 100644 --- a/apps/next/next.config.mjs +++ b/apps/next/next.config.mjs @@ -1,6 +1,6 @@ -import {withVercelToolbar} from '@vercel/toolbar/plugins/next' -import withBundleAnalyzer from '@next/bundle-analyzer' import {createRequire} from 'node:module' +import withBundleAnalyzer from '@next/bundle-analyzer' +import {withVercelToolbar} from '@vercel/toolbar/plugins/next' const require = createRequire(import.meta.url) @@ -18,15 +18,11 @@ const nextConfig = { }, }, - transpilePackages: ['apps-common'], - images: { remotePatterns: [{hostname: 'cdn.sanity.io'}, {hostname: 'source.unsplash.com'}], }, - experimental: { - taint: true, - }, + transpilePackages: ['@repo/env'], async headers() { return [ diff --git a/apps/next/package.json b/apps/next/package.json index c177ca5fb..7f07f43ba 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -3,38 +3,45 @@ "version": "0.1.0", "private": true, "scripts": { - "build": "next build", + "build": "sanity typegen generate && next build", "dev": "next dev -p 3001", "lint": "next lint", "start": "next start" }, + "prettier": "@repo/prettier-config", "dependencies": { - "@next/bundle-analyzer": "14.2.9", + "@next/bundle-analyzer": "14.2.15", "@portabletext/react": "^3.1.0", "@repo/channels": "workspace:*", - "@sanity/client": "^6.21.3", + "@repo/env": "workspace:*", + "@repo/sanity-extracted-schema": "workspace:*", + "@repo/studio-url": "workspace:*", + "@sanity/client": "^6.22.1", "@sanity/image-url": "^1.0.2", + "@sanity/next-loader": "workspace:*", "@sanity/preview-kit-compat": "workspace:*", "@sanity/preview-url-secret": "workspace:*", "@sanity/react-loader": "workspace:*", "@sanity/visual-editing": "workspace:*", "@types/node": "20.8.7", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", "@vercel/stega": "0.1.2", - "@vercel/toolbar": "^0.1.18", - "apps-common": "workspace:*", + "@vercel/toolbar": "^0.1.22", "autoprefixer": "10.4.20", - "eslint": "^8.57.0", - "eslint-config-next": "14.2.9", - "next": "14.2.9", - "next-sanity": "9.4.7", - "postcss": "8.4.45", + "eslint": "^8.57.1", + "eslint-config-next": "14.2.15", + "next": "14.2.15", + "next-sanity": "9.7.0-canary.15", + "postcss": "8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", "styled-components": "6.1.13", "suspend-react": "0.1.3", - "tailwindcss": "^3.4.10", - "typescript": "5.6.2" + "tailwindcss": "^3.4.14", + "typescript": "5.6.3" + }, + "devDependencies": { + "@repo/prettier-config": "workspace:*" } } diff --git a/apps/next/sanity-typegen.json b/apps/next/sanity-typegen.json new file mode 100644 index 000000000..c863e3400 --- /dev/null +++ b/apps/next/sanity-typegen.json @@ -0,0 +1,4 @@ +{ + "schema": "./node_modules/@repo/sanity-extracted-schema/shoes.json", + "generates": "./src/types.ts" +} diff --git a/apps/next/src/app/Timesince.tsx b/apps/next/src/app/Timesince.tsx index 700f28e7a..9b7f9544d 100644 --- a/apps/next/src/app/Timesince.tsx +++ b/apps/next/src/app/Timesince.tsx @@ -1,6 +1,5 @@ 'use client' -import {formatTimeSince} from 'apps-common/utils' import {useEffect, useMemo, useState, useSyncExternalStore} from 'react' const subscribe = () => () => {} @@ -19,3 +18,21 @@ export function Timesince(props: {since: string}) { if (!mounted) return 'now' return {formatTimeSince(from, now)} } + +const rtf = new Intl.RelativeTimeFormat('en', {style: 'short'}) +export function formatTimeSince(from: Date, to: Date): string { + const seconds = Math.floor((from.getTime() - to.getTime()) / 1000) + if (seconds > -60) { + return rtf.format(Math.min(seconds, -1), 'second') + } + const minutes = Math.ceil(seconds / 60) + if (minutes > -60) { + return rtf.format(minutes, 'minute') + } + const hours = Math.ceil(minutes / 60) + if (hours > -24) { + return rtf.format(hours, 'hour') + } + const days = Math.ceil(hours / 24) + return rtf.format(days, 'day') +} diff --git a/apps/next/src/app/api/disable-draft/route.ts b/apps/next/src/app/api/draft-mode/disable/route.ts similarity index 73% rename from apps/next/src/app/api/disable-draft/route.ts rename to apps/next/src/app/api/draft-mode/disable/route.ts index 77c8d9eed..4736e3b7c 100644 --- a/apps/next/src/app/api/disable-draft/route.ts +++ b/apps/next/src/app/api/draft-mode/disable/route.ts @@ -4,5 +4,5 @@ import {NextRequest, NextResponse} from 'next/server' export function GET(request: NextRequest) { draftMode().disable() const url = new URL(request.nextUrl) - return NextResponse.redirect(new URL('/shoes', url.origin)) + return NextResponse.redirect(new URL('/pages-router/shoes', url.origin)) } diff --git a/apps/next/src/app/api/draft-mode/enable/route.ts b/apps/next/src/app/api/draft-mode/enable/route.ts new file mode 100644 index 000000000..573e6eec1 --- /dev/null +++ b/apps/next/src/app/api/draft-mode/enable/route.ts @@ -0,0 +1,8 @@ +import {client} from '@/components/sanity.client' +import {defineEnableDraftMode} from 'next-sanity/draft-mode' + +export const {GET} = defineEnableDraftMode({ + client: client.withConfig({ + token: process.env.SANITY_API_READ_TOKEN, + }), +}) diff --git a/apps/next/src/app/api/draft/route.ts b/apps/next/src/app/api/draft/route.ts deleted file mode 100644 index f9290738c..000000000 --- a/apps/next/src/app/api/draft/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {draftMode} from 'next/headers' -import {redirect} from 'next/navigation' -import {validatePreviewUrl} from '@sanity/preview-url-secret' -import {client} from '@/components/sanity.client' - -const clientWithToken = client.withConfig({ - token: process.env.SANITY_API_READ_TOKEN, -}) - -export async function GET(req: Request) { - const {isValid, redirectTo = '/shoes'} = await validatePreviewUrl(clientWithToken, req.url) - if (!isValid) { - return new Response('Invalid secret', {status: 401}) - } - - draftMode().enable() - - redirect(redirectTo) -} diff --git a/apps/next/src/app/compat/layout.tsx b/apps/next/src/app/compat/layout.tsx index 1f4694d66..4a1ca98e5 100644 --- a/apps/next/src/app/compat/layout.tsx +++ b/apps/next/src/app/compat/layout.tsx @@ -1,8 +1,7 @@ -import {draftMode} from 'next/headers' -import {unstable_cache} from 'next/cache' import {VisualEditing} from 'next-sanity' +import {unstable_cache} from 'next/cache' +import {draftMode} from 'next/headers' import '../../tailwind.css' - import {Timesince} from '../Timesince' export const metadata = { @@ -16,11 +15,7 @@ export default async function RootLayout({children}: {children: React.ReactNode} {children} {draftMode().isEnabled && } - + app-router:{' '} {draftMode().isEnabled ? 'draftMode' diff --git a/apps/next/src/app/compat/page.client.tsx b/apps/next/src/app/compat/page.client.tsx index 9543f3a13..b215a0039 100644 --- a/apps/next/src/app/compat/page.client.tsx +++ b/apps/next/src/app/compat/page.client.tsx @@ -1,14 +1,14 @@ 'use client' -import {type ShoesListResult} from 'apps-common/queries' -import {formatCurrency} from 'apps-common/utils' -import Image from 'next/image' -import Link from 'next/link' -import {urlFor, urlForCrossDatasetReference} from '../shoes/utils' +import type {ShoesListResult} from '@/types' +import {formatCurrency} from '@/utils' +import {workspaces} from '@repo/env' import {ContentSourceMap, ContentSourceMapDocuments} from '@sanity/client/csm' import {useDocumentsInUse} from '@sanity/preview-kit-compat' +import Image from 'next/image' +import Link from 'next/link' import {useMemo} from 'react' -import {workspaces} from 'apps-common/env' +import {urlFor, urlForCrossDatasetReference} from '../shoes/utils' const {projectId, dataset} = workspaces['next-app-router'] @@ -55,7 +55,7 @@ export default function ShoesPageClient(props: Props) { {
{products?.map?.((product, i) => ( - +
{product.brand?.logo?.alt - {product.brand.name} + + { + // @ts-expect-error - cross dataset reference typegen not working yet + product.brand.name + } +
)}
diff --git a/apps/next/src/app/compat/page.tsx b/apps/next/src/app/compat/page.tsx index 01bbadabc..175ad5468 100644 --- a/apps/next/src/app/compat/page.tsx +++ b/apps/next/src/app/compat/page.tsx @@ -1,7 +1,7 @@ -import {ShoesListResult, shoesList} from 'apps-common/queries' -import ShoesPageClient from './page.client' -import {client} from '../shoes/sanity.client' +import {shoesList} from '@/queries' import {draftMode} from 'next/headers' +import {client} from '../shoes/sanity.client' +import ShoesPageClient from './page.client' const stegaClient = client.withConfig({ stega: {enabled: true}, @@ -11,7 +11,7 @@ const stegaClient = client.withConfig({ }) export default async function ShoesPage() { - const {result, resultSourceMap} = await stegaClient.fetch( + const {result, resultSourceMap} = await stegaClient.fetch( shoesList, {}, { diff --git a/apps/next/src/app/only-visual-editing/layout.tsx b/apps/next/src/app/only-visual-editing/layout.tsx index 77c734d5e..41896c526 100644 --- a/apps/next/src/app/only-visual-editing/layout.tsx +++ b/apps/next/src/app/only-visual-editing/layout.tsx @@ -1,9 +1,8 @@ -import {draftMode} from 'next/headers' -import {revalidatePath, revalidateTag, unstable_cache} from 'next/cache' -import {VisualEditing} from 'next-sanity' import {VercelToolbar} from '@vercel/toolbar/next' +import {VisualEditing} from 'next-sanity' +import {revalidatePath, revalidateTag, unstable_cache} from 'next/cache' +import {draftMode} from 'next/headers' import '../../tailwind.css' - import {Timesince} from '../Timesince' export const metadata = { @@ -41,11 +40,7 @@ export default async function RootLayout({children}: {children: React.ReactNode} }} /> )} - + app-router:{' '} {draftMode().isEnabled ? 'draftMode' diff --git a/apps/next/src/app/only-visual-editing/page.client.tsx b/apps/next/src/app/only-visual-editing/page.client.tsx index 477d12602..26c5bd955 100644 --- a/apps/next/src/app/only-visual-editing/page.client.tsx +++ b/apps/next/src/app/only-visual-editing/page.client.tsx @@ -1,9 +1,9 @@ -import {type ShoesListResult} from 'apps-common/queries' -import {formatCurrency} from 'apps-common/utils' +import type {ShoesListResult} from '@/types' +import {formatCurrency} from '@/utils' +import {ContentSourceMap} from '@sanity/client/csm' import Image from 'next/image' import Link from 'next/link' import {urlFor, urlForCrossDatasetReference} from '../shoes/utils' -import {ContentSourceMap} from '@sanity/client/csm' type Props = { data: ShoesListResult @@ -39,7 +39,7 @@ export default function ShoesPageClient(props: Props) { {
{products?.map?.((product, i) => ( - +
{product.brand?.logo?.alt - {product.brand.name} + + { + // @ts-expect-error - cross dataset reference typegen not working yet + product.brand.name + } +
)}
diff --git a/apps/next/src/app/only-visual-editing/page.tsx b/apps/next/src/app/only-visual-editing/page.tsx index 27c3a7152..5d6823fc7 100644 --- a/apps/next/src/app/only-visual-editing/page.tsx +++ b/apps/next/src/app/only-visual-editing/page.tsx @@ -1,7 +1,7 @@ -import {ShoesListResult, shoesList} from 'apps-common/queries' -import ShoesPageClient from './page.client' -import {client} from '../shoes/sanity.client' +import {shoesList} from '@/queries' import {draftMode} from 'next/headers' +import {client} from '../shoes/sanity.client' +import ShoesPageClient from './page.client' const stegaClient = client.withConfig({ stega: {enabled: true}, @@ -11,7 +11,7 @@ const stegaClient = client.withConfig({ }) export default async function ShoesPage() { - const {result, resultSourceMap} = await stegaClient.fetch( + const {result, resultSourceMap} = await stegaClient.fetch( shoesList, {}, { diff --git a/apps/next/src/app/shoes/VisualEditing.tsx b/apps/next/src/app/shoes/VisualEditing.tsx index c43f5e501..400ced7a4 100644 --- a/apps/next/src/app/shoes/VisualEditing.tsx +++ b/apps/next/src/app/shoes/VisualEditing.tsx @@ -2,7 +2,6 @@ import {useLiveMode} from '@sanity/react-loader' import {VisualEditing} from 'next-sanity' -import {useEffect} from 'react' import {client} from './sanity.client' // Always enable stega in Live Mode @@ -10,12 +9,6 @@ const stegaClient = client.withConfig({stega: true}) export default function LiveVisualEditing(props: React.ComponentProps) { useLiveMode({client: stegaClient}) - useEffect(() => { - if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && window === parent && !opener) { - // If not an iframe, turn off Draft Mode - location.href = '/api/disable-draft' - } - }, []) return } diff --git a/apps/next/src/app/shoes/[slug]/page.client.tsx b/apps/next/src/app/shoes/[slug]/page.client.tsx index 62941402b..190146013 100644 --- a/apps/next/src/app/shoes/[slug]/page.client.tsx +++ b/apps/next/src/app/shoes/[slug]/page.client.tsx @@ -1,14 +1,15 @@ 'use client' +import {shoe} from '@/queries' +import type {ShoeResult} from '@/types' +import {formatCurrency} from '@/utils' import {PortableText} from '@portabletext/react' +import {LemonIcon} from '@sanity/icons' import {QueryResponseInitial, useQuery} from '@sanity/react-loader' -import {shoe, type ShoeParams, type ShoeResult} from 'apps-common/queries' -import {formatCurrency} from 'apps-common/utils' import Image from 'next/image' import Link from 'next/link' import {use} from 'react' import {urlFor, urlForCrossDatasetReference} from '../utils' -import {LemonIcon} from '@sanity/icons' type Props = { params: {slug: string} @@ -24,7 +25,7 @@ export default function ShoePage(props: Props) { error, loading: _loading, encodeDataAttribute, - } = useQuery(shoe, params satisfies ShoeParams, {initial}) + } = useQuery(shoe, params, {initial}) if (error) { throw error @@ -144,30 +145,42 @@ export default function ShoePage(props: Props) { {product.price ? formatCurrency(product.price) : 'FREE'}

- {product.brand?.name && ( -
-

Brand

-
- {product.brand?.logo?.alt - {product.brand.name} + { + // @ts-expect-error - cross dataset reference typegen not working yet + product.brand?.name && ( +
+

Brand

+
+ {product.brand?.logo?.alt + + { + // @ts-expect-error - cross dataset reference typegen not working yet + product.brand.name + } + +
-
- )} + ) + }