mirror of
https://github.com/vercel/commerce.git
synced 2025-05-19 16:07:01 +00:00
Ported sanity studio to Next js app
This commit is contained in:
parent
de85d266bd
commit
d32baa7782
@ -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();
|
||||
}
|
8
app/(studio)/studio/[[...index]]/page.tsx
Normal file
8
app/(studio)/studio/[[...index]]/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { NextStudio } from 'next-sanity/studio';
|
||||
import config from '@/sanity.config';
|
||||
|
||||
export default function AdminPage() {
|
||||
return <NextStudio config={config} />;
|
||||
}
|
14
app/(studio)/studio/layout.tsx
Normal file
14
app/(studio)/studio/layout.tsx
Normal file
@ -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 (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -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() });
|
||||
}
|
38
lib/sanity/components/hotspots/ProductTooltip.tsx
Normal file
38
lib/sanity/components/hotspots/ProductTooltip.tsx
Normal file
@ -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<HotspotFields>) {
|
||||
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 (
|
||||
<StyledBox padding={2}>
|
||||
{hasProduct && previewProps ? renderPreview(previewProps) : `No product selected`}
|
||||
</StyledBox>
|
||||
)
|
||||
}
|
27
lib/sanity/components/icons/kodamera.tsx
Normal file
27
lib/sanity/components/icons/kodamera.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
const Kodamera = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 109 80"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '36px',
|
||||
height: 'auto',
|
||||
}}
|
||||
>
|
||||
<path fill="currentColor" d="M54.6,0C32.8,0,15.1,17.9,15.1,40c0,10.6,4.3,18.1,4.6,18.8h20.6c-0.7-0.5-9.1-6.9-9.1-18.8 c0-13.1,10.5-23.7,23.4-23.7S78,26.9,78,40S67.5,63.7,54.6,63.7H0V80h54.6c21.8,0,39.5-17.9,39.5-40S76.5,0,54.6,0z"/>
|
||||
<path fill="currentColor" d="M109,63.7V80H75.3c7.2-3.7,13.2-9.4,17.4-16.3H109z"/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Kodamera
|
20
lib/sanity/components/inputs/PlaceholderString.tsx
Normal file
20
lib/sanity/components/inputs/PlaceholderString.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import {StringInputProps, useFormValue, SanityDocument, StringSchemaType} from 'sanity'
|
||||
import get from 'lodash.get'
|
||||
|
||||
type Props = StringInputProps<StringSchemaType & {options?: {field?: string}}>
|
||||
|
||||
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
|
32
lib/sanity/components/inputs/ProxyString.tsx
Normal file
32
lib/sanity/components/inputs/ProxyString.tsx
Normal file
@ -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<StringSchemaType & {options?: {field?: string}}>
|
||||
|
||||
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 (
|
||||
<Tooltip
|
||||
content={
|
||||
<Box padding={2}>
|
||||
<Text muted size={1}>
|
||||
This value is set in Shopify (<code>{path}</code>)
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
portal
|
||||
>
|
||||
<TextInput iconRight={LockIcon} readOnly={true} value={proxyValue} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProxyString
|
48
lib/sanity/constants.ts
Normal file
48
lib/sanity/constants.ts
Normal file
@ -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';
|
15
lib/sanity/desk/blurbStructure.ts
Normal file
15
lib/sanity/desk/blurbStructure.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ListItemBuilder} from 'sanity/desk'
|
||||
import defineStructure from '../utils/defineStructure'
|
||||
|
||||
export default defineStructure<ListItemBuilder>((S) =>
|
||||
S.listItem()
|
||||
.title('Blurbs')
|
||||
.schemaType('blurb')
|
||||
.child (
|
||||
S.documentTypeList('blurb')
|
||||
.child (
|
||||
S.document()
|
||||
.schemaType("blurb")
|
||||
)
|
||||
)
|
||||
)
|
33
lib/sanity/desk/categoryStructure.ts
Normal file
33
lib/sanity/desk/categoryStructure.ts
Normal file
@ -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<ListItemBuilder>((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')
|
||||
])
|
||||
)
|
||||
|
||||
)
|
||||
)
|
34
lib/sanity/desk/homeStructure.ts
Normal file
34
lib/sanity/desk/homeStructure.ts
Normal file
@ -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<ListItemBuilder>((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')
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
73
lib/sanity/desk/index.ts
Normal file
73
lib/sanity/desk/index.ts
Normal file
@ -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(),
|
||||
])
|
31
lib/sanity/desk/navigationStructure.ts
Normal file
31
lib/sanity/desk/navigationStructure.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {ListItemBuilder} from 'sanity/desk'
|
||||
import defineStructure from '../utils/defineStructure'
|
||||
import {MenuIcon} from '@sanity/icons'
|
||||
|
||||
export default defineStructure<ListItemBuilder>((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")
|
||||
)
|
||||
),
|
||||
])
|
||||
),
|
||||
)
|
35
lib/sanity/desk/pageStructure.ts
Normal file
35
lib/sanity/desk/pageStructure.ts
Normal file
@ -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<ListItemBuilder>((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')
|
||||
])
|
||||
)
|
||||
|
||||
)
|
||||
)
|
73
lib/sanity/desk/productStructure.ts
Normal file
73
lib/sanity/desk/productStructure.ts
Normal file
@ -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<ListItemBuilder>((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<ListItemBuilder>((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-', '')),
|
||||
// })
|
||||
// ),
|
||||
// ])
|
||||
// )
|
||||
// )
|
||||
// )
|
15
lib/sanity/desk/sectionStructure.ts
Normal file
15
lib/sanity/desk/sectionStructure.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ListItemBuilder} from 'sanity/desk'
|
||||
import defineStructure from '../utils/defineStructure'
|
||||
|
||||
export default defineStructure<ListItemBuilder>((S) =>
|
||||
S.listItem()
|
||||
.title('Sections')
|
||||
.schemaType('section')
|
||||
.child (
|
||||
S.documentTypeList('section')
|
||||
.child (
|
||||
S.document()
|
||||
.schemaType("section")
|
||||
)
|
||||
)
|
||||
)
|
15
lib/sanity/desk/settingStructure.ts
Normal file
15
lib/sanity/desk/settingStructure.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ListItemBuilder} from 'sanity/desk'
|
||||
import defineStructure from '../utils/defineStructure'
|
||||
|
||||
export default defineStructure<ListItemBuilder>((S) =>
|
||||
S.listItem()
|
||||
.title('Settings')
|
||||
.schemaType('settings')
|
||||
.child (
|
||||
S.documentTypeList('settings')
|
||||
.child (
|
||||
S.document()
|
||||
.schemaType("settings")
|
||||
)
|
||||
)
|
||||
)
|
15
lib/sanity/desk/uspStructure.ts
Normal file
15
lib/sanity/desk/uspStructure.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ListItemBuilder} from 'sanity/desk'
|
||||
import defineStructure from '../utils/defineStructure'
|
||||
|
||||
export default defineStructure<ListItemBuilder>((S) =>
|
||||
S.listItem()
|
||||
.title('USPs')
|
||||
.schemaType('usp')
|
||||
.child (
|
||||
S.documentTypeList('usp')
|
||||
.child (
|
||||
S.document()
|
||||
.schemaType("usp")
|
||||
)
|
||||
)
|
||||
)
|
21
lib/sanity/languages.ts
Normal file
21
lib/sanity/languages.ts
Normal file
@ -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}
|
23
lib/sanity/localizedTypes.ts
Normal file
23
lib/sanity/localizedTypes.ts
Normal file
@ -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,
|
||||
]
|
54
lib/sanity/schemas/blocks/body.tsx
Normal file
54
lib/sanity/schemas/blocks/body.tsx
Normal file
@ -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',
|
||||
// },
|
||||
],
|
||||
})
|
122
lib/sanity/schemas/documents/blurb.tsx
Normal file
122
lib/sanity/schemas/documents/blurb.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
126
lib/sanity/schemas/documents/category.tsx
Normal file
126
lib/sanity/schemas/documents/category.tsx
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
48
lib/sanity/schemas/documents/footerMenu.tsx
Normal file
48
lib/sanity/schemas/documents/footerMenu.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
75
lib/sanity/schemas/documents/page.tsx
Normal file
75
lib/sanity/schemas/documents/page.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
167
lib/sanity/schemas/documents/product.tsx
Normal file
167
lib/sanity/schemas/documents/product.tsx
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
30
lib/sanity/schemas/documents/productVariant.tsx
Normal file
30
lib/sanity/schemas/documents/productVariant.tsx
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
64
lib/sanity/schemas/documents/section.tsx
Normal file
64
lib/sanity/schemas/documents/section.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
56
lib/sanity/schemas/documents/slugWithLocalizedType.ts
Normal file
56
lib/sanity/schemas/documents/slugWithLocalizedType.ts
Normal file
@ -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
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
62
lib/sanity/schemas/documents/usp.tsx
Normal file
62
lib/sanity/schemas/documents/usp.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
91
lib/sanity/schemas/index.ts
Normal file
91
lib/sanity/schemas/index.ts
Normal file
@ -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
|
||||
]
|
50
lib/sanity/schemas/objects/banner.ts
Normal file
50
lib/sanity/schemas/objects/banner.ts
Normal file
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
132
lib/sanity/schemas/objects/blurbSection.tsx
Normal file
132
lib/sanity/schemas/objects/blurbSection.tsx
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
70
lib/sanity/schemas/objects/filteredProductList.ts
Normal file
70
lib/sanity/schemas/objects/filteredProductList.ts
Normal file
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
125
lib/sanity/schemas/objects/hero.ts
Normal file
125
lib/sanity/schemas/objects/hero.ts
Normal file
@ -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)' : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
54
lib/sanity/schemas/objects/linkExternal.ts
Normal file
54
lib/sanity/schemas/objects/linkExternal.ts
Normal file
@ -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(' '),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
56
lib/sanity/schemas/objects/linkInternal.ts
Normal file
56
lib/sanity/schemas/objects/linkInternal.ts
Normal file
@ -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(' '),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
26
lib/sanity/schemas/objects/mainImage.ts
Normal file
26
lib/sanity/schemas/objects/mainImage.ts
Normal file
@ -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.',
|
||||
}),
|
||||
],
|
||||
})
|
34
lib/sanity/schemas/objects/menu.ts
Normal file
34
lib/sanity/schemas/objects/menu.ts
Normal file
@ -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}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
10
lib/sanity/schemas/objects/placeholderString.ts
Normal file
10
lib/sanity/schemas/objects/placeholderString.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import PlaceholderStringInput from '../../components/inputs/PlaceholderString'
|
||||
|
||||
export default {
|
||||
name: 'placeholderString',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
components: {
|
||||
input: PlaceholderStringInput,
|
||||
},
|
||||
}
|
25
lib/sanity/schemas/objects/productOption.ts
Normal file
25
lib/sanity/schemas/objects/productOption.ts
Normal file
@ -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
|
||||
}
|
||||
})
|
||||
],
|
||||
})
|
32
lib/sanity/schemas/objects/productOptions.ts
Normal file
32
lib/sanity/schemas/objects/productOptions.ts
Normal file
@ -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(),
|
||||
}),
|
||||
],
|
||||
})
|
11
lib/sanity/schemas/objects/proxyString.ts
Normal file
11
lib/sanity/schemas/objects/proxyString.ts
Normal file
@ -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,
|
||||
},
|
||||
})
|
54
lib/sanity/schemas/objects/reusableSection.tsx
Normal file
54
lib/sanity/schemas/objects/reusableSection.tsx
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
45
lib/sanity/schemas/objects/seo.tsx
Normal file
45
lib/sanity/schemas/objects/seo.tsx
Normal file
@ -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.<br />
|
||||
If empty, displays the current document title (<i>title</i>).
|
||||
</>
|
||||
),
|
||||
}),
|
||||
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(),
|
||||
})
|
97
lib/sanity/schemas/objects/slider.ts
Normal file
97
lib/sanity/schemas/objects/slider.ts
Normal file
@ -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)' : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
63
lib/sanity/schemas/objects/uspSection.ts
Normal file
63
lib/sanity/schemas/objects/uspSection.ts
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
69
lib/sanity/schemas/singletons/home.tsx
Normal file
69
lib/sanity/schemas/singletons/home.tsx
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
158
lib/sanity/schemas/singletons/settings.ts
Normal file
158
lib/sanity/schemas/singletons/settings.ts
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
48
lib/sanity/schemas/singletons/utilityMenu.ts
Normal file
48
lib/sanity/schemas/singletons/utilityMenu.ts
Normal file
@ -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 : ''}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
11
lib/sanity/utils/defineStructure.ts
Normal file
11
lib/sanity/utils/defineStructure.ts
Normal file
@ -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<StructureType>(
|
||||
factory: (S: StructureBuilder, context: ConfigContext) => StructureType
|
||||
) {
|
||||
return factory
|
||||
}
|
19
lib/sanity/utils/getPreviewUrl.ts
Normal file
19
lib/sanity/utils/getPreviewUrl.ts
Normal file
@ -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}`
|
||||
}
|
||||
}
|
59
lib/sanity/utils/validation.ts
Normal file
59
lib/sanity/utils/validation.ts
Normal file
@ -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
|
||||
})
|
||||
}
|
||||
}
|
@ -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|.*\\..*).*)']
|
||||
};
|
@ -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"
|
||||
|
959
pnpm-lock.yaml
generated
959
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
75
sanity.config.ts
Normal file
75
sanity.config.ts
Normal file
@ -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,
|
||||
}
|
||||
},
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user