From d32baa7782ce09083f3cc63de3fd07d43089f758 Mon Sep 17 00:00:00 2001 From: Henrik Larsson Date: Mon, 14 Aug 2023 12:06:46 +0200 Subject: [PATCH] Ported sanity studio to Next js app --- app/{ => (site)}/[locale]/[...slug]/page.tsx | 0 .../[...slug]/pages/category-page.tsx | 0 .../[locale]/[...slug]/pages/product-page.tsx | 0 .../[locale]/[...slug]/pages/single-page.tsx | 0 app/{ => (site)}/[locale]/error.tsx | 0 app/{ => (site)}/[locale]/layout.tsx | 6 +- app/{ => (site)}/[locale]/not-found.tsx | 0 app/{ => (site)}/[locale]/page.tsx | 0 app/{ => (site)}/[locale]/sok/page.tsx | 0 app/(studio)/studio/[[...index]]/page.tsx | 8 + app/(studio)/studio/layout.tsx | 14 + app/api/revalidate/sanity/route.ts | 39 - app/{[locale] => }/globals.css | 0 .../components/hotspots/ProductTooltip.tsx | 38 + lib/sanity/components/icons/kodamera.tsx | 27 + .../components/inputs/PlaceholderString.tsx | 20 + lib/sanity/components/inputs/ProxyString.tsx | 32 + lib/sanity/constants.ts | 48 + lib/sanity/desk/blurbStructure.ts | 15 + lib/sanity/desk/categoryStructure.ts | 33 + lib/sanity/desk/homeStructure.ts | 34 + lib/sanity/desk/index.ts | 73 ++ lib/sanity/desk/navigationStructure.ts | 31 + lib/sanity/desk/pageStructure.ts | 35 + lib/sanity/desk/productStructure.ts | 73 ++ lib/sanity/desk/sectionStructure.ts | 15 + lib/sanity/desk/settingStructure.ts | 15 + lib/sanity/desk/uspStructure.ts | 15 + lib/sanity/languages.ts | 21 + lib/sanity/localizedTypes.ts | 23 + lib/sanity/schemas/blocks/body.tsx | 54 + lib/sanity/schemas/documents/blurb.tsx | 122 +++ lib/sanity/schemas/documents/category.tsx | 126 +++ lib/sanity/schemas/documents/footerMenu.tsx | 48 + lib/sanity/schemas/documents/page.tsx | 75 ++ lib/sanity/schemas/documents/product.tsx | 167 +++ .../schemas/documents/productVariant.tsx | 30 + lib/sanity/schemas/documents/section.tsx | 64 ++ .../documents/slugWithLocalizedType.ts | 56 + lib/sanity/schemas/documents/usp.tsx | 62 ++ lib/sanity/schemas/index.ts | 91 ++ lib/sanity/schemas/objects/banner.ts | 50 + lib/sanity/schemas/objects/blurbSection.tsx | 132 +++ .../schemas/objects/filteredProductList.ts | 70 ++ lib/sanity/schemas/objects/hero.ts | 125 +++ lib/sanity/schemas/objects/linkExternal.ts | 54 + lib/sanity/schemas/objects/linkInternal.ts | 56 + lib/sanity/schemas/objects/mainImage.ts | 26 + lib/sanity/schemas/objects/menu.ts | 34 + .../schemas/objects/placeholderString.ts | 10 + lib/sanity/schemas/objects/productOption.ts | 25 + lib/sanity/schemas/objects/productOptions.ts | 32 + lib/sanity/schemas/objects/proxyString.ts | 11 + .../schemas/objects/reusableSection.tsx | 54 + lib/sanity/schemas/objects/seo.tsx | 45 + lib/sanity/schemas/objects/slider.ts | 97 ++ lib/sanity/schemas/objects/uspSection.ts | 63 ++ lib/sanity/schemas/singletons/home.tsx | 69 ++ lib/sanity/schemas/singletons/settings.ts | 158 +++ lib/sanity/schemas/singletons/utilityMenu.ts | 48 + lib/sanity/utils/defineStructure.ts | 11 + lib/sanity/utils/getPreviewUrl.ts | 19 + lib/sanity/utils/validation.ts | 59 ++ middleware.ts | 2 +- package.json | 7 + pnpm-lock.yaml | 959 +++++++++++++++++- sanity.config.ts | 75 ++ 67 files changed, 3755 insertions(+), 46 deletions(-) rename app/{ => (site)}/[locale]/[...slug]/page.tsx (100%) rename app/{ => (site)}/[locale]/[...slug]/pages/category-page.tsx (100%) rename app/{ => (site)}/[locale]/[...slug]/pages/product-page.tsx (100%) rename app/{ => (site)}/[locale]/[...slug]/pages/single-page.tsx (100%) rename app/{ => (site)}/[locale]/error.tsx (100%) rename app/{ => (site)}/[locale]/layout.tsx (91%) rename app/{ => (site)}/[locale]/not-found.tsx (100%) rename app/{ => (site)}/[locale]/page.tsx (100%) rename app/{ => (site)}/[locale]/sok/page.tsx (100%) create mode 100644 app/(studio)/studio/[[...index]]/page.tsx create mode 100644 app/(studio)/studio/layout.tsx delete mode 100644 app/api/revalidate/sanity/route.ts rename app/{[locale] => }/globals.css (100%) create mode 100644 lib/sanity/components/hotspots/ProductTooltip.tsx create mode 100644 lib/sanity/components/icons/kodamera.tsx create mode 100644 lib/sanity/components/inputs/PlaceholderString.tsx create mode 100644 lib/sanity/components/inputs/ProxyString.tsx create mode 100644 lib/sanity/constants.ts create mode 100644 lib/sanity/desk/blurbStructure.ts create mode 100644 lib/sanity/desk/categoryStructure.ts create mode 100644 lib/sanity/desk/homeStructure.ts create mode 100644 lib/sanity/desk/index.ts create mode 100644 lib/sanity/desk/navigationStructure.ts create mode 100644 lib/sanity/desk/pageStructure.ts create mode 100644 lib/sanity/desk/productStructure.ts create mode 100644 lib/sanity/desk/sectionStructure.ts create mode 100644 lib/sanity/desk/settingStructure.ts create mode 100644 lib/sanity/desk/uspStructure.ts create mode 100644 lib/sanity/languages.ts create mode 100644 lib/sanity/localizedTypes.ts create mode 100644 lib/sanity/schemas/blocks/body.tsx create mode 100644 lib/sanity/schemas/documents/blurb.tsx create mode 100644 lib/sanity/schemas/documents/category.tsx create mode 100644 lib/sanity/schemas/documents/footerMenu.tsx create mode 100644 lib/sanity/schemas/documents/page.tsx create mode 100644 lib/sanity/schemas/documents/product.tsx create mode 100644 lib/sanity/schemas/documents/productVariant.tsx create mode 100644 lib/sanity/schemas/documents/section.tsx create mode 100644 lib/sanity/schemas/documents/slugWithLocalizedType.ts create mode 100644 lib/sanity/schemas/documents/usp.tsx create mode 100644 lib/sanity/schemas/index.ts create mode 100644 lib/sanity/schemas/objects/banner.ts create mode 100644 lib/sanity/schemas/objects/blurbSection.tsx create mode 100644 lib/sanity/schemas/objects/filteredProductList.ts create mode 100644 lib/sanity/schemas/objects/hero.ts create mode 100644 lib/sanity/schemas/objects/linkExternal.ts create mode 100644 lib/sanity/schemas/objects/linkInternal.ts create mode 100644 lib/sanity/schemas/objects/mainImage.ts create mode 100644 lib/sanity/schemas/objects/menu.ts create mode 100644 lib/sanity/schemas/objects/placeholderString.ts create mode 100644 lib/sanity/schemas/objects/productOption.ts create mode 100644 lib/sanity/schemas/objects/productOptions.ts create mode 100644 lib/sanity/schemas/objects/proxyString.ts create mode 100644 lib/sanity/schemas/objects/reusableSection.tsx create mode 100644 lib/sanity/schemas/objects/seo.tsx create mode 100644 lib/sanity/schemas/objects/slider.ts create mode 100644 lib/sanity/schemas/objects/uspSection.ts create mode 100644 lib/sanity/schemas/singletons/home.tsx create mode 100644 lib/sanity/schemas/singletons/settings.ts create mode 100644 lib/sanity/schemas/singletons/utilityMenu.ts create mode 100644 lib/sanity/utils/defineStructure.ts create mode 100644 lib/sanity/utils/getPreviewUrl.ts create mode 100644 lib/sanity/utils/validation.ts create mode 100644 sanity.config.ts diff --git a/app/[locale]/[...slug]/page.tsx b/app/(site)/[locale]/[...slug]/page.tsx similarity index 100% rename from app/[locale]/[...slug]/page.tsx rename to app/(site)/[locale]/[...slug]/page.tsx diff --git a/app/[locale]/[...slug]/pages/category-page.tsx b/app/(site)/[locale]/[...slug]/pages/category-page.tsx similarity index 100% rename from app/[locale]/[...slug]/pages/category-page.tsx rename to app/(site)/[locale]/[...slug]/pages/category-page.tsx diff --git a/app/[locale]/[...slug]/pages/product-page.tsx b/app/(site)/[locale]/[...slug]/pages/product-page.tsx similarity index 100% rename from app/[locale]/[...slug]/pages/product-page.tsx rename to app/(site)/[locale]/[...slug]/pages/product-page.tsx diff --git a/app/[locale]/[...slug]/pages/single-page.tsx b/app/(site)/[locale]/[...slug]/pages/single-page.tsx similarity index 100% rename from app/[locale]/[...slug]/pages/single-page.tsx rename to app/(site)/[locale]/[...slug]/pages/single-page.tsx diff --git a/app/[locale]/error.tsx b/app/(site)/[locale]/error.tsx similarity index 100% rename from app/[locale]/error.tsx rename to app/(site)/[locale]/error.tsx diff --git a/app/[locale]/layout.tsx b/app/(site)/[locale]/layout.tsx similarity index 91% rename from app/[locale]/layout.tsx rename to app/(site)/[locale]/layout.tsx index 7c8240522..21e7b80ab 100644 --- a/app/[locale]/layout.tsx +++ b/app/(site)/[locale]/layout.tsx @@ -4,8 +4,8 @@ import { NextIntlClientProvider } from 'next-intl'; import { Inter } from 'next/font/google'; import { notFound } from 'next/navigation'; import { ReactNode, Suspense } from 'react'; -import { supportedLanguages } from '../../i18n-config'; -import './globals.css'; +import { supportedLanguages } from '../../../i18n-config'; +import './../../globals.css'; export const metadata = { title: { @@ -48,7 +48,7 @@ export default async function LocaleLayout({ children, params: { locale } }: Loc let messages; try { - messages = (await import(`../../messages/${locale}.json`)).default; + messages = (await import(`../../../messages/${locale}.json`)).default; } catch (error) { notFound(); } diff --git a/app/[locale]/not-found.tsx b/app/(site)/[locale]/not-found.tsx similarity index 100% rename from app/[locale]/not-found.tsx rename to app/(site)/[locale]/not-found.tsx diff --git a/app/[locale]/page.tsx b/app/(site)/[locale]/page.tsx similarity index 100% rename from app/[locale]/page.tsx rename to app/(site)/[locale]/page.tsx diff --git a/app/[locale]/sok/page.tsx b/app/(site)/[locale]/sok/page.tsx similarity index 100% rename from app/[locale]/sok/page.tsx rename to app/(site)/[locale]/sok/page.tsx diff --git a/app/(studio)/studio/[[...index]]/page.tsx b/app/(studio)/studio/[[...index]]/page.tsx new file mode 100644 index 000000000..665afa1ef --- /dev/null +++ b/app/(studio)/studio/[[...index]]/page.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { NextStudio } from 'next-sanity/studio'; +import config from '@/sanity.config'; + +export default function AdminPage() { + return ; +} diff --git a/app/(studio)/studio/layout.tsx b/app/(studio)/studio/layout.tsx new file mode 100644 index 000000000..4877d0dc6 --- /dev/null +++ b/app/(studio)/studio/layout.tsx @@ -0,0 +1,14 @@ +import './../../globals.css'; + +export const metadata = { + title: `Studio | ${process.env.SITE_NAME}`, + description: 'KM Storefront studio admin interface.' +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/api/revalidate/sanity/route.ts b/app/api/revalidate/sanity/route.ts deleted file mode 100644 index 63146640a..000000000 --- a/app/api/revalidate/sanity/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isValidSignature, SIGNATURE_HEADER_NAME } from '@sanity/webhook'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; - -const SANITY_WEBHOOK_SECRET = `${process.env.SANITY_WEBHOOK_SECRET}`; - -export async function POST(request: NextRequest) { - // Await the response from our request. - const requestData = await request.json(); - - // Get headers. - const headersList = headers(); - - // Get Sanity webhook signature header name. - const signature = `${headersList.get(SIGNATURE_HEADER_NAME)}`; - const isValid = isValidSignature(JSON.stringify(requestData), signature, SANITY_WEBHOOK_SECRET); - - // Log out validity of request. - console.log(`Webhook request valid? ${isValid}`); - - // If not valid, return. - if (!isValid) { - NextResponse.json({ success: false, message: 'Invalid signature' }); - return; - } - - const slug = requestData.slug; - const type = requestData.type; - - if (type === 'home') { - revalidatePath(`${slug}`) - } else { - revalidatePath(`${slug}`) - } - - console.log(`Revalidated path: ${slug}`); - return NextResponse.json({ revalidated: true, now: Date.now() }); -} \ No newline at end of file diff --git a/app/[locale]/globals.css b/app/globals.css similarity index 100% rename from app/[locale]/globals.css rename to app/globals.css diff --git a/lib/sanity/components/hotspots/ProductTooltip.tsx b/lib/sanity/components/hotspots/ProductTooltip.tsx new file mode 100644 index 000000000..4b39100c8 --- /dev/null +++ b/lib/sanity/components/hotspots/ProductTooltip.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components' +import {PreviewLayoutKey, SchemaType, useSchema} from 'sanity' +import {Box} from '@sanity/ui' +import {HotspotTooltipProps} from 'sanity-plugin-hotspot-array' +import {useMemo} from 'react' + +interface HotspotFields { + productWithVariant?: { + product: { + _ref: string + } + } +} + +const StyledBox = styled(Box)` + width: 200px; +` + +export default function ProductPreview(props: HotspotTooltipProps) { + const {value, renderPreview} = props + const productSchemaType = useSchema().get('product') + const hasProduct = value?.productWithVariant?.product?._ref && productSchemaType + + const previewProps = useMemo( + () => ({ + value: value?.productWithVariant?.product, + schemaType: productSchemaType as SchemaType, + layout: 'default' as PreviewLayoutKey, + }), + [productSchemaType, value?.productWithVariant?.product] + ) + + return ( + + {hasProduct && previewProps ? renderPreview(previewProps) : `No product selected`} + + ) +} diff --git a/lib/sanity/components/icons/kodamera.tsx b/lib/sanity/components/icons/kodamera.tsx new file mode 100644 index 000000000..51af6859b --- /dev/null +++ b/lib/sanity/components/icons/kodamera.tsx @@ -0,0 +1,27 @@ +const Kodamera = () => { + return ( +
+ + + + +
+ ) +} + +export default Kodamera \ No newline at end of file diff --git a/lib/sanity/components/inputs/PlaceholderString.tsx b/lib/sanity/components/inputs/PlaceholderString.tsx new file mode 100644 index 000000000..22e96526d --- /dev/null +++ b/lib/sanity/components/inputs/PlaceholderString.tsx @@ -0,0 +1,20 @@ +import {StringInputProps, useFormValue, SanityDocument, StringSchemaType} from 'sanity' +import get from 'lodash.get' + +type Props = StringInputProps + +const PlaceholderStringInput = (props: Props) => { + const {schemaType} = props + + const path = schemaType?.options?.field + const doc = useFormValue([]) as SanityDocument + + const proxyValue = path ? (get(doc, path) as string) : '' + + return props.renderDefault({ + ...props, + elementProps: {placeholder: proxyValue, ...props.elementProps}, + }) +} + +export default PlaceholderStringInput diff --git a/lib/sanity/components/inputs/ProxyString.tsx b/lib/sanity/components/inputs/ProxyString.tsx new file mode 100644 index 000000000..3129370c7 --- /dev/null +++ b/lib/sanity/components/inputs/ProxyString.tsx @@ -0,0 +1,32 @@ +import {LockIcon} from '@sanity/icons' +import {Box, Text, TextInput, Tooltip} from '@sanity/ui' +import {StringInputProps, useFormValue, SanityDocument, StringSchemaType} from 'sanity' +import get from 'lodash.get' + +type Props = StringInputProps + +const ProxyString = (props: Props) => { + const {schemaType} = props + + const path = schemaType?.options?.field + const doc = useFormValue([]) as SanityDocument + + const proxyValue = path ? (get(doc, path) as string) : '' + + return ( + + + This value is set in Shopify ({path}) + + + } + portal + > + + + ) +} + +export default ProxyString diff --git a/lib/sanity/constants.ts b/lib/sanity/constants.ts new file mode 100644 index 000000000..9551f2347 --- /dev/null +++ b/lib/sanity/constants.ts @@ -0,0 +1,48 @@ +// Currency code (ISO 4217) to use when displaying prices in the studio +// https://en.wikipedia.org/wiki/ISO_4217 +export const DEFAULT_CURRENCY_CODE = 'SEK' + +// Document types which: +// - cannot be created in the 'new document' menu +// - cannot be duplicated, unpublished or deleted +export const LOCKED_DOCUMENT_TYPES = ['settings', 'home', 'media.tag'] + +// Document types which: +// - cannot be created in the 'new document' menu +// - cannot be duplicated, unpublished or deleted +// - are from the KM-COMMERCE connect app. +export const STORM_DOCUMENT_TYPES = ['product', 'productVariant', 'category'] + +// References to include in 'internal' links +export const PAGE_REFERENCES = [ + {type: 'category'}, + {type: 'home'}, + {type: 'page'}, + {type: 'product'}, + {type: 'productVariant'}, +] + +// Objects to include in page building arrays. +export const COMPONENT_REFERENCES = [ + {type: 'hero'}, + {type: 'slider'}, + {type: 'filteredProductList'}, + {type: 'blurbSection'}, + {type: 'uspSection'}, + {type: 'reusableSection'}, +] + +// API version to use when using the Sanity client within the studio +// https://www.sanity.io/help/studio-client-specify-api-version +export const SANITY_API_VERSION = '2022-10-25' + +// Your Shopify store ID. +// This is your unique store URL (e.g. 'my-store-name.myshopify.com'). +// Set this to enable helper links in document status banners and shortcut links on products and collections. +export const STORM_STORE_ID = '' + +// Project preview URLs +export const localStorefrontUrl = 'http://localhost:3000'; +export const localStorefrontPreviewUrl = 'http://localhost:3000/api/draft'; +export const publicStorefrontUrl = 'https://km-storefront.vercel.app'; +export const publicStorefrontPreviewUrl = 'https://km-storefront.vercel.app/api/draft'; \ No newline at end of file diff --git a/lib/sanity/desk/blurbStructure.ts b/lib/sanity/desk/blurbStructure.ts new file mode 100644 index 000000000..3d9dd8d39 --- /dev/null +++ b/lib/sanity/desk/blurbStructure.ts @@ -0,0 +1,15 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' + +export default defineStructure((S) => + S.listItem() + .title('Blurbs') + .schemaType('blurb') + .child ( + S.documentTypeList('blurb') + .child ( + S.document() + .schemaType("blurb") + ) + ) +) diff --git a/lib/sanity/desk/categoryStructure.ts b/lib/sanity/desk/categoryStructure.ts new file mode 100644 index 000000000..35892698a --- /dev/null +++ b/lib/sanity/desk/categoryStructure.ts @@ -0,0 +1,33 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' +import Iframe from 'sanity-plugin-iframe-pane' +import {SanityDocument} from 'sanity' +import {EyeOpenIcon, MasterDetailIcon} from '@sanity/icons' +import getPreviewUrl from '../utils/getPreviewUrl' + +export default defineStructure((S) => + S.listItem() + .title('Categories') + .schemaType('category') + .child ( + S.documentTypeList('category') + .child (id => + S.document() + .schemaType("category") + .id(id) + .views([ + S.view + .form() + .icon(MasterDetailIcon), + S.view + .component(Iframe) + .icon(EyeOpenIcon) + .options({ + url: (doc: SanityDocument) => getPreviewUrl(doc), + }) + .title('Preview') + ]) + ) + + ) +) diff --git a/lib/sanity/desk/homeStructure.ts b/lib/sanity/desk/homeStructure.ts new file mode 100644 index 000000000..3b7aacf4b --- /dev/null +++ b/lib/sanity/desk/homeStructure.ts @@ -0,0 +1,34 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' +import Iframe from 'sanity-plugin-iframe-pane' +import {SanityDocument} from 'sanity' +import {EyeOpenIcon, MasterDetailIcon} from '@sanity/icons' +import getPreviewUrl from '../utils/getPreviewUrl' + +export default defineStructure((S) => + S.listItem() + .title('Home') + .schemaType('home') + .child ( + S.documentList() + .title('Home pages') + .filter('_type == "home"') + .child(id => + S.document() + .schemaType("home") + .id(id) + .views([ + S.view + .form() + .icon(MasterDetailIcon), + S.view + .component(Iframe) + .icon(EyeOpenIcon) + .options({ + url: (doc: SanityDocument) => getPreviewUrl(doc), + }) + .title('Preview') + ]) + ) + ) +) diff --git a/lib/sanity/desk/index.ts b/lib/sanity/desk/index.ts new file mode 100644 index 000000000..f6a82cecc --- /dev/null +++ b/lib/sanity/desk/index.ts @@ -0,0 +1,73 @@ +/** + * Desk structure overrides + */ +import {ListItemBuilder, StructureResolver} from 'sanity/desk' +import categories from './categoryStructure' +import home from './homeStructure' +import pages from './pageStructure' +import products from './productStructure' +import settings from './settingStructure' +import blurbs from './blurbStructure' +import sections from './sectionStructure' +import usps from './uspStructure' +import navigation from './navigationStructure' + +/** + * Desk structure overrides + * + * Sanity Studio automatically lists document types out of the box. + * With this custom desk structure we achieve things like showing the `home` + * and `settings` document types as singletons, and grouping product details + * and variants for easy editorial access. + * + * You can customize this even further as your schemas progress. + * To learn more about structure builder, visit our docs: + * https://www.sanity.io/docs/overview-structure-builder + */ + +// If you add document types to desk structure manually, you can add them to this function to prevent duplicates in the root pane +const hiddenDocTypes = (listItem: ListItemBuilder) => { + const id = listItem.getId() + + if (!id) { + return false + } + + return ![ + 'category', + 'home', + 'media.tag', + 'page', + 'product', + 'productVariant', + 'settings', + 'blurb', + 'section', + 'usp', + 'navigation', + 'footerMenu', + 'utilityMenu' + ].includes(id) +} + +export const structure: StructureResolver = (S, context) => + S.list() + .title('Content') + .items([ + home(S, context), + pages(S, context), + S.divider(), + products(S, context), + categories(S, context), + S.divider(), + blurbs(S, context), + usps(S, context), + sections(S, context), + S.divider(), + navigation(S, context), + S.divider(), + settings(S, context), + S.divider(), + ...S.documentTypeListItems().filter(hiddenDocTypes), + S.divider(), + ]) diff --git a/lib/sanity/desk/navigationStructure.ts b/lib/sanity/desk/navigationStructure.ts new file mode 100644 index 000000000..155c23260 --- /dev/null +++ b/lib/sanity/desk/navigationStructure.ts @@ -0,0 +1,31 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' +import {MenuIcon} from '@sanity/icons' + +export default defineStructure((S) => + S.listItem() + .title('Navigation') + .icon(MenuIcon) + .child( + S.list() + // Sets a title for our new list + .title('Navigation Documents') + // Add items to the array + // Each will pull one of our new documents/singletons + .items([ + S.listItem() + .title('Utility menu') + .child(S.document().schemaType('utilityMenu').documentId('utilityMenu')), + S.listItem() + .title('Footer menus') + .child( + S.documentTypeList('footerMenu') + .title('Footer menus') + .child ( + S.document() + .schemaType("footerMenu") + ) + ), + ]) + ), +) \ No newline at end of file diff --git a/lib/sanity/desk/pageStructure.ts b/lib/sanity/desk/pageStructure.ts new file mode 100644 index 000000000..a2ae46e4b --- /dev/null +++ b/lib/sanity/desk/pageStructure.ts @@ -0,0 +1,35 @@ + import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' +import {DocumentsIcon} from '@sanity/icons' +import Iframe from 'sanity-plugin-iframe-pane' +import {SanityDocument} from 'sanity' +import {EyeOpenIcon, MasterDetailIcon} from '@sanity/icons' +import getPreviewUrl from '../utils/getPreviewUrl' + +export default defineStructure((S) => + S.listItem() + .title('Pages') + .schemaType('page') + .icon(DocumentsIcon) + .child ( + S.documentTypeList('page') + .child (id => + S.document() + .schemaType("page") + .id(id) + .views([ + S.view + .form() + .icon(MasterDetailIcon), + S.view + .component(Iframe) + .icon(EyeOpenIcon) + .options({ + url: (doc: SanityDocument) => getPreviewUrl(doc), + }) + .title('Preview') + ]) + ) + + ) +) \ No newline at end of file diff --git a/lib/sanity/desk/productStructure.ts b/lib/sanity/desk/productStructure.ts new file mode 100644 index 000000000..6f985dc4f --- /dev/null +++ b/lib/sanity/desk/productStructure.ts @@ -0,0 +1,73 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' +import Iframe from 'sanity-plugin-iframe-pane' +import {SanityDocument} from 'sanity' +import {EyeOpenIcon, MasterDetailIcon} from '@sanity/icons' +import getPreviewUrl from '../utils/getPreviewUrl' + +export default defineStructure((S) => + S.listItem() + .title('Products') + .schemaType('product') + .child ( + S.documentTypeList('product') + .child (id => + S.document() + .schemaType("product") + .id(id) + .views([ + S.view + .form() + .icon(MasterDetailIcon), + S.view + .component(Iframe) + .icon(EyeOpenIcon) + .options({ + url: (doc: SanityDocument) => getPreviewUrl(doc), + }) + .title('Preview') + ]) + ) + + ) +) + +// @TODO - FIX THIS STRUCTURE. +// export default defineStructure((S) => +// S.listItem() +// .title('Products') +// .schemaType('product') +// .child( +// S.documentTypeList('product') +// // .defaultLayout('detail') +// .child(async (id) => +// S.list() +// .title('Product') +// .items([ +// // Details +// S.listItem() +// .title('Details') +// .icon(InfoOutlineIcon) +// .child(S.document().schemaType('product').documentId(id)), +// // Product variants +// S.listItem() +// .title('Variants') +// .schemaType('productVariant') +// .child( +// S.documentList() +// .title('Variants') +// .schemaType('productVariant') +// .filter( +// ` +// _type == "productVariant" +// && store.productId == $productId +// ` +// ) +// .params({ +// productId: Number(id.replace('shopifyProduct-', '')), +// }) +// ), +// ]) +// ) +// ) +// ) diff --git a/lib/sanity/desk/sectionStructure.ts b/lib/sanity/desk/sectionStructure.ts new file mode 100644 index 000000000..7a03a3010 --- /dev/null +++ b/lib/sanity/desk/sectionStructure.ts @@ -0,0 +1,15 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' + +export default defineStructure((S) => + S.listItem() + .title('Sections') + .schemaType('section') + .child ( + S.documentTypeList('section') + .child ( + S.document() + .schemaType("section") + ) + ) +) diff --git a/lib/sanity/desk/settingStructure.ts b/lib/sanity/desk/settingStructure.ts new file mode 100644 index 000000000..881a2b769 --- /dev/null +++ b/lib/sanity/desk/settingStructure.ts @@ -0,0 +1,15 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' + +export default defineStructure((S) => + S.listItem() + .title('Settings') + .schemaType('settings') + .child ( + S.documentTypeList('settings') + .child ( + S.document() + .schemaType("settings") + ) + ) +) diff --git a/lib/sanity/desk/uspStructure.ts b/lib/sanity/desk/uspStructure.ts new file mode 100644 index 000000000..e4e0af8eb --- /dev/null +++ b/lib/sanity/desk/uspStructure.ts @@ -0,0 +1,15 @@ +import {ListItemBuilder} from 'sanity/desk' +import defineStructure from '../utils/defineStructure' + +export default defineStructure((S) => + S.listItem() + .title('USPs') + .schemaType('usp') + .child ( + S.documentTypeList('usp') + .child ( + S.document() + .schemaType("usp") + ) + ) +) \ No newline at end of file diff --git a/lib/sanity/languages.ts b/lib/sanity/languages.ts new file mode 100644 index 000000000..d92d13230 --- /dev/null +++ b/lib/sanity/languages.ts @@ -0,0 +1,21 @@ + +export const languages = [ + { + id: 'sv', + title: 'Swedish', + flag: '🇸🇪' + }, + { + id: 'en', + title: 'English', + flag: '🇬🇧' + }, +] + +const i18n = { + languages: languages, + base: 'sv' +} + +// For v3 studio +export {i18n} \ No newline at end of file diff --git a/lib/sanity/localizedTypes.ts b/lib/sanity/localizedTypes.ts new file mode 100644 index 000000000..0b93d8b1b --- /dev/null +++ b/lib/sanity/localizedTypes.ts @@ -0,0 +1,23 @@ +const product = { + type: 'product', + sv: 'produkt', + en: 'product' +} + +const category = { + type: 'category', + 'sv': 'kategori', + 'en': 'category' +} + +const page = { + type: 'page', + 'sv': '', + 'en': '' +} + +export const localizedTypes = [ + page, + product, + category, +] \ No newline at end of file diff --git a/lib/sanity/schemas/blocks/body.tsx b/lib/sanity/schemas/blocks/body.tsx new file mode 100644 index 000000000..94626c058 --- /dev/null +++ b/lib/sanity/schemas/blocks/body.tsx @@ -0,0 +1,54 @@ +import {defineField} from 'sanity' + +export default defineField({ + name: 'body', + title: 'Body', + type: 'array', + of: [ + { + lists: [ + {title: 'Bullet', value: 'bullet'}, + {title: 'Numbered', value: 'number'}, + ], + marks: { + decorators: [ + { + title: 'Italic', + value: 'em', + }, + { + title: 'Strong', + value: 'strong', + }, + ], + }, + // Paragraphs + type: 'block', + }, + // Custom blocks + // { + // name: 'blockAccordion', + // type: 'module.accordion', + // }, + // { + // name: 'blockCallout', + // type: 'module.callout', + // }, + // { + // name: 'blockGrid', + // type: 'module.grid', + // }, + // { + // name: 'blockImages', + // type: 'module.images', + // }, + // { + // name: 'blockInstagram', + // type: 'module.instagram', + // }, + // { + // name: 'blockProducts', + // type: 'module.products', + // }, + ], +}) diff --git a/lib/sanity/schemas/documents/blurb.tsx b/lib/sanity/schemas/documents/blurb.tsx new file mode 100644 index 000000000..eead7f8dc --- /dev/null +++ b/lib/sanity/schemas/documents/blurb.tsx @@ -0,0 +1,122 @@ +import {CommentIcon} from '@sanity/icons' +import {defineField} from 'sanity' +import {languages} from '../../languages' +import {validateImage} from '../../utils/validation' + +export default defineField({ + name: 'blurb', + title: 'Blurb', + type: 'document', + icon: CommentIcon, + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'What do you want to convey?', + validation: (Rule) => Rule.required(), + }), + // Image + defineField({ + name: 'image', + title: 'Image', + type: 'mainImage', + validation: (Rule) => validateImage(Rule, true), + }), + // Text + defineField({ + name: 'text', + title: 'Text', + type: 'text', + description: 'Small text displayed below title.', + rows: 5, + }), + // Link + { + name: 'link', + title: 'Link', + type: 'object', + fields: [ + { + name: 'linkType', + type: 'string', + title: 'Link type', + initialValue: 'internal', + description: 'Link to internal or external content.', + validation: (Rule) => Rule.required(), + options: { + list: ['internal', 'external'], + layout: 'radio', + }, + }, + { + name: 'internalLink', + type: 'linkInternal', + title: 'Internal link', + hidden: ({parent}) => parent?.linkType !== 'internal', + options: { + collapsible: false, + }, + validation: (Rule) => + Rule.custom((value: any, context: any) => { + if (context.parent.linkType == 'internal') { + const currentLink = value && value.reference + if (!currentLink) { + return 'Reference is required' + } + } + return true + }), + }, + { + name: 'externalLink', + type: 'linkExternal', + title: 'External link', + hidden: ({parent}) => parent?.linkType !== 'external', + options: { + collapsible: false, + }, + validation: (Rule) => + Rule.custom((value: any, context: any) => { + if (context.parent.linkType == 'external') { + const currentTitle = value?.title + const currentUrl = value?.url + if (!currentTitle) { + return 'Title is required' + } else if (!currentUrl) { + return 'URL is required' + } + } + return true + }), + }, + ], + }, + ], + preview: { + select: { + title: 'title', + image: 'image', + language: 'language', + }, + prepare(selection) { + const {image, title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + media: image, + title, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/category.tsx b/lib/sanity/schemas/documents/category.tsx new file mode 100644 index 000000000..493df701c --- /dev/null +++ b/lib/sanity/schemas/documents/category.tsx @@ -0,0 +1,126 @@ +import {TagIcon} from '@sanity/icons' +import {defineField, defineType} from 'sanity' +import {slugWithLocalizedType} from './slugWithLocalizedType' +import {languages} from '../../languages' +import {validateImage} from '../../utils/validation' + +const GROUPS = [ + { + name: 'editorial', + title: 'Editorial', + }, + { + name: 'seo', + title: 'SEO', + }, +] + +export default defineType({ + name: 'category', + title: 'Category', + type: 'document', + icon: TagIcon, + groups: GROUPS, + fields: [ + // Language + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'Category title.', + }), + // Slug + slugWithLocalizedType('category', 'title'), + // // Show banner + // defineField({ + // name: 'showBanner', + // title: 'Show banner', + // type: 'boolean', + // description: 'If disabled, category title will be displayed instead.', + // group: 'editorial', + // }), + // // Banner + // defineField({ + // name: 'banner', + // title: 'Banner', + // type: 'banner', + // hidden: ({document}) => !document?.showBanner, + // group: 'editorial', + // }), + // Image + defineField({ + name: 'image', + type: 'mainImage', + title: 'Image', + validation: (Rule) => validateImage(Rule, true), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + rows: 5, + description: 'Description of this category.', + }), + defineField({ + name: 'id', + title: 'ID', + type: 'number', + description: 'Unique ID.', + }), + defineField({ + name: 'categoryId', + title: 'Category ID', + type: 'number', + description: 'Unique category ID.', + }), + defineField({ + name: 'parentId', + title: 'Parent ID', + type: 'number', + description: 'Unique parent category ID.', + }), + // SEO + defineField({ + name: 'seo', + title: 'SEO', + type: 'seo', + group: 'seo', + }), + ], + orderings: [ + { + name: 'titleAsc', + title: 'Title (A-Z)', + by: [{field: 'title', direction: 'asc'}], + }, + { + name: 'titleDesc', + title: 'Title (Z-A)', + by: [{field: 'title', direction: 'desc'}], + }, + ], + preview: { + select: { + title: 'title', + language: 'language', + }, + prepare(selection) { + const {title, language} = selection + const currentLang = languages.find((lang) => lang.id === language) + + return { + title, + subtitle: `${currentLang ? currentLang.title : ''}`, + media: TagIcon, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/footerMenu.tsx b/lib/sanity/schemas/documents/footerMenu.tsx new file mode 100644 index 000000000..b57dda013 --- /dev/null +++ b/lib/sanity/schemas/documents/footerMenu.tsx @@ -0,0 +1,48 @@ +import {MenuIcon} from '@sanity/icons' +import {defineType, defineField} from 'sanity' +import {languages} from '../../languages' + +export default defineType({ + name: 'footerMenu', + title: 'Footer menu', + type: 'document', + icon: MenuIcon, + groups: [], + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + defineField({ + name: 'title', + type: 'string', + title: 'Title', + description: 'Menu title or designation for menu.', + }), + // Menu + defineField({ + name: 'menu', + title: 'Menu', + type: 'menu', + }), + ], + preview: { + select: { + title: 'title', + language: 'language', + }, + prepare(selection) { + const {title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + title: `${title}`, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/page.tsx b/lib/sanity/schemas/documents/page.tsx new file mode 100644 index 000000000..7b040327b --- /dev/null +++ b/lib/sanity/schemas/documents/page.tsx @@ -0,0 +1,75 @@ +import {DocumentIcon} from '@sanity/icons' +import {defineField} from 'sanity' +import {languages} from '../../languages' +import {COMPONENT_REFERENCES} from '../../constants' +import {slugWithLocalizedType} from './slugWithLocalizedType' + +export default defineField({ + name: 'page', + title: 'Page', + type: 'document', + icon: DocumentIcon, + groups: [ + { + name: 'editorial', + title: 'Editorial', + }, + { + name: 'seo', + title: 'SEO', + }, + ], + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'Page title.', + validation: (Rule) => Rule.required(), + }), + // Slug + slugWithLocalizedType('page', 'title'), + // Content + defineField({ + name: 'content', + title: 'Page sections', + type: 'array', + group: 'editorial', + description: 'Add, reorder, edit or delete page sections.', + of: COMPONENT_REFERENCES, + }), + // SEO + defineField({ + name: 'seo', + title: 'SEO', + type: 'seo', + group: 'seo', + }), + ], + preview: { + select: { + seoImage: 'seo.image', + title: 'title', + language: 'language', + }, + prepare(selection) { + const {seoImage, title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + media: seoImage, + title, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/product.tsx b/lib/sanity/schemas/documents/product.tsx new file mode 100644 index 000000000..0e773cf41 --- /dev/null +++ b/lib/sanity/schemas/documents/product.tsx @@ -0,0 +1,167 @@ +import {PackageIcon} from '@sanity/icons' +import {defineField, defineType} from 'sanity' +import {slugWithLocalizedType} from './slugWithLocalizedType' +import {languages} from '../../languages' +import {validateImage} from '../../utils/validation' + +const GROUPS = [ + { + name: 'editorial', + title: 'Editorial', + }, + { + name: 'seo', + title: 'SEO', + }, +] + +export default defineType({ + name: 'product', + title: 'Product', + type: 'document', + icon: PackageIcon, + groups: GROUPS, + fields: [ + // Language + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // ID + defineField({ + name: 'id', + title: 'ID', + type: 'number', + description: 'Unique product ID.', + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'Product title/name.', + validation: (Rule) => Rule.required(), + }), + // Slug + slugWithLocalizedType('product', 'title'), + defineField({ + name: 'images', + title: 'Images', + description: 'Images of this product, the first image will be used as main image.', + type: 'array', + of: [ + { + title: 'Image', + type: 'mainImage', + validation: (Rule) => validateImage(Rule, true), + }, + ], + validation: (Rule) => Rule.required().min(1).max(5), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + description: 'Product description.', + }), + defineField({ + name: 'price', + title: 'Price', + type: 'object', + description: 'Product price information.', + fields: [ + defineField({ + name: 'value', + title: 'Value', + type: 'number', + description: 'Product price.', + }), + defineField({ + name: 'currencyCode', + title: 'Currency code', + type: 'string', + description: 'Product currency code.', + options: { + list: [ + {title: 'SEK', value: 'SEK'}, + {title: 'GBP', value: 'GBP'}, + {title: 'EUR', value: 'EUR'}, + ], + layout: 'radio', + }, + initialValue: 'SEK', + }), + defineField({ + name: 'retailPrice', + title: 'Retail price', + type: 'number', + description: 'Product retail price.', + }), + ], + }), + defineField({ + name: 'options', + title: 'Product options', + type: 'array', + description: 'What product options are available?', + of: [{type: 'productOptions'}], + }), + defineField({ + name: 'categories', + title: 'Categories', + type: 'array', + description: 'What category/categories does this product belong to?', + of: [{type: 'reference', to: {type: 'category'}}], + }), + defineField({ + name: 'seo', + title: 'SEO', + type: 'seo', + group: 'seo', + }), + ], + orderings: [ + { + name: 'titleAsc', + title: 'Title (A-Z)', + by: [{field: 'title', direction: 'asc'}], + }, + { + name: 'titleDesc', + title: 'Title (Z-A)', + by: [{field: 'title', direction: 'desc'}], + }, + { + name: 'priceDesc', + title: 'Price (Highest first)', + by: [{field: 'price', direction: 'desc'}], + }, + { + name: 'priceAsc', + title: 'Title (Lowest first)', + by: [{field: 'price', direction: 'asc'}], + }, + ], + preview: { + select: { + images: 'images', + title: 'title', + language: 'language', + }, + prepare(selection) { + const {images, title, language} = selection + const currentLang = languages.find((lang) => lang.id === language) + + const firstImage = images[0] + + return { + title, + subtitle: `${currentLang ? currentLang.title : ''}`, + media: firstImage ? firstImage : PackageIcon, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/productVariant.tsx b/lib/sanity/schemas/documents/productVariant.tsx new file mode 100644 index 000000000..74a6f9769 --- /dev/null +++ b/lib/sanity/schemas/documents/productVariant.tsx @@ -0,0 +1,30 @@ +import {CopyIcon} from '@sanity/icons' +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'productVariant', + title: 'Product variant', + type: 'document', + icon: CopyIcon, + fields: [ + // Title + defineField({ + title: 'Title', + name: 'title', + type: 'string', + description: 'Product variant title/name.' + }), + ], + preview: { + select: { + title: 'title', + }, + prepare(selection) { + const {title} = selection + + return { + title, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/section.tsx b/lib/sanity/schemas/documents/section.tsx new file mode 100644 index 000000000..3de3cada1 --- /dev/null +++ b/lib/sanity/schemas/documents/section.tsx @@ -0,0 +1,64 @@ +import {defineField} from 'sanity' +import {BlockElementIcon} from '@sanity/icons' +import {languages} from '../../languages' + +export default defineField({ + name: 'section', + type: 'document', + title: 'Reusable section', + icon: BlockElementIcon, + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + }), + defineField({ + name: 'section', + title: 'Section', + type: 'object', + description: 'Reusable section to refer to from other pages.', + fields: [ + defineField({ + name: 'sectionType', + type: 'array', + title: 'Section type', + description: 'Select reusable component (only 1 allowed).', + of: [ + {type: 'hero'}, + {type: 'filteredProductList'}, + {type: 'slider'}, + {type: 'blurbSection'}, + {type: 'uspSection'}, + ], + validation: (Rule) => Rule.length(1), + }), + ], + }), + ], + preview: { + select: { + title: 'title', + language: 'language', + }, + prepare(selection) { + const {title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + title: `${title}`, + media: BlockElementIcon, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/documents/slugWithLocalizedType.ts b/lib/sanity/schemas/documents/slugWithLocalizedType.ts new file mode 100644 index 000000000..d2880d440 --- /dev/null +++ b/lib/sanity/schemas/documents/slugWithLocalizedType.ts @@ -0,0 +1,56 @@ +import {Rule, Slug} from 'sanity' +import slugify from "slugify"; +import { i18n } from "../../languages"; +import { localizedTypes } from "../../localizedTypes"; + +const MAX_LENGTH = 96 + +function formatSlug(input: string, docType: string, context: any, schemaType: object | any) { + const locale = schemaType?.parent?.language ? schemaType?.parent?.language : i18n.base; + + let currentDocType: any; + + currentDocType = localizedTypes.find(item => item.type === docType); + const currentDocTypeLocalized = currentDocType[locale]; + + const slugStart = currentDocTypeLocalized ? `/${currentDocTypeLocalized}/` : `/`; + const slug = slugify(input, { lower: true }); + + return slugStart + slug; +} + +export function slugWithLocalizedType(documentType = '', source = `title`) { + const docType = documentType; + + return { + name: `slug`, + type: `slug`, + options: { + source, + slugify: (value: any, context: any, schemaType: object | any) => formatSlug(value, docType, context, schemaType), + }, + validation: (Rule: Rule) => { + return Rule.required().custom(async (value: Slug) => { + + const currentSlug = value && value.current + + if (!currentSlug) { + return true + } + + if (currentSlug.length >= MAX_LENGTH) { + return `Must be less than ${MAX_LENGTH} characters` + } + + if (currentSlug.length === 0) { + return 'Slug cannot be empty' + } + + if (currentSlug.endsWith("/")) { + return 'Slug cannot end with "/"' + } + return true + }) + } + }; +} \ No newline at end of file diff --git a/lib/sanity/schemas/documents/usp.tsx b/lib/sanity/schemas/documents/usp.tsx new file mode 100644 index 000000000..b372884e9 --- /dev/null +++ b/lib/sanity/schemas/documents/usp.tsx @@ -0,0 +1,62 @@ +import {StarIcon} from '@sanity/icons' +import {defineField} from 'sanity' +import {languages} from '../../languages' +import {validateImage} from '../../utils/validation' + +export default defineField({ + name: 'usp', + title: 'USPs', + type: 'document', + icon: StarIcon, + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'USP title', + validation: (Rule) => Rule.required(), + }), + // Image + defineField({ + name: 'image', + title: 'Image', + type: 'mainImage', + description: 'USP icon', + validation: (Rule) => validateImage(Rule, true), + }), + // Text + defineField({ + name: 'text', + title: 'Text', + type: 'text', + description: 'Small text displayed below title.', + rows: 5, + }), + ], + preview: { + select: { + title: 'title', + image: 'image', + language: 'language', + }, + prepare(selection) { + const {image, title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + media: image, + title, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/index.ts b/lib/sanity/schemas/index.ts new file mode 100644 index 000000000..b5492acb2 --- /dev/null +++ b/lib/sanity/schemas/index.ts @@ -0,0 +1,91 @@ +// Rich text annotations used in the block content editor +// import annotationLinkEmail from './annotations/linkEmail' +// import annotationLinkExternal from './annotations/linkExternal' +// import annotationLinkInternal from './annotations/linkInternal' +// import annotationProduct from './annotations/product' + +// const annotations = [ +// annotationLinkEmail, +// annotationLinkExternal, +// annotationLinkInternal, +// annotationProduct, +// ] + +// Document types +import category from './documents/category' +import page from './documents/page' +import product from './documents/product' +import productVariant from './documents/productVariant' +import blurb from './documents/blurb' +import section from './documents/section' +import usp from './documents/usp' +import footerMenu from './documents/footerMenu' + +const documents = [ + category, + page, + product, + productVariant, + blurb, + section, + usp, + footerMenu +] + +// Singleton document types +import home from './singletons/home' +import settings from './singletons/settings' +import utilityMenu from './singletons/utilityMenu' +// import navigation from './singletons/navigation' + +const singletons = [home, settings, utilityMenu] + +// Block content +import body from './blocks/body' + +const blocks = [body] + +// Object types +import banner from './objects/banner' +import linkExternal from './objects/linkExternal' +import linkInternal from './objects/linkInternal' +import hero from './objects/hero' +import placeholderString from './objects/placeholderString' +import proxyString from './objects/proxyString' +import seo from './objects/seo' +import mainImage from './objects/mainImage' +import slider from './objects/slider' +import productOption from './objects/productOption' +import productOptions from './objects/productOptions' +import blurbSection from './objects/blurbSection' +import filteredProductList from './objects/filteredProductList' +import uspSection from './objects/uspSection' +import reusableSection from './objects/reusableSection' +import menu from './objects/menu' + +const objects = [ + linkExternal, + linkInternal, + hero, + placeholderString, + proxyString, + seo, + mainImage, + slider, + productOption, + productOptions, + filteredProductList, + banner, + blurbSection, + uspSection, + reusableSection, + menu +] + +export const schemaTypes = [ + // ...annotations, + ...documents, + ...singletons, + ...objects, + ...blocks +] diff --git a/lib/sanity/schemas/objects/banner.ts b/lib/sanity/schemas/objects/banner.ts new file mode 100644 index 000000000..fe950b1a9 --- /dev/null +++ b/lib/sanity/schemas/objects/banner.ts @@ -0,0 +1,50 @@ +import {defineField} from 'sanity' +import { validateImage } from '../../utils/validation' + +export default defineField({ + name: 'banner', + type: 'object', + title: 'Banner', + description: 'Normally used in the top of a page to display current page information.', + fields: [ + { + name: 'title', + type: 'string', + title: 'Title', + description: 'What do you want to convey?', + validation: Rule => [ + Rule.required(), + Rule.max(50).warning('Shorter titles are usually better.') + ] + }, + { + name: 'text', + type: 'text', + title: 'Text', + rows: 5, + description: 'Small text below title.' + }, + { + name: 'image', + type: 'mainImage', + title: 'Image', + validation: (Rule) => validateImage(Rule, true) + }, + ], + preview: { + select: { + title: 'title', + image: 'image', + text: 'text' + }, + prepare(selection) { + const {title, image, text} = selection + + return { + title: `${title}`, + subtitle: `Banner`, + media: image + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/blurbSection.tsx b/lib/sanity/schemas/objects/blurbSection.tsx new file mode 100644 index 000000000..c946c8b78 --- /dev/null +++ b/lib/sanity/schemas/objects/blurbSection.tsx @@ -0,0 +1,132 @@ +import {defineField} from 'sanity' +import {CommentIcon} from '@sanity/icons' + +export default defineField({ + name: 'blurbSection', + type: 'object', + title: 'Blurb section', + icon: CommentIcon, + fieldsets: [ + { + name: 'layoutSettings', + title: 'Layout settings', + }, + ], + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: 'false', + validation: (Rule) => Rule.required(), + }, + { + name: 'title', + type: 'string', + title: 'Title', + description: 'Text displayed above blurbs.', + validation: (Rule) => Rule.required(), + }, + { + name: 'mobileLayout', + type: 'string', + title: 'Mobile layout', + initialValue: 'stacked', + fieldset: 'layoutSettings', + description: 'Display blurbs stacked on top of each other or in a slider.', + validation: (Rule) => Rule.required(), + options: { + list: [ + { + title: 'Vertical (1 column)', + value: 'vertical', + }, + { + title: 'Horizontal (1 row with scroll)', + value: 'horizontal', + }, + ], + layout: 'radio', + direction: 'horizontal', + }, + }, + { + name: 'desktopLayout', + type: 'string', + title: 'Desktop layout', + initialValue: '3-column', + fieldset: 'layoutSettings', + description: 'Display blurbs in a 2- 3- or 4-column layout.', + validation: (Rule) => Rule.required(), + options: { + list: [ + { + title: '2 columns', + value: '2-column', + }, + { + title: '3 columns', + value: '3-column', + }, + { + title: '4 columns', + value: '4-column', + }, + ], + layout: 'radio', + direction: 'horizontal', + }, + }, + { + name: 'imageFormat', + type: 'string', + title: 'Blurb image format', + initialValue: 'square', + description: 'Choose format to display blurb images in.', + validation: (Rule) => Rule.required(), + fieldset: 'layoutSettings', + options: { + list: [ + {title: 'Square (1:1)', value: 'square'}, + {title: 'Portrait (3:4)', value: 'portrait'}, + {title: 'Landscape (16:9)', value: 'landscape'}, + ], + layout: 'radio', + }, + }, + { + name: 'blurbs', + type: 'array', + title: 'Blurbs', + description: 'Create blurbs or refer to existing blurbs.', + of: [ + { + type: 'reference', + to: [ + { + type: 'blurb', + }, + ], + }, + ], + validation: (Rule) => Rule.required(), + }, + ], + + preview: { + select: { + title: 'title', + disabled: 'disabled', + }, + prepare(selection) { + const {title, disabled} = selection + + return { + title: `${title}`, + subtitle: `Blurb section ${disabled ? '(⚠️ Disabled)' : ''}`, + media: CommentIcon, + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/filteredProductList.ts b/lib/sanity/schemas/objects/filteredProductList.ts new file mode 100644 index 000000000..fd2172c47 --- /dev/null +++ b/lib/sanity/schemas/objects/filteredProductList.ts @@ -0,0 +1,70 @@ +import {defineField} from 'sanity' +import {FilterIcon} from '@sanity/icons' + +export default defineField({ + name: 'filteredProductList', + type: 'object', + title: 'Filtered product list', + icon: FilterIcon, + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: 'false', + validation: (Rule) => Rule.required(), + }, + { + name: 'title', + type: 'string', + title: 'Title', + description: 'Text displayed above product list.' + }, + { + name: 'productCategories', + type: 'array', + title: 'Product categories', + description: 'Select category/categories to display products from.', + of: [ + { + type: 'reference', + to: [{ + type: 'category', + }], + options: { + disableNew: true, + }, + }, + ], + validation: (Rule) => Rule.required(), + }, + { + name: 'itemsToShow', + type: 'number', + title: 'Number of products', + initialValue: 4, + description: 'Amount of products to be displayed.', + validation: (Rule) => Rule.required(), + options: { + list: [4, 8, 12], + layout: 'radio', + }, + }, + ], + preview: { + select: { + title: 'title', + disabled: 'disabled' + }, + prepare(selection) { + const {title, disabled} = selection + + return { + title: `${title}`, + subtitle: `Filtered product list ${disabled ? '(⚠️ Disabled)' : ''}`, + media: FilterIcon + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/hero.ts b/lib/sanity/schemas/objects/hero.ts new file mode 100644 index 000000000..c60d9c5b0 --- /dev/null +++ b/lib/sanity/schemas/objects/hero.ts @@ -0,0 +1,125 @@ +import {defineField} from 'sanity' +import {StarIcon} from '@sanity/icons' +import { validateImage } from '../../utils/validation' + +export default defineField({ + name: 'hero', + type: 'object', + title: 'Hero', + icon: StarIcon, + fieldsets: [ + { + name: 'settings', + title: 'Hero settings', + description: 'Hero layout and semantic settings.', + options: { + collapsed: true, + collapsible: true, + }, + } + ], + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: false, + validation: (Rule) => Rule.required(), + }, + { + name: 'variant', + type: 'string', + title: 'Hero variant', + initialValue: 'fullScreen', + description: 'Choose display for larger screens: Full or half screen height.', + validation: Rule => Rule.required(), + fieldset: 'settings', + options: { + list: [ + {title: 'Full screen', value: 'fullScreen'}, + {title: '50% height', value: 'halfScreen'}, + ], + layout: 'radio', + } + }, + { + name: 'headingLevel', + type: 'string', + title: 'Heading level', + initialValue: 'h1', + fieldset: 'settings', + description: 'Set appropriate heading level depending on the current document structure.', + options: { + list: [ + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + ], + layout: 'radio', + }, + }, + { + name: 'label', + type: 'string', + title: 'Label', + description: 'Small text displayed above title.' + }, + { + name: 'title', + type: 'string', + title: 'Title', + description: 'What you want to convey.', + validation: Rule => [ + Rule.required(), + Rule.max(50).warning('Shorter titles are usually better.') + ] + }, + { + name: 'text', + type: 'text', + title: 'Text', + rows: 5, + description: 'Short text displayed below title.', + validation: Rule => [ + Rule.max(100).warning('Strive to be short, precise and on point.') + ] + }, + { + name: 'image', + type: 'mainImage', + title: 'Image', + validation: Rule => validateImage(Rule, true), + options: { + hotspot: true, + collapsed: false, + collapsible: true, + }, + }, + { + name: 'link', + type: 'linkInternal', + title: 'Link', + description: 'Link to internal page.', + options: { + collapsed: true, + collapsible: true, + }, + }, + ], + preview: { + select: { + title: 'title', + image: 'image', + disabled: 'disabled' + }, + prepare(selection) { + const {title, image, disabled} = selection + + return { + title: `${title}`, + media: image?.asset ? image : StarIcon, + subtitle: `Hero ${disabled ? '(⚠️ Disabled)' : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/linkExternal.ts b/lib/sanity/schemas/objects/linkExternal.ts new file mode 100644 index 000000000..0f9becb8e --- /dev/null +++ b/lib/sanity/schemas/objects/linkExternal.ts @@ -0,0 +1,54 @@ +import {EarthGlobeIcon} from '@sanity/icons' +import {defineField} from 'sanity' + +export default defineField({ + title: 'External Link', + name: 'linkExternal', + type: 'object', + icon: EarthGlobeIcon, + description: 'Link to content on external site.', + fields: [ + // Title + defineField({ + title: 'Title', + name: 'title', + type: 'string', + description: 'Descriptive text for the content on this link.' + }), + // URL + defineField({ + name: 'url', + title: 'URL', + type: 'url', + description: 'Link to websites, e-mail address or phone number.', + validation: (Rule) => Rule.required().uri({scheme: ['http', 'https', 'mailto', 'tel']}) + }), + // Open in a new window? + defineField({ + title: 'Open in a new window?', + name: 'newWindow', + type: 'boolean', + description: 'If set to true, opens the link in a new window.', + initialValue: false, + }) + ], + preview: { + select: { + title: 'title', + url: 'url', + }, + prepare(selection) { + const {title, url} = selection + + let subtitle = [] + if (url) { + subtitle.push(`→ ${url}`) + } + + return { + title, + subtitle: subtitle.join(' '), + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/linkInternal.ts b/lib/sanity/schemas/objects/linkInternal.ts new file mode 100644 index 000000000..eef2225fc --- /dev/null +++ b/lib/sanity/schemas/objects/linkInternal.ts @@ -0,0 +1,56 @@ +import {LinkIcon} from '@sanity/icons' +import {defineField} from 'sanity' +import {PAGE_REFERENCES} from '../../constants' + +export default defineField({ + title: 'Internal Link', + name: 'linkInternal', + type: 'object', + description: 'Link to content on this site.', + icon: LinkIcon, + fields: [ + // Title + defineField({ + title: 'Title', + name: 'title', + type: 'string', + description: 'If empty, displays the current reference title.' + }), + // Reference + defineField({ + name: 'reference', + type: 'reference', + title: 'Content reference', + description: 'Link to already created, internal content.', + weak: true, + to: PAGE_REFERENCES, + }), + ], + preview: { + select: { + reference: 'reference', + referenceTitle: 'reference.title', + referenceType: 'reference._type', + title: 'title', + }, + prepare(selection) { + const { + reference, + referenceTitle, + title, + } = selection + + let subtitle = [] + if (reference) { + subtitle.push([`→ ${referenceTitle || reference?._id}`]) + } else { + subtitle.push('(Nonexistent document reference)') + } + + return { + title, + subtitle: subtitle.join(' '), + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/mainImage.ts b/lib/sanity/schemas/objects/mainImage.ts new file mode 100644 index 000000000..ff16b6b77 --- /dev/null +++ b/lib/sanity/schemas/objects/mainImage.ts @@ -0,0 +1,26 @@ +import { defineField, defineType } from "sanity" + +export default defineType({ + name: 'mainImage', + type: 'image', + title: 'Main image', + description: 'Select or upload image. Edit by using the `menu` icon. Modify by using the `crop` icon.', + options: { + hotspot: true, + metadata: [ + 'blurhash', // Default: included + 'lqip', // Default: included + 'palette', // Default: included + 'exif', // Default: not included + 'location', // Default: not included + ], + }, + fields: [ + defineField({ + name: 'alt', + type: 'string', + title: 'Alternative text', + description: 'Note: Important for SEO and accessibility.', + }), + ], +}) \ No newline at end of file diff --git a/lib/sanity/schemas/objects/menu.ts b/lib/sanity/schemas/objects/menu.ts new file mode 100644 index 000000000..0d5613081 --- /dev/null +++ b/lib/sanity/schemas/objects/menu.ts @@ -0,0 +1,34 @@ +import {MenuIcon} from '@sanity/icons' +import {defineType, defineField} from 'sanity' + +export default defineType({ + name: 'menu', + title: 'Menu', + type: 'object', + icon: MenuIcon, + groups: [], + fields: [ + // Links + defineField({ + name: 'links', + title: 'Links', + type: 'array', + of: [ + {type: 'linkInternal'}, + {type: 'linkExternal'}, + ], + }) + ], + preview: { + select: { + title: 'title', + }, + prepare(selection) { + const {title} = selection; + + return { + title: `${title}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/placeholderString.ts b/lib/sanity/schemas/objects/placeholderString.ts new file mode 100644 index 000000000..c8c2ff209 --- /dev/null +++ b/lib/sanity/schemas/objects/placeholderString.ts @@ -0,0 +1,10 @@ +import PlaceholderStringInput from '../../components/inputs/PlaceholderString' + +export default { + name: 'placeholderString', + title: 'Title', + type: 'string', + components: { + input: PlaceholderStringInput, + }, +} diff --git a/lib/sanity/schemas/objects/productOption.ts b/lib/sanity/schemas/objects/productOption.ts new file mode 100644 index 000000000..47eae289a --- /dev/null +++ b/lib/sanity/schemas/objects/productOption.ts @@ -0,0 +1,25 @@ +import {defineField} from 'sanity' + +export default defineField({ + name: 'productOption', + title: 'Product option', + type: 'object', + fields: [ + defineField({ + name: 'label', + title: 'Label', + type: 'string', + validation: Rule => Rule.required(), + description: 'Product option label.' + }), + defineField({ + name: 'hexColors', + title: 'Color hex code', + type: 'color', + description: 'Hex color code for product option.', + options: { + disableAlpha: true + } + }) + ], +}) diff --git a/lib/sanity/schemas/objects/productOptions.ts b/lib/sanity/schemas/objects/productOptions.ts new file mode 100644 index 000000000..88a5066d0 --- /dev/null +++ b/lib/sanity/schemas/objects/productOptions.ts @@ -0,0 +1,32 @@ +import {defineField} from 'sanity' + +export default defineField({ + name: 'productOptions', + title: 'Product options', + type: 'object', + fields: [ + defineField({ + name: 'id', + title: 'ID (string)', + type: 'string', + validation: Rule => Rule.required(), + description: 'Unique product option ID.' + }), + defineField({ + name: 'displayName', + title: 'Display name', + type: 'string', + description: 'Name displayed for this collection of product options.', + validation: Rule => Rule.required(), + }), + defineField({ + name: 'values', + title: 'Values', + type: 'array', + description: 'What kind of values are available?', + of: [{type: 'productOption'}], + options: {}, + validation: Rule => Rule.required(), + }), + ], +}) diff --git a/lib/sanity/schemas/objects/proxyString.ts b/lib/sanity/schemas/objects/proxyString.ts new file mode 100644 index 000000000..f5e839e17 --- /dev/null +++ b/lib/sanity/schemas/objects/proxyString.ts @@ -0,0 +1,11 @@ +import {defineField} from 'sanity' +import ProxyStringInput from '../../components/inputs/ProxyString' + +export default defineField({ + name: 'proxyString', + title: 'Title', + type: 'string', + components: { + input: ProxyStringInput, + }, +}) diff --git a/lib/sanity/schemas/objects/reusableSection.tsx b/lib/sanity/schemas/objects/reusableSection.tsx new file mode 100644 index 000000000..d9c8535c3 --- /dev/null +++ b/lib/sanity/schemas/objects/reusableSection.tsx @@ -0,0 +1,54 @@ +import {defineField} from 'sanity' +import {BlockElementIcon} from '@sanity/icons' + +export default defineField({ + name: 'reusableSection', + type: 'object', + title: 'Reusable section', + icon: BlockElementIcon, + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: 'false', + validation: (Rule) => Rule.required(), + }, + { + name: 'title', + type: 'string', + title: 'Title', + }, + { + name: 'section', + type: 'object', + title: 'Section', + description: 'Reference to an existing section (only 1 allowed).', + fields: [ + { + title: 'Existing section', + name: 'existingSection', + type: 'reference', + to: [{type: 'section'}], + }, + ], + }, + ], + + preview: { + select: { + title: 'title', + disabled: 'disabled', + }, + prepare(selection) { + const {title, disabled} = selection + + return { + title: `${title}`, + subtitle: `Reusable section ${disabled ? '(⚠️ Disabled)' : ''}`, + media: BlockElementIcon, + } + }, + }, +}) diff --git a/lib/sanity/schemas/objects/seo.tsx b/lib/sanity/schemas/objects/seo.tsx new file mode 100644 index 000000000..c0a1c2555 --- /dev/null +++ b/lib/sanity/schemas/objects/seo.tsx @@ -0,0 +1,45 @@ +import {defineField} from 'sanity' +import { validateImage } from '../../utils/validation' + +export default defineField({ + name: 'seo', + title: 'SEO', + type: 'object', + description: 'Optimise content for search engines.', + options: { + collapsed: false, + collapsible: true, + }, + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => + Rule.max(50).warning('Longer titles may be truncated by search engines'), + description: ( + <> + A short and accurate title representative of the content on this page.
+ If empty, displays the current document title (title). + + ), + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + rows: 2, + validation: (Rule) => + Rule.max(150).warning('Longer descriptions may be truncated by search engines'), + description: 'A brief description of the content on this page.' + }), + defineField({ + name: 'image', + title: 'Image', + type: 'mainImage', + validation: Rule => validateImage(Rule, false), + description: 'A representative image of the content on this page.' + }), + ], + validation: (Rule) => Rule.required(), +}) diff --git a/lib/sanity/schemas/objects/slider.ts b/lib/sanity/schemas/objects/slider.ts new file mode 100644 index 000000000..3cff3c286 --- /dev/null +++ b/lib/sanity/schemas/objects/slider.ts @@ -0,0 +1,97 @@ + +import {defineField} from 'sanity' +import {PackageIcon} from '@sanity/icons' +import {TagIcon, NumberIcon} from '@sanity/icons' + +export default defineField({ + name: 'slider', + type: 'object', + title: 'Slider', + icon: NumberIcon, + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: 'false', + validation: (Rule) => Rule.required(), + }, + { + name: 'title', + type: 'string', + title: 'Title', + description: 'Title displayed above slider items.', + validation: Rule => Rule.required(), + }, + { + name: 'sliderType', + type: 'string', + title: 'Slider type', + initialValue: 'products', + description: 'Select content type to display.', + validation: Rule => Rule.required(), + options: { + list: [ + {title: 'Products', value: 'products'}, + {title: 'Categories', value: 'categories'}, + ], + layout: 'radio' + } + }, + { + title: 'Products', + name: 'products', + type: 'array', + description: 'Select products to display.', + of: [ + { + type: 'reference', + to: [{type: 'product'}], + }, + ], + validation: Rule => Rule.custom((x:any, context:any) => { + if (context.parent.sliderType == 'products' && context?.parent?.products?.length < 3 || context?.parent?.products?.length > 8) { + return 'Must have between 3 and 8 items' + } + return true + }), + hidden: ({ parent }) => parent?.sliderType !== "products" + }, + { + title: 'Categories', + name: 'categories', + type: 'array', + description: 'Select categories to display.', + of: [ + { + type: 'reference', + to: [{type: 'category'}], + }, + ], + validation: Rule => Rule.custom((x:any, context:any) => { + if (context.parent.sliderType == 'categories' && context?.parent?.categories?.length < 3 || context?.parent?.categories?.length > 8) { + return 'Must have between 3 and 8 items' + } + return true + }), + hidden: ({ parent }) => parent?.sliderType !== "categories" + }, + ], + preview: { + select: { + title: 'title', + sliderType: 'sliderType', + disabled: 'disabled' + }, + prepare(selection) { + const {title, sliderType, disabled} = selection + + return { + title: `${title}`, + media: sliderType === 'products' ? PackageIcon : TagIcon, + subtitle: `${sliderType === 'products' ? 'Product' : 'Category'} slider ${disabled ? '(⚠️ Disabled)' : ''}`, + } + }, + }, +}) \ No newline at end of file diff --git a/lib/sanity/schemas/objects/uspSection.ts b/lib/sanity/schemas/objects/uspSection.ts new file mode 100644 index 000000000..9232e252e --- /dev/null +++ b/lib/sanity/schemas/objects/uspSection.ts @@ -0,0 +1,63 @@ +import {defineField} from 'sanity' +import {StarIcon} from '@sanity/icons' + +export default defineField({ + name: 'uspSection', + type: 'object', + title: 'USP section', + icon: StarIcon, + fieldsets: [ + { + name: 'layoutSettings', + title: 'Layout settings' + }, + ], + fields: [ + { + name: 'disabled', + type: 'boolean', + title: 'Disabled?', + description: 'Set to true to disable this section.', + initialValue: 'false', + validation: (Rule) => Rule.required(), + }, + { + name: 'title', + type: 'string', + title: 'Title', + description: 'Title will only be used internally.', + validation: (Rule) => Rule.required(), + }, + { + name: 'usps', + type: 'array', + title: 'USPs', + description: 'Create USPs or refer to existing USP.', + of: [ + { + type: 'reference', + to: [{ + type: 'usp', + }], + }, + ], + validation: (Rule) => Rule.required().min(2).max(4), + }, + ], + + preview: { + select: { + title: 'title', + disabled: 'disabled' + }, + prepare(selection) { + const {title, disabled} = selection + + return { + title: `${title}`, + subtitle: `USP section ${disabled ? '(⚠️ Disabled)' : ''}`, + media: StarIcon, + } + }, + }, +}) diff --git a/lib/sanity/schemas/singletons/home.tsx b/lib/sanity/schemas/singletons/home.tsx new file mode 100644 index 000000000..c17aa29ad --- /dev/null +++ b/lib/sanity/schemas/singletons/home.tsx @@ -0,0 +1,69 @@ +import {HomeIcon} from '@sanity/icons' +import {defineField} from 'sanity' +import {languages} from '../../languages' +import {COMPONENT_REFERENCES} from '../../constants' + +export default defineField({ + name: 'home', + title: 'Home', + type: 'document', + icon: HomeIcon, + groups: [ + { + name: 'editorial', + title: 'Editorial', + }, + { + name: 'seo', + title: 'SEO', + }, + ], + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.', + // hidden: true, + }), + // Title + { + name: 'title', + title: 'Title', + type: 'string', + description: 'Page title.', + validation: (Rule) => Rule.required(), + }, + defineField({ + name: 'content', + title: 'Page sections', + type: 'array', + group: 'editorial', + description: 'Add, reorder, edit or delete page sections.', + of: COMPONENT_REFERENCES, + }), + // SEO + defineField({ + name: 'seo', + title: 'SEO', + type: 'seo', + group: 'seo', + }), + ], + preview: { + select: { + title: 'title', + language: 'language', + }, + prepare(selection) { + const {title, language} = selection + + const currentLang = languages.find((lang) => lang.id === language) + + return { + title: `${title}`, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/singletons/settings.ts b/lib/sanity/schemas/singletons/settings.ts new file mode 100644 index 000000000..5c1a20f3e --- /dev/null +++ b/lib/sanity/schemas/singletons/settings.ts @@ -0,0 +1,158 @@ +import {CogIcon} from '@sanity/icons' +import {defineType, defineField} from 'sanity' +import { languages } from '../../languages' + +const TITLE = 'Settings' + +export default defineType({ + name: 'settings', + title: TITLE, + type: 'document', + icon: CogIcon, + groups: [ + { + name: 'notFoundPage', + title: '404 page', + }, + { + name: 'socialMedia', + title: 'Social media', + }, + { + name: 'usps', + title: 'USPs', + }, + { + name: 'contact', + title: 'Contact', + }, + { + name: 'seo', + title: 'SEO', + }, + ], + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.' + // hidden: true, + }), + defineField({ + name: 'title', + type: 'string', + title: 'Title', + description: 'Document title.', + }), + // Not found page + defineField({ + name: 'notFoundPage', + title: '404 page', + type: 'object', + group: 'notFoundPage', + description: 'Information displayed on 404 page.', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => Rule.required(), + description: 'Page title displayed on 404 error page.', + }), + defineField({ + name: 'body', + title: 'Body', + type: 'text', + rows: 5, + description: 'Text displayed adjacent to the title on 404 error page.' + }), + defineField({ + name: 'category', + title: 'Category', + type: 'reference', + description: 'Category of products displayed on 404 error page.', + weak: true, + to: [ + { + name: 'category', + type: 'category', + }, + ], + }), + ] + }), + // Contact + defineField({ + name: 'contact', + title: 'Contact options', + type: 'object', + group: 'contact', + description: 'Contact options for your business.', + options: { + collapsed: false, + collapsible: true, + }, + fields: [ + // Selling points + defineField({ + name: 'contactOption', + title: 'Options (links)', + description: 'Links, e-mail address and phone numbers.', + type: 'array', + of: [ + {type: 'linkExternal'}, + ], + }), + ], + }), + // Social media links + defineField({ + name: 'socialMedia', + title: 'Social Media', + type: 'object', + group: 'socialMedia', + description: "Links to your business's social media accounts", + options: { + collapsed: false, + collapsible: true, + }, + fields: [ + // Links + defineField({ + name: 'links', + title: 'Links', + type: 'array', + description: 'Facebook, Twitter and Instgram etc.', + of: [ + {type: 'linkExternal'}, + ], + }), + ], + }), + // SEO + defineField({ + name: 'seo', + title: 'SEO', + type: 'seo', + group: 'seo', + description: 'Default SEO displayed for every page unless overwritten on page/document level.', + }), + ], + preview: { + select: { + title: 'title', + language: 'language' + }, + prepare(selection) { + const {title, language} = selection; + + const currentLang = languages.find(lang => lang.id === language); + + return { + title: `${title}`, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/schemas/singletons/utilityMenu.ts b/lib/sanity/schemas/singletons/utilityMenu.ts new file mode 100644 index 000000000..637087b62 --- /dev/null +++ b/lib/sanity/schemas/singletons/utilityMenu.ts @@ -0,0 +1,48 @@ +import {MenuIcon} from '@sanity/icons' +import {defineType, defineField} from 'sanity' +import { languages } from '../../languages' + +export default defineType({ + name: 'utilityMenu', + title: 'Utility menu', + type: 'document', + icon: MenuIcon, + groups: [], + fields: [ + defineField({ + name: 'language', + type: 'string', + readOnly: true, + description: 'Language of this document.' + // hidden: true, + }), + defineField({ + name: 'title', + type: 'string', + title: 'Title', + description: 'Menu title or designation for menu.', + }), + // Menu + defineField({ + name: 'menu', + title: 'Menu', + type: 'menu', + }) + ], + preview: { + select: { + title: 'title', + language: 'language' + }, + prepare(selection) { + const {title, language} = selection; + + const currentLang = languages.find(lang => lang.id === language); + + return { + title: `${title}`, + subtitle: `${currentLang ? currentLang.title : ''}`, + } + }, + }, +}) diff --git a/lib/sanity/utils/defineStructure.ts b/lib/sanity/utils/defineStructure.ts new file mode 100644 index 000000000..da1f86ad7 --- /dev/null +++ b/lib/sanity/utils/defineStructure.ts @@ -0,0 +1,11 @@ +import {ConfigContext} from 'sanity' +import {StructureBuilder} from 'sanity/desk' + +/** + * Helper for creating and typing composable desk structure parts. + */ +export default function defineStructure( + factory: (S: StructureBuilder, context: ConfigContext) => StructureType +) { + return factory +} diff --git a/lib/sanity/utils/getPreviewUrl.ts b/lib/sanity/utils/getPreviewUrl.ts new file mode 100644 index 000000000..564a0c324 --- /dev/null +++ b/lib/sanity/utils/getPreviewUrl.ts @@ -0,0 +1,19 @@ +import {isDev, SanityDocument} from 'sanity' +import { localStorefrontPreviewUrl, publicStorefrontPreviewUrl } from '../constants' + +// Customise this function to show the correct URL based on the current document +export default function getPreviewUrl(doc: SanityDocument) { + if (isDev) { + if (!doc.slug) { + return + } + + return `${localStorefrontPreviewUrl}?slug=${doc.slug.current}&locale=${doc.language}&secret=secret&type=${doc._type}` + } else { + if (!doc.slug) { + return + } + + return `${publicStorefrontPreviewUrl}?slug=${doc.slug.current}&locale=${doc.language}&secret=secret&type=${doc._type}` + } +} \ No newline at end of file diff --git a/lib/sanity/utils/validation.ts b/lib/sanity/utils/validation.ts new file mode 100644 index 000000000..0a74d5d75 --- /dev/null +++ b/lib/sanity/utils/validation.ts @@ -0,0 +1,59 @@ +import {Rule, Slug} from 'sanity' +import slug from 'slug' + +// SLUG VALIDATION +export const validateSlug = (Rule: Rule) => { + const MAX_LENGTH = 96 + + return Rule.required().custom(async(value: Slug) => { + const currentSlug = value && value.current + + if (!currentSlug) { + return true + } + + if (currentSlug.length >= MAX_LENGTH) { + return `Must be less than ${MAX_LENGTH} characters.` + } + + if (currentSlug !== slug(currentSlug, {lower: true})) { + return 'Must be a valid slug.' + } + return true + }) +} + +// IMAGE VALIDATION +export const validateImage = (Rule: Rule, isRequired: boolean = false) => { + if (isRequired) { + return Rule.required().custom((value: object | any) => { + const currentImage = value && value.asset; + const currentImageAlt = value && value.alt; + + if (!currentImage) { + return true + } + + if (!currentImageAlt) { + return "Image and alt text is required." + } + + return true + }) + } else { + return Rule.custom((value: object | any) => { + const currentImage = value && value.asset; + const currentImageAlt = value && value.alt; + + if (!currentImage) { + return true + } + + if (currentImage && !currentImageAlt) { + return "Alt text is required." + } + + return true + }) + } +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index 2a1cb0d55..554afd35b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -12,5 +12,5 @@ export default createMiddleware({ export const config = { // Skip all paths that should not be internationalized - matcher: ['/((?!api|_next|.*\\..*).*)'] + matcher: ['/((?!api|studio|_next|.*\\..*).*)'] }; \ No newline at end of file diff --git a/package.json b/package.json index 21c8a53a1..c78568af8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "@sanity/types": "^3.11.1", "@sanity/ui": "^1.3.3", "@sanity/webhook": "^2.0.0", + "@sanity/color-input": "^3.0.2", + "@sanity/document-internationalization": "^2.0.1", + "@sanity/vision": "^3.0.0", "@types/styled-components": "^5.1.26", "@vercel/og": "^0.1.0", "algoliasearch": "^4.19.1", @@ -47,7 +50,11 @@ "react-glider": "^4.0.2", "react-instantsearch": "^7.0.1", "sanity": "^3.11.1", + "sanity-plugin-iframe-pane": "^2.3.0", + "sanity-plugin-media": "^2.0.4", "sharp": "^0.32.1", + "slug": "^8.2.2", + "slugify": "^1.6.5", "styled-components": "^5.3.10", "tailwind-merge": "^1.12.0", "tailwindcss-animate": "^1.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b6c4df46..4e98498c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ dependencies: '@sanity/client': specifier: ^6.4.4 version: 6.4.4 + '@sanity/color-input': + specifier: ^3.0.2 + version: 3.0.2(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11) + '@sanity/document-internationalization': + specifier: ^2.0.1 + version: 2.0.1(@babel/core@7.22.10)(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-fast-compare@3.2.2)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(sanity@3.15.0)(styled-components@5.3.11) '@sanity/image-url': specifier: ^1.0.2 version: 1.0.2 @@ -38,6 +44,9 @@ dependencies: '@sanity/ui': specifier: ^1.3.3 version: 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + '@sanity/vision': + specifier: ^3.0.0 + version: 3.0.0(@babel/runtime@7.22.10)(@codemirror/lint@6.4.0)(@codemirror/state@6.2.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.0.3)(@sanity/client@6.4.4)(codemirror@6.0.1)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(styled-components@5.3.11) '@sanity/webhook': specifier: ^2.0.0 version: 2.0.0 @@ -89,9 +98,21 @@ dependencies: sanity: specifier: ^3.11.1 version: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + sanity-plugin-iframe-pane: + specifier: ^2.3.0 + version: 2.3.0(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11) + sanity-plugin-media: + specifier: ^2.0.4 + version: 2.0.4(@sanity/color@2.2.5)(@sanity/icons@2.4.1)(@types/react@18.2.19)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11) sharp: specifier: ^0.32.1 version: 0.32.4 + slug: + specifier: ^8.2.2 + version: 8.2.2 + slugify: + specifier: ^1.6.5 + version: 1.6.5 styled-components: specifier: ^5.3.10 version: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) @@ -524,6 +545,89 @@ packages: to-fast-properties: 2.0.0 dev: false + /@codemirror/autocomplete@6.9.0(@codemirror/language@6.8.0)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0)(@lezer/common@1.0.3): + resolution: {integrity: sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + dependencies: + '@codemirror/language': 6.8.0 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + '@lezer/common': 1.0.3 + dev: false + + /@codemirror/commands@6.2.4: + resolution: {integrity: sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==} + dependencies: + '@codemirror/language': 6.8.0 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + '@lezer/common': 1.0.3 + dev: false + + /@codemirror/lang-javascript@6.1.9: + resolution: {integrity: sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==} + dependencies: + '@codemirror/autocomplete': 6.9.0(@codemirror/language@6.8.0)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0)(@lezer/common@1.0.3) + '@codemirror/language': 6.8.0 + '@codemirror/lint': 6.4.0 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + '@lezer/common': 1.0.3 + '@lezer/javascript': 1.4.5 + dev: false + + /@codemirror/language@6.8.0: + resolution: {integrity: sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==} + dependencies: + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + '@lezer/common': 1.0.3 + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.9 + style-mod: 4.0.3 + dev: false + + /@codemirror/lint@6.4.0: + resolution: {integrity: sha512-6VZ44Ysh/Zn07xrGkdtNfmHCbGSHZzFBdzWi0pbd7chAQ/iUcpLGX99NYRZTa7Ugqg4kEHCqiHhcZnH0gLIgSg==} + dependencies: + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + crelt: 1.0.6 + dev: false + + /@codemirror/search@6.5.1: + resolution: {integrity: sha512-4jupk4JwkeVbrN2pStY74q6OJEYqwosB4koA66nyLeVedadtX9MHI38j2vbYmnfDGurDApP3OZO46MrWalcjiQ==} + dependencies: + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + crelt: 1.0.6 + dev: false + + /@codemirror/state@6.2.1: + resolution: {integrity: sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==} + dev: false + + /@codemirror/theme-one-dark@6.1.2: + resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} + dependencies: + '@codemirror/language': 6.8.0 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + '@lezer/highlight': 1.1.6 + dev: false + + /@codemirror/view@6.16.0: + resolution: {integrity: sha512-1Z2HkvkC3KR/oEZVuW9Ivmp8TWLzGEd8T8TA04TTwPvqogfkHBdYSlflytDOqmkUxM2d1ywTg7X2dU5mC+SXvg==} + dependencies: + '@codemirror/state': 6.2.1 + style-mod: 4.0.3 + w3c-keyname: 2.2.8 + dev: false + /@dnd-kit/accessibility@3.0.1(react@18.2.0): resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==} peerDependencies: @@ -579,6 +683,36 @@ packages: tslib: 2.6.1 dev: false + /@emotion/babel-plugin@11.11.0: + resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + dependencies: + '@babel/helper-module-imports': 7.22.5 + '@babel/runtime': 7.22.10 + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/serialize': 1.1.2 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + dev: false + + /@emotion/cache@11.11.0: + resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/sheet': 1.2.2 + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + stylis: 4.2.0 + dev: false + + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: false + /@emotion/is-prop-valid@0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} requiresBuild: true @@ -603,6 +737,41 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false + /@emotion/react@11.11.1(@types/react@18.2.19)(react@18.2.0): + resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.19 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@emotion/serialize@1.1.2: + resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.2 + dev: false + + /@emotion/sheet@1.2.2: + resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + dev: false + /@emotion/stylis@0.8.5: resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==} dev: false @@ -611,6 +780,26 @@ packages: resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} dev: false + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /@emotion/utils@1.2.1: + resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + dev: false + + /@emotion/weak-memoize@0.3.1: + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + dev: false + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -946,6 +1135,14 @@ packages: react: 18.2.0 dev: false + /@hookform/resolvers@2.0.0-beta.3(react-hook-form@6.15.8): + resolution: {integrity: sha512-sOP+IX7TglN34WbMVt8eRqWgnXAQcvFc4XUU6x3bIiIjTkjV5L3N7sBjff2Ln4QPTMYnmdXaZV2Yf5WxOiC5YQ==} + peerDependencies: + react-hook-form: '>=6.6.0' + dependencies: + react-hook-form: 6.15.8(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -970,6 +1167,14 @@ packages: resolution: {integrity: sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==} dev: false + /@icons/material@0.2.4(react@18.2.0): + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -999,6 +1204,29 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: false + /@lezer/common@1.0.3: + resolution: {integrity: sha512-JH4wAXCgUOcCGNekQPLhVeUtIqjH0yPBs7vvUdSjyQama9618IOKFJwkv2kcqdhF0my8hQEgCTEJU0GIgnahvA==} + dev: false + + /@lezer/highlight@1.1.6: + resolution: {integrity: sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==} + dependencies: + '@lezer/common': 1.0.3 + dev: false + + /@lezer/javascript@1.4.5: + resolution: {integrity: sha512-FmBUHz8K1V22DgjTd6SrIG9owbzOYZ1t3rY6vGEmw+e2RVBd7sqjM8uXEVRFmfxKFn1Mx2ABJehHjrN3G2ZpmA==} + dependencies: + '@lezer/highlight': 1.1.6 + '@lezer/lr': 1.3.9 + dev: false + + /@lezer/lr@1.3.9: + resolution: {integrity: sha512-XPz6dzuTHlnsbA5M2DZgjflNQ+9Hi5Swhic0RULdp3oOs3rh6bqGZolosVqN/fQIT8uNiepzINJDnS39oweTHQ==} + dependencies: + '@lezer/common': 1.0.3 + dev: false + /@motionone/animation@10.15.1: resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==} dependencies: @@ -1841,6 +2069,25 @@ packages: '@babel/runtime': 7.22.10 dev: false + /@reduxjs/toolkit@1.9.5(react-redux@7.2.9)(react@18.2.0): + resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 9.0.21 + react: 18.2.0 + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + redux-thunk: 2.4.2(redux@4.2.1) + reselect: 4.1.8 + dev: false + /@resvg/resvg-wasm@2.0.0-alpha.4: resolution: {integrity: sha512-pWIG9a/x1ky8gXKRhPH1OPKpHFoMN1ISLbJ+O+gPXQHIAKhNd5I28RlWf7q576hAOQA9JZTlo3p/M2uyLzJmmw==} engines: {node: '>= 10'} @@ -1909,6 +2156,27 @@ packages: - supports-color dev: false + /@sanity/color-input@3.0.2(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-jeNLxuVmt2P7LhBKCPwXL/SSjA5uryIje3AdqhJT0nO/9l7Ghmd7h11GaXwMcm5GntUbm/AtLVyQtV3/v686WQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18 + sanity: ^3 + styled-components: ^5.2 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + lodash: 4.17.21 + react: 18.2.0 + react-color: 2.19.3(react@18.2.0) + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react-dom + - react-is + dev: false + /@sanity/color@2.2.5: resolution: {integrity: sha512-tTi22KoKuER3sldXYl4c1Dq2zU7tMLDkljFiaUKVkBbu4PBvRGCFw75kXZnD2b4Bsp6vin+7sI+AKdCKRhfRuw==} dev: false @@ -1920,6 +2188,33 @@ packages: diff-match-patch: 1.0.5 dev: false + /@sanity/document-internationalization@2.0.1(@babel/core@7.22.10)(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-fast-compare@3.2.2)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-pfLx7rfu2rfTn9xRm4hTpKGSK2HwE8oTA91CNfxWrRjvfgF/pgJ2nzTTEbLZ1FfIn/EEQ+MgQwrS2GqbE3uhfw==} + engines: {node: '>=14'} + peerDependencies: + '@sanity/ui': ^1.2.2 + react: ^18 + sanity: ^3.0.0 + styled-components: ^5.3.6 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + '@sanity/uuid': 3.0.2 + react: 18.2.0 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + sanity-plugin-internationalized-array: 1.10.1(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11) + sanity-plugin-utils: 1.6.2(@babel/core@7.22.10)(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-fast-compare@3.2.2)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(sanity@3.15.0) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@babel/core' + - react-dom + - react-fast-compare + - react-is + - rxjs + - supports-color + dev: false + /@sanity/eventsource@5.0.0: resolution: {integrity: sha512-0ewT+BDzfiamHwitUfRcwsl/RREHjWv6VNZvQ8Q4OnnNKXfEEGXbWmqzof0okOTkp4XELgyliht4Qj28o9AU2g==} dependencies: @@ -1983,6 +2278,14 @@ packages: - supports-color dev: false + /@sanity/icons@1.3.10(react@18.2.0): + resolution: {integrity: sha512-5wVG/vIiGuGrSmq+Bl3PY7XDgQrGv0fyHdJI64FSulnr2wH3NMqZ6C59UFxnrZ93sr7kOt0zQFoNv2lkPBi0Cg==} + peerDependencies: + react: ^16.9 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /@sanity/icons@2.4.1(react@18.2.0): resolution: {integrity: sha512-/yxcIT0k1RxStI/pP/oHM44fHI6Oxiygf0jPcdV06Ce0xfFo+51UEqJSfE8hQ3fh+uFkat8ZZObZjq5v1ceJzw==} engines: {node: '>=14.0.0'} @@ -2023,6 +2326,39 @@ packages: - supports-color dev: false + /@sanity/incompatible-plugin@1.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2z39G9PTM8MXOF4fJNx3TG4tH0RrTjtH6dVLW93DSjCPbIS7FgCY5yWjZfQ+HVkwhLsF7ATDAGLA/jp65pFjAg==} + peerDependencies: + react: ^16.9 || ^17 || ^18 + react-dom: ^16.9 || ^17 || ^18 + dependencies: + '@sanity/icons': 1.3.10(react@18.2.0) + react: 18.2.0 + react-copy-to-clipboard: 5.1.0(react@18.2.0) + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@sanity/language-filter@3.2.1(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-QSPUZ3frdUF+v2WQHwBu/27ivD1Oja4AYSphtpqwyIftxMVcSyiX8x4aqlBw+jisvquypqgMDbzVP4z87o6OoA==} + engines: {node: '>=14'} + peerDependencies: + react: ^18 + sanity: ^3 + styled-components: ^5.2 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + '@sanity/util': 3.15.0 + react: 18.2.0 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react-dom + - react-is + - supports-color + dev: false + /@sanity/logos@2.1.2(@sanity/color@2.2.5)(react@18.2.0): resolution: {integrity: sha512-nxJUQQzEEG8EqjiOEswQQpBUuFc3iSxTVF9D9Memg/tlOChX76dStNHoa1RWuvSPu895aqJV+9zxijAa0kF9Vg==} engines: {node: '>=14.0.0'} @@ -2196,6 +2532,47 @@ packages: uuid: 8.3.2 dev: false + /@sanity/vision@3.0.0(@babel/runtime@7.22.10)(@codemirror/lint@6.4.0)(@codemirror/state@6.2.1)(@codemirror/theme-one-dark@6.1.2)(@lezer/common@1.0.3)(@sanity/client@6.4.4)(codemirror@6.0.1)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(styled-components@5.3.11): + resolution: {integrity: sha512-Dr18Iugz2xHEavag4asOzYBIjwnj+5SEB7DlbyNow0mQ5fQWYh8eRES2OBDvrOLbBJ5rVuZMIzrcokQ+nzmrBw==} + peerDependencies: + '@sanity/client': ^3.4.1 + react: ^18 + rxjs: ^6.5.3 + styled-components: ^5.2 + dependencies: + '@codemirror/autocomplete': 6.9.0(@codemirror/language@6.8.0)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0)(@lezer/common@1.0.3) + '@codemirror/commands': 6.2.4 + '@codemirror/lang-javascript': 6.1.9 + '@codemirror/language': 6.8.0 + '@codemirror/search': 6.5.1 + '@codemirror/view': 6.16.0 + '@juggle/resize-observer': 3.4.0 + '@lezer/highlight': 1.1.6 + '@rexxars/react-json-inspector': 8.0.1(react@18.2.0) + '@sanity/client': 6.4.4 + '@sanity/color': 2.2.5 + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + '@uiw/react-codemirror': 4.21.9(@babel/runtime@7.22.10)(@codemirror/autocomplete@6.9.0)(@codemirror/language@6.8.0)(@codemirror/lint@6.4.0)(@codemirror/search@6.5.1)(@codemirror/state@6.2.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.16.0)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0) + hashlru: 2.3.0 + is-hotkey: 0.1.8 + json5: 1.0.2 + lodash: 4.17.21 + react: 18.2.0 + react-split-pane: 0.1.92(react-dom@18.2.0)(react@18.2.0) + rxjs: 7.8.1 + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@babel/runtime' + - '@codemirror/lint' + - '@codemirror/state' + - '@codemirror/theme-one-dark' + - '@lezer/common' + - codemirror + - react-dom + - react-is + dev: false + /@sanity/webhook@2.0.0: resolution: {integrity: sha512-KusHeWVy6yW3hcgVHbGx6qVv//bUj3CD9Nr0EOw086+mo/ESrUV21HjOCrNQ+AYqXvJ9H5qhqmMiuc8H5dXUGA==} engines: {node: '>=12.0.0'} @@ -2231,6 +2608,18 @@ packages: tailwindcss: 3.3.3 dev: true + /@tanem/react-nprogress@5.0.47(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DIW4bkXxPa2Mc8WKAoxdJudLsl2N4QinMeoG3Z+bQa4RNdV97FYVGcryie9ZjJU+ZUxvjT5eq504wnW/xzHlmA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.10 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0): resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} peerDependencies: @@ -2323,6 +2712,10 @@ packages: /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + /@types/parse-json@4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: false + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -2347,6 +2740,21 @@ packages: '@types/react': 18.2.19 dev: false + /@types/react-redux@7.1.25: + resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.19 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + + /@types/react-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.19 + dev: false + /@types/react@18.2.19: resolution: {integrity: sha512-e2S8wmY1ePfM517PqCG80CcE48Xs5k0pwJzuDZsfE8IZRRBfOMCF+XqnFxu6mWtyivum1MQm4aco+WIt6Coimw==} dependencies: @@ -2448,6 +2856,53 @@ packages: eslint-visitor-keys: 3.4.2 dev: true + /@uiw/codemirror-extensions-basic-setup@4.21.9(@codemirror/autocomplete@6.9.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.8.0)(@codemirror/lint@6.4.0)(@codemirror/search@6.5.1)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0): + resolution: {integrity: sha512-TQT6aF8brxZpFnk/K4fm/K/9k9eF3PMav/KKjHlYrGUT8BTNk/qL+ximLtIzvTUhmBFchjM1lrqSJdvpVom7/w==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + dependencies: + '@codemirror/autocomplete': 6.9.0(@codemirror/language@6.8.0)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0)(@lezer/common@1.0.3) + '@codemirror/commands': 6.2.4 + '@codemirror/language': 6.8.0 + '@codemirror/lint': 6.4.0 + '@codemirror/search': 6.5.1 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + dev: false + + /@uiw/react-codemirror@4.21.9(@babel/runtime@7.22.10)(@codemirror/autocomplete@6.9.0)(@codemirror/language@6.8.0)(@codemirror/lint@6.4.0)(@codemirror/search@6.5.1)(@codemirror/state@6.2.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.16.0)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aeLegPz2iCvqJjhzXp2WUMqpMZDqxsTnF3rX9kGRlfY6vQLsrjoctj0cQ29uxEtFYJChOVjtCOtnQUlyIuNAHQ==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@babel/runtime': 7.22.10 + '@codemirror/commands': 6.2.4 + '@codemirror/state': 6.2.1 + '@codemirror/theme-one-dark': 6.1.2 + '@codemirror/view': 6.16.0 + '@uiw/codemirror-extensions-basic-setup': 4.21.9(@codemirror/autocomplete@6.9.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.8.0)(@codemirror/lint@6.4.0)(@codemirror/search@6.5.1)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0) + codemirror: 6.0.1(@lezer/common@1.0.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + dev: false + /@vercel/error-utils@1.0.10: resolution: {integrity: sha512-nsKy2sy+pjUWyKI1V/XXKspVzHMYgSalmj5+EsKWFXZbnNZicqxNtMR94J8Hs7SB4TQxh0s4KhczJtL59AVGMg==} dev: false @@ -2502,6 +2957,20 @@ packages: resolution: {integrity: sha512-5b0PkOJsFBX5alChuIO3qpkt5vIZBevzLPhUQ1UP8UzVjL3F1VllnZxp/thfD8R5ol7D7WHkgZHIjdUBX4tDpQ==} dev: false + /@virtuoso.dev/react-urx@0.2.13(react@18.2.0): + resolution: {integrity: sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + dependencies: + '@virtuoso.dev/urx': 0.2.13 + react: 18.2.0 + dev: false + + /@virtuoso.dev/urx@0.2.13: + resolution: {integrity: sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==} + dev: false + /@vitejs/plugin-react@4.0.4(vite@4.4.9): resolution: {integrity: sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2796,6 +3265,11 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /attr-accept@2.2.2: + resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} + engines: {node: '>=4'} + dev: false + /autoprefixer@10.4.14(postcss@8.4.27): resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} engines: {node: ^10 || ^12 || >=14} @@ -2832,6 +3306,15 @@ packages: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} dev: false + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.22.10 + cosmiconfig: 7.1.0 + resolve: 1.22.4 + dev: false + /babel-plugin-styled-components@2.1.4(@babel/core@7.22.10)(styled-components@5.3.11): resolution: {integrity: sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==} peerDependencies: @@ -3091,6 +3574,20 @@ packages: engines: {node: '>=6'} dev: false + /codemirror@6.0.1(@lezer/common@1.0.3): + resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} + dependencies: + '@codemirror/autocomplete': 6.9.0(@codemirror/language@6.8.0)(@codemirror/state@6.2.1)(@codemirror/view@6.16.0)(@lezer/common@1.0.3) + '@codemirror/commands': 6.2.4 + '@codemirror/language': 6.8.0 + '@codemirror/lint': 6.4.0 + '@codemirror/search': 6.5.1 + '@codemirror/state': 6.2.1 + '@codemirror/view': 6.16.0 + transitivePeerDependencies: + - '@lezer/common' + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -3127,6 +3624,10 @@ packages: color-string: 1.9.1 dev: false + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: false + /colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: true @@ -3229,6 +3730,17 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -3250,6 +3762,10 @@ packages: object-assign: 4.1.1 dev: false + /crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + dev: false + /cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -3507,6 +4023,13 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.22.10 + csstype: 3.1.2 + dev: false + /dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false @@ -3710,7 +4233,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} @@ -4144,6 +4666,13 @@ packages: flat-cache: 3.0.4 dev: true + /file-selector@0.4.0: + resolution: {integrity: sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==} + engines: {node: '>= 10'} + dependencies: + tslib: 2.6.1 + dev: false + /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: false @@ -4153,12 +4682,21 @@ packages: engines: {node: '>=4'} dev: false + /filesize@8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4559,6 +5097,11 @@ packages: engines: {node: '>= 14'} dev: false + /groq@2.33.2: + resolution: {integrity: sha512-5pf4c91JESCS28IJCgolJq/WBw4Xvf2m8FgZVUlrCig17I+qERosALAHLyyutXu403EYnyCD3DdCLaPb3aGneA==} + engines: {node: '>=6'} + dev: false + /groq@3.15.0: resolution: {integrity: sha512-OjdYDrAHZC6Ku7LOgXUuh8DjcITJ7piz6WsuzrHUCiZEg3nEHWy2n3lU/INglVrIVp5o6QqqUh3fVlrBjp6X7A==} engines: {node: '>=14'} @@ -4942,6 +5485,10 @@ packages: resolution: {integrity: sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==} dev: false + /is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + dev: false + /is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -5180,7 +5727,6 @@ packages: hasBin: true dependencies: minimist: 1.2.8 - dev: true /json5@2.2.2: resolution: {integrity: sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==} @@ -5295,6 +5841,10 @@ packages: dependencies: p-locate: 5.0.0 + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true @@ -5326,6 +5876,10 @@ packages: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} dev: false + /lodash.uniqueid@4.0.1: + resolution: {integrity: sha512-GQQWaIeGlL6DIIr06kj1j6sSmBxyNMwI8kaX9aKpHR/XsMTiaXDVPNPAkiboOTK9OJpTJF/dXT3xYoFQnj386Q==} + dev: false + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5376,10 +5930,18 @@ packages: semver: 6.3.1 dev: false + /material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + dev: false + /md5-o-matic@0.1.1: resolution: {integrity: sha512-QBJSFpsedXUl/Lgs4ySdB2XCzUEcJ3ujpbagdZCkRaYIaC0kFnID8jhc84KEiVv6dNFtIrmW7bqow0lDxgJi6A==} dev: false + /memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + /memoize-resolver@1.0.0: resolution: {integrity: sha512-mXfNXte0RSWl0rEIsQhXutfM2R2Oa7UyKDD7XoZMEbKeucTRms04y5y41U8gLqPzRx7ViN/QyYnTR2TX/5tawA==} dev: false @@ -5519,6 +6081,10 @@ packages: resolution: {integrity: sha512-RWgGP2TdeKZLx+guR5a7/BzYs85sj6yrXXyj0o/znbgzPlz/Ez9wQuKDpwUZ8q+u2RxXpqZ1iTkPXCIU+GHhpA==} dev: false + /nanoclone@0.2.1: + resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} + dev: false + /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6010,7 +6576,6 @@ packages: /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - dev: true /polished@4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} @@ -6220,6 +6785,10 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /property-expr@2.0.5: + resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} + dev: false + /property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} dependencies: @@ -6297,6 +6866,21 @@ packages: react: 18.2.0 dev: false + /react-color@2.19.3(react@18.2.0): + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + dependencies: + '@icons/material': 0.2.4(react@18.2.0) + lodash: 4.17.21 + lodash-es: 4.17.21 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 18.2.0 + reactcss: 1.2.3(react@18.2.0) + tinycolor2: 1.6.0 + dev: false + /react-cookie@4.1.1(react@18.2.0): resolution: {integrity: sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==} peerDependencies: @@ -6328,10 +6912,35 @@ packages: scheduler: 0.23.0 dev: false + /react-dropzone@11.7.1(react@18.2.0): + resolution: {integrity: sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8' + dependencies: + attr-accept: 2.2.2 + file-selector: 0.4.0 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false + /react-file-icon@1.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wxl/WwSX5twQKVXloPHbS71iZQUKO84KgZ44Kh7vYZGu1qH2kagx+RSTNfk/+IHtXfjPWPNIHPGi2Y8S94N1CQ==} + peerDependencies: + react: ^18.0.0 || ^17.0.0 || ^16.2.0 + react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0 + dependencies: + colord: 2.9.3 + lodash.uniqueid: 4.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-focus-lock@2.9.5(@types/react@18.2.19)(react@18.2.0): resolution: {integrity: sha512-h6vrdgUbsH2HeD5I7I3Cx1PPrmwGuKYICS+kB9m+32X/9xHRrAbxgvaBpG7BFBN9h3tO+C3qX1QAVESmi4CiIA==} peerDependencies: @@ -6362,6 +6971,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-hook-form@6.15.8(react@18.2.0): + resolution: {integrity: sha512-prq82ofMbnRyj5wqDe8hsTRcdR25jQ+B8KtCS7BLCzjFHAwNuCjRwzPuP4eYLsEBjEIeYd6try+pdLdw0kPkpg==} + peerDependencies: + react: ^16.8.0 || ^17 + dependencies: + react: 18.2.0 + dev: false + /react-instantsearch-core@7.0.1(algoliasearch@4.19.1)(react@18.2.0): resolution: {integrity: sha512-qKe8sV03kyoc3HjEYmpGF2k038uaBV5Mzdxgf3ZUP1B7OyCPnzeC7YPvjZFZV89rvcfxr0lwzdExKVFV1GMR7g==} peerDependencies: @@ -6394,10 +7011,40 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@types/react-redux': 7.1.25 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + /react-refractor@2.1.7(react@18.2.0): resolution: {integrity: sha512-avNxSSsnjYg+BKpO8LVCK14KRn5pLZ+8DInMiUEeZPL6hs0SN0zafl3mJIxavGQPKyihqbXqzq4CYNflJQjaaw==} peerDependencies: @@ -6462,6 +7109,46 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /react-select@5.7.4(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.10 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.2.19)(react@18.2.0) + '@floating-ui/dom': 1.5.1 + '@types/react-transition-group': 4.4.6 + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.19)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /react-split-pane@0.1.92(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==} + peerDependencies: + react: ^16.0.0-0 + react-dom: ^16.0.0-0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + react-style-proptype: 3.2.2 + dev: false + + /react-style-proptype@3.2.2: + resolution: {integrity: sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==} + dependencies: + prop-types: 15.8.1 + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.19)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -6479,6 +7166,33 @@ packages: tslib: 2.6.1 dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.22.10 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-virtuoso@2.19.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zF6MAwujNGy2nJWCx/Df92ay/RnV2Kj4glUZfdyadI4suAn0kAZHB1BeI7yPFVp2iSccLzFlszhakWyr+fJ4Dw==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16 || >=17 || >= 18' + react-dom: '>=16 || >=17 || >= 18' + dependencies: + '@virtuoso.dev/react-urx': 0.2.13(react@18.2.0) + '@virtuoso.dev/urx': 0.2.13 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -6486,6 +7200,15 @@ packages: loose-envify: 1.4.0 dev: false + /reactcss@1.2.3(react@18.2.0): + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + dependencies: + lodash: 4.17.21 + react: 18.2.0 + dev: false + /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -6550,6 +7273,30 @@ packages: dependencies: picomatch: 2.3.1 + /redux-observable@2.0.0(redux@4.2.1): + resolution: {integrity: sha512-FJz4rLXX+VmDDwZS/LpvQsKnSanDOe8UVjiLryx1g3seZiS69iLpMrcvXD5oFO7rtkPyRdo/FmTqldnT3X3m+w==} + peerDependencies: + redux: '>=4 <5' + dependencies: + redux: 4.2.1 + rxjs: 7.8.1 + tslib: 2.1.0 + dev: false + + /redux-thunk@2.4.2(redux@4.2.1): + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + dependencies: + redux: 4.2.1 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.22.10 + dev: false + /refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} dependencies: @@ -6591,6 +7338,10 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: false + /reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -6728,6 +7479,119 @@ packages: diff-match-patch: 1.0.5 dev: false + /sanity-plugin-iframe-pane@2.3.0(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-bTq1Fnj1AQv5FXOE/88z41tSlJkfd7Ra/+19CvFOkOdopr7nnCWtztaG+phlqm8cndIs+CMnSxaBByLf/RGcPQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18 + sanity: ^3.0.0 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + react: 18.2.0 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + usehooks-ts: 2.9.1(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react-dom + - react-is + - styled-components + dev: false + + /sanity-plugin-internationalized-array@1.10.1(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-rGf96PdLkj00jC7/UlRVVk7Z9MpN9399bNby0wGm3CLxYYrQjj4A6oZChYHWTwc4RIhBUQqXJXNe9K1xjlVa6Q==} + engines: {node: '>=14'} + peerDependencies: + '@sanity/ui': ^1.2.2 + react: ^18 + sanity: ^3.0.0 + styled-components: ^5.3.6 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/language-filter': 3.2.1(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + fast-deep-equal: 3.1.3 + lodash.get: 4.4.2 + react: 18.2.0 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + suspend-react: 0.0.8(react@18.2.0) + transitivePeerDependencies: + - react-dom + - react-is + - supports-color + dev: false + + /sanity-plugin-media@2.0.4(@sanity/color@2.2.5)(@sanity/icons@2.4.1)(@types/react@18.2.19)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(sanity@3.15.0)(styled-components@5.3.11): + resolution: {integrity: sha512-cLJOa1LZKHWCCJJdjf3j/STiiNj6WtYNQCwF52FTV1zI/PQBhVClRsBBEw5FJM6ru9c+MHlv09PPh+HRpGxQVw==} + engines: {node: '>=14'} + peerDependencies: + '@sanity/color': ^2.1.20 + '@sanity/icons': ^2.0.0 + react: ^18 + react-dom: ^18 + sanity: ^3.0.0 + styled-components: ^5.3.3 + dependencies: + '@hookform/resolvers': 2.0.0-beta.3(react-hook-form@6.15.8) + '@reduxjs/toolkit': 1.9.5(react-redux@7.2.9)(react@18.2.0) + '@sanity/color': 2.2.5 + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + '@tanem/react-nprogress': 5.0.47(react-dom@18.2.0)(react@18.2.0) + copy-to-clipboard: 3.3.3 + date-fns: 2.30.0 + filesize: 8.0.7 + groq: 2.33.2 + is-hotkey: 0.2.0 + nanoid: 3.3.6 + pluralize: 8.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-dropzone: 11.7.1(react@18.2.0) + react-file-icon: 1.3.0(react-dom@18.2.0)(react@18.2.0) + react-hook-form: 6.15.8(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + react-select: 5.7.4(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0) + react-virtuoso: 2.19.1(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + redux-observable: 2.0.0(redux@4.2.1) + rxjs: 7.8.1 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + yup: 0.32.11 + transitivePeerDependencies: + - '@types/react' + - react-is + - react-native + dev: false + + /sanity-plugin-utils@1.6.2(@babel/core@7.22.10)(@sanity/ui@1.7.4)(react-dom@18.2.0)(react-fast-compare@3.2.2)(react-is@18.2.0)(react@18.2.0)(rxjs@7.8.1)(sanity@3.15.0): + resolution: {integrity: sha512-zn4sKLaLG5ZcibPNB8RfTjU32sear2XIHPxIoIWJCHXO3yD+JEq+/MvkwW7CnfdEcZeXPfb5DEx1CGjIQwO3Iw==} + engines: {node: '>=14'} + peerDependencies: + '@sanity/ui': ^1.2.2 + react: ^18 + react-fast-compare: ^3.2.0 + rxjs: ^7.0.0 + sanity: ^3 + dependencies: + '@sanity/icons': 2.4.1(react@18.2.0) + '@sanity/incompatible-plugin': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@sanity/ui': 1.7.4(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)(styled-components@5.3.11) + react: 18.2.0 + react-fast-compare: 3.2.2 + rxjs: 7.8.1 + sanity: 3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) + styled-components: 5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@babel/core' + - react-dom + - react-is + dev: false + /sanity@3.15.0(@types/node@18.13.0)(@types/react@18.2.19)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11): resolution: {integrity: sha512-b0T71nzwOrMonnvZ1Jt3Jc2dY4hTT+T99cpdBjFERBH+m5w2FtEJhbdzmAyz7WgabcnhHwiZ/rzzAkXPmdtnLw==} engines: {node: '>=14.18.0'} @@ -7037,10 +7901,25 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true + /slug@8.2.2: + resolution: {integrity: sha512-5ByW6qXqPeG0Tmlkh24JhdXhvQsbaJSjVr3GgGxUV0BSskZKKBZZfFWxezap8+fh1vxBN9GVbqI1V6nqAFxlBg==} + hasBin: true + dev: false + + /slugify@1.6.5: + resolution: {integrity: sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==} + engines: {node: '>=8.0.0'} + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -7241,6 +8120,10 @@ packages: engines: {node: '>=8'} dev: true + /style-mod@4.0.3: + resolution: {integrity: sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==} + dev: false + /styled-components@5.3.11(@babel/core@7.22.10)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0): resolution: {integrity: sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==} engines: {node: '>=10'} @@ -7284,6 +8167,10 @@ packages: react: 18.2.0 dev: false + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + /sucrase@3.34.0: resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} engines: {node: '>=8'} @@ -7321,6 +8208,14 @@ packages: react: 18.2.0 dev: false + /suspend-react@0.0.8(react@18.2.0): + resolution: {integrity: sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==} + peerDependencies: + react: '>=17.0' + dependencies: + react: 18.2.0 + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false @@ -7466,6 +8361,10 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false + /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -7486,6 +8385,10 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /totalist@1.1.0: resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==} engines: {node: '>=6'} @@ -7529,6 +8432,10 @@ packages: strip-bom: 3.0.0 dev: true + /tslib@2.1.0: + resolution: {integrity: sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==} + dev: false + /tslib@2.6.1: resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} @@ -7731,6 +8638,19 @@ packages: react: 18.2.0 dev: false + /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.19)(react@18.2.0): + resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.19 + react: 18.2.0 + dev: false + /use-sidecar@1.1.2(@types/react@18.2.19)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -7755,6 +8675,17 @@ packages: react: 18.2.0 dev: false + /usehooks-ts@2.9.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==} + engines: {node: '>=16.15.0', npm: '>=8'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7805,6 +8736,10 @@ packages: fsevents: 2.3.2 dev: false + /w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + dev: false + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -7982,6 +8917,11 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} @@ -8012,6 +8952,19 @@ packages: resolution: {integrity: sha512-rD3L4jyMlO1m+RWU60lNwZQK5zmzglCV5fI1gTRikmpv3YzmNIZQbjyfE6cMNb9Xaly/C1SwemYGbsiOekMvnQ==} dev: false + /yup@0.32.11: + resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} + engines: {node: '>=10'} + dependencies: + '@babel/runtime': 7.22.10 + '@types/lodash': 4.14.196 + lodash: 4.17.21 + lodash-es: 4.17.21 + nanoclone: 0.2.1 + property-expr: 2.0.5 + toposort: 2.0.2 + dev: false + /zip-stream@4.1.0: resolution: {integrity: sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==} engines: {node: '>= 10'} diff --git a/sanity.config.ts b/sanity.config.ts new file mode 100644 index 000000000..8709e903d --- /dev/null +++ b/sanity.config.ts @@ -0,0 +1,75 @@ +import {defineConfig, isDev} from 'sanity' +import {deskTool} from 'sanity/desk' +import {visionTool} from '@sanity/vision' +import { colorInput } from "@sanity/color-input"; +import {media} from 'sanity-plugin-media' +import {schemaTypes} from '@/lib/sanity/schemas' +import {structure} from '@/lib/sanity/desk' +import Kodamera from '@/lib/sanity/components/icons/kodamera' +import {documentInternationalization} from '@sanity/document-internationalization' + +const devOnlyPlugins = [visionTool()] + +// Define the actions that should be available for singleton documents +const singletonActions = new Set(["publish", "discardChanges", "restore"]) + +// Define the singleton document types +const singletonTypes = new Set(["settings", "home", "utilityMenu", "media.tag"]) + +export default defineConfig({ + name: 'default', + title: 'KM Storefront CMS', + projectId: 'opfmivlh', + basePath: '/studio', + dataset: 'production', + plugins: [ + deskTool({structure}), + media(), + ...(isDev ? devOnlyPlugins : []), + documentInternationalization({ + // Required, either: + // An array of supported languages + supportedLanguages: [ + {id: 'sv', title: 'Swedish'}, + {id: 'en', title: 'English'} + ], + // Required + schemaTypes: [ + 'home', + 'page', + 'product', + 'category', + 'settings', + 'blurb', + 'section', + 'usp', + 'footerMenu', + 'utilityMenu' + ], + // Optional + // languageField: `language`, // defauts to "language" + // Optional, requires access to the Publishing API + // bulkPublish: true // defaults to false + }), + colorInput(), + ], + schema: { + types: schemaTypes, + // Filter out singleton types from the global “New document” menu options + templates: (templates) => + templates.filter(({ schemaType }) => !singletonTypes.has(schemaType)), + }, + document: { + // For singleton types, filter out actions that are not explicitly included + // in the `singletonActions` list defined above + actions: (input, context) => + singletonTypes.has(context.schemaType) + ? input.filter(({ action }) => action && singletonActions.has(action)) + : input, + }, + studio: { + components: { + logo: Kodamera, + } + }, +})